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

947 lines
31 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, maximum-scale=1.0, user-scalable=yes">
<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;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(145deg, #0b1c34 0%, #1a3650 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 12px;
}
/* Шапка */
.header {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 30px;
padding: 15px 20px;
margin-bottom: 20px;
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.logo {
display: flex;
align-items: center;
gap: 10px;
}
.logo i {
font-size: 28px;
color: #3b82f6;
background: rgba(59, 130, 246, 0.2);
padding: 8px;
border-radius: 16px;
}
.logo span {
font-size: 20px;
font-weight: 700;
color: white;
}
.nav {
display: flex;
gap: 8px;
}
.nav a {
color: white;
text-decoration: none;
padding: 8px 12px;
border-radius: 30px;
font-size: 14px;
font-weight: 500;
transition: 0.2s;
white-space: nowrap;
}
.nav a:hover {
background: rgba(255, 255, 255, 0.1);
}
.nav .login-btn {
background: #3b82f6;
color: white;
}
/* Мобильное меню */
.mobile-menu {
display: none;
}
@media (max-width: 480px) {
.nav {
display: none;
}
.mobile-menu {
display: block;
position: relative;
}
.mobile-menu-btn {
background: rgba(255, 255, 255, 0.1);
border: none;
color: white;
font-size: 24px;
width: 44px;
height: 44px;
border-radius: 22px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.mobile-menu-dropdown {
position: absolute;
top: 50px;
right: 0;
background: white;
border-radius: 20px;
padding: 10px;
min-width: 180px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
display: none;
z-index: 1000;
}
.mobile-menu-dropdown.active {
display: block;
animation: fadeIn 0.2s;
}
.mobile-menu-dropdown a {
display: block;
padding: 12px 16px;
color: #0b1c34;
text-decoration: none;
font-weight: 500;
border-radius: 12px;
}
.mobile-menu-dropdown a:hover {
background: #f0f7ff;
}
.mobile-menu-dropdown .login-btn {
background: #3b82f6;
color: white;
}
}
/* Анимации */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes countUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Герой секция */
.hero {
background: white;
border-radius: 40px;
padding: 30px 20px;
margin-bottom: 25px;
text-align: center;
box-shadow: 0 15px 30px rgba(0,0,0,0.2);
}
.hero h1 {
font-size: clamp(28px, 8vw, 42px);
font-weight: 800;
color: #0b1c34;
margin-bottom: 10px;
line-height: 1.2;
}
.hero p {
font-size: clamp(14px, 4vw, 18px);
color: #4f7092;
margin-bottom: 25px;
padding: 0 10px;
}
.hero-buttons {
display: flex;
flex-direction: column;
gap: 12px;
max-width: 300px;
margin: 0 auto;
}
.btn {
padding: 16px 24px;
border-radius: 40px;
font-size: 16px;
font-weight: 700;
text-decoration: none;
text-align: center;
transition: 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
-webkit-tap-highlight-color: transparent;
}
.btn:active {
transform: scale(0.97);
}
.btn-primary {
background: #0b1c34;
color: white;
}
.btn-primary i {
color: #3b82f6;
}
.btn-secondary {
background: #e8f0fe;
color: #0b1c34;
border: 2px solid #3b82f6;
}
/* Статистика */
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 25px;
}
.stat-card {
background: white;
border-radius: 30px;
padding: 20px 12px;
text-align: center;
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
transition: 0.2s;
cursor: pointer;
animation: countUp 0.5s ease-out;
}
.stat-card:active {
transform: translateY(-2px);
}
.stat-card i {
font-size: 24px;
color: #3b82f6;
margin-bottom: 8px;
}
.stat-number {
font-size: clamp(20px, 6vw, 28px);
font-weight: 800;
color: #0b1c34;
line-height: 1.2;
margin-bottom: 4px;
}
.stat-label {
font-size: 11px;
color: #4f7092;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Секции с карточками */
.section-title {
display: flex;
justify-content: space-between;
align-items: center;
margin: 25px 0 15px;
}
.section-title h2 {
font-size: 20px;
color: white;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.section-title h2 i {
color: #3b82f6;
}
.section-title a {
color: #9bb8da;
text-decoration: none;
font-size: 14px;
padding: 8px 12px;
background: rgba(255,255,255,0.05);
border-radius: 30px;
}
.cards-grid {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
}
.card {
background: white;
border-radius: 30px;
padding: 20px;
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
cursor: pointer;
transition: 0.2s;
border-left: 5px solid #3b82f6;
}
.card:active {
transform: translateX(5px);
}
.card-title {
font-size: 18px;
font-weight: 700;
color: #0b1c34;
margin-bottom: 5px;
}
.card-subtitle {
color: #3b82f6;
font-size: 14px;
margin-bottom: 10px;
}
.card-salary {
font-size: 16px;
font-weight: 700;
color: #0f2b4f;
margin: 10px 0;
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 10px 0;
}
.tag {
background: #eef4fa;
padding: 4px 10px;
border-radius: 20px;
font-size: 11px;
color: #1f3f60;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #dee9f5;
font-size: 12px;
color: #4f7092;
}
/* Преимущества */
.features {
background: white;
border-radius: 40px;
padding: 30px 20px;
margin: 30px 0;
}
.features h2 {
text-align: center;
color: #0b1c34;
margin-bottom: 25px;
font-size: 22px;
}
.features-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.feature-item {
text-align: center;
}
.feature-item i {
font-size: 32px;
color: #3b82f6;
background: #eef4fa;
width: 60px;
height: 60px;
line-height: 60px;
border-radius: 30px;
margin-bottom: 10px;
}
.feature-item h3 {
font-size: 14px;
color: #0b1c34;
margin-bottom: 4px;
}
.feature-item p {
font-size: 11px;
color: #4f7092;
}
/* Призыв к действию */
.cta {
background: linear-gradient(135deg, #0b1c34 0%, #1f4b8a 100%);
border-radius: 40px;
padding: 30px 20px;
margin: 30px 0;
text-align: center;
color: white;
}
.cta h2 {
font-size: 24px;
margin-bottom: 10px;
}
.cta p {
color: #9bb8da;
margin-bottom: 20px;
}
.cta .btn-primary {
background: #3b82f6;
display: inline-flex;
max-width: 250px;
margin: 0 auto;
}
/* Подвал */
.footer {
text-align: center;
padding: 25px 0;
color: #9bb8da;
font-size: 13px;
border-top: 1px solid rgba(255,255,255,0.1);
}
.footer a {
color: white;
text-decoration: none;
}
/* Лоадер */
.loader {
text-align: center;
padding: 30px;
color: white;
}
.spinner {
animation: spin 1s linear infinite;
display: inline-block;
margin-right: 8px;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Уведомления */
.notification {
position: fixed;
top: 20px;
right: 20px;
left: 20px;
background: white;
border-radius: 30px;
padding: 15px 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
display: none;
z-index: 2000;
animation: slideDown 0.3s;
}
@keyframes slideDown {
from { transform: translateY(-100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.notification.success {
background: #10b981;
color: white;
}
.notification.error {
background: #ef4444;
color: white;
}
.notification.info {
background: #3b82f6;
color: white;
}
/* Стили для авторизованных */
.admin-badge {
background: #f59e0b;
color: white;
padding: 2px 6px;
border-radius: 12px;
font-size: 10px;
margin-left: 4px;
}
.profile-link {
display: flex;
align-items: center;
gap: 5px;
}
</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 class="mobile-menu">
<button class="mobile-menu-btn" onclick="toggleMobileMenu()">
<i class="fas fa-bars"></i>
</button>
<div class="mobile-menu-dropdown" id="mobileMenu">
<a href="/">Главная</a>
<a href="/vacancies">Вакансии</a>
<a href="/resumes">Резюме</a>
<a href="/login" class="login-btn">Войти</a>
<a href="/register">Регистрация</a>
</div>
</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> <!-- Изменено с resumesCount -->
<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-title">
<h2><i class="fas fa-fire"></i> Горячие вакансии</h2>
<a href="/vacancies">Все</a>
</div>
<div class="cards-grid" id="recentVacancies">
<div class="loader"><i class="fas fa-spinner fa-spin"></i> Загрузка...</div>
</div>
<!-- Последние резюме -->
<div class="section-title">
<h2><i class="fas fa-file-alt"></i> Новые резюме</h2>
<a href="/resumes">Все</a>
</div>
<div class="cards-grid" id="recentResumes">
<div class="loader"><i class="fas fa-spinner fa-spin"></i> Загрузка...</div>
</div>
<!-- Преимущества -->
<div class="features">
<h2>Почему выбирают нас</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 - Ярмарка вакансий
</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);
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 toggleMobileMenu() {
const menu = document.getElementById('mobileMenu');
menu.classList.toggle('active');
}
document.addEventListener('click', function(event) {
const menu = document.getElementById('mobileMenu');
const btn = document.querySelector('.mobile-menu-btn');
if (btn && !btn.contains(event.target) && !menu.contains(event.target)) {
menu.classList.remove('active');
}
});
// ========== ФУНКЦИИ ДЛЯ УВЕДОМЛЕНИЙ ==========
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();
} else {
localStorage.removeItem('accessToken');
}
} catch (error) {
console.error('Auth check error:', error);
}
}
}
function updateNavForAuth() {
if (!currentUser) return;
const nav = document.querySelector('.nav');
const mobileMenu = document.getElementById('mobileMenu');
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="/profile" class="login-btn profile-link">
<i class="fas fa-user-circle"></i> ${firstName} ${adminBadge}
</a>
`;
// Обновляем мобильное меню
mobileMenu.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-link">
<i class="fas fa-user-circle"></i> ${firstName} ${adminBadge}
</a>
<a href="#" onclick="logout()">Выйти</a>
`;
}
function logout() {
localStorage.removeItem('accessToken');
window.location.reload();
}
// ========== ЗАГРУЗКА СТАТИСТИКИ ==========
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) {
const totalUsersEl = document.getElementById('totalUsers');
if (totalUsersEl) {
totalUsersEl.textContent = formatNumber(stats.total_users) + ' ' +
(stats.total_users > 1000 ? 'тысяч' : '');
}
}
console.log('✅ Статистика загружена:', stats);
} 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">${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()}</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">${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>';
}
}
// ========== ЭКРАНИРОВАНИЕ 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 startAutoRefresh() {
// Обновляем статистику каждые 30 секунд
setInterval(() => {
loadStats();
loadRecentVacancies();
loadRecentResumes();
}, 30000);
}
// ========== ИНИЦИАЛИЗАЦИЯ ==========
window.addEventListener('load', async () => {
console.log('🚀 Мобильная главная страница загружена');
// Проверяем авторизацию
await checkAuth();
// Загружаем данные
await Promise.all([
loadStats(),
loadRecentVacancies(),
loadRecentResumes()
]);
// Запускаем автообновление
startAutoRefresh();
});
</script>
</body>
</html>