vacancy detail SEO
This commit is contained in:
@@ -4,10 +4,10 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
<!-- Базовые SEO теги -->
|
<!-- SEO теги будут заменены сервером -->
|
||||||
<title id="pageTitle">Вакансия | Rabota.Today</title>
|
<title id="pageTitle">Вакансия | Rabota.Today</title>
|
||||||
<meta name="description" id="metaDescription" content="Подробная информация о вакансии на Rabota.Today. Зарплата, требования, условия работы, контакты работодателя.">
|
<meta name="description" id="metaDescription" content="Подробная информация о вакансии на Rabota.Today. Зарплата, требования, условия работы, контакты работодателя.">
|
||||||
<meta name="keywords" content="вакансия, работа, поиск работы, Rabota.Today, трудоустройство">
|
<meta name="keywords" id="metaKeywords" content="вакансия, работа, поиск работы, Rabota.Today, трудоустройство">
|
||||||
<meta name="author" content="Rabota.Today">
|
<meta name="author" content="Rabota.Today">
|
||||||
<meta name="robots" content="index, follow">
|
<meta name="robots" content="index, follow">
|
||||||
|
|
||||||
@@ -33,46 +33,17 @@
|
|||||||
<meta name="format-detection" content="telephone=no">
|
<meta name="format-detection" content="telephone=no">
|
||||||
<meta name="theme-color" content="#0b1c34">
|
<meta name="theme-color" content="#0b1c34">
|
||||||
|
|
||||||
<!-- Структурированные данные (JSON-LD) -->
|
<!-- Структурированные данные (JSON-LD) будут заменены сервером -->
|
||||||
<script type="application/ld+json" id="structuredData">
|
<script type="application/ld+json" id="structuredData">
|
||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "JobPosting",
|
"@type": "JobPosting",
|
||||||
"title": "",
|
"title": "",
|
||||||
"description": "",
|
"description": ""
|
||||||
"datePosted": "",
|
|
||||||
"validThrough": "",
|
|
||||||
"employmentType": "FULL_TIME",
|
|
||||||
"hiringOrganization": {
|
|
||||||
"@type": "Organization",
|
|
||||||
"name": "",
|
|
||||||
"sameAs": "",
|
|
||||||
"logo": "https://yarmarka.rabota.today/static/images/logo.png"
|
|
||||||
},
|
|
||||||
"jobLocation": {
|
|
||||||
"@type": "Place",
|
|
||||||
"address": {
|
|
||||||
"@type": "PostalAddress",
|
|
||||||
"addressLocality": "Москва",
|
|
||||||
"addressCountry": "RU"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"baseSalary": {
|
|
||||||
"@type": "MonetaryAmount",
|
|
||||||
"currency": "RUB",
|
|
||||||
"value": {
|
|
||||||
"@type": "QuantitativeValue",
|
|
||||||
"value": 0,
|
|
||||||
"unitText": "MONTH"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"employmentType": "FULL_TIME",
|
|
||||||
"workHours": "Полный день"
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
<!-- Подключаем библиотеку для генерации QR-кодов -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.1/build/qrcode.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.1/build/qrcode.min.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -204,7 +175,6 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Кнопка QR в шапке */
|
|
||||||
.qr-button {
|
.qr-button {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
@@ -252,7 +222,6 @@
|
|||||||
box-shadow: 0 2px 4px rgba(239,68,68,0.3);
|
box-shadow: 0 2px 4px rgba(239,68,68,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Кликабельная компания */
|
|
||||||
.vacancy-company-link {
|
.vacancy-company-link {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -345,7 +314,6 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Информация о компании */
|
|
||||||
.company-info {
|
.company-info {
|
||||||
background: #f9fcff;
|
background: #f9fcff;
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
@@ -506,15 +474,6 @@
|
|||||||
background: #eef4fa;
|
background: #eef4fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-success {
|
|
||||||
background: #10b981;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success:hover {
|
|
||||||
background: #059669;
|
|
||||||
}
|
|
||||||
|
|
||||||
.applied-badge {
|
.applied-badge {
|
||||||
background: #10b981;
|
background: #10b981;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -536,7 +495,7 @@
|
|||||||
border-top: 1px solid #dee9f5;
|
border-top: 1px solid #dee9f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Модальное окно для QR */
|
/* QR Modal Styles */
|
||||||
.qr-modal {
|
.qr-modal {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -831,6 +790,25 @@
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fallback-link {
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
margin: 10px;
|
||||||
|
word-break: break-all;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-link a {
|
||||||
|
color: #3b82f6;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback-link a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from { transform: translateX(100%); opacity: 0; }
|
from { transform: translateX(100%); opacity: 0; }
|
||||||
to { transform: translateX(0%); opacity: 1; }
|
to { transform: translateX(0%); opacity: 1; }
|
||||||
@@ -957,11 +935,9 @@
|
|||||||
let currentVacancy = null;
|
let currentVacancy = null;
|
||||||
let qrViewCount = 0;
|
let qrViewCount = 0;
|
||||||
|
|
||||||
// Получаем ID вакансии из URL
|
|
||||||
const pathParts = window.location.pathname.split('/');
|
const pathParts = window.location.pathname.split('/');
|
||||||
const vacancyId = pathParts[pathParts.length - 1];
|
const vacancyId = pathParts[pathParts.length - 1];
|
||||||
|
|
||||||
// Функция для декодирования HTML-сущностей
|
|
||||||
function decodeHtmlEntities(text) {
|
function decodeHtmlEntities(text) {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
const textarea = document.createElement('textarea');
|
const textarea = document.createElement('textarea');
|
||||||
@@ -969,7 +945,6 @@
|
|||||||
return textarea.value;
|
return textarea.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция для экранирования HTML
|
|
||||||
function escapeHtml(unsafe) {
|
function escapeHtml(unsafe) {
|
||||||
if (!unsafe) return '';
|
if (!unsafe) return '';
|
||||||
return unsafe.toString()
|
return unsafe.toString()
|
||||||
@@ -980,92 +955,22 @@
|
|||||||
.replace(/'/g, "'");
|
.replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция для обновления SEO тегов
|
function showNotification(message, type = 'info') {
|
||||||
function updateSEOTags(vacancy) {
|
const notification = document.getElementById('notification');
|
||||||
const decodedTitle = decodeHtmlEntities(vacancy.title);
|
if (!notification) return;
|
||||||
const decodedCompany = decodeHtmlEntities(vacancy.company_name || 'Компания');
|
notification.className = `notification ${type}`;
|
||||||
const salary = vacancy.salary || 'Зарплата не указана';
|
notification.innerHTML = message;
|
||||||
const description = vacancy.description || 'Подробная информация о вакансии';
|
notification.style.display = 'block';
|
||||||
|
setTimeout(() => { notification.style.display = 'none'; }, 3000);
|
||||||
// Формируем описание для SEO
|
|
||||||
const shortDescription = description.length > 160 ? description.substring(0, 157) + '...' : description;
|
|
||||||
const seoDescription = `${decodedTitle} в компании ${decodedCompany}. ${salary}. ${shortDescription}`;
|
|
||||||
|
|
||||||
// Заголовок страницы
|
|
||||||
document.title = `${decodedTitle} в ${decodedCompany} | Rabota.Today`;
|
|
||||||
|
|
||||||
// Мета-теги
|
|
||||||
document.querySelector('meta[name="description"]')?.setAttribute('content', seoDescription);
|
|
||||||
document.querySelector('meta[name="keywords"]')?.setAttribute('content', `${decodedTitle}, работа, вакансия, ${decodedCompany}, трудоустройство, поиск работы`);
|
|
||||||
|
|
||||||
// Open Graph теги
|
|
||||||
document.querySelector('meta[property="og:title"]')?.setAttribute('content', `${decodedTitle} в ${decodedCompany}`);
|
|
||||||
document.querySelector('meta[property="og:description"]')?.setAttribute('content', seoDescription);
|
|
||||||
document.querySelector('meta[property="og:url"]')?.setAttribute('content', window.location.href);
|
|
||||||
|
|
||||||
// Twitter Card
|
|
||||||
document.querySelector('meta[name="twitter:title"]')?.setAttribute('content', `${decodedTitle} в ${decodedCompany}`);
|
|
||||||
document.querySelector('meta[name="twitter:description"]')?.setAttribute('content', seoDescription);
|
|
||||||
|
|
||||||
// Canonical URL
|
|
||||||
document.querySelector('link[rel="canonical"]')?.setAttribute('href', window.location.href);
|
|
||||||
|
|
||||||
// Структурированные данные (JSON-LD)
|
|
||||||
const salaryMatch = salary.match(/(\d+)/);
|
|
||||||
const salaryValue = salaryMatch ? parseInt(salaryMatch[0]) : 0;
|
|
||||||
|
|
||||||
const structuredData = {
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": "JobPosting",
|
|
||||||
"title": decodedTitle,
|
|
||||||
"description": description,
|
|
||||||
"datePosted": vacancy.created_at,
|
|
||||||
"validThrough": new Date(new Date(vacancy.created_at).getTime() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
"employmentType": "FULL_TIME",
|
|
||||||
"hiringOrganization": {
|
|
||||||
"@type": "Organization",
|
|
||||||
"name": decodedCompany,
|
|
||||||
"sameAs": vacancy.company_website || "",
|
|
||||||
"logo": vacancy.company_logo || "https://yarmarka.rabota.today/static/images/logo.png"
|
|
||||||
},
|
|
||||||
"jobLocation": {
|
|
||||||
"@type": "Place",
|
|
||||||
"address": {
|
|
||||||
"@type": "PostalAddress",
|
|
||||||
"addressLocality": vacancy.company_address || "Москва",
|
|
||||||
"addressCountry": "RU"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"baseSalary": {
|
|
||||||
"@type": "MonetaryAmount",
|
|
||||||
"currency": "RUB",
|
|
||||||
"value": {
|
|
||||||
"@type": "QuantitativeValue",
|
|
||||||
"value": salaryValue,
|
|
||||||
"unitText": "MONTH"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"workHours": "Полный день"
|
|
||||||
};
|
|
||||||
|
|
||||||
const scriptElement = document.getElementById('structuredData');
|
|
||||||
if (scriptElement) {
|
|
||||||
scriptElement.textContent = JSON.stringify(structuredData, null, 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ SEO теги обновлены');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверка авторизации
|
|
||||||
async function checkAuth() {
|
async function checkAuth() {
|
||||||
const token = localStorage.getItem('accessToken');
|
const token = localStorage.getItem('accessToken');
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/user`, {
|
const response = await fetch(`${API_BASE_URL}/user`, {
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
currentUser = await response.json();
|
currentUser = await response.json();
|
||||||
} else {
|
} else {
|
||||||
@@ -1075,18 +980,14 @@
|
|||||||
console.error('Error checking auth:', error);
|
console.error('Error checking auth:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNavigation();
|
updateNavigation();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновление навигации
|
|
||||||
function updateNavigation() {
|
function updateNavigation() {
|
||||||
const nav = document.getElementById('nav');
|
const nav = document.getElementById('nav');
|
||||||
|
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
const firstName = currentUser.full_name.split(' ')[0];
|
const firstName = currentUser.full_name.split(' ')[0];
|
||||||
const adminBadge = currentUser.is_admin ? '<span class="admin-badge">Admin</span>' : '';
|
const adminBadge = currentUser.is_admin ? '<span class="admin-badge">Admin</span>' : '';
|
||||||
|
|
||||||
nav.innerHTML = `
|
nav.innerHTML = `
|
||||||
<a href="/">Главная</a>
|
<a href="/">Главная</a>
|
||||||
<a href="/vacancies" class="active">Вакансии</a>
|
<a href="/vacancies" class="active">Вакансии</a>
|
||||||
@@ -1108,31 +1009,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загрузка вакансии
|
|
||||||
async function loadVacancy() {
|
async function loadVacancy() {
|
||||||
const token = localStorage.getItem('accessToken');
|
const token = localStorage.getItem('accessToken');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const headers = {};
|
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
|
||||||
if (token) {
|
const response = await fetch(`${API_BASE_URL}/vacancies/${vacancyId}`, { headers });
|
||||||
headers['Authorization'] = `Bearer ${token}`;
|
if (!response.ok) throw new Error('Вакансия не найдена');
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}/vacancies/${vacancyId}`, {
|
|
||||||
headers: headers
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Вакансия не найдена');
|
|
||||||
}
|
|
||||||
|
|
||||||
currentVacancy = await response.json();
|
currentVacancy = await response.json();
|
||||||
|
|
||||||
// Обновляем SEO теги
|
|
||||||
updateSEOTags(currentVacancy);
|
|
||||||
|
|
||||||
renderVacancy(currentVacancy);
|
renderVacancy(currentVacancy);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading vacancy:', error);
|
console.error('Error loading vacancy:', error);
|
||||||
document.getElementById('vacancyDetail').innerHTML = `
|
document.getElementById('vacancyDetail').innerHTML = `
|
||||||
@@ -1147,18 +1031,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отображение вакансии
|
|
||||||
function renderVacancy(vacancy) {
|
function renderVacancy(vacancy) {
|
||||||
const container = document.getElementById('vacancyDetail');
|
const container = document.getElementById('vacancyDetail');
|
||||||
const token = localStorage.getItem('accessToken');
|
const token = localStorage.getItem('accessToken');
|
||||||
|
|
||||||
const isApplied = vacancy.has_applied;
|
const isApplied = vacancy.has_applied;
|
||||||
const canApply = token && currentUser && currentUser.role === 'employee' && !isApplied;
|
const canApply = token && currentUser && currentUser.role === 'employee' && !isApplied;
|
||||||
|
|
||||||
// Получаем ID компании
|
|
||||||
const companyId = vacancy.company_id || '';
|
const companyId = vacancy.company_id || '';
|
||||||
|
|
||||||
// Декодируем названия
|
|
||||||
const decodedTitle = decodeHtmlEntities(vacancy.title);
|
const decodedTitle = decodeHtmlEntities(vacancy.title);
|
||||||
const decodedCompanyName = decodeHtmlEntities(vacancy.company_name || '');
|
const decodedCompanyName = decodeHtmlEntities(vacancy.company_name || '');
|
||||||
|
|
||||||
@@ -1171,7 +1049,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Кликабельная компания -->
|
|
||||||
${vacancy.company_name ? `
|
${vacancy.company_name ? `
|
||||||
<a href="/company/${companyId}" class="vacancy-company-link" ${!companyId ? 'onclick="event.preventDefault(); showNotification(\'Страница компании в разработке\')"' : ''}>
|
<a href="/company/${companyId}" class="vacancy-company-link" ${!companyId ? 'onclick="event.preventDefault(); showNotification(\'Страница компании в разработке\')"' : ''}>
|
||||||
<i class="fas fa-building"></i>
|
<i class="fas fa-building"></i>
|
||||||
@@ -1193,43 +1070,22 @@
|
|||||||
<h2 class="section-title">Описание вакансии</h2>
|
<h2 class="section-title">Описание вакансии</h2>
|
||||||
<div class="vacancy-description">${escapeHtml(vacancy.description || 'Описание отсутствует').replace(/\n/g, '<br>')}</div>
|
<div class="vacancy-description">${escapeHtml(vacancy.description || 'Описание отсутствует').replace(/\n/g, '<br>')}</div>
|
||||||
|
|
||||||
<!-- Информация о компании -->
|
|
||||||
${vacancy.company_name ? `
|
${vacancy.company_name ? `
|
||||||
<div class="company-info">
|
<div class="company-info">
|
||||||
<div class="company-info-header">
|
<div class="company-info-header">
|
||||||
<h3>
|
<h3><i class="fas fa-building"></i> О компании</h3>
|
||||||
<i class="fas fa-building"></i> О компании
|
${companyId ? `<a href="/company/${companyId}" class="company-view-all"><i class="fas fa-external-link-alt"></i> Все вакансии компании</a>` : ''}
|
||||||
</h3>
|
|
||||||
${companyId ? `
|
|
||||||
<a href="/company/${companyId}" class="company-view-all">
|
|
||||||
<i class="fas fa-external-link-alt"></i> Все вакансии компании
|
|
||||||
</a>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="company-info-grid">
|
<div class="company-info-grid">
|
||||||
<div class="company-info-item">
|
<div class="company-info-item">
|
||||||
<i class="fas fa-building"></i>
|
<i class="fas fa-building"></i>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight: 600;">${escapeHtml(decodedCompanyName)}</div>
|
<div style="font-weight: 600;">${escapeHtml(decodedCompanyName)}</div>
|
||||||
${companyId ? `
|
${companyId ? `<a href="/company/${companyId}" style="font-size: 13px; color: #3b82f6;">перейти в профиль компании →</a>` : ''}
|
||||||
<a href="/company/${companyId}" style="font-size: 13px; color: #3b82f6;">
|
|
||||||
перейти в профиль компании →
|
|
||||||
</a>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${vacancy.company_website ? `
|
${vacancy.company_website ? `<div class="company-info-item"><i class="fas fa-globe"></i><a href="${escapeHtml(vacancy.company_website)}" target="_blank">${escapeHtml(vacancy.company_website)}</a></div>` : ''}
|
||||||
<div class="company-info-item">
|
${vacancy.company_address ? `<div class="company-info-item"><i class="fas fa-map-marker-alt"></i><span>${escapeHtml(vacancy.company_address)}</span></div>` : ''}
|
||||||
<i class="fas fa-globe"></i>
|
|
||||||
<a href="${escapeHtml(vacancy.company_website)}" target="_blank">${escapeHtml(vacancy.company_website)}</a>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${vacancy.company_address ? `
|
|
||||||
<div class="company-info-item">
|
|
||||||
<i class="fas fa-map-marker-alt"></i>
|
|
||||||
<span>${escapeHtml(vacancy.company_address)}</span>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
${vacancy.company_description ? `
|
${vacancy.company_description ? `
|
||||||
<div style="margin-top: 20px; color: #1f3f60; line-height: 1.6; padding: 15px; background: white; border-radius: 15px;">
|
<div style="margin-top: 20px; color: #1f3f60; line-height: 1.6; padding: 15px; background: white; border-radius: 15px;">
|
||||||
@@ -1243,50 +1099,18 @@
|
|||||||
|
|
||||||
<h2 class="section-title">Контактная информация</h2>
|
<h2 class="section-title">Контактная информация</h2>
|
||||||
<div class="contact-info">
|
<div class="contact-info">
|
||||||
<div class="contact-item">
|
<div class="contact-item"><i class="fab fa-telegram"></i><span>${escapeHtml(vacancy.contact || vacancy.user_telegram || 'Контакт не указан')}</span></div>
|
||||||
<i class="fab fa-telegram"></i>
|
${vacancy.company_email ? `<div class="contact-item"><i class="fas fa-envelope"></i><a href="mailto:${escapeHtml(vacancy.company_email)}">${escapeHtml(vacancy.company_email)}</a></div>` : ''}
|
||||||
<span>${escapeHtml(vacancy.contact || vacancy.user_telegram || 'Контакт не указан')}</span>
|
${vacancy.company_phone ? `<div class="contact-item"><i class="fas fa-phone"></i><a href="tel:${escapeHtml(vacancy.company_phone)}">${escapeHtml(vacancy.company_phone)}</a></div>` : ''}
|
||||||
</div>
|
|
||||||
${vacancy.company_email ? `
|
|
||||||
<div class="contact-item">
|
|
||||||
<i class="fas fa-envelope"></i>
|
|
||||||
<a href="mailto:${escapeHtml(vacancy.company_email)}">${escapeHtml(vacancy.company_email)}</a>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${vacancy.company_phone ? `
|
|
||||||
<div class="contact-item">
|
|
||||||
<i class="fas fa-phone"></i>
|
|
||||||
<a href="tel:${escapeHtml(vacancy.company_phone)}">${escapeHtml(vacancy.company_phone)}</a>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
${isApplied ? `
|
${isApplied ? `<div class="applied-badge"><i class="fas fa-check-circle"></i> Вы уже откликнулись</div>` :
|
||||||
<div class="applied-badge">
|
canApply ? `<button class="btn btn-primary" onclick="applyForVacancy()"><i class="fas fa-paper-plane"></i> Откликнуться</button>` :
|
||||||
<i class="fas fa-check-circle"></i> Вы уже откликнулись
|
token && currentUser && currentUser.role === 'employer' ? `<button class="btn btn-primary" disabled><i class="fas fa-ban"></i> Отклик недоступен</button>` :
|
||||||
</div>
|
!token ? `<button class="btn btn-primary" onclick="redirectToLogin()"><i class="fas fa-sign-in-alt"></i> Войдите чтобы откликнуться</button>` : ''}
|
||||||
` : canApply ? `
|
<button class="btn btn-outline" onclick="saveToFavorites()"><i class="fas fa-heart"></i> В избранное</button>
|
||||||
<button class="btn btn-primary" onclick="applyForVacancy()">
|
<button class="btn btn-outline" onclick="shareVacancy()"><i class="fas fa-share-alt"></i> Поделиться</button>
|
||||||
<i class="fas fa-paper-plane"></i> Откликнуться
|
|
||||||
</button>
|
|
||||||
` : token && currentUser && currentUser.role === 'employer' ? `
|
|
||||||
<button class="btn btn-primary" disabled title="Работодатели не могут откликаться на вакансии">
|
|
||||||
<i class="fas fa-ban"></i> Отклик недоступен
|
|
||||||
</button>
|
|
||||||
` : !token ? `
|
|
||||||
<button class="btn btn-primary" onclick="redirectToLogin()">
|
|
||||||
<i class="fas fa-sign-in-alt"></i> Войдите чтобы откликнуться
|
|
||||||
</button>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
<button class="btn btn-outline" onclick="saveToFavorites()">
|
|
||||||
<i class="fas fa-heart"></i> В избранное
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-outline" onclick="shareVacancy()">
|
|
||||||
<i class="fas fa-share-alt"></i> Поделиться
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
@@ -1296,27 +1120,19 @@
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== ФУНКЦИИ ДЛЯ QR-КОДА ==========
|
// QR Code functions
|
||||||
|
|
||||||
// Открыть модальное окно с QR
|
|
||||||
function openQRModal() {
|
function openQRModal() {
|
||||||
if (!currentVacancy) return;
|
if (!currentVacancy) return;
|
||||||
|
|
||||||
const decodedTitle = decodeHtmlEntities(currentVacancy.title);
|
const decodedTitle = decodeHtmlEntities(currentVacancy.title);
|
||||||
const decodedCompanyName = decodeHtmlEntities(currentVacancy.company_name || '');
|
const decodedCompanyName = decodeHtmlEntities(currentVacancy.company_name || '');
|
||||||
|
|
||||||
document.getElementById('qrVacancyTitle').textContent = decodedTitle;
|
document.getElementById('qrVacancyTitle').textContent = decodedTitle;
|
||||||
|
|
||||||
const vacancyUrl = window.location.origin + '/vacancy/' + vacancyId;
|
const vacancyUrl = window.location.origin + '/vacancy/' + vacancyId;
|
||||||
document.getElementById('qrVacancyUrl').textContent = vacancyUrl.replace('https://', '').replace('http://', '');
|
document.getElementById('qrVacancyUrl').textContent = vacancyUrl.replace('https://', '').replace('http://', '');
|
||||||
|
|
||||||
document.getElementById('qrViewCount').textContent = ++qrViewCount;
|
document.getElementById('qrViewCount').textContent = ++qrViewCount;
|
||||||
document.getElementById('qrViewBadge').textContent = qrViewCount;
|
document.getElementById('qrViewBadge').textContent = qrViewCount;
|
||||||
document.getElementById('qrSalaryInfo').textContent = currentVacancy.salary || 'з/п не указана';
|
document.getElementById('qrSalaryInfo').textContent = currentVacancy.salary || 'з/п не указана';
|
||||||
document.getElementById('qrCompanyName').textContent = decodedCompanyName || '—';
|
document.getElementById('qrCompanyName').textContent = decodedCompanyName || '—';
|
||||||
|
|
||||||
generateQRCodeWithLogo(vacancyUrl, currentVacancy.company_logo);
|
generateQRCodeWithLogo(vacancyUrl, currentVacancy.company_logo);
|
||||||
|
|
||||||
document.getElementById('qrModal').classList.add('active');
|
document.getElementById('qrModal').classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1326,34 +1142,35 @@
|
|||||||
|
|
||||||
function generateQRCodeWithLogo(text, logoUrl) {
|
function generateQRCodeWithLogo(text, logoUrl) {
|
||||||
const canvas = document.getElementById('qrCanvas');
|
const canvas = document.getElementById('qrCanvas');
|
||||||
|
if (!canvas) { showNotification('Ошибка: не найден элемент для QR-кода', 'error'); return; }
|
||||||
|
if (typeof QRCode === 'undefined') {
|
||||||
|
showNotification('Ошибка загрузки библиотеки QR-кода', 'error');
|
||||||
|
const container = document.getElementById('qrContainer');
|
||||||
|
if (container && !container.querySelector('.fallback-link')) {
|
||||||
|
const fallback = document.createElement('div');
|
||||||
|
fallback.className = 'fallback-link';
|
||||||
|
fallback.innerHTML = `<a href="${text}" target="_blank">${text}</a>`;
|
||||||
|
container.appendChild(fallback);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const options = { width: 250, height: 250, color: { dark: '#0b1c34', light: '#ffffff' }, errorCorrectionLevel: 'H' };
|
||||||
|
QRCode.toCanvas(canvas, text, options, function(error) {
|
||||||
|
if (error) { showNotification('Ошибка генерации QR-кода', 'error'); }
|
||||||
|
else {
|
||||||
const logoOverlay = document.getElementById('qrLogoOverlay');
|
const logoOverlay = document.getElementById('qrLogoOverlay');
|
||||||
const logoImg = document.getElementById('qrLogoImg');
|
const logoImg = document.getElementById('qrLogoImg');
|
||||||
const logoIcon = document.getElementById('qrLogoIcon');
|
const logoIcon = document.getElementById('qrLogoIcon');
|
||||||
|
if (logoOverlay) logoOverlay.style.display = 'flex';
|
||||||
const options = {
|
if (logoUrl && logoImg) {
|
||||||
width: 250,
|
|
||||||
height: 250,
|
|
||||||
color: { dark: '#0b1c34', light: '#ffffff' },
|
|
||||||
errorCorrectionLevel: 'H'
|
|
||||||
};
|
|
||||||
|
|
||||||
QRCode.toCanvas(canvas, text, options, function(error) {
|
|
||||||
if (error) {
|
|
||||||
console.error('Error generating QR code:', error);
|
|
||||||
showNotification('Ошибка генерации QR-кода', 'error');
|
|
||||||
} else {
|
|
||||||
logoOverlay.style.display = 'flex';
|
|
||||||
|
|
||||||
if (logoUrl) {
|
|
||||||
logoImg.src = logoUrl;
|
logoImg.src = logoUrl;
|
||||||
logoImg.style.display = 'block';
|
logoImg.style.display = 'block';
|
||||||
logoIcon.style.display = 'none';
|
if (logoIcon) logoIcon.style.display = 'none';
|
||||||
logoImg.onerror = function() {
|
logoImg.onerror = function() {
|
||||||
logoImg.style.display = 'none';
|
if (logoImg) logoImg.style.display = 'none';
|
||||||
logoIcon.style.display = 'block';
|
if (logoIcon) { logoIcon.style.display = 'block'; logoIcon.className = 'fas fa-briefcase'; }
|
||||||
logoIcon.className = 'fas fa-briefcase';
|
|
||||||
};
|
};
|
||||||
} else {
|
} else if (logoIcon) {
|
||||||
logoImg.style.display = 'none';
|
logoImg.style.display = 'none';
|
||||||
logoIcon.style.display = 'block';
|
logoIcon.style.display = 'block';
|
||||||
logoIcon.className = 'fas fa-briefcase';
|
logoIcon.className = 'fas fa-briefcase';
|
||||||
@@ -1364,11 +1181,11 @@
|
|||||||
|
|
||||||
function downloadQR() {
|
function downloadQR() {
|
||||||
const canvas = document.getElementById('qrCanvas');
|
const canvas = document.getElementById('qrCanvas');
|
||||||
|
if (!canvas) { showNotification('QR-код не найден', 'error'); return; }
|
||||||
const combinedCanvas = document.createElement('canvas');
|
const combinedCanvas = document.createElement('canvas');
|
||||||
combinedCanvas.width = canvas.width;
|
combinedCanvas.width = canvas.width;
|
||||||
combinedCanvas.height = canvas.height;
|
combinedCanvas.height = canvas.height;
|
||||||
const ctx = combinedCanvas.getContext('2d');
|
const ctx = combinedCanvas.getContext('2d');
|
||||||
|
|
||||||
ctx.drawImage(canvas, 0, 0);
|
ctx.drawImage(canvas, 0, 0);
|
||||||
ctx.fillStyle = 'white';
|
ctx.fillStyle = 'white';
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -1379,59 +1196,40 @@
|
|||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.fillText('', 125, 125);
|
ctx.fillText('', 125, 125);
|
||||||
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
const decodedTitle = decodeHtmlEntities(currentVacancy.title);
|
const decodedTitle = decodeHtmlEntities(currentVacancy.title);
|
||||||
const filename = `vacancy_${decodedTitle.toLowerCase().replace(/[^a-zа-я0-9]/g, '_')}.png`;
|
const filename = `vacancy_${decodedTitle.toLowerCase().replace(/[^a-zа-я0-9]/g, '_')}.png`;
|
||||||
link.download = filename;
|
link.download = filename;
|
||||||
link.href = combinedCanvas.toDataURL('image/png');
|
link.href = combinedCanvas.toDataURL('image/png');
|
||||||
link.click();
|
link.click();
|
||||||
|
|
||||||
showNotification('QR-код скачан', 'success');
|
showNotification('QR-код скачан', 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyVacancyLink() {
|
function copyVacancyLink() {
|
||||||
const url = window.location.origin + '/vacancy/' + vacancyId;
|
const url = window.location.origin + '/vacancy/' + vacancyId;
|
||||||
navigator.clipboard.writeText(url).then(() => {
|
navigator.clipboard.writeText(url).then(() => showNotification('Ссылка скопирована', 'success')).catch(() => showNotification('Ошибка копирования', 'error'));
|
||||||
showNotification('Ссылка скопирована', 'success');
|
|
||||||
}).catch(() => {
|
|
||||||
showNotification('Ошибка копирования', 'error');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function shareVacancy() {
|
function shareVacancy() {
|
||||||
const url = window.location.href;
|
const url = window.location.href;
|
||||||
if (navigator.share) {
|
if (navigator.share) { navigator.share({ title: document.title, url: url }).catch(() => copyVacancyLink()); }
|
||||||
navigator.share({ title: document.title, url: url }).catch(() => copyVacancyLink());
|
else { copyVacancyLink(); }
|
||||||
} else {
|
|
||||||
copyVacancyLink();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function printQR() {
|
function printQR() {
|
||||||
const canvas = document.getElementById('qrCanvas');
|
const canvas = document.getElementById('qrCanvas');
|
||||||
const decodedTitle = decodeHtmlEntities(currentVacancy.title);
|
const decodedTitle = decodeHtmlEntities(currentVacancy.title);
|
||||||
const decodedCompany = decodeHtmlEntities(currentVacancy.company_name || '');
|
const decodedCompany = decodeHtmlEntities(currentVacancy.company_name || '');
|
||||||
|
|
||||||
const printWindow = window.open('', '_blank');
|
const printWindow = window.open('', '_blank');
|
||||||
printWindow.document.write(`
|
printWindow.document.write(`
|
||||||
<html>
|
<html><head><title>QR-код вакансии</title>
|
||||||
<head><title>QR-код вакансии</title>
|
<style>body{display:flex;justify-content:center;align-items:center;height:100vh;flex-direction:column;font-family:Arial;margin:0;padding:20px;}h2{color:#0b1c34;text-align:center;}h3{color:#3b82f6;text-align:center;}img{max-width:300px;box-shadow:0 10px 30px rgba(0,0,0,0.1);border-radius:20px;}.url{color:#4f7092;margin-top:20px;text-align:center;}</style>
|
||||||
<style>
|
</head><body>
|
||||||
body { display: flex; justify-content: center; align-items: center; height: 100vh; flex-direction: column; font-family: Arial; margin: 0; padding: 20px; }
|
|
||||||
h2 { color: #0b1c34; text-align: center; word-break: break-word; }
|
|
||||||
h3 { color: #3b82f6; text-align: center; }
|
|
||||||
img { max-width: 300px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); border-radius: 20px; }
|
|
||||||
.url { color: #4f7092; margin-top: 20px; word-break: break-all; text-align: center; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h2>${escapeHtml(decodedTitle)}</h2>
|
<h2>${escapeHtml(decodedTitle)}</h2>
|
||||||
<h3>${escapeHtml(decodedCompany)}</h3>
|
<h3>${escapeHtml(decodedCompany)}</h3>
|
||||||
<img src="${canvas.toDataURL()}" />
|
<img src="${canvas.toDataURL()}" />
|
||||||
<div class="url">${window.location.origin}/vacancy/${vacancyId}</div>
|
<div class="url">${window.location.origin}/vacancy/${vacancyId}</div>
|
||||||
</body>
|
</body></html>
|
||||||
</html>
|
|
||||||
`);
|
`);
|
||||||
printWindow.document.close();
|
printWindow.document.close();
|
||||||
printWindow.focus();
|
printWindow.focus();
|
||||||
@@ -1442,60 +1240,31 @@
|
|||||||
const url = window.location.origin + '/vacancy/' + vacancyId;
|
const url = window.location.origin + '/vacancy/' + vacancyId;
|
||||||
const decodedTitle = decodeHtmlEntities(currentVacancy.title);
|
const decodedTitle = decodeHtmlEntities(currentVacancy.title);
|
||||||
const text = `Вакансия: ${decodedTitle} на Rabota.Today`;
|
const text = `Вакансия: ${decodedTitle} на Rabota.Today`;
|
||||||
|
|
||||||
let shareUrl = '';
|
let shareUrl = '';
|
||||||
switch(platform) {
|
if (platform === 'whatsapp') shareUrl = `https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`;
|
||||||
case 'whatsapp': shareUrl = `https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`; break;
|
else if (platform === 'telegram') shareUrl = `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`;
|
||||||
case 'telegram': shareUrl = `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`; break;
|
else if (platform === 'email') shareUrl = `mailto:?subject=${encodeURIComponent('Вакансия ' + decodedTitle)}&body=${encodeURIComponent(text + '\n\n' + url)}`;
|
||||||
case 'email': shareUrl = `mailto:?subject=${encodeURIComponent('Вакансия ' + decodedTitle)}&body=${encodeURIComponent(text + '\n\n' + url)}`; break;
|
|
||||||
}
|
|
||||||
if (shareUrl) window.open(shareUrl, '_blank');
|
if (shareUrl) window.open(shareUrl, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyForVacancy() {
|
async function applyForVacancy() {
|
||||||
const token = localStorage.getItem('accessToken');
|
const token = localStorage.getItem('accessToken');
|
||||||
if (!token) { redirectToLogin(); return; }
|
if (!token) { redirectToLogin(); return; }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/vacancies/${vacancyId}/apply`, {
|
const response = await fetch(`${API_BASE_URL}/vacancies/${vacancyId}/apply`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` } });
|
||||||
method: 'POST',
|
if (response.ok) { showNotification('Отклик отправлен! Работодатель свяжется с вами.', 'success'); loadVacancy(); }
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
else { const error = await response.json(); throw new Error(error.detail || 'Ошибка отправки'); }
|
||||||
});
|
} catch (error) { showNotification(error.message, 'error'); }
|
||||||
if (response.ok) {
|
|
||||||
showNotification('Отклик отправлен! Работодатель свяжется с вами.', 'success');
|
|
||||||
loadVacancy();
|
|
||||||
} else {
|
|
||||||
const error = await response.json();
|
|
||||||
throw new Error(error.detail || 'Ошибка отправки');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
showNotification(error.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveToFavorites() {
|
function saveToFavorites() {
|
||||||
const token = localStorage.getItem('accessToken');
|
const token = localStorage.getItem('accessToken');
|
||||||
if (!token) {
|
if (!token) { if (confirm('Для добавления в избранное нужно войти в систему. Перейти на страницу входа?')) { window.location.href = '/login'; } return; }
|
||||||
if (confirm('Для добавления в избранное нужно войти в систему. Перейти на страницу входа?')) {
|
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showNotification('Вакансия добавлена в избранное', 'success');
|
showNotification('Вакансия добавлена в избранное', 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
function redirectToLogin() {
|
function redirectToLogin() {
|
||||||
if (confirm('Для отклика нужно войти в систему. Перейти на страницу входа?')) {
|
if (confirm('Для отклика нужно войти в систему. Перейти на страницу входа?')) { window.location.href = '/login'; }
|
||||||
window.location.href = '/login';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showNotification(message, type = 'info') {
|
|
||||||
const notification = document.getElementById('notification');
|
|
||||||
notification.className = `notification ${type}`;
|
|
||||||
notification.innerHTML = message;
|
|
||||||
notification.style.display = 'block';
|
|
||||||
setTimeout(() => { notification.style.display = 'none'; }, 3000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.onclick = function(event) {
|
window.onclick = function(event) {
|
||||||
|
|||||||
Reference in New Issue
Block a user