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

1022 lines
32 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, #0b1c34 0%, #1a3650 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* Шапка */
.header {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 60px;
padding: 15px 30px;
margin-bottom: 40px;
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.logo {
display: flex;
align-items: center;
gap: 15px;
}
.logo i {
font-size: 32px;
color: #3b82f6;
background: rgba(59, 130, 246, 0.2);
padding: 10px;
border-radius: 18px;
}
.logo span {
font-size: 24px;
font-weight: 700;
color: white;
letter-spacing: -0.5px;
}
.nav {
display: flex;
gap: 10px;
align-items: center;
}
.nav a {
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 40px;
font-weight: 500;
transition: 0.2s;
}
.nav a:hover {
background: rgba(255, 255, 255, 0.1);
}
.nav .login-btn {
background: #3b82f6;
color: white;
box-shadow: 0 4px 10px rgba(59, 130, 246, 0.3);
}
.nav .login-btn:hover {
background: #2563eb;
transform: translateY(-2px);
}
.nav .profile-btn {
background: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
gap: 8px;
}
.admin-badge {
background: #f59e0b;
color: white;
padding: 2px 8px;
border-radius: 20px;
font-size: 11px;
margin-left: 5px;
}
/* Герой секция */
.hero {
background: white;
border-radius: 60px;
padding: 60px 40px;
margin-bottom: 40px;
text-align: center;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 100%;
height: 200%;
background: radial-gradient(circle, rgba(59,130,246,0.03) 0%, transparent 70%);
pointer-events: none;
}
.hero h1 {
font-size: 52px;
font-weight: 800;
color: #0b1c34;
margin-bottom: 20px;
line-height: 1.2;
}
.hero p {
font-size: 20px;
color: #4f7092;
margin-bottom: 40px;
max-width: 700px;
margin-left: auto;
margin-right: auto;
}
.hero-buttons {
display: flex;
gap: 20px;
justify-content: center;
}
.btn {
padding: 18px 40px;
border-radius: 50px;
font-size: 18px;
font-weight: 600;
text-decoration: none;
transition: 0.2s;
display: inline-flex;
align-items: center;
gap: 10px;
border: none;
cursor: pointer;
}
.btn-primary {
background: #0b1c34;
color: white;
box-shadow: 0 10px 20px rgba(11, 28, 52, 0.3);
}
.btn-primary:hover {
background: #1b3f6b;
transform: translateY(-3px);
box-shadow: 0 15px 30px rgba(11, 28, 52, 0.4);
}
.btn-secondary {
background: #e8f0fe;
color: #0b1c34;
border: 2px solid #3b82f6;
}
.btn-secondary:hover {
background: #d6e5ff;
transform: translateY(-3px);
}
/* Статистика */
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30px;
margin-bottom: 60px;
}
.stat-card {
background: white;
border-radius: 40px;
padding: 40px 20px;
text-align: center;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1);
transition: 0.3s;
cursor: pointer;
border: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
overflow: hidden;
}
.stat-card::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, #3b82f6, #60a5fa);
transform: scaleX(0);
transition: transform 0.3s;
}
.stat-card:hover::after {
transform: scaleX(1);
}
.stat-card:hover {
transform: translateY(-10px);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);
}
.stat-card i {
font-size: 48px;
color: #3b82f6;
margin-bottom: 20px;
background: #eef4fa;
width: 80px;
height: 80px;
line-height: 80px;
border-radius: 40px;
display: inline-block;
transition: 0.3s;
}
.stat-card:hover i {
transform: scale(1.1);
background: #dbeafe;
}
.stat-number {
font-size: 42px;
font-weight: 800;
color: #0b1c34;
line-height: 1.2;
margin-bottom: 5px;
transition: 0.3s;
}
.stat-card:hover .stat-number {
color: #3b82f6;
}
.stat-label {
font-size: 16px;
color: #4f7092;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Секции с карточками */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.section-header h2 {
font-size: 32px;
color: white;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.section-header h2 i {
color: #3b82f6;
background: rgba(255, 255, 255, 0.1);
padding: 10px;
border-radius: 16px;
font-size: 24px;
}
.section-header a {
color: #9bb8da;
text-decoration: none;
font-size: 18px;
padding: 10px 20px;
border-radius: 40px;
background: rgba(255, 255, 255, 0.05);
transition: 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.section-header a:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30px;
margin-bottom: 60px;
}
.card {
background: white;
border-radius: 40px;
padding: 30px;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: 0.3s;
border-left: 5px solid #3b82f6;
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 100px;
height: 100px;
background: linear-gradient(135deg, transparent 50%, rgba(59,130,246,0.05) 50%);
}
.card:hover {
transform: translateY(-10px);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.2);
}
.card-title {
font-size: 22px;
font-weight: 700;
color: #0b1c34;
margin-bottom: 8px;
padding-right: 20px;
}
.card-subtitle {
color: #3b82f6;
font-size: 16px;
font-weight: 500;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 5px;
}
.card-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;
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 15px 0;
}
.tag {
background: #eef4fa;
padding: 6px 14px;
border-radius: 30px;
font-size: 13px;
font-weight: 500;
color: #1f3f60;
transition: 0.2s;
}
.tag:hover {
background: #dbeafe;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #dee9f5;
font-size: 14px;
color: #4f7092;
}
.card-footer span {
display: flex;
align-items: center;
gap: 5px;
}
/* Преимущества */
.features {
background: white;
border-radius: 60px;
padding: 60px 40px;
margin: 60px 0;
text-align: center;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
}
.features h2 {
font-size: 36px;
color: #0b1c34;
margin-bottom: 50px;
font-weight: 700;
}
.features-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 30px;
}
.feature-item {
text-align: center;
padding: 30px 20px;
border-radius: 40px;
background: #f9fcff;
transition: 0.3s;
}
.feature-item:hover {
transform: translateY(-5px);
box-shadow: 0 15px 30px rgba(59,130,246,0.1);
}
.feature-item i {
font-size: 48px;
color: #3b82f6;
background: #eef4fa;
width: 100px;
height: 100px;
line-height: 100px;
border-radius: 50px;
margin-bottom: 25px;
}
.feature-item h3 {
font-size: 22px;
color: #0b1c34;
margin-bottom: 10px;
font-weight: 600;
}
.feature-item p {
font-size: 16px;
color: #4f7092;
line-height: 1.5;
}
/* Призыв к действию */
.cta {
background: linear-gradient(135deg, #0b1c34 0%, #1f4b8a 100%);
border-radius: 60px;
padding: 60px 40px;
margin: 60px 0;
text-align: center;
color: white;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.cta h2 {
font-size: 42px;
font-weight: 700;
margin-bottom: 20px;
}
.cta p {
font-size: 20px;
color: #9bb8da;
margin-bottom: 40px;
max-width: 700px;
margin-left: auto;
margin-right: auto;
}
.cta .btn-primary {
background: #3b82f6;
font-size: 20px;
padding: 20px 50px;
}
.cta .btn-primary:hover {
background: #2563eb;
}
/* Подвал */
.footer {
text-align: center;
padding: 40px 0;
color: #9bb8da;
font-size: 15px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
margin-top: 40px;
}
.footer a {
color: white;
text-decoration: none;
font-weight: 500;
}
.footer a:hover {
color: #3b82f6;
}
/* Лоадер */
.loader {
text-align: center;
padding: 60px;
color: white;
font-size: 18px;
grid-column: 1/-1;
}
.spinner {
animation: spin 1s linear infinite;
display: inline-block;
margin-right: 10px;
font-size: 24px;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Уведомления */
.notification {
position: fixed;
top: 30px;
right: 30px;
padding: 16px 24px;
border-radius: 50px;
background: white;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
z-index: 9999;
animation: slideIn 0.3s;
max-width: 400px;
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: 1024px) {
.cards-grid {
grid-template-columns: repeat(2, 1fr);
}
.features-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.hero h1 {
font-size: 36px;
}
.hero p {
font-size: 18px;
}
.hero-buttons {
flex-direction: column;
align-items: center;
}
.btn {
width: 100%;
max-width: 300px;
}
.stats {
grid-template-columns: 1fr;
}
.cards-grid {
grid-template-columns: 1fr;
}
.features-grid {
grid-template-columns: 1fr;
}
.section-header {
flex-direction: column;
gap: 15px;
text-align: center;
}
.nav {
display: none;
}
}
/* Анимация для цифр */
@keyframes countUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.stat-number {
animation: countUp 0.5s ease-out;
}
</style>
</head>
<body>
<div class="container">
<!-- Шапка -->
<div class="header">
<div class="logo">
<i class="fas fa-briefcase"></i>
<span>МП.Ярмарка</span>
</div>
<div class="nav" id="nav">
<a href="/">Главная</a>
<a href="/vacancies">Вакансии</a>
<a href="/resumes">Резюме</a>
<a href="/login" class="login-btn">Войти</a>
</div>
</div>
<!-- Герой секция -->
<div class="hero">
<h1>Найди работу мечты</h1>
<p>Тысячи вакансий от ведущих компаний и резюме лучших специалистов на одной платформе</p>
<div class="hero-buttons">
<a href="/register" class="btn btn-primary">
<i class="fas fa-rocket"></i> Начать карьеру
</a>
<a href="/vacancies" class="btn btn-secondary">
<i class="fas fa-search"></i> Смотреть вакансии
</a>
</div>
</div>
<!-- Статистика -->
<div class="stats">
<div class="stat-card" onclick="window.location.href='/vacancies'">
<i class="fas fa-briefcase"></i>
<div class="stat-number" id="vacanciesCount">0</div>
<div class="stat-label">активных вакансий</div>
</div>
<div class="stat-card" onclick="window.location.href='/resumes'">
<i class="fas fa-users"></i>
<div class="stat-number" id="employeesCount">0</div>
<div class="stat-label">соискателей</div>
</div>
<div class="stat-card" onclick="window.location.href='/companies'">
<i class="fas fa-building"></i>
<div class="stat-number" id="companiesCount">0</div>
<div class="stat-label">компаний</div>
</div>
</div>
<!-- Последние вакансии -->
<div class="section-header">
<h2><i class="fas fa-fire"></i> Горячие вакансии</h2>
<a href="/vacancies">Все вакансии <i class="fas fa-arrow-right"></i></a>
</div>
<div class="cards-grid" id="recentVacancies">
<div class="loader"><i class="fas fa-spinner fa-spin spinner"></i> Загрузка вакансий...</div>
</div>
<!-- Последние резюме -->
<div class="section-header">
<h2><i class="fas fa-file-alt"></i> Новые резюме</h2>
<a href="/resumes">Все резюме <i class="fas fa-arrow-right"></i></a>
</div>
<div class="cards-grid" id="recentResumes">
<div class="loader"><i class="fas fa-spinner fa-spin spinner"></i> Загрузка резюме...</div>
</div>
<!-- Преимущества -->
<div class="features">
<h2>Почему выбирают Rabota.Today</h2>
<div class="features-grid">
<div class="feature-item">
<i class="fas fa-bolt"></i>
<h3>Быстрый отклик</h3>
<p>Мгновенное уведомление работодателя о вашем интересе</p>
</div>
<div class="feature-item">
<i class="fas fa-shield-alt"></i>
<h3>Безопасность</h3>
<p>Ваши персональные данные надежно защищены</p>
</div>
<div class="feature-item">
<i class="fas fa-mobile-alt"></i>
<h3>Удобный доступ</h3>
<p>Работает на всех устройствах — компьютер, планшет, телефон</p>
</div>
<div class="feature-item">
<i class="fas fa-heart"></i>
<h3>Бесплатно</h3>
<p>Для соискателей все функции абсолютно бесплатны</p>
</div>
</div>
</div>
<!-- Призыв к действию -->
<div class="cta">
<h2>Готовы начать?</h2>
<p>Присоединяйтесь к <span id="totalUsers">тысячам</span> соискателей и работодателей уже сегодня</p>
<a href="/register" class="btn btn-primary">
<i class="fas fa-user-plus"></i> Создать аккаунт
</a>
</div>
<!-- Подвал -->
<div class="footer">
© 2026 Rabota.Today - Ярмарка вакансий. Все права защищены. |
<a href="/terms">Пользовательское соглашение</a> |
<a href="/privacy">Политика конфиденциальности</a>
</div>
</div>
<!-- Уведомления -->
<div class="notification" id="notification"></div>
<script>
const API_BASE_URL = window.location.protocol + '//' + window.location.host + '/api';
let currentUser = null;
// Форматирование чисел
function formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}
// Плавное обновление цифр
function animateNumber(element, finalNumber, duration = 1000) {
const startNumber = parseInt(element.textContent) || 0;
const startTime = performance.now();
const updateNumber = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Функция easing для плавности
const easeOutQuart = 1 - Math.pow(1 - progress, 3);
const currentNumber = Math.floor(startNumber + (finalNumber - startNumber) * easeOutQuart);
element.textContent = formatNumber(currentNumber);
if (progress < 1) {
requestAnimationFrame(updateNumber);
} else {
element.textContent = formatNumber(finalNumber);
}
};
requestAnimationFrame(updateNumber);
}
// Показать уведомление
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);
}
// Проверка авторизации
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();
updateNavForAuth();
}
} catch (error) {
console.error('Auth check error:', error);
}
}
}
// Обновление навигации для авторизованных
function updateNavForAuth() {
if (!currentUser) return;
const nav = document.getElementById('nav');
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">Резюме</a>
<a href="/favorites">Избранное</a>
<a href="/applications">Отклики</a>
<a href="/profile" class="login-btn profile-btn">
<i class="fas fa-user-circle"></i> ${firstName} ${adminBadge}
</a>
`;
}
// Загрузка статистики
async function loadStats() {
try {
// Загружаем общую статистику с сервера
const statsResponse = await fetch(`${API_BASE_URL}/public/stats`);
if (statsResponse.ok) {
const stats = await statsResponse.json();
// Анимируем цифры
animateNumber(document.getElementById('vacanciesCount'), stats.active_vacancies || 1234);
animateNumber(document.getElementById('employeesCount'), stats.total_employees || 5678); // Изменено
animateNumber(document.getElementById('companiesCount'), stats.total_employers || 500);
// Обновляем текст в CTA секции
if (stats.total_users) {
document.getElementById('totalUsers').textContent = formatNumber(stats.total_users) + ' ' +
(stats.total_users > 1000 ? 'тысяч' : '');
}
} else {
// Если публичная статистика не работает, используем отдельные запросы
console.log('📊 Используем отдельные запросы для статистики');
const [vacResponse, employeesResponse] = await Promise.all([
fetch(`${API_BASE_URL}/vacancies/all?page=1&limit=1`),
fetch(`${API_BASE_URL}/users/count?role=employee`) // Новый эндпоинт
]);
const vacData = await vacResponse.json();
const employeesData = await employeesResponse.json();
animateNumber(document.getElementById('vacanciesCount'), vacData.total || 1234);
animateNumber(document.getElementById('employeesCount'), employeesData.count || 5678);
document.getElementById('companiesCount').textContent = '500+';
}
} catch (error) {
console.error('❌ Ошибка загрузки статистики:', error);
// Заглушки на случай ошибки
document.getElementById('vacanciesCount').textContent = '1,234';
document.getElementById('employeesCount').textContent = '5,678';
document.getElementById('companiesCount').textContent = '500+';
}
}
// Загрузка последних вакансий
async function loadRecentVacancies() {
try {
const response = await fetch(`${API_BASE_URL}/vacancies/all?page=1&limit=3`);
const data = await response.json();
const container = document.getElementById('recentVacancies');
if (!data.vacancies || data.vacancies.length === 0) {
container.innerHTML = '<div class="loader">Нет активных вакансий</div>';
return;
}
container.innerHTML = data.vacancies.map(v => `
<div class="card" onclick="window.location.href='/vacancy/${v.id}'">
<div class="card-title">${escapeHtml(v.title)}</div>
<div class="card-subtitle">
<i class="fas fa-building"></i> ${escapeHtml(v.company_name || 'Компания')}
</div>
<div class="card-salary">${escapeHtml(v.salary || 'Зарплата не указана')}</div>
<div class="card-tags">
${(v.tags || []).map(t => `<span class="tag">${escapeHtml(t.name)}</span>`).join('')}
</div>
<div class="card-footer">
<span><i class="fas fa-eye"></i> ${v.views || 0}</span>
<span><i class="fas fa-calendar"></i> ${new Date(v.created_at).toLocaleDateString('ru-RU')}</span>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading vacancies:', error);
document.getElementById('recentVacancies').innerHTML = '<div class="loader">Ошибка загрузки вакансий</div>';
}
}
// Загрузка последних резюме
async function loadRecentResumes() {
try {
const response = await fetch(`${API_BASE_URL}/resumes/all?page=1&limit=3`);
const data = await response.json();
const container = document.getElementById('recentResumes');
if (!data.resumes || data.resumes.length === 0) {
container.innerHTML = '<div class="loader">Нет резюме</div>';
return;
}
container.innerHTML = data.resumes.map(r => `
<div class="card" onclick="window.location.href='/resume/${r.id}'">
<div class="card-title">${escapeHtml(r.full_name)}</div>
<div class="card-subtitle">
<i class="fas fa-briefcase"></i> ${escapeHtml(r.desired_position || 'Должность не указана')}
</div>
<div class="card-salary">${escapeHtml(r.desired_salary || 'Зарплата не указана')}</div>
<div class="card-tags">
${(r.tags || []).map(t => `<span class="tag">${escapeHtml(t.name)}</span>`).join('')}
</div>
<div class="card-footer">
<span><i class="fas fa-briefcase"></i> ${r.experience_count || 0} мест</span>
<span><i class="fas fa-eye"></i> ${r.views || 0}</span>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading resumes:', error);
document.getElementById('recentResumes').innerHTML = '<div class="loader">Ошибка загрузки резюме</div>';
}
}
// Загрузка трендов (изменение за последний месяц)
async function loadTrends() {
try {
// Здесь можно добавить логику для загрузки трендов
// Например, сравнивать с данными месяц назад
} catch (error) {
console.error('Error loading trends:', error);
}
}
// Экранирование 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;");
}
// Выход
function logout() {
localStorage.removeItem('accessToken');
window.location.reload();
}
// Обновление статистики в реальном времени (каждые 30 секунд)
function startAutoRefresh() {
setInterval(() => {
loadStats();
loadRecentVacancies();
loadRecentResumes();
}, 30000);
}
// Инициализация
window.addEventListener('load', () => {
checkAuth();
loadStats();
loadRecentVacancies();
loadRecentResumes();
loadTrends();
startAutoRefresh();
});
</script>
</body>
</html>