resume detail SEO

This commit is contained in:
2026-03-20 19:56:10 +03:00
parent a5ee736987
commit 99b661000f

View File

@@ -4,10 +4,10 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Базовые SEO теги --> <!-- SEO теги будут заменены сервером -->
<title id="pageTitle">Резюме | Rabota.Today</title> <title id="pageTitle">Резюме | Rabota.Today</title>
<meta name="description" id="metaDescription" content="Подробная информация о резюме кандидата на Rabota.Today. Опыт работы, образование, навыки, контактная информация."> <meta name="description" id="metaDescription" content="Подробная информация о резюме кандидата на Rabota.Today. Опыт работы, образование, навыки, контактная информация.">
<meta name="keywords" content="резюме, поиск сотрудников, кандидат, Rabota.Today, подбор персонала"> <meta name="keywords" id="metaKeywords" content="резюме, поиск сотрудников, кандидат, Rabota.Today, подбор персонала">
<meta name="author" content="Rabota.Today"> <meta name="author" content="Rabota.Today">
<meta name="robots" content="index, follow"> <meta name="robots" content="index, follow">
@@ -35,27 +35,21 @@
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">
<meta name="theme-color" content="#0b1c34"> <meta name="theme-color" content="#0b1c34">
<!-- Структурированные данные (JSON-LD) для резюме --> <!-- Структурированные данные (JSON-LD) будут заменены сервером -->
<script type="application/ld+json" id="structuredData"> <script type="application/ld+json">
{ {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "Person", "@type": "Person",
"name": "", "name": "",
"jobTitle": "", "jobTitle": "",
"description": "", "description": ""
"worksFor": [],
"alumniOf": [],
"knowsAbout": [],
"url": ""
} }
</script> </script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<!-- Подключаем библиотеку для генерации QR-кодов -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.1/build/qrcode.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.1/build/qrcode.min.js"></script>
<style> <style>
/* ... все существующие стили ... */
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -213,7 +207,6 @@
color: #0b1c34; color: #0b1c34;
} }
/* QR-кнопка */
.qr-button { .qr-button {
width: 50px; width: 50px;
height: 50px; height: 50px;
@@ -482,7 +475,7 @@
background: #eef4fa; background: #eef4fa;
} }
/* Модальное окно для QR */ /* QR Modal Styles */
.qr-modal { .qr-modal {
display: none; display: none;
position: fixed; position: fixed;
@@ -788,6 +781,25 @@
color: white; color: white;
} }
.fallback-link {
padding: 20px;
background: white;
border-radius: 15px;
margin: 10px;
word-break: break-all;
text-align: center;
}
.fallback-link a {
color: #3b82f6;
text-decoration: none;
font-size: 14px;
}
.fallback-link a:hover {
text-decoration: underline;
}
@keyframes slideIn { @keyframes slideIn {
from { transform: translateX(100%); opacity: 0; } from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0%); opacity: 1; } to { transform: translateX(0%); opacity: 1; }
@@ -919,11 +931,9 @@
let currentResume = null; let currentResume = null;
let qrViewCount = 0; let qrViewCount = 0;
// Получаем ID резюме из URL
const pathParts = window.location.pathname.split('/'); const pathParts = window.location.pathname.split('/');
const resumeId = pathParts[pathParts.length - 1]; const resumeId = pathParts[pathParts.length - 1];
// Функция для декодирования HTML-сущностей
function decodeHtmlEntities(text) { function decodeHtmlEntities(text) {
if (!text) return ''; if (!text) return '';
const textarea = document.createElement('textarea'); const textarea = document.createElement('textarea');
@@ -931,7 +941,6 @@
return textarea.value; return textarea.value;
} }
// Функция для экранирования HTML
function escapeHtml(unsafe) { function escapeHtml(unsafe) {
if (!unsafe) return ''; if (!unsafe) return '';
return unsafe.toString() return unsafe.toString()
@@ -942,84 +951,22 @@
.replace(/'/g, "&#039;"); .replace(/'/g, "&#039;");
} }
// Функция для обновления SEO тегов function showNotification(message, type = 'info') {
function updateSEOTags(resume) { const notification = document.getElementById('notification');
const decodedName = decodeHtmlEntities(resume.full_name || ''); if (!notification) return;
const nameParts = decodedName.split(' '); notification.className = `notification ${type}`;
const firstName = nameParts[0] || ''; notification.innerHTML = message;
const lastName = nameParts.slice(1).join(' ') || ''; notification.style.display = 'block';
const decodedPosition = decodeHtmlEntities(resume.desired_position || 'Специалист'); setTimeout(() => { notification.style.display = 'none'; }, 3000);
const salary = resume.desired_salary || 'Зарплата не указана';
const about = resume.about_me || 'Профессиональный опыт и навыки';
const shortAbout = about.length > 160 ? about.substring(0, 157) + '...' : about;
const experienceCount = resume.work_experience ? resume.work_experience.length : 0;
const skillsList = resume.tags ? resume.tags.map(t => t.name).join(', ') : '';
const seoDescription = `${decodedName} - ${decodedPosition}. ${salary}. Опыт работы: ${experienceCount} мест. Навыки: ${skillsList}. ${shortAbout}`;
document.title = `${decodedName} - ${decodedPosition} | Rabota.Today`;
document.querySelector('meta[name="description"]')?.setAttribute('content', seoDescription.substring(0, 320));
document.querySelector('meta[name="keywords"]')?.setAttribute('content', `${decodedName}, ${decodedPosition}, резюме, поиск сотрудников, навыки: ${skillsList}`);
document.querySelector('meta[property="og:title"]')?.setAttribute('content', `${decodedName} - ${decodedPosition}`);
document.querySelector('meta[property="og:description"]')?.setAttribute('content', seoDescription.substring(0, 300));
document.querySelector('meta[property="og:url"]')?.setAttribute('content', window.location.href);
document.querySelector('meta[property="profile:first_name"]')?.setAttribute('content', firstName);
document.querySelector('meta[property="profile:last_name"]')?.setAttribute('content', lastName);
document.querySelector('meta[name="twitter:title"]')?.setAttribute('content', `${decodedName} - ${decodedPosition}`);
document.querySelector('meta[name="twitter:description"]')?.setAttribute('content', seoDescription.substring(0, 300));
document.querySelector('link[rel="canonical"]')?.setAttribute('href', window.location.href);
const workExperience = (resume.work_experience || []).map(exp => ({
"@type": "OrganizationRole",
"roleName": exp.position,
"startDate": exp.period ? exp.period.split('')[0] : null,
"endDate": exp.period ? exp.period.split('')[1] : null,
"organization": { "@type": "Organization", "name": exp.company }
}));
const education = (resume.education || []).map(edu => ({
"@type": "EducationalOccupationalCredential",
"credentialCategory": "Degree",
"name": edu.specialty,
"educationalLevel": edu.institution,
"dateCreated": edu.graduation_year
}));
const structuredData = {
"@context": "https://schema.org",
"@type": "Person",
"name": decodedName,
"jobTitle": decodedPosition,
"description": resume.about_me || "",
"worksFor": workExperience,
"alumniOf": education,
"knowsAbout": resume.tags ? resume.tags.map(t => t.name) : [],
"url": window.location.href
};
const scriptElement = document.getElementById('structuredData');
if (scriptElement) {
scriptElement.textContent = JSON.stringify(structuredData, null, 2);
} }
console.log('✅ SEO теги для резюме обновлены');
}
// Проверка авторизации
async function checkAuth() { async function checkAuth() {
const token = localStorage.getItem('accessToken'); const token = localStorage.getItem('accessToken');
if (token) { if (token) {
try { try {
const response = await fetch(`${API_BASE_URL}/user`, { const response = await fetch(`${API_BASE_URL}/user`, {
headers: { 'Authorization': `Bearer ${token}` } headers: { 'Authorization': `Bearer ${token}` }
}); });
if (response.ok) { if (response.ok) {
currentUser = await response.json(); currentUser = await response.json();
} else { } else {
@@ -1029,17 +976,14 @@
console.error('Error checking auth:', error); console.error('Error checking auth:', error);
} }
} }
updateNavigation(); updateNavigation();
} }
function updateNavigation() { function updateNavigation() {
const nav = document.getElementById('nav'); const nav = document.getElementById('nav');
if (currentUser) { if (currentUser) {
const firstName = currentUser.full_name.split(' ')[0]; const firstName = currentUser.full_name.split(' ')[0];
const adminBadge = currentUser.is_admin ? '<span class="admin-badge">Admin</span>' : ''; const adminBadge = currentUser.is_admin ? '<span class="admin-badge">Admin</span>' : '';
nav.innerHTML = ` nav.innerHTML = `
<a href="/">Главная</a> <a href="/">Главная</a>
<a href="/vacancies">Вакансии</a> <a href="/vacancies">Вакансии</a>
@@ -1061,22 +1005,12 @@
} }
} }
// Загрузка резюме
async function loadResume() { async function loadResume() {
try { try {
const response = await fetch(`${API_BASE_URL}/resumes/${resumeId}`); const response = await fetch(`${API_BASE_URL}/resumes/${resumeId}`);
if (!response.ok) throw new Error('Резюме не найдено');
if (!response.ok) {
throw new Error('Резюме не найдено');
}
currentResume = await response.json(); currentResume = await response.json();
// Обновляем SEO теги
updateSEOTags(currentResume);
renderResume(currentResume); renderResume(currentResume);
} catch (error) { } catch (error) {
console.error('Error loading resume:', error); console.error('Error loading resume:', error);
document.getElementById('resumeDetail').innerHTML = ` document.getElementById('resumeDetail').innerHTML = `
@@ -1091,41 +1025,26 @@
} }
} }
// Отображение резюме
function renderResume(resume) { function renderResume(resume) {
const container = document.getElementById('resumeDetail'); const container = document.getElementById('resumeDetail');
const token = localStorage.getItem('accessToken'); const token = localStorage.getItem('accessToken');
const canContact = token && currentUser && currentUser.role === 'employer'; const canContact = token && currentUser && currentUser.role === 'employer';
// Декодируем имя
const decodedName = decodeHtmlEntities(resume.full_name || ''); const decodedName = decodeHtmlEntities(resume.full_name || '');
const decodedPosition = decodeHtmlEntities(resume.desired_position || ''); const decodedPosition = decodeHtmlEntities(resume.desired_position || '');
// Формируем блок с опытом работы
const experienceHtml = resume.work_experience && resume.work_experience.length > 0 const experienceHtml = resume.work_experience && resume.work_experience.length > 0
? resume.work_experience.map(exp => ` ? resume.work_experience.map(exp => `
<div class="experience-item"> <div class="experience-item">
<div class="experience-header"> <div class="experience-header">
<span class="experience-title">${escapeHtml(exp.position)}</span> <span class="experience-title">${escapeHtml(exp.position)}</span>
<span class="experience-period"> <span class="experience-period"><i class="far fa-calendar"></i> ${escapeHtml(exp.period || 'Период не указан')}</span>
<i class="far fa-calendar"></i> ${escapeHtml(exp.period || 'Период не указан')}
</span>
</div> </div>
<div class="experience-company"> <div class="experience-company"><i class="fas fa-building"></i> ${escapeHtml(exp.company)}</div>
<i class="fas fa-building"></i> ${escapeHtml(exp.company)} ${exp.description ? `<div class="experience-description"><strong>📋 Обязанности и достижения:</strong><p style="margin-top: 10px;">${escapeHtml(exp.description).replace(/\n/g, '<br>')}</p></div>` : ''}
</div>
${exp.description ? `
<div class="experience-description">
<strong>📋 Обязанности и достижения:</strong>
<p style="margin-top: 10px;">${escapeHtml(exp.description).replace(/\n/g, '<br>')}</p>
</div>
` : ''}
</div> </div>
`).join('') `).join('')
: '<p style="color: #4f7092; text-align: center; padding: 20px;">Опыт работы не указан</p>'; : '<p style="color: #4f7092; text-align: center; padding: 20px;">Опыт работы не указан</p>';
// Формируем блок с образованием
const educationHtml = resume.education && resume.education.length > 0 const educationHtml = resume.education && resume.education.length > 0
? resume.education.map(edu => ` ? resume.education.map(edu => `
<div class="education-item"> <div class="education-item">
@@ -1140,9 +1059,7 @@
container.innerHTML = ` container.innerHTML = `
<div class="resume-header"> <div class="resume-header">
<div class="resume-avatar"> <div class="resume-avatar"><i class="fas fa-user"></i></div>
<i class="fas fa-user"></i>
</div>
<div class="resume-title"> <div class="resume-title">
<div class="resume-title-header"> <div class="resume-title-header">
<div class="resume-name">${escapeHtml(decodedName)}</div> <div class="resume-name">${escapeHtml(decodedName)}</div>
@@ -1153,9 +1070,7 @@
</div> </div>
<div class="resume-position">${escapeHtml(decodedPosition)}</div> <div class="resume-position">${escapeHtml(decodedPosition)}</div>
<div class="resume-stats"> <div class="resume-stats">
<span class="view-counter"> <span class="view-counter"><i class="fas fa-eye"></i> ${resume.views || 0} просмотров</span>
<i class="fas fa-eye"></i> ${resume.views || 0} просмотров
</span>
<span><i class="fas fa-calendar"></i> Обновлено: ${new Date(resume.updated_at).toLocaleDateString()}</span> <span><i class="fas fa-calendar"></i> Обновлено: ${new Date(resume.updated_at).toLocaleDateString()}</span>
</div> </div>
</div> </div>
@@ -1164,9 +1079,7 @@
<div class="resume-salary">${escapeHtml(resume.desired_salary || 'Зарплатные ожидания не указаны')}</div> <div class="resume-salary">${escapeHtml(resume.desired_salary || 'Зарплатные ожидания не указаны')}</div>
${resume.tags && resume.tags.length > 0 ? ` ${resume.tags && resume.tags.length > 0 ? `
<div class="resume-tags"> <div class="resume-tags">${resume.tags.map(t => `<span class="tag">${escapeHtml(t.name)}</span>`).join('')}</div>
${resume.tags.map(t => `<span class="tag">${escapeHtml(t.name)}</span>`).join('')}
</div>
` : ''} ` : ''}
<h2 class="section-title">О себе</h2> <h2 class="section-title">О себе</h2>
@@ -1180,76 +1093,42 @@
<div class="contact-section"> <div class="contact-section">
<h3 style="color: #0b1c34; margin-bottom: 20px;">Контактная информация</h3> <h3 style="color: #0b1c34; margin-bottom: 20px;">Контактная информация</h3>
${canContact ? ` ${canContact ? `
<div class="contact-grid"> <div class="contact-grid">
<div class="contact-item"> <div class="contact-item"><i class="fas fa-envelope"></i><span>${escapeHtml(resume.email || 'Email не указан')}</span></div>
<i class="fas fa-envelope"></i> <div class="contact-item"><i class="fas fa-phone"></i><span>${escapeHtml(resume.phone || 'Телефон не указан')}</span></div>
<span>${escapeHtml(resume.email || 'Email не указан')}</span> <div class="contact-item"><i class="fab fa-telegram"></i><span>${escapeHtml(resume.telegram || 'Telegram не указан')}</span></div>
</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>
` : ` ` : `
<div style="text-align: center; padding: 30px; background: white; border-radius: 20px;"> <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> <i class="fas fa-lock" style="font-size: 48px; color: #4f7092; margin-bottom: 15px;"></i>
<p style="color: #1f3f60; margin-bottom: 20px;"> <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>'}
</p>
${!token ?
'<a href="/login" class="btn btn-primary">Войти как работодатель</a>' :
'<p class="btn btn-primary" style="opacity: 0.6;">Только для работодателей</p>'
}
</div> </div>
`} `}
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
${canContact ? ` ${canContact ? `<button class="btn btn-primary" onclick="contactCandidate()"><i class="fas fa-paper-plane"></i> Связаться</button>` : ''}
<button class="btn btn-primary" onclick="contactCandidate()"> <button class="btn btn-outline" onclick="saveToFavorites()"><i class="fas fa-heart"></i> В избранное</button>
<i class="fas fa-paper-plane"></i> Связаться <button class="btn btn-outline" onclick="shareResume()"><i class="fas fa-share-alt"></i> Поделиться</button>
</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> </div>
`; `;
} }
// ========== ФУНКЦИИ ДЛЯ QR-КОДА ========== // QR Code functions
function openQRModal() { function openQRModal() {
if (!currentResume) return; if (!currentResume) return;
const decodedName = decodeHtmlEntities(currentResume.full_name || ''); const decodedName = decodeHtmlEntities(currentResume.full_name || '');
const decodedPosition = decodeHtmlEntities(currentResume.desired_position || ''); const decodedPosition = decodeHtmlEntities(currentResume.desired_position || '');
document.getElementById('qrResumeName').textContent = decodedName; document.getElementById('qrResumeName').textContent = decodedName;
const resumeUrl = window.location.origin + '/resume/' + resumeId; const resumeUrl = window.location.origin + '/resume/' + resumeId;
document.getElementById('qrResumeUrl').textContent = resumeUrl.replace('https://', '').replace('http://', ''); document.getElementById('qrResumeUrl').textContent = resumeUrl.replace('https://', '').replace('http://', '');
document.getElementById('qrViewCount').textContent = ++qrViewCount; document.getElementById('qrViewCount').textContent = ++qrViewCount;
document.getElementById('qrViewBadge').textContent = qrViewCount; document.getElementById('qrViewBadge').textContent = qrViewCount;
document.getElementById('qrExperienceCount').textContent = currentResume.work_experience?.length || 0;
const expCount = currentResume.work_experience ? currentResume.work_experience.length : 0;
document.getElementById('qrExperienceCount').textContent = expCount;
document.getElementById('qrPosition').textContent = decodedPosition || '—'; document.getElementById('qrPosition').textContent = decodedPosition || '—';
generateQRCodeWithLogo(resumeUrl); generateQRCodeWithLogo(resumeUrl);
document.getElementById('qrModal').classList.add('active'); document.getElementById('qrModal').classList.add('active');
} }
@@ -1259,35 +1138,37 @@
function generateQRCodeWithLogo(text) { function generateQRCodeWithLogo(text) {
const canvas = document.getElementById('qrCanvas'); const canvas = document.getElementById('qrCanvas');
if (!canvas) { showNotification('Ошибка: не найден элемент для QR-кода', 'error'); return; }
if (typeof QRCode === 'undefined') {
showNotification('Ошибка загрузки библиотеки QR-кода', 'error');
const container = document.getElementById('qrContainer');
if (container && !container.querySelector('.fallback-link')) {
const fallback = document.createElement('div');
fallback.className = 'fallback-link';
fallback.innerHTML = `<a href="${text}" target="_blank">${text}</a>`;
container.appendChild(fallback);
}
return;
}
const options = { width: 250, height: 250, color: { dark: '#0b1c34', light: '#ffffff' }, errorCorrectionLevel: 'H' };
QRCode.toCanvas(canvas, text, options, function(error) {
if (error) { showNotification('Ошибка генерации QR-кода', 'error'); }
else {
const logoOverlay = document.getElementById('qrLogoOverlay'); const logoOverlay = document.getElementById('qrLogoOverlay');
const logoIcon = document.getElementById('qrLogoIcon'); const logoIcon = document.getElementById('qrLogoIcon');
if (logoOverlay) logoOverlay.style.display = 'flex';
const options = { if (logoIcon) { logoIcon.style.display = 'block'; logoIcon.className = 'fas fa-user'; }
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';
logoIcon.style.display = 'block';
logoIcon.className = 'fas fa-user';
} }
}); });
} }
function downloadQR() { function downloadQR() {
const canvas = document.getElementById('qrCanvas'); const canvas = document.getElementById('qrCanvas');
if (!canvas) { showNotification('QR-код не найден', 'error'); return; }
const combinedCanvas = document.createElement('canvas'); const combinedCanvas = document.createElement('canvas');
combinedCanvas.width = canvas.width; combinedCanvas.width = canvas.width;
combinedCanvas.height = canvas.height; combinedCanvas.height = canvas.height;
const ctx = combinedCanvas.getContext('2d'); const ctx = combinedCanvas.getContext('2d');
ctx.drawImage(canvas, 0, 0); ctx.drawImage(canvas, 0, 0);
ctx.fillStyle = 'white'; ctx.fillStyle = 'white';
ctx.beginPath(); ctx.beginPath();
@@ -1298,42 +1179,25 @@
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.fillText('', 125, 125); ctx.fillText('', 125, 125);
const link = document.createElement('a'); const link = document.createElement('a');
const decodedName = decodeHtmlEntities(currentResume.full_name || ''); const decodedName = decodeHtmlEntities(currentResume.full_name || '');
const filename = `resume_${decodedName.toLowerCase().replace(/[^a-zа-я0-9]/g, '_')}.png`; const filename = `resume_${decodedName.toLowerCase().replace(/[^a-zа-я0-9]/g, '_')}.png`;
link.download = filename; link.download = filename;
link.href = combinedCanvas.toDataURL('image/png'); link.href = combinedCanvas.toDataURL('image/png');
link.click(); link.click();
showNotification('QR-код скачан', 'success'); showNotification('QR-код скачан', 'success');
} }
function copyResumeLink() { function copyResumeLink() {
const url = window.location.origin + '/resume/' + resumeId; const url = window.location.origin + '/resume/' + resumeId;
navigator.clipboard.writeText(url).then(() => { navigator.clipboard.writeText(url).then(() => showNotification('Ссылка скопирована', 'success')).catch(() => showNotification('Ошибка копирования', 'error'));
showNotification('Ссылка скопирована', 'success');
}).catch(() => {
showNotification('Ошибка копирования', 'error');
});
} }
function saveToContacts() { function saveToContacts() {
if (!currentResume) return; if (!currentResume) return;
const decodedName = decodeHtmlEntities(currentResume.full_name || ''); const decodedName = decodeHtmlEntities(currentResume.full_name || '');
const decodedPosition = decodeHtmlEntities(currentResume.desired_position || ''); const decodedPosition = decodeHtmlEntities(currentResume.desired_position || '');
const vCard = `BEGIN:VCARD\nVERSION:3.0\nFN:${decodedName}\nTITLE:${decodedPosition}\n${currentResume.email ? `EMAIL:${currentResume.email}` : ''}\n${currentResume.phone ? `TEL:${currentResume.phone}` : ''}\n${currentResume.telegram ? `X-SOCIALPROFILE;TYPE=telegram:https://t.me/${currentResume.telegram.replace('@', '')}` : ''}\nNOTE:Кандидат на позицию ${decodedPosition}. Подробнее: ${window.location.origin}/resume/${resumeId}\nEND: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 blob = new Blob([vCard], { type: 'text/vcard' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement('a');
@@ -1341,7 +1205,6 @@ END:VCARD`;
link.download = `${decodedName}.vcf`; link.download = `${decodedName}.vcf`;
link.click(); link.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
showNotification('Контакт сохранен', 'success'); showNotification('Контакт сохранен', 'success');
} }
@@ -1349,26 +1212,16 @@ END:VCARD`;
const canvas = document.getElementById('qrCanvas'); const canvas = document.getElementById('qrCanvas');
const decodedName = decodeHtmlEntities(currentResume.full_name || ''); const decodedName = decodeHtmlEntities(currentResume.full_name || '');
const decodedPosition = decodeHtmlEntities(currentResume.desired_position || ''); const decodedPosition = decodeHtmlEntities(currentResume.desired_position || '');
const printWindow = window.open('', '_blank'); const printWindow = window.open('', '_blank');
printWindow.document.write(` printWindow.document.write(`
<html> <html><head><title>QR-код резюме ${decodedName}</title>
<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;}h3{color:#3b82f6;text-align:center;}img{max-width:300px;box-shadow:0 10px 30px rgba(0,0,0,0.1);border-radius:20px;}.url{color:#4f7092;margin-top:20px;text-align:center;}</style>
<style> </head><body>
body { display: flex; justify-content: center; align-items: center; height: 100vh; flex-direction: column; font-family: Arial; margin: 0; padding: 20px; }
h2 { color: #0b1c34; text-align: center; word-break: break-word; }
h3 { color: #3b82f6; text-align: center; }
img { max-width: 300px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); border-radius: 20px; }
.url { color: #4f7092; margin-top: 20px; word-break: break-all; text-align: center; }
</style>
</head>
<body>
<h2>${escapeHtml(decodedName)}</h2> <h2>${escapeHtml(decodedName)}</h2>
<h3>${escapeHtml(decodedPosition)}</h3> <h3>${escapeHtml(decodedPosition)}</h3>
<img src="${canvas.toDataURL()}" /> <img src="${canvas.toDataURL()}" />
<div class="url">${window.location.origin}/resume/${resumeId}</div> <div class="url">${window.location.origin}/resume/${resumeId}</div>
</body> </body></html>
</html>
`); `);
printWindow.document.close(); printWindow.document.close();
printWindow.focus(); printWindow.focus();
@@ -1379,46 +1232,23 @@ END:VCARD`;
const url = window.location.origin + '/resume/' + resumeId; const url = window.location.origin + '/resume/' + resumeId;
const decodedName = decodeHtmlEntities(currentResume.full_name || ''); const decodedName = decodeHtmlEntities(currentResume.full_name || '');
const text = `Резюме ${decodedName} на Rabota.Today`; const text = `Резюме ${decodedName} на Rabota.Today`;
let shareUrl = ''; let shareUrl = '';
switch(platform) { if (platform === 'whatsapp') shareUrl = `https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`;
case 'whatsapp': shareUrl = `https://wa.me/?text=${encodeURIComponent(text + ' ' + url)}`; break; else if (platform === 'telegram') shareUrl = `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`;
case 'telegram': shareUrl = `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(text)}`; break; else if (platform === 'email') shareUrl = `mailto:?subject=${encodeURIComponent('Резюме ' + decodedName)}&body=${encodeURIComponent(text + '\n\n' + url)}`;
case 'email': shareUrl = `mailto:?subject=${encodeURIComponent('Резюме ' + decodedName)}&body=${encodeURIComponent(text + '\n\n' + url)}`; break;
}
if (shareUrl) window.open(shareUrl, '_blank'); if (shareUrl) window.open(shareUrl, '_blank');
} }
function contactCandidate() { function contactCandidate() { showNotification('Функция связи будет доступна в ближайшее время', 'info'); }
showNotification('Функция связи будет доступна в ближайшее время', 'info');
}
function saveToFavorites() { function saveToFavorites() {
const token = localStorage.getItem('accessToken'); const token = localStorage.getItem('accessToken');
if (!token) { if (!token) { if (confirm('Для добавления в избранное нужно войти в систему. Перейти на страницу входа?')) { window.location.href = '/login'; } return; }
if (confirm('Для добавления в избранное нужно войти в систему. Перейти на страницу входа?')) {
window.location.href = '/login';
}
return;
}
showNotification('Резюме добавлено в избранное', 'success'); showNotification('Резюме добавлено в избранное', 'success');
} }
function shareResume() { function shareResume() {
const url = window.location.href; const url = window.location.href;
if (navigator.share) { if (navigator.share) { navigator.share({ title: document.title, url: url }).catch(() => copyResumeLink()); }
navigator.share({ title: document.title, url: url }).catch(() => copyResumeLink()); else { copyResumeLink(); }
} else {
copyResumeLink();
}
}
function showNotification(message, type = 'info') {
const notification = document.getElementById('notification');
notification.className = `notification ${type}`;
notification.innerHTML = message;
notification.style.display = 'block';
setTimeout(() => { notification.style.display = 'none'; }, 3000);
} }
window.onclick = function(event) { window.onclick = function(event) {