Files
yarmarka/templates/vacancy_detail.html
2026-03-16 23:55:04 +03:00

1644 lines
53 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">
<title>Вакансия | Rabota.Today</title>
<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.6);
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(5px);
}
.qr-modal.active {
display: flex;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.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;
}
@keyframes slideUp {
from { transform: translateY(30px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.qr-modal-close {
position: absolute;
top: 20px;
right: 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;
}
.qr-modal-close:hover {
background: #dbeafe;
color: #0b1c34;
}
.qr-modal h2 {
color: #0b1c34;
margin-bottom: 10px;
font-size: 28px;
display: flex;
align-items: center;
gap: 10px;
word-break: break-word;
}
.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;
}
.qr-stat-item {
text-align: center;
}
.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: 25px 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;
}
.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; }
}
/* Улучшенные стили для модального окна с прокруткой */
.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; /* Изменено с center на 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; /* Меняем с absolute на 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);
}
/* Фиксированная кнопка закрытия для мобильных */
@media (max-width: 768px) {
.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);
}
}
/* Заголовок с фиксацией */
.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 с центрированием */
.qr-container {
display: flex;
justify-content: center;
margin: 20px 0;
padding: 20px;
background: #f9fcff;
border-radius: 30px;
position: relative;
}
/* Статистика */
.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);
}
/* Анимации */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(30px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
</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;");
}
// Проверка авторизации
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();
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>
`;
}
}
// Отображение вакансии с QR-кнопкой
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 || '—';
// Генерируем QR-код
generateQRCodeWithLogo(vacancyUrl, currentVacancy.company_logo);
document.getElementById('qrModal').classList.add('active');
}
// Закрыть модальное окно
function closeQRModal() {
document.getElementById('qrModal').classList.remove('active');
}
// Генерация QR-кода с логотипом компании
function generateQRCodeWithLogo(text, logoUrl) {
const canvas = document.getElementById('qrCanvas');
const logoOverlay = document.getElementById('qrLogoOverlay');
const logoImg = document.getElementById('qrLogoImg');
const logoIcon = document.getElementById('qrLogoIcon');
// Настройки QR-кода
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 {
console.log('QR code generated successfully');
// Показываем оверлей с логотипом или иконкой
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';
}
}
});
}
// Скачать QR-код
function downloadQR() {
const canvas = document.getElementById('qrCanvas');
const logoOverlay = document.getElementById('qrLogoOverlay');
const logoImg = document.getElementById('qrLogoImg');
const logoIcon = document.getElementById('qrLogoIcon');
// Создаем временный canvas для объединения QR и логотипа
const combinedCanvas = document.createElement('canvas');
combinedCanvas.width = canvas.width;
combinedCanvas.height = canvas.height;
const ctx = combinedCanvas.getContext('2d');
// Рисуем QR-код
ctx.drawImage(canvas, 0, 0);
// Рисуем логотип поверх
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(125, 125, 35, 0, 2 * Math.PI);
ctx.fill();
if (logoImg.style.display === 'block' && logoImg.complete) {
// Если есть загруженный логотип
ctx.save();
ctx.beginPath();
ctx.arc(125, 125, 30, 0, 2 * Math.PI);
ctx.clip();
const logoSize = 50;
ctx.drawImage(logoImg, 125 - logoSize/2, 125 - logoSize/2, logoSize, logoSize);
ctx.restore();
} else {
// Если нет логотипа, рисуем иконку
ctx.fillStyle = '#3b82f6';
ctx.font = '30px "Font Awesome 6 Free"';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('', 125, 125); // Unicode для иконки briefcase
}
// Скачиваем
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();
}
}
// Распечатать QR
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; }
.company { margin-bottom: 20px; color: #1f3f60; }
</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();
}
// Поделиться QR в соцсетях
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>