Files
yarmarka/templates/resumes.html
2026-03-25 18:44:41 +03:00

499 lines
15 KiB
HTML

<!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;
}
.filters {
background: white;
border-radius: 40px;
padding: 30px;
margin-bottom: 30px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.filter-group input,
.filter-group select {
width: 100%;
padding: 12px 16px;
border: 2px solid #dee9f5;
border-radius: 30px;
font-size: 16px;
}
.filter-group input:focus,
.filter-group select:focus {
border-color: #3b82f6;
outline: none;
}
.resumes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 30px;
}
.resume-card {
background: white;
border-radius: 40px;
padding: 30px;
transition: 0.2s;
cursor: pointer;
}
.resume-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0,20,40,0.15);
}
.resume-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
}
.resume-avatar {
width: 60px;
height: 60px;
background: #eef4fa;
border-radius: 30px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #3b82f6;
}
.resume-name {
font-size: 18px;
font-weight: 700;
color: #0b1c34;
}
.resume-position {
color: #3b82f6;
font-weight: 600;
margin: 10px 0;
font-size: 16px;
}
.resume-salary {
font-size: 20px;
font-weight: 700;
color: #0f2b4f;
margin: 15px 0;
}
.resume-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 15px 0;
}
.tag {
background: #eef4fa;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
color: #1f3f60;
}
.resume-footer {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #dee9f5;
padding-top: 20px;
margin-top: 10px;
}
.resume-experience {
color: #4f7092;
font-size: 14px;
}
.resume-views {
color: #8099b3;
font-size: 14px;
}
.btn-contact {
background: #0b1c34;
color: white;
border: none;
padding: 8px 16px;
border-radius: 30px;
cursor: pointer;
transition: 0.2s;
}
.btn-contact:hover {
background: #1b3f6b;
}
.loading {
text-align: center;
padding: 60px;
color: #4f7092;
grid-column: 1/-1;
}
.pagination {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 40px;
flex-wrap: wrap;
}
.page-btn {
padding: 10px 16px;
border: 2px solid #dee9f5;
background: white;
border-radius: 30px;
cursor: pointer;
transition: 0.2s;
}
.page-btn:hover {
background: #eef4fa;
}
.page-btn.active {
background: #0b1c34;
color: white;
border-color: #0b1c34;
}
.no-results {
text-align: center;
padding: 60px;
color: #4f7092;
font-size: 18px;
grid-column: 1/-1;
}
</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>
<div class="filters">
<div class="filter-group">
<input type="text" id="searchInput" placeholder="Поиск резюме..." oninput="debounceSearch()">
</div>
<div class="filter-group">
<input type="text" id="tagInput" placeholder="Теги (через запятую)" oninput="debounceSearch()">
</div>
<div class="filter-group">
<select id="salaryFilter" onchange="applyFilters()">
<option value="">Любая зарплата</option>
<option value="50000">от 50 000 ₽</option>
<option value="100000">от 100 000 ₽</option>
<option value="150000">от 150 000 ₽</option>
<option value="200000">от 200 000 ₽</option>
</select>
</div>
</div>
<div id="resumesContainer" class="resumes-grid">
<div class="loading">Загрузка резюме...</div>
</div>
<div class="pagination" id="pagination"></div>
</div>
<script>
const API_BASE_URL = window.location.protocol + '//' + window.location.host + '/api';
let currentPage = 1;
let totalPages = 1;
let searchTimeout;
let currentUser = null;
// Проверка авторизации при загрузке
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) {
// Пользователь авторизован
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>
<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" class="active">Резюме</a>
<a href="/login">Войти</a>
<a href="/register">Регистрация</a>
`;
}
}
// Загрузка всех резюме
async function loadResumes(page = 1, search = '', tags = '', salary = '') {
try {
let url = `${API_BASE_URL}/resumes/all?page=${page}`;
if (search) url += `&search=${encodeURIComponent(search)}`;
if (tags) url += `&tags=${encodeURIComponent(tags)}`;
if (salary) url += `&min_salary=${salary}`;
const response = await fetch(url);
const data = await response.json();
const container = document.getElementById('resumesContainer');
if (!data.resumes || data.resumes.length === 0) {
container.innerHTML = '<div class="no-results">Резюме не найдены</div>';
return;
}
container.innerHTML = data.resumes.map(r => `
<div class="resume-card" onclick="window.location.href='/resume/${r.id}'">
<div class="resume-header">
<div class="resume-avatar">
<i class="fas fa-user"></i>
</div>
<div>
<div class="resume-name">${escapeHtml(r.full_name)}</div>
<div class="resume-position">${escapeHtml(r.desired_position || 'Должность не указана')}</div>
</div>
</div>
<div class="resume-salary">${escapeHtml(r.desired_salary || 'з/п не указана')}</div>
<div class="resume-tags">
${(r.tags || []).map(t =>
`<span class="tag">${escapeHtml(t.name)}</span>`
).join('')}
</div>
<div class="resume-footer">
<span class="resume-experience">
<i class="fas fa-briefcase"></i> ${r.experience_count || 0} мест
</span>
<span class="resume-views">
<i class="fas fa-eye"></i> ${r.views || 0}
</span>
</div>
</div>
`).join('');
totalPages = data.total_pages || 1;
renderPagination();
} catch (error) {
console.error('Error loading resumes:', error);
document.getElementById('resumesContainer').innerHTML =
'<div class="loading">Ошибка загрузки резюме</div>';
}
}
// Применение фильтров
function applyFilters() {
const search = document.getElementById('searchInput').value;
const tags = document.getElementById('tagInput').value;
const salary = document.getElementById('salaryFilter').value;
loadResumes(1, search, tags, salary);
}
// Debounce для поиска
function debounceSearch() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
applyFilters();
}, 500);
}
// Рендер пагинации
function renderPagination() {
const pagination = document.getElementById('pagination');
let html = '';
for (let i = 1; i <= totalPages; i++) {
html += `<button class="page-btn ${i === currentPage ? 'active' : ''}" onclick="goToPage(${i})">${i}</button>`;
}
pagination.innerHTML = html;
}
// Переход на страницу
function goToPage(page) {
currentPage = page;
const search = document.getElementById('searchInput').value;
const tags = document.getElementById('tagInput').value;
const salary = document.getElementById('salaryFilter').value;
loadResumes(page, search, tags, salary);
}
// Экранирование 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(() => {
loadResumes();
});
</script>
</body>
</html>