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

1595 lines
51 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;
}
.resume-detail {
background: white;
border-radius: 40px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0,20,40,0.1);
}
.resume-header {
display: flex;
align-items: center;
gap: 30px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.resume-avatar {
width: 120px;
height: 120px;
background: #eef4fa;
border-radius: 60px;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: #3b82f6;
flex-shrink: 0;
overflow: hidden;
}
.resume-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.resume-title {
flex: 1;
}
.resume-title-header {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.resume-name {
font-size: 32px;
font-weight: 700;
color: #0b1c34;
}
/* 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;
margin-left: auto;
}
.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);
}
.resume-position {
font-size: 20px;
color: #3b82f6;
margin-bottom: 10px;
}
.resume-stats {
display: flex;
gap: 20px;
color: #4f7092;
font-size: 14px;
}
.resume-salary {
font-size: 28px;
font-weight: 700;
color: #0f2b4f;
margin: 20px 0;
padding: 20px 0;
border-top: 2px solid #dee9f5;
border-bottom: 2px solid #dee9f5;
}
.resume-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 20px 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: 40px 0 20px;
font-weight: 600;
border-bottom: 2px solid #dee9f5;
padding-bottom: 10px;
}
.about-me {
background: #f9fcff;
border-radius: 20px;
padding: 25px;
line-height: 1.8;
color: #1f3f60;
font-size: 16px;
}
.experience-item {
background: #f9fcff;
border-radius: 20px;
padding: 25px;
margin-bottom: 20px;
border-left: 4px solid #3b82f6;
}
.experience-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
flex-wrap: wrap;
gap: 10px;
}
.experience-title {
font-size: 20px;
font-weight: 700;
color: #0b1c34;
}
.experience-company {
font-size: 18px;
color: #3b82f6;
font-weight: 600;
margin-bottom: 5px;
}
.experience-period {
color: #4f7092;
font-size: 14px;
display: flex;
align-items: center;
gap: 5px;
}
.experience-description {
margin-top: 15px;
padding: 15px;
background: white;
border-radius: 15px;
line-height: 1.6;
color: #1f3f60;
white-space: pre-line;
border-left: 3px solid #10b981;
}
.education-item {
background: #f9fcff;
border-radius: 20px;
padding: 20px;
margin-bottom: 15px;
}
.education-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
flex-wrap: wrap;
}
.education-institution {
font-size: 18px;
font-weight: 700;
color: #0b1c34;
}
.education-specialty {
color: #3b82f6;
font-size: 16px;
margin-bottom: 5px;
}
.education-year {
color: #4f7092;
font-size: 14px;
}
.contact-section {
background: #eef4fa;
border-radius: 30px;
padding: 30px;
margin: 40px 0 20px;
}
.contact-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.contact-item {
display: flex;
align-items: center;
gap: 15px;
padding: 12px;
background: white;
border-radius: 15px;
transition: 0.2s;
}
.contact-item:hover {
background: #f9fcff;
transform: translateX(5px);
}
.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 {
background: #1b3f6b;
transform: translateY(-2px);
}
.btn-outline {
background: transparent;
border: 2px solid #3b82f6;
color: #0b1c34;
}
.btn-outline:hover {
background: #eef4fa;
}
/* Модальное окно для 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;
}
.view-counter {
display: inline-flex;
align-items: center;
gap: 5px;
background: #eef4fa;
padding: 5px 15px;
border-radius: 30px;
font-size: 14px;
color: #1f3f60;
}
.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; }
}
@media (max-width: 768px) {
.resume-header {
flex-direction: column;
text-align: center;
}
.resume-title-header {
justify-content: center;
}
.qr-button {
margin: 0 auto;
}
}
/* Улучшенные стили для модального окна с прокруткой */
.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="/resumes" class="back-link"><i class="fas fa-arrow-left"></i> Назад к резюме</a>
<div id="resumeDetail" class="resume-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="qrResumeName"></span></h2>
<div class="qr-subtitle" id="qrResumeUrl">yarmarka.rabota.today/resume/</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="avatar" style="display: none;">
<i id="qrLogoIcon" class="fas fa-user"></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="qrExperienceCount">0</div>
<div class="qr-stat-label">мест работы</div>
</div>
<div class="qr-stat-item">
<div class="qr-stat-value" id="qrPosition"></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="copyResumeLink()">
<i class="fas fa-link"></i> Копировать ссылку
</button>
<button class="qr-action-btn secondary" onclick="saveToContacts()">
<i class="fas fa-address-card"></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 currentResume = null;
let qrViewCount = 0;
// Получаем ID резюме из URL
const pathParts = window.location.pathname.split('/');
const resumeId = 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">Вакансии</a>
<a href="/resumes" class="active">Резюме</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">Вакансии</a>
<a href="/resumes" class="active">Резюме</a>
<a href="/login">Войти</a>
<a href="/register">Регистрация</a>
`;
}
}
// Загрузка резюме
async function loadResume() {
try {
const response = await fetch(`${API_BASE_URL}/resumes/${resumeId}`);
if (!response.ok) {
throw new Error('Резюме не найдено');
}
currentResume = await response.json();
renderResume(currentResume);
} catch (error) {
console.error('Error loading resume:', error);
document.getElementById('resumeDetail').innerHTML = `
<div class="error-message">
<i class="fas fa-exclamation-circle"></i>
Резюме не найдено или было удалено
</div>
<div style="text-align: center; margin-top: 20px;">
<a href="/resumes" class="btn btn-primary">Вернуться к списку</a>
</div>
`;
}
}
// Отображение резюме с QR-кнопкой
function renderResume(resume) {
const container = document.getElementById('resumeDetail');
const token = localStorage.getItem('accessToken');
const canContact = token && currentUser && currentUser.role === 'employer';
// Декодируем имя
const decodedName = decodeHtmlEntities(resume.full_name || '');
const decodedPosition = decodeHtmlEntities(resume.desired_position || '');
// Формируем блок с опытом работы
const experienceHtml = resume.work_experience && resume.work_experience.length > 0
? resume.work_experience.map(exp => `
<div class="experience-item">
<div class="experience-header">
<span class="experience-title">${escapeHtml(exp.position)}</span>
<span class="experience-period">
<i class="far fa-calendar"></i> ${escapeHtml(exp.period || 'Период не указан')}
</span>
</div>
<div class="experience-company">
<i class="fas fa-building"></i> ${escapeHtml(exp.company)}
</div>
${exp.description ? `
<div class="experience-description">
<strong>📋 Обязанности и достижения:</strong>
<p style="margin-top: 10px;">${escapeHtml(exp.description).replace(/\n/g, '<br>')}</p>
</div>
` : ''}
</div>
`).join('')
: '<p style="color: #4f7092; text-align: center; padding: 20px;">Опыт работы не указан</p>';
// Формируем блок с образованием
const educationHtml = resume.education && resume.education.length > 0
? resume.education.map(edu => `
<div class="education-item">
<div class="education-header">
<span class="education-institution">${escapeHtml(edu.institution)}</span>
<span class="education-year">${escapeHtml(edu.graduation_year || 'Год не указан')}</span>
</div>
<div class="education-specialty">${escapeHtml(edu.specialty || 'Специальность не указана')}</div>
</div>
`).join('')
: '<p style="color: #4f7092; text-align: center; padding: 20px;">Образование не указано</p>';
container.innerHTML = `
<div class="resume-header">
<div class="resume-avatar">
<i class="fas fa-user"></i>
</div>
<div class="resume-title">
<div class="resume-title-header">
<div class="resume-name">${escapeHtml(decodedName)}</div>
<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>
<div class="resume-position">${escapeHtml(decodedPosition)}</div>
<div class="resume-stats">
<span class="view-counter">
<i class="fas fa-eye"></i> ${resume.views || 0} просмотров
</span>
<span><i class="fas fa-calendar"></i> Обновлено: ${new Date(resume.updated_at).toLocaleDateString()}</span>
</div>
</div>
</div>
<div class="resume-salary">${escapeHtml(resume.desired_salary || 'Зарплатные ожидания не указаны')}</div>
${resume.tags && resume.tags.length > 0 ? `
<div class="resume-tags">
${resume.tags.map(t => `<span class="tag">${escapeHtml(t.name)}</span>`).join('')}
</div>
` : ''}
<h2 class="section-title">О себе</h2>
<div class="about-me">${escapeHtml(resume.about_me || 'Информация не заполнена').replace(/\n/g, '<br>')}</div>
<h2 class="section-title">Опыт работы</h2>
${experienceHtml}
<h2 class="section-title">Образование</h2>
${educationHtml}
<div class="contact-section">
<h3 style="color: #0b1c34; margin-bottom: 20px;">Контактная информация</h3>
${canContact ? `
<div class="contact-grid">
<div class="contact-item">
<i class="fas fa-envelope"></i>
<span>${escapeHtml(resume.email || 'Email не указан')}</span>
</div>
<div class="contact-item">
<i class="fas fa-phone"></i>
<span>${escapeHtml(resume.phone || 'Телефон не указан')}</span>
</div>
<div class="contact-item">
<i class="fab fa-telegram"></i>
<span>${escapeHtml(resume.telegram || 'Telegram не указан')}</span>
</div>
</div>
` : `
<div style="text-align: center; padding: 30px; background: white; border-radius: 20px;">
<i class="fas fa-lock" style="font-size: 48px; color: #4f7092; margin-bottom: 15px;"></i>
<p style="color: #1f3f60; margin-bottom: 20px;">
Контактные данные доступны только авторизованным работодателям
</p>
${!token ?
'<a href="/login" class="btn btn-primary">Войти как работодатель</a>' :
'<p class="btn btn-primary" style="opacity: 0.6;">Только для работодателей</p>'
}
</div>
`}
</div>
<div class="action-buttons">
${canContact ? `
<button class="btn btn-primary" onclick="contactCandidate()">
<i class="fas fa-paper-plane"></i> Связаться
</button>
` : ''}
<button class="btn btn-outline" onclick="saveToFavorites()">
<i class="fas fa-heart"></i> В избранное
</button>
<button class="btn btn-outline" onclick="shareResume()">
<i class="fas fa-share-alt"></i> Поделиться
</button>
</div>
`;
}
// ========== ФУНКЦИИ ДЛЯ QR-КОДА ==========
// Открыть модальное окно с QR
function openQRModal() {
if (!currentResume) return;
// Декодируем имя
const decodedName = decodeHtmlEntities(currentResume.full_name || '');
const decodedPosition = decodeHtmlEntities(currentResume.desired_position || '');
document.getElementById('qrResumeName').textContent = decodedName;
const resumeUrl = window.location.origin + '/resume/' + resumeId;
document.getElementById('qrResumeUrl').textContent = resumeUrl.replace('https://', '').replace('http://', '');
// Обновляем статистику
document.getElementById('qrViewCount').textContent = ++qrViewCount;
document.getElementById('qrViewBadge').textContent = qrViewCount;
// Количество мест работы
const expCount = currentResume.work_experience ? currentResume.work_experience.length : 0;
document.getElementById('qrExperienceCount').textContent = expCount;
// Желаемая должность
document.getElementById('qrPosition').textContent = decodedPosition || '—';
// Генерируем QR-код
generateQRCodeWithLogo(resumeUrl);
document.getElementById('qrModal').classList.add('active');
}
// Закрыть модальное окно
function closeQRModal() {
document.getElementById('qrModal').classList.remove('active');
}
// Генерация QR-кода с иконкой пользователя
function generateQRCodeWithLogo(text) {
const canvas = document.getElementById('qrCanvas');
const logoOverlay = document.getElementById('qrLogoOverlay');
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';
logoIcon.style.display = 'block';
logoIcon.className = 'fas fa-user';
}
});
}
// Скачать QR-код
function downloadQR() {
const canvas = document.getElementById('qrCanvas');
// Создаем временный 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();
ctx.fillStyle = '#3b82f6';
ctx.font = '30px "Font Awesome 6 Free"';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('', 125, 125); // Unicode для иконки user
// Скачиваем
const link = document.createElement('a');
const decodedName = decodeHtmlEntities(currentResume.full_name || '');
const filename = `resume_${decodedName.toLowerCase().replace(/[^a-zа-я0-9]/g, '_')}.png`;
link.download = filename;
link.href = combinedCanvas.toDataURL('image/png');
link.click();
showNotification('QR-код скачан', 'success');
}
// Копировать ссылку на резюме
function copyResumeLink() {
const url = window.location.origin + '/resume/' + resumeId;
navigator.clipboard.writeText(url).then(() => {
showNotification('Ссылка скопирована', 'success');
}).catch(() => {
showNotification('Ошибка копирования', 'error');
});
}
// Сохранить в контакты (vCard)
function saveToContacts() {
if (!currentResume) return;
const decodedName = decodeHtmlEntities(currentResume.full_name || '');
const decodedPosition = decodeHtmlEntities(currentResume.desired_position || '');
// Создаем vCard
const vCard = `BEGIN:VCARD
VERSION:3.0
FN:${decodedName}
TITLE:${decodedPosition}
${currentResume.email ? `EMAIL:${currentResume.email}` : ''}
${currentResume.phone ? `TEL:${currentResume.phone}` : ''}
${currentResume.telegram ? `X-SOCIALPROFILE;TYPE=telegram:https://t.me/${currentResume.telegram.replace('@', '')}` : ''}
NOTE:Кандидат на позицию ${decodedPosition}. Подробнее: ${window.location.origin}/resume/${resumeId}
END:VCARD`;
const blob = new Blob([vCard], { type: 'text/vcard' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${decodedName}.vcf`;
link.click();
URL.revokeObjectURL(url);
showNotification('Контакт сохранен', 'success');
}
// Распечатать QR
function printQR() {
const canvas = document.getElementById('qrCanvas');
const decodedName = decodeHtmlEntities(currentResume.full_name || '');
const decodedPosition = decodeHtmlEntities(currentResume.desired_position || '');
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<html>
<head>
<title>QR-код резюме ${decodedName}</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; }
.position { margin-bottom: 20px; color: #1f3f60; }
</style>
</head>
<body>
<h2>${escapeHtml(decodedName)}</h2>
<h3>${escapeHtml(decodedPosition)}</h3>
<img src="${canvas.toDataURL()}" />
<div class="url">${window.location.origin}/resume/${resumeId}</div>
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
printWindow.print();
}
// Поделиться QR в соцсетях
function shareQR(platform) {
const url = window.location.origin + '/resume/' + resumeId;
const decodedName = decodeHtmlEntities(currentResume.full_name || '');
const text = `Резюме ${decodedName} на 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('Резюме ' + decodedName)}&body=${encodeURIComponent(text + '\n\n' + url)}`;
break;
}
if (shareUrl) {
window.open(shareUrl, '_blank');
}
}
// Связаться с кандидатом
function contactCandidate() {
const token = localStorage.getItem('accessToken');
if (!token) {
redirectToLogin();
return;
}
showNotification('Функция связи будет доступна в ближайшее время', 'info');
}
// Добавить в избранное
function saveToFavorites() {
const token = localStorage.getItem('accessToken');
if (!token) {
if (confirm('Для добавления в избранное нужно войти в систему. Перейти на страницу входа?')) {
window.location.href = '/login';
}
return;
}
showNotification('Резюме добавлено в избранное', 'success');
}
// Поделиться резюме
function shareResume() {
const url = window.location.href;
if (navigator.share) {
navigator.share({
title: document.title,
url: url
}).catch(() => {
copyResumeLink();
});
} else {
copyResumeLink();
}
}
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(() => {
loadResume();
});
</script>
</body>
</html>