Files
yarmarka/templates/vacancy_detail.html
2026-03-20 15:28:19 +03:00

1509 lines
52 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Базовые SEO теги -->
<title id="pageTitle">Вакансия | Rabota.Today</title>
<meta name="description" id="metaDescription" content="Подробная информация о вакансии на Rabota.Today. Зарплата, требования, условия работы, контакты работодателя.">
<meta name="keywords" content="вакансия, работа, поиск работы, Rabota.Today, трудоустройство">
<meta name="author" content="Rabota.Today">
<meta name="robots" content="index, follow">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" id="ogUrl" content="https://yarmarka.rabota.today/">
<meta property="og:title" id="ogTitle" content="Вакансия | Rabota.Today">
<meta property="og:description" id="ogDescription" content="Подробная информация о вакансии на Rabota.Today">
<meta property="og:image" id="ogImage" content="https://yarmarka.rabota.today/static/images/og-image.jpg">
<meta property="og:site_name" content="Rabota.Today">
<meta property="og:locale" content="ru_RU">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" id="twitterTitle" content="Вакансия | Rabota.Today">
<meta name="twitter:description" id="twitterDescription" content="Подробная информация о вакансии на Rabota.Today">
<meta name="twitter:image" id="twitterImage" content="https://yarmarka.rabota.today/static/images/og-image.jpg">
<!-- Canonical URL -->
<link rel="canonical" id="canonicalUrl" href="https://yarmarka.rabota.today/">
<!-- Дополнительные SEO метатеги -->
<meta name="format-detection" content="telephone=no">
<meta name="theme-color" content="#0b1c34">
<!-- Структурированные данные (JSON-LD) -->
<script type="application/ld+json" id="structuredData">
{
"@context": "https://schema.org",
"@type": "JobPosting",
"title": "",
"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>
<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>
<style>
/* ... все существующие стили ... */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
body {
background: linear-gradient(145deg, #eef5fa 0%, #e0eaf5 100%);
min-height: 100vh;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.header {
background: #0b1c34;
color: white;
padding: 20px 40px;
border-radius: 40px;
margin-bottom: 40px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
.logo {
font-size: 28px;
font-weight: 700;
display: flex;
align-items: center;
gap: 15px;
cursor: pointer;
}
.logo i {
color: #3b82f6;
background: rgba(255,255,255,0.1);
padding: 12px;
border-radius: 20px;
}
.nav {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.nav a {
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 30px;
transition: 0.2s;
}
.nav a:hover {
background: rgba(255,255,255,0.1);
}
.nav .active {
background: #3b82f6;
}
.profile-link {
display: flex;
align-items: center;
gap: 8px;
background: #3b82f6;
padding: 8px 20px !important;
}
.admin-badge {
background: #f59e0b;
color: white;
padding: 2px 8px;
border-radius: 20px;
font-size: 12px;
margin-left: 5px;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #4f7092;
text-decoration: none;
font-size: 16px;
transition: 0.2s;
}
.back-link i {
margin-right: 8px;
}
.back-link:hover {
color: #0b1c34;
}
.vacancy-detail {
background: white;
border-radius: 40px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0,20,40,0.1);
}
.vacancy-header {
display: flex;
justify-content: space-between;
align-items: start;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
}
.vacancy-title {
font-size: 36px;
color: #0b1c34;
font-weight: 700;
flex: 1;
}
/* Кнопка QR в шапке */
.qr-button {
width: 50px;
height: 50px;
background: linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(59,130,246,0.3);
color: white;
font-size: 24px;
position: relative;
overflow: hidden;
border: none;
flex-shrink: 0;
}
.qr-button:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(59,130,246,0.4);
}
.qr-button:active {
transform: scale(0.95);
}
.qr-button i {
font-size: 24px;
}
.qr-view-count {
position: absolute;
top: -8px;
right: -8px;
background: #ef4444;
color: white;
font-size: 11px;
font-weight: 600;
padding: 2px 6px;
border-radius: 20px;
min-width: 20px;
text-align: center;
box-shadow: 0 2px 4px rgba(239,68,68,0.3);
}
/* Кликабельная компания */
.vacancy-company-link {
display: inline-flex;
align-items: center;
gap: 12px;
color: #3b82f6;
font-size: 20px;
font-weight: 600;
text-decoration: none;
padding: 8px 16px;
border-radius: 40px;
background: #f0f7ff;
transition: all 0.3s ease;
margin: 15px 0;
border: 2px solid transparent;
}
.vacancy-company-link:hover {
background: #dbeafe;
border-color: #3b82f6;
transform: translateX(5px);
box-shadow: 0 4px 12px rgba(59,130,246,0.2);
}
.vacancy-company-link i {
font-size: 24px;
}
.vacancy-company-link .company-name {
border-bottom: 2px dashed #3b82f6;
}
.vacancy-company-link:hover .company-name {
border-bottom-style: solid;
}
.company-link-hint {
font-size: 14px;
color: #4f7092;
margin-left: 5px;
opacity: 0;
transition: 0.2s;
}
.vacancy-company-link:hover .company-link-hint {
opacity: 1;
}
.vacancy-salary {
font-size: 32px;
font-weight: 700;
color: #0f2b4f;
margin: 25px 0;
padding: 25px 0;
border-top: 2px solid #dee9f5;
border-bottom: 2px solid #dee9f5;
}
.vacancy-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 25px 0;
}
.tag {
background: #eef4fa;
padding: 8px 16px;
border-radius: 30px;
font-size: 14px;
color: #1f3f60;
font-weight: 500;
transition: 0.2s;
}
.tag:hover {
background: #dbeafe;
}
.section-title {
font-size: 24px;
color: #0b1c34;
margin: 30px 0 20px;
font-weight: 600;
}
.vacancy-description {
color: #1f3f60;
line-height: 1.8;
white-space: pre-line;
font-size: 16px;
}
/* Информация о компании */
.company-info {
background: #f9fcff;
border-radius: 30px;
padding: 30px;
margin: 30px 0;
border-left: 4px solid #3b82f6;
}
.company-info-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.company-info-header h3 {
color: #0b1c34;
font-size: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.company-info-header h3 i {
color: #3b82f6;
}
.company-view-all {
color: #3b82f6;
text-decoration: none;
font-weight: 600;
padding: 8px 16px;
border-radius: 30px;
background: white;
transition: 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.company-view-all:hover {
background: #dbeafe;
transform: translateX(5px);
}
.company-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.company-info-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: white;
border-radius: 20px;
transition: 0.2s;
}
.company-info-item:hover {
background: #eef4fa;
transform: translateX(5px);
}
.company-info-item i {
color: #3b82f6;
width: 24px;
font-size: 18px;
}
.company-info-item a {
color: #1f3f60;
text-decoration: none;
font-weight: 500;
}
.company-info-item a:hover {
color: #3b82f6;
text-decoration: underline;
}
.contact-info {
background: #eef4fa;
border-radius: 30px;
padding: 25px;
margin: 30px 0;
}
.contact-item {
display: flex;
align-items: center;
gap: 15px;
padding: 12px 0;
color: #1f3f60;
border-bottom: 1px solid #d9e6f5;
}
.contact-item:last-child {
border-bottom: none;
}
.contact-item i {
color: #3b82f6;
width: 24px;
font-size: 18px;
}
.action-buttons {
display: flex;
gap: 15px;
margin-top: 30px;
flex-wrap: wrap;
}
.btn {
padding: 16px 32px;
border-radius: 40px;
border: none;
font-weight: 600;
cursor: pointer;
flex: 1;
min-width: 200px;
font-size: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
transition: 0.2s;
text-decoration: none;
}
.btn-primary {
background: #0b1c34;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #1b3f6b;
transform: translateY(-2px);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-outline {
background: transparent;
border: 2px solid #3b82f6;
color: #0b1c34;
}
.btn-outline:hover {
background: #eef4fa;
}
.btn-success {
background: #10b981;
color: white;
}
.btn-success:hover {
background: #059669;
}
.applied-badge {
background: #10b981;
color: white;
padding: 16px 32px;
border-radius: 40px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 10px;
}
.stats {
display: flex;
gap: 20px;
color: #4f7092;
font-size: 14px;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #dee9f5;
}
/* Модальное окно для QR */
.qr-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
align-items: flex-start;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(5px);
overflow-y: auto;
padding: 20px 0;
}
.qr-modal.active {
display: flex;
animation: fadeIn 0.3s;
}
.qr-modal-content {
background: white;
border-radius: 40px;
padding: 40px;
max-width: 500px;
width: 90%;
position: relative;
box-shadow: 0 30px 60px rgba(0,0,0,0.3);
animation: slideUp 0.3s;
margin: auto;
max-height: 90vh;
overflow-y: auto;
}
.qr-modal-close {
position: sticky;
top: 0;
right: 0;
margin-left: auto;
margin-bottom: 20px;
width: 40px;
height: 40px;
border-radius: 20px;
background: #eef4fa;
border: none;
font-size: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #4f7092;
transition: 0.2s;
z-index: 10;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.qr-modal-close:hover {
background: #dbeafe;
color: #0b1c34;
transform: scale(1.1);
}
.qr-modal h2 {
color: #0b1c34;
margin-bottom: 10px;
font-size: 24px;
display: flex;
align-items: center;
gap: 10px;
word-break: break-word;
padding-right: 40px;
position: sticky;
top: 0;
background: white;
padding-top: 0;
z-index: 5;
}
.qr-modal h2 i {
color: #3b82f6;
}
.qr-subtitle {
color: #4f7092;
margin-bottom: 25px;
font-size: 16px;
word-break: break-all;
}
.qr-container {
display: flex;
justify-content: center;
margin: 30px 0;
padding: 20px;
background: #f9fcff;
border-radius: 30px;
position: relative;
}
#qrCanvas {
width: 250px;
height: 250px;
image-rendering: crisp-edges;
display: block;
}
.qr-logo-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60px;
height: 60px;
background: white;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
border: 3px solid white;
pointer-events: none;
}
.qr-logo-overlay img {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 12px;
}
.qr-logo-overlay i {
font-size: 30px;
color: #3b82f6;
}
.qr-stats {
display: flex;
justify-content: space-around;
margin: 20px 0;
padding: 15px;
background: #f0f7ff;
border-radius: 20px;
flex-wrap: wrap;
gap: 10px;
}
.qr-stat-item {
text-align: center;
min-width: 80px;
}
.qr-stat-value {
font-size: 20px;
font-weight: 700;
color: #0b1c34;
}
.qr-stat-label {
font-size: 12px;
color: #4f7092;
}
.qr-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin: 20px 0;
}
.qr-action-btn {
padding: 14px;
border-radius: 30px;
border: none;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: 0.2s;
font-size: 14px;
}
.qr-action-btn.primary {
background: #0b1c34;
color: white;
}
.qr-action-btn.primary:hover {
background: #1b3f6b;
}
.qr-action-btn.secondary {
background: #eef4fa;
color: #1f3f60;
}
.qr-action-btn.secondary:hover {
background: #dbeafe;
}
.qr-action-btn i {
font-size: 16px;
}
.qr-share-buttons {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #dee9f5;
flex-wrap: wrap;
}
.qr-share-btn {
width: 44px;
height: 44px;
border-radius: 22px;
border: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
cursor: pointer;
transition: 0.2s;
}
.qr-share-btn.whatsapp {
background: #25D366;
color: white;
}
.qr-share-btn.telegram {
background: #0088cc;
color: white;
}
.qr-share-btn.email {
background: #ea4335;
color: white;
}
.qr-share-btn:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.loading {
text-align: center;
padding: 60px;
color: #4f7092;
font-size: 18px;
}
.error-message {
background: #fee2e2;
color: #b91c1c;
padding: 20px;
border-radius: 30px;
text-align: center;
margin: 40px 0;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 16px 24px;
border-radius: 30px;
background: white;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
z-index: 9999;
animation: slideIn 0.3s;
max-width: 350px;
display: none;
}
.notification.success {
background: #10b981;
color: white;
}
.notification.error {
background: #ef4444;
color: white;
}
.notification.info {
background: #3b82f6;
color: white;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0%); opacity: 1; }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(30px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@media (max-width: 768px) {
.vacancy-title {
font-size: 28px;
}
.vacancy-salary {
font-size: 24px;
}
.qr-modal-content {
padding: 30px 20px;
width: 95%;
}
.qr-modal-close {
position: fixed;
top: 10px;
right: 10px;
background: white;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo" onclick="window.location.href='/'">
<i class="fas fa-briefcase"></i>
Rabota.Today
</div>
<div class="nav" id="nav">
<!-- Навигация будет заполнена динамически -->
</div>
</div>
<a href="/vacancies" class="back-link"><i class="fas fa-arrow-left"></i> Назад к вакансиям</a>
<div id="vacancyDetail" class="vacancy-detail">
<div class="loading">Загрузка вакансии...</div>
</div>
</div>
<!-- Модальное окно для QR-кода -->
<div class="qr-modal" id="qrModal">
<div class="qr-modal-content">
<button class="qr-modal-close" onclick="closeQRModal()">&times;</button>
<h2><i class="fas fa-qrcode"></i> <span id="qrVacancyTitle"></span></h2>
<div class="qr-subtitle" id="qrVacancyUrl">yarmarka.rabota.today/vacancy/</div>
<div class="qr-container" id="qrContainer">
<canvas id="qrCanvas" width="250" height="250"></canvas>
<div class="qr-logo-overlay" id="qrLogoOverlay" style="display: none;">
<img id="qrLogoImg" src="" alt="logo" style="display: none;">
<i id="qrLogoIcon" class="fas fa-briefcase"></i>
</div>
</div>
<div class="qr-stats">
<div class="qr-stat-item">
<div class="qr-stat-value" id="qrViewCount">0</div>
<div class="qr-stat-label">просмотров</div>
</div>
<div class="qr-stat-item">
<div class="qr-stat-value" id="qrSalaryInfo"></div>
<div class="qr-stat-label">зарплата</div>
</div>
<div class="qr-stat-item">
<div class="qr-stat-value" id="qrCompanyName"></div>
<div class="qr-stat-label">компания</div>
</div>
</div>
<div class="qr-actions">
<button class="qr-action-btn primary" onclick="downloadQR()">
<i class="fas fa-download"></i> Скачать PNG
</button>
<button class="qr-action-btn secondary" onclick="copyVacancyLink()">
<i class="fas fa-link"></i> Копировать ссылку
</button>
<button class="qr-action-btn secondary" onclick="shareVacancy()">
<i class="fas fa-share-alt"></i> Поделиться
</button>
<button class="qr-action-btn secondary" onclick="printQR()">
<i class="fas fa-print"></i> Распечатать
</button>
</div>
<div class="qr-share-buttons">
<button class="qr-share-btn whatsapp" onclick="shareQR('whatsapp')">
<i class="fab fa-whatsapp"></i>
</button>
<button class="qr-share-btn telegram" onclick="shareQR('telegram')">
<i class="fab fa-telegram"></i>
</button>
<button class="qr-share-btn email" onclick="shareQR('email')">
<i class="fas fa-envelope"></i>
</button>
</div>
</div>
</div>
<!-- Уведомления -->
<div class="notification" id="notification"></div>
<script>
const API_BASE_URL = window.location.protocol + '//' + window.location.host + '/api';
let currentUser = null;
let currentVacancy = null;
let qrViewCount = 0;
// Получаем ID вакансии из URL
const pathParts = window.location.pathname.split('/');
const vacancyId = pathParts[pathParts.length - 1];
// Функция для декодирования HTML-сущностей
function decodeHtmlEntities(text) {
if (!text) return '';
const textarea = document.createElement('textarea');
textarea.innerHTML = text;
return textarea.value;
}
// Функция для экранирования HTML
function escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe.toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Функция для обновления SEO тегов
function updateSEOTags(vacancy) {
const decodedTitle = decodeHtmlEntities(vacancy.title);
const decodedCompany = decodeHtmlEntities(vacancy.company_name || 'Компания');
const salary = vacancy.salary || 'Зарплата не указана';
const description = vacancy.description || 'Подробная информация о вакансии';
// Формируем описание для 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() {
const token = localStorage.getItem('accessToken');
if (token) {
try {
const response = await fetch(`${API_BASE_URL}/user`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
currentUser = await response.json();
} else {
localStorage.removeItem('accessToken');
}
} catch (error) {
console.error('Error checking auth:', error);
}
}
updateNavigation();
}
// Обновление навигации
function updateNavigation() {
const nav = document.getElementById('nav');
if (currentUser) {
const firstName = currentUser.full_name.split(' ')[0];
const adminBadge = currentUser.is_admin ? '<span class="admin-badge">Admin</span>' : '';
nav.innerHTML = `
<a href="/">Главная</a>
<a href="/vacancies" class="active">Вакансии</a>
<a href="/resumes">Резюме</a>
<a href="/favorites">Избранное</a>
<a href="/applications">Отклики</a>
<a href="/profile" class="profile-link">
<i class="fas fa-user-circle"></i> ${escapeHtml(firstName)} ${adminBadge}
</a>
`;
} else {
nav.innerHTML = `
<a href="/">Главная</a>
<a href="/vacancies" class="active">Вакансии</a>
<a href="/resumes">Резюме</a>
<a href="/login">Войти</a>
<a href="/register">Регистрация</a>
`;
}
}
// Загрузка вакансии
async function loadVacancy() {
const token = localStorage.getItem('accessToken');
try {
const headers = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${API_BASE_URL}/vacancies/${vacancyId}`, {
headers: headers
});
if (!response.ok) {
throw new Error('Вакансия не найдена');
}
currentVacancy = await response.json();
// Обновляем SEO теги
updateSEOTags(currentVacancy);
renderVacancy(currentVacancy);
} catch (error) {
console.error('Error loading vacancy:', error);
document.getElementById('vacancyDetail').innerHTML = `
<div class="error-message">
<i class="fas fa-exclamation-circle"></i>
Вакансия не найдена или была удалена
</div>
<div style="text-align: center; margin-top: 20px;">
<a href="/vacancies" class="btn btn-primary">Вернуться к списку</a>
</div>
`;
}
}
// Отображение вакансии
function renderVacancy(vacancy) {
const container = document.getElementById('vacancyDetail');
const token = localStorage.getItem('accessToken');
const isApplied = vacancy.has_applied;
const canApply = token && currentUser && currentUser.role === 'employee' && !isApplied;
// Получаем ID компании
const companyId = vacancy.company_id || '';
// Декодируем названия
const decodedTitle = decodeHtmlEntities(vacancy.title);
const decodedCompanyName = decodeHtmlEntities(vacancy.company_name || '');
container.innerHTML = `
<div class="vacancy-header">
<h1 class="vacancy-title">${escapeHtml(decodedTitle)}</h1>
<button class="qr-button" onclick="openQRModal()" title="QR-код вакансии">
<i class="fas fa-qrcode"></i>
<span class="qr-view-count" id="qrViewBadge">0</span>
</button>
</div>
<!-- Кликабельная компания -->
${vacancy.company_name ? `
<a href="/company/${companyId}" class="vacancy-company-link" ${!companyId ? 'onclick="event.preventDefault(); showNotification(\'Страница компании в разработке\')"' : ''}>
<i class="fas fa-building"></i>
<span class="company-name">${escapeHtml(decodedCompanyName)}</span>
<span class="company-link-hint">
<i class="fas fa-external-link-alt"></i> перейти
</span>
</a>
` : ''}
<div class="vacancy-salary">${escapeHtml(vacancy.salary || 'Зарплата не указана')}</div>
${vacancy.tags && vacancy.tags.length > 0 ? `
<div class="vacancy-tags">
${vacancy.tags.map(t => `<span class="tag">${escapeHtml(t.name)}</span>`).join('')}
</div>
` : ''}
<h2 class="section-title">Описание вакансии</h2>
<div class="vacancy-description">${escapeHtml(vacancy.description || 'Описание отсутствует').replace(/\n/g, '<br>')}</div>
<!-- Информация о компании -->
${vacancy.company_name ? `
<div class="company-info">
<div class="company-info-header">
<h3>
<i class="fas fa-building"></i> О компании
</h3>
${companyId ? `
<a href="/company/${companyId}" class="company-view-all">
<i class="fas fa-external-link-alt"></i> Все вакансии компании
</a>
` : ''}
</div>
<div class="company-info-grid">
<div class="company-info-item">
<i class="fas fa-building"></i>
<div>
<div style="font-weight: 600;">${escapeHtml(decodedCompanyName)}</div>
${companyId ? `
<a href="/company/${companyId}" style="font-size: 13px; color: #3b82f6;">
перейти в профиль компании →
</a>
` : ''}
</div>
</div>
${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>
` : ''}
${vacancy.company_address ? `
<div class="company-info-item">
<i class="fas fa-map-marker-alt"></i>
<span>${escapeHtml(vacancy.company_address)}</span>
</div>
` : ''}
</div>
${vacancy.company_description ? `
<div style="margin-top: 20px; color: #1f3f60; line-height: 1.6; padding: 15px; background: white; border-radius: 15px;">
<i class="fas fa-quote-left" style="color: #3b82f6; opacity: 0.5;"></i>
${escapeHtml(vacancy.company_description.substring(0, 200))}...
${companyId ? `<a href="/company/${companyId}" style="color: #3b82f6; margin-left: 5px;">читать полностью</a>` : ''}
</div>
` : ''}
</div>
` : ''}
<h2 class="section-title">Контактная информация</h2>
<div class="contact-info">
<div class="contact-item">
<i class="fab fa-telegram"></i>
<span>${escapeHtml(vacancy.contact || vacancy.user_telegram || 'Контакт не указан')}</span>
</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 class="action-buttons">
${isApplied ? `
<div class="applied-badge">
<i class="fas fa-check-circle"></i> Вы уже откликнулись
</div>
` : canApply ? `
<button class="btn btn-primary" onclick="applyForVacancy()">
<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 class="stats">
<span><i class="fas fa-eye"></i> ${vacancy.views || 0} просмотров</span>
<span><i class="fas fa-calendar"></i> ${new Date(vacancy.created_at).toLocaleDateString()}</span>
</div>
`;
}
// ========== ФУНКЦИИ ДЛЯ QR-КОДА ==========
// Открыть модальное окно с QR
function openQRModal() {
if (!currentVacancy) return;
const decodedTitle = decodeHtmlEntities(currentVacancy.title);
const decodedCompanyName = decodeHtmlEntities(currentVacancy.company_name || '');
document.getElementById('qrVacancyTitle').textContent = decodedTitle;
const vacancyUrl = window.location.origin + '/vacancy/' + vacancyId;
document.getElementById('qrVacancyUrl').textContent = vacancyUrl.replace('https://', '').replace('http://', '');
document.getElementById('qrViewCount').textContent = ++qrViewCount;
document.getElementById('qrViewBadge').textContent = qrViewCount;
document.getElementById('qrSalaryInfo').textContent = currentVacancy.salary || 'з/п не указана';
document.getElementById('qrCompanyName').textContent = decodedCompanyName || '—';
generateQRCodeWithLogo(vacancyUrl, currentVacancy.company_logo);
document.getElementById('qrModal').classList.add('active');
}
function closeQRModal() {
document.getElementById('qrModal').classList.remove('active');
}
function generateQRCodeWithLogo(text, logoUrl) {
const canvas = document.getElementById('qrCanvas');
const logoOverlay = document.getElementById('qrLogoOverlay');
const logoImg = document.getElementById('qrLogoImg');
const logoIcon = document.getElementById('qrLogoIcon');
const options = {
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.style.display = 'block';
logoIcon.style.display = 'none';
logoImg.onerror = function() {
logoImg.style.display = 'none';
logoIcon.style.display = 'block';
logoIcon.className = 'fas fa-briefcase';
};
} else {
logoImg.style.display = 'none';
logoIcon.style.display = 'block';
logoIcon.className = 'fas fa-briefcase';
}
}
});
}
function downloadQR() {
const canvas = document.getElementById('qrCanvas');
const combinedCanvas = document.createElement('canvas');
combinedCanvas.width = canvas.width;
combinedCanvas.height = canvas.height;
const ctx = combinedCanvas.getContext('2d');
ctx.drawImage(canvas, 0, 0);
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(125, 125, 35, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = '#3b82f6';
ctx.font = '30px "Font Awesome 6 Free"';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('', 125, 125);
const link = document.createElement('a');
const decodedTitle = decodeHtmlEntities(currentVacancy.title);
const filename = `vacancy_${decodedTitle.toLowerCase().replace(/[^a-zа-я0-9]/g, '_')}.png`;
link.download = filename;
link.href = combinedCanvas.toDataURL('image/png');
link.click();
showNotification('QR-код скачан', 'success');
}
function copyVacancyLink() {
const url = window.location.origin + '/vacancy/' + vacancyId;
navigator.clipboard.writeText(url).then(() => {
showNotification('Ссылка скопирована', 'success');
}).catch(() => {
showNotification('Ошибка копирования', 'error');
});
}
function shareVacancy() {
const url = window.location.href;
if (navigator.share) {
navigator.share({ title: document.title, url: url }).catch(() => copyVacancyLink());
} else {
copyVacancyLink();
}
}
function printQR() {
const canvas = document.getElementById('qrCanvas');
const decodedTitle = decodeHtmlEntities(currentVacancy.title);
const decodedCompany = decodeHtmlEntities(currentVacancy.company_name || '');
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<html>
<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; 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>
<h3>${escapeHtml(decodedCompany)}</h3>
<img src="${canvas.toDataURL()}" />
<div class="url">${window.location.origin}/vacancy/${vacancyId}</div>
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
printWindow.print();
}
function shareQR(platform) {
const url = window.location.origin + '/vacancy/' + vacancyId;
const decodedTitle = decodeHtmlEntities(currentVacancy.title);
const text = `Вакансия: ${decodedTitle} на Rabota.Today`;
let shareUrl = '';
switch(platform) {
case 'whatsapp': shareUrl = `https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`; break;
case 'telegram': shareUrl = `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`; break;
case 'email': shareUrl = `mailto:?subject=${encodeURIComponent('Вакансия ' + decodedTitle)}&body=${encodeURIComponent(text + '\n\n' + url)}`; break;
}
if (shareUrl) window.open(shareUrl, '_blank');
}
async function applyForVacancy() {
const token = localStorage.getItem('accessToken');
if (!token) { redirectToLogin(); return; }
try {
const response = await fetch(`${API_BASE_URL}/vacancies/${vacancyId}/apply`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
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() {
const token = localStorage.getItem('accessToken');
if (!token) {
if (confirm('Для добавления в избранное нужно войти в систему. Перейти на страницу входа?')) {
window.location.href = '/login';
}
return;
}
showNotification('Вакансия добавлена в избранное', 'success');
}
function redirectToLogin() {
if (confirm('Для отклика нужно войти в систему. Перейти на страницу входа?')) {
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) {
const modal = document.getElementById('qrModal');
if (event.target === modal) modal.classList.remove('active');
};
checkAuth().then(() => loadVacancy());
</script>
</body>
</html>