Files
yarmarka/templates/vacancy_detail.html
2026-03-13 19:58:52 +03:00

867 lines
29 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">
<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;
}
.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;
}
.profile-link i {
font-size: 18px;
}
.user-avatar {
width: 35px;
height: 35px;
background: #3b82f6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.user-name {
font-weight: 500;
}
.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;
}
.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;
}
.vacancy-company {
font-size: 20px;
color: #3b82f6;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.company-link {
color: #3b82f6;
text-decoration: none;
border-bottom: 1px dashed #3b82f6;
}
.company-link:hover {
border-bottom: 1px solid #3b82f6;
}
.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;
}
.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;
}
.company-info h3 {
color: #0b1c34;
margin-bottom: 20px;
font-size: 20px;
}
.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: 10px 0;
color: #1f3f60;
}
.company-info-item i {
color: #3b82f6;
width: 24px;
font-size: 18px;
}
.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;
}
.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;
}
.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;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">
<i class="fas fa-briefcase"></i>
МП.Ярмарка
</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>
<script>
const API_BASE_URL = 'http://localhost:8000/api';
let currentUser = null;
// Получаем ID вакансии из URL
const pathParts = window.location.pathname.split('/');
const vacancyId = pathParts[pathParts.length - 1];
// Проверка авторизации
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');
const token = localStorage.getItem('accessToken');
if (currentUser) {
// Пользователь авторизован
nav.innerHTML = `
<a href="/">Главная</a>
<a href="/vacancies">Вакансии</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>
<span class="user-name">${escapeHtml(currentUser.full_name.split(' ')[0])}</span>
${currentUser.is_admin ? '<span class="admin-badge">Admin</span>' : ''}
</a>
`;
} else {
// Пользователь не авторизован
nav.innerHTML = `
<a href="/">Главная</a>
<a href="/vacancies">Вакансии</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('Вакансия не найдена');
}
const vacancy = await response.json();
renderVacancy(vacancy);
} 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>
`;
}
}
// Отображение вакансии
async 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;
// Проверяем, в избранном ли вакансия
let isFavorite = false;
if (token) {
isFavorite = await checkFavorite();
}
// Формируем информацию о компании
const companyHtml = vacancy.company_name ? `
<div class="company-info">
<h3>О компании</h3>
<div class="company-info-grid">
<div class="company-info-item">
<i class="fas fa-building"></i>
<span><strong>${escapeHtml(vacancy.company_name)}</strong></span>
</div>
${vacancy.company_website ? `
<div class="company-info-item">
<i class="fas fa-globe"></i>
<a href="${escapeHtml(vacancy.company_website)}" target="_blank" class="company-link">${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-top: 20px; border-top: 1px solid #dee9f5;">
${escapeHtml(vacancy.company_description)}
</div>
` : ''}
</div>
` : '';
container.innerHTML = `
<div class="vacancy-header">
<h1 class="vacancy-title">${escapeHtml(vacancy.title)}</h1>
<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>
</div>
<div class="vacancy-company">
<i class="fas fa-building"></i>
<strong>${escapeHtml(vacancy.company_name || 'Компания не указана')}</strong>
${vacancy.company_website ? `
<a href="${escapeHtml(vacancy.company_website)}" target="_blank" style="margin-left: 10px; font-size: 14px;">
<i class="fas fa-external-link-alt"></i> Сайт
</a>
` : ''}
</div>
<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 || 'Описание отсутствует')}</div>
${companyHtml}
<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>
<span>${escapeHtml(vacancy.company_email)}</span>
</div>
` : ''}
${vacancy.company_phone ? `
<div class="contact-item">
<i class="fas fa-phone"></i>
<span>${escapeHtml(vacancy.company_phone)}</span>
</div>
` : ''}
${!vacancy.company_email && !vacancy.company_phone && vacancy.user_email ? `
<div class="contact-item">
<i class="fas fa-envelope"></i>
<span>${escapeHtml(vacancy.user_email)}</span>
</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 favorite-btn ${isFavorite ? 'favorite-active' : ''}"
onclick="${isFavorite ? 'removeFromFavorites()' : 'addToFavorites()'}">
<i class="${isFavorite ? 'fas fa-heart' : 'far fa-heart'}"></i>
${isFavorite ? 'В избранном' : 'В избранное'}
</button>
<button class="btn btn-outline" onclick="shareVacancy()">
<i class="fas fa-share-alt"></i> Поделиться
</button>
</div>
`;
}
// Отклик на вакансию
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) {
alert('Отклик отправлен! Работодатель свяжется с вами.');
// Перезагружаем вакансию, чтобы увидеть обновленный статус
loadVacancy();
} else {
const error = await response.json();
throw new Error(error.detail || 'Ошибка отправки');
}
} catch (error) {
alert(error.message);
}
}
// Добавить в избранное
function saveToFavorites() {
const token = localStorage.getItem('accessToken');
if (!token) {
if (confirm('Для добавления в избранное нужно войти в систему. Перейти на страницу входа?')) {
window.location.href = '/login';
}
return;
}
// Здесь будет логика добавления в избранное
alert('Вакансия добавлена в избранное');
}
// Поделиться вакансией
function shareVacancy() {
const url = window.location.href;
navigator.clipboard.writeText(url).then(() => {
alert('Ссылка скопирована в буфер обмена');
}).catch(() => {
alert('Ссылка: ' + url);
});
}
// Проверка, в избранном ли вакансия
async function checkFavorite() {
const token = localStorage.getItem('accessToken');
if (!token) return false;
try {
const response = await fetch(`${API_BASE_URL}/favorites/check/vacancy/${vacancyId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
return data.is_favorite;
}
} catch (error) {
console.error('Error checking favorite:', error);
}
return false;
}
// Добавить в избранное
async function addToFavorites() {
const token = localStorage.getItem('accessToken');
if (!token) {
if (confirm('Для добавления в избранное нужно войти в систему. Перейти на страницу входа?')) {
window.location.href = '/login';
}
return;
}
try {
const response = await fetch(`${API_BASE_URL}/favorites`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
item_type: 'vacancy',
item_id: parseInt(vacancyId)
})
});
if (response.ok) {
// Обновляем кнопку
const favBtn = document.querySelector('.favorite-btn');
if (favBtn) {
favBtn.innerHTML = '<i class="fas fa-heart" style="color: #b91c1c;"></i> В избранном';
favBtn.classList.add('favorite-active');
favBtn.onclick = removeFromFavorites;
}
showNotification('Добавлено в избранное', 'success');
} else {
const error = await response.json();
throw new Error(error.detail || 'Ошибка');
}
} catch (error) {
if (error.message === 'Уже в избранном') {
// Если уже в избранном, предлагаем удалить
if (confirm('Эта вакансия уже в избранном. Удалить?')) {
removeFromFavorites();
}
} else {
showNotification(error.message, 'error');
}
}
}
// Удалить из избранного
async function removeFromFavorites() {
const token = localStorage.getItem('accessToken');
try {
const response = await fetch(`${API_BASE_URL}/favorites`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
item_type: 'vacancy',
item_id: parseInt(vacancyId)
})
});
if (response.ok) {
// Обновляем кнопку
const favBtn = document.querySelector('.favorite-btn');
if (favBtn) {
favBtn.innerHTML = '<i class="far fa-heart"></i> В избранное';
favBtn.classList.remove('favorite-active');
favBtn.onclick = addToFavorites;
}
showNotification('Удалено из избранного', 'success');
} else {
const error = await response.json();
throw new Error(error.detail || 'Ошибка');
}
} catch (error) {
showNotification(error.message, 'error');
}
}
// Функция для показа уведомлений
function showNotification(message, type = 'success') {
// Создаем элемент уведомления, если его нет
let notification = document.getElementById('notification');
if (!notification) {
notification = document.createElement('div');
notification.id = 'notification';
document.body.appendChild(notification);
// Добавляем стили
const style = document.createElement('style');
style.textContent = `
#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;
}
#notification.success {
background: #10b981;
color: white;
}
#notification.error {
background: #ef4444;
color: white;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.favorite-btn {
transition: all 0.2s;
}
.favorite-btn.favorite-active {
background: #fee2e2;
border-color: #b91c1c;
color: #b91c1c;
}
.favorite-btn.favorite-active i {
color: #b91c1c;
}
`;
document.head.appendChild(style);
}
notification.className = type;
notification.innerHTML = message;
notification.style.display = 'block';
setTimeout(() => {
notification.style.display = 'none';
}, 3000);
}
// Редирект на страницу входа
function redirectToLogin() {
if (confirm('Для отклика нужно войти в систему. Перейти на страницу входа?')) {
window.location.href = '/login';
}
}
// Экранирование 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;");
}
// Загрузка при старте
checkAuth().then(() => {
loadVacancy();
});
</script>
</body>
</html>