Files
yarmarka/templates/favorites.html
2026-03-17 20:01:50 +03:00

864 lines
28 KiB
HTML
Raw Permalink 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: 1200px;
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-to-profile {
display: inline-flex;
align-items: center;
gap: 8px;
color: #4f7092;
text-decoration: none;
margin-bottom: 20px;
padding: 8px 16px;
border-radius: 30px;
background: white;
transition: 0.2s;
}
.back-to-profile:hover {
background: #eef4fa;
color: #0b1c34;
}
.favorites-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
flex-wrap: wrap;
gap: 20px;
}
.favorites-header h1 {
font-size: 32px;
color: #0b1c34;
display: flex;
align-items: center;
gap: 10px;
}
.favorites-header h1 i {
color: #b91c1c;
font-size: 28px;
}
.filter-tabs {
display: flex;
gap: 10px;
background: white;
padding: 8px;
border-radius: 50px;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
}
.filter-tab {
padding: 10px 24px;
border-radius: 40px;
cursor: pointer;
font-weight: 600;
transition: 0.2s;
color: #4f7092;
}
.filter-tab:hover {
background: #eef4fa;
}
.filter-tab.active {
background: #0b1c34;
color: white;
}
.stats-summary {
background: white;
border-radius: 30px;
padding: 20px 30px;
margin-bottom: 30px;
display: flex;
gap: 40px;
flex-wrap: wrap;
box-shadow: 0 10px 30px rgba(0,20,40,0.05);
}
.stat-item {
display: flex;
align-items: center;
gap: 10px;
}
.stat-item .value {
font-size: 28px;
font-weight: 700;
color: #0b1c34;
}
.stat-item .label {
color: #4f7092;
font-size: 16px;
}
.favorites-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 30px;
}
.favorite-card {
background: white;
border-radius: 40px;
padding: 30px;
transition: 0.3s;
cursor: pointer;
position: relative;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
}
.favorite-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0,20,40,0.15);
}
.favorite-card.vacancy {
border-left: 5px solid #3b82f6;
}
.favorite-card.resume {
border-left: 5px solid #10b981;
}
.favorite-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 15px;
}
.favorite-type {
background: #eef4fa;
padding: 6px 14px;
border-radius: 30px;
font-size: 13px;
font-weight: 600;
color: #1f3f60;
}
.favorite-type.vacancy {
background: #dbeafe;
color: #1e40af;
}
.favorite-type.resume {
background: #d1fae5;
color: #065f46;
}
.remove-btn {
background: #fee2e2;
border: none;
color: #b91c1c;
width: 36px;
height: 36px;
border-radius: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: 0.2s;
}
.remove-btn:hover {
background: #fecaca;
transform: scale(1.1);
}
.favorite-title {
font-size: 20px;
font-weight: 700;
color: #0b1c34;
margin-bottom: 8px;
line-height: 1.3;
}
.favorite-subtitle {
color: #3b82f6;
font-weight: 600;
margin-bottom: 10px;
font-size: 15px;
}
.favorite-salary {
font-size: 20px;
font-weight: 700;
color: #0f2b4f;
margin: 15px 0;
padding: 10px 0;
border-top: 1px solid #dee9f5;
border-bottom: 1px solid #dee9f5;
}
.favorite-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 15px 0;
}
.tag {
background: #eef4fa;
padding: 6px 14px;
border-radius: 30px;
font-size: 13px;
color: #1f3f60;
font-weight: 500;
}
.favorite-footer {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #dee9f5;
padding-top: 20px;
margin-top: 10px;
}
.favorite-date {
color: #4f7092;
font-size: 14px;
display: flex;
align-items: center;
gap: 5px;
}
.empty-state {
text-align: center;
padding: 80px 40px;
background: white;
border-radius: 40px;
grid-column: 1/-1;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
}
.empty-state i {
font-size: 80px;
color: #cbd5e1;
margin-bottom: 25px;
}
.empty-state h2 {
font-size: 28px;
color: #0b1c34;
margin-bottom: 15px;
}
.empty-state p {
color: #4f7092;
margin-bottom: 30px;
font-size: 18px;
}
.empty-state-actions {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.btn {
padding: 14px 32px;
border-radius: 40px;
border: none;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 10px;
text-decoration: none;
font-size: 16px;
transition: 0.2s;
}
.btn-primary {
background: #0b1c34;
color: white;
}
.btn-primary:hover {
background: #1b3f6b;
transform: scale(1.05);
}
.btn-outline {
background: transparent;
border: 2px solid #3b82f6;
color: #0b1c34;
}
.btn-outline:hover {
background: #eef4fa;
}
.loading {
text-align: center;
padding: 80px;
color: #4f7092;
font-size: 18px;
grid-column: 1/-1;
background: white;
border-radius: 40px;
}
.loading i {
font-size: 40px;
margin-bottom: 15px;
color: #3b82f6;
}
#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;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@media (max-width: 768px) {
.favorites-header {
flex-direction: column;
align-items: flex-start;
}
.filter-tabs {
width: 100%;
justify-content: stretch;
}
.filter-tab {
flex: 1;
text-align: center;
padding: 10px 16px;
}
.stats-summary {
justify-content: space-around;
}
.favorites-grid {
grid-template-columns: 1fr;
}
}
</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="/profile" class="back-to-profile">
<i class="fas fa-arrow-left"></i> Вернуться в профиль
</a>
<div class="favorites-header">
<h1>
<i class="fas fa-heart"></i>
Избранное
</h1>
<div class="filter-tabs">
<div class="filter-tab" data-filter="all" onclick="filterFavorites('all')">Все</div>
<div class="filter-tab" data-filter="vacancy" onclick="filterFavorites('vacancy')">Вакансии</div>
<div class="filter-tab" data-filter="resume" onclick="filterFavorites('resume')">Резюме</div>
</div>
</div>
<!-- Статистика избранного -->
<div class="stats-summary" id="statsSummary">
<div class="stat-item">
<span class="value" id="totalCount">0</span>
<span class="label">всего</span>
</div>
<div class="stat-item">
<span class="value" id="vacancyCount">0</span>
<span class="label">вакансий</span>
</div>
<div class="stat-item">
<span class="value" id="resumeCount">0</span>
<span class="label">резюме</span>
</div>
</div>
<div id="favoritesContainer" class="favorites-grid">
<div class="loading">
<i class="fas fa-spinner fa-spin"></i>
<div>Загрузка избранного...</div>
</div>
</div>
</div>
<div id="notification"></div>
<script>
const API_BASE_URL = 'http://localhost:8000/api';
let currentUser = null;
let currentFilter = 'all';
let favorites = [];
// Получаем параметр filter из URL
const urlParams = new URLSearchParams(window.location.search);
const urlFilter = urlParams.get('type');
if (urlFilter && ['all', 'vacancy', 'resume'].includes(urlFilter)) {
currentFilter = urlFilter;
}
// Проверка авторизации
const token = localStorage.getItem('accessToken');
if (!token) {
window.location.href = '/login';
}
// Проверка авторизации и получение данных пользователя
async function checkAuth() {
try {
const response = await fetch(`${API_BASE_URL}/user`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
currentUser = await response.json();
} else {
localStorage.removeItem('accessToken');
window.location.href = '/login';
}
} catch (error) {
console.error('Error checking auth:', error);
}
updateNavigation();
activateTabFromUrl();
}
// Активация таба из URL
function activateTabFromUrl() {
document.querySelectorAll('.filter-tab').forEach(tab => {
tab.classList.remove('active');
if (tab.dataset.filter === currentFilter) {
tab.classList.add('active');
}
});
}
// Обновление навигации
function updateNavigation() {
const nav = document.getElementById('nav');
if (currentUser) {
nav.innerHTML = `
<a href="/">Главная</a>
<a href="/vacancies">Вакансии</a>
<a href="/resumes">Резюме</a>
<a href="/favorites" class="active">Избранное</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>
`;
}
}
// Загрузка избранного
async function loadFavorites() {
try {
const response = await fetch(`${API_BASE_URL}/favorites`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) throw new Error('Ошибка загрузки');
favorites = await response.json();
// Обновляем статистику
updateStats();
// Отображаем избранное с текущим фильтром
renderFavorites();
} catch (error) {
console.error('Error loading favorites:', error);
document.getElementById('favoritesContainer').innerHTML =
'<div class="loading">Ошибка загрузки избранного</div>';
}
}
// Обновление статистики
function updateStats() {
const vacancyCount = favorites.filter(f => f.item_type === 'vacancy').length;
const resumeCount = favorites.filter(f => f.item_type === 'resume').length;
document.getElementById('totalCount').textContent = favorites.length;
document.getElementById('vacancyCount').textContent = vacancyCount;
document.getElementById('resumeCount').textContent = resumeCount;
}
// Отображение избранного с фильтром
function renderFavorites() {
const container = document.getElementById('favoritesContainer');
const filtered = currentFilter === 'all'
? favorites
: favorites.filter(f => f.item_type === currentFilter);
if (filtered.length === 0) {
let title = '';
let message = '';
let actionButtons = '';
if (currentFilter === 'vacancy') {
title = 'Нет избранных вакансий';
message = 'Добавляйте вакансии в избранное, чтобы не потерять интересные предложения';
actionButtons = `
<div class="empty-state-actions">
<a href="/vacancies" class="btn btn-primary">
<i class="fas fa-search"></i> Найти вакансии
</a>
<a href="/resumes" class="btn btn-outline">
<i class="fas fa-users"></i> Смотреть резюме
</a>
</div>
`;
} else if (currentFilter === 'resume') {
title = 'Нет избранных резюме';
message = 'Сохраняйте резюме кандидатов, чтобы вернуться к ним позже';
actionButtons = `
<div class="empty-state-actions">
<a href="/resumes" class="btn btn-primary">
<i class="fas fa-users"></i> Найти резюме
</a>
<a href="/vacancies" class="btn btn-outline">
<i class="fas fa-briefcase"></i> Смотреть вакансии
</a>
</div>
`;
} else {
title = 'Список избранного пуст';
message = 'Добавляйте вакансии и резюме в избранное, чтобы не потерять их';
actionButtons = `
<div class="empty-state-actions">
<a href="/vacancies" class="btn btn-primary">
<i class="fas fa-search"></i> Найти вакансии
</a>
<a href="/resumes" class="btn btn-primary">
<i class="fas fa-users"></i> Найти резюме
</a>
</div>
`;
}
container.innerHTML = `
<div class="empty-state">
<i class="far fa-heart"></i>
<h2>${title}</h2>
<p>${message}</p>
${actionButtons}
</div>
`;
return;
}
container.innerHTML = filtered.map(f => {
const data = f.item_data;
const date = new Date(f.created_at).toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
if (f.item_type === 'vacancy') {
return `
<div class="favorite-card vacancy" onclick="window.location.href='${data.url}'">
<div class="favorite-header">
<span class="favorite-type vacancy">
<i class="fas fa-briefcase"></i> Вакансия
</span>
<button class="remove-btn" onclick="event.stopPropagation(); removeFromFavorites('vacancy', ${f.item_id})">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="favorite-title">${escapeHtml(data.title)}</div>
<div class="favorite-subtitle">
<i class="fas fa-building"></i> ${escapeHtml(data.company || 'Компания')}
</div>
<div class="favorite-salary">${escapeHtml(data.salary || 'Зарплата не указана')}</div>
<div class="favorite-tags">
${(data.tags || []).map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('')}
</div>
<div class="favorite-footer">
<span class="favorite-date">
<i class="far fa-calendar"></i> ${date}
</span>
</div>
</div>
`;
} else {
return `
<div class="favorite-card resume" onclick="window.location.href='${data.url}'">
<div class="favorite-header">
<span class="favorite-type resume">
<i class="fas fa-user"></i> Резюме
</span>
<button class="remove-btn" onclick="event.stopPropagation(); removeFromFavorites('resume', ${f.item_id})">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="favorite-title">${escapeHtml(data.name)}</div>
<div class="favorite-subtitle">
<i class="fas fa-briefcase"></i> ${escapeHtml(data.title || 'Должность не указана')}
</div>
<div class="favorite-salary">${escapeHtml(data.salary || 'Зарплата не указана')}</div>
<div class="favorite-tags">
${(data.tags || []).map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('')}
</div>
<div class="favorite-footer">
<span class="favorite-date">
<i class="far fa-calendar"></i> ${date}
</span>
</div>
</div>
`;
}
}).join('');
}
// Фильтрация избранного
function filterFavorites(filter) {
currentFilter = filter;
// Обновляем URL без перезагрузки страницы
const url = new URL(window.location);
url.searchParams.set('type', filter);
window.history.pushState({}, '', url);
// Обновляем активный таб
document.querySelectorAll('.filter-tab').forEach(tab => {
tab.classList.remove('active');
if (tab.dataset.filter === filter) {
tab.classList.add('active');
}
});
renderFavorites();
}
// Удаление из избранного
async function removeFromFavorites(itemType, itemId) {
if (!confirm('Удалить из избранного?')) return;
try {
const response = await fetch(`${API_BASE_URL}/favorites`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
item_type: itemType,
item_id: itemId
})
});
if (response.ok) {
// Удаляем из локального массива
favorites = favorites.filter(f => !(f.item_type === itemType && f.item_id === itemId));
updateStats();
renderFavorites();
showNotification('Удалено из избранного', 'success');
} else {
const error = await response.json();
throw new Error(error.detail);
}
} catch (error) {
showNotification(error.message, 'error');
}
}
// Функция для показа уведомлений
function showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
notification.className = type;
notification.innerHTML = message;
notification.style.display = 'block';
setTimeout(() => {
notification.style.display = 'none';
}, 3000);
}
// Экранирование 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;");
}
// Обработка клика по кнопке "Назад"
window.onpopstate = function() {
const params = new URLSearchParams(window.location.search);
const filter = params.get('type') || 'all';
if (['all', 'vacancy', 'resume'].includes(filter)) {
currentFilter = filter;
activateTabFromUrl();
renderFavorites();
}
};
// Загрузка при старте
checkAuth().then(() => {
loadFavorites();
});
</script>
</body>
</html>