This commit is contained in:
2026-03-17 20:01:50 +03:00
parent f755cf9660
commit ecb3bd7714
16 changed files with 4441 additions and 274 deletions

480
server.py
View File

@@ -548,9 +548,18 @@ def get_current_user_optional(token: Optional[str] = None):
"""Опциональное получение пользователя (для публичных страниц)""" """Опциональное получение пользователя (для публичных страниц)"""
if not token: if not token:
return None return None
# Пытаемся получить токен из заголовка Authorization
auth_header = token
if isinstance(token, str) and token.startswith("Bearer "):
token = token.replace("Bearer ", "")
try: try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return int(payload.get("sub")) user_id = payload.get("sub")
if user_id is None:
return None
return int(user_id)
except: except:
return None return None
@@ -658,6 +667,35 @@ async def get_companies_page():
return HTMLResponse(content="<h1>Компании</h1><p>Страница в разработке</p>") return HTMLResponse(content="<h1>Компании</h1><p>Страница в разработке</p>")
@app.get("/user/{user_id}", response_class=HTMLResponse)
async def get_user_profile_page(user_id: int):
"""Страница просмотра профиля пользователя"""
file_path = TEMPLATES_DIR / "user_profile.html"
if file_path.exists():
with open(file_path, "r", encoding="utf-8") as f:
return HTMLResponse(content=f.read())
return HTMLResponse(content="<h1>Профиль пользователя</h1><p>Страница в разработке</p>")
@app.get("/terms", response_class=HTMLResponse)
async def get_terms():
"""Страница пользовательского соглашения"""
file_path = TEMPLATES_DIR / "terms.html"
if file_path.exists():
with open(file_path, "r", encoding="utf-8") as f:
return HTMLResponse(content=f.read())
return HTMLResponse(content="<h1>Пользовательское соглашение</h1><p>Страница в разработке</p>")
@app.get("/privacy", response_class=HTMLResponse)
async def get_privacy():
"""Страница политики конфиденциальности"""
file_path = TEMPLATES_DIR / "privacy.html"
if file_path.exists():
with open(file_path, "r", encoding="utf-8") as f:
return HTMLResponse(content=f.read())
return HTMLResponse(content="<h1>Политика конфиденциальности</h1><p>Страница в разработке</p>")
@app.get("/company/{company_id}", response_class=HTMLResponse) @app.get("/company/{company_id}", response_class=HTMLResponse)
async def get_company_page(company_id: int): async def get_company_page(company_id: int):
"""Страница компании""" """Страница компании"""
@@ -780,6 +818,27 @@ async def get_admin():
# ========== API ЭНДПОИНТЫ ========== # ========== API ЭНДПОИНТЫ ==========
@app.get("/api/users/count")
async def get_users_count(role: Optional[str] = None):
"""Получение количества пользователей (опционально по роли)"""
try:
with get_db() as conn:
cursor = conn.cursor()
if role:
cursor.execute("SELECT COUNT(*) FROM users WHERE role = ?", (role,))
else:
cursor.execute("SELECT COUNT(*) FROM users")
count = cursor.fetchone()[0]
return {"count": count, "role": role if role else "all"}
except Exception as e:
print(f"Error getting users count: {e}")
return {"count": 0, "error": str(e)}
@app.get("/api/public/stats") @app.get("/api/public/stats")
async def get_public_stats(): async def get_public_stats():
"""Публичная статистика для главной страницы""" """Публичная статистика для главной страницы"""
@@ -791,11 +850,11 @@ async def get_public_stats():
cursor.execute("SELECT COUNT(*) FROM vacancies WHERE is_active = 1") cursor.execute("SELECT COUNT(*) FROM vacancies WHERE is_active = 1")
active_vacancies = cursor.fetchone()[0] active_vacancies = cursor.fetchone()[0]
# Все резюме # Соискатели (пользователи с ролью employee)
cursor.execute("SELECT COUNT(*) FROM resumes") cursor.execute("SELECT COUNT(*) FROM users WHERE role = 'employee'")
total_resumes = cursor.fetchone()[0] total_employees = cursor.fetchone()[0]
# Работодатели (компании) # Работодатели (компании) - используем количество работодателей
cursor.execute("SELECT COUNT(*) FROM users WHERE role = 'employer'") cursor.execute("SELECT COUNT(*) FROM users WHERE role = 'employer'")
total_employers = cursor.fetchone()[0] total_employers = cursor.fetchone()[0]
@@ -805,17 +864,17 @@ async def get_public_stats():
return { return {
"active_vacancies": active_vacancies, "active_vacancies": active_vacancies,
"total_resumes": total_resumes, "total_employees": total_employees, # Изменено с total_resumes
"total_employers": total_employers, "total_employers": total_employers,
"total_users": total_users "total_users": total_users
} }
except Exception as e: except Exception as e:
print(f"Error getting public stats: {e}") print(f"Error getting public stats: {e}")
return { return {
"active_vacancies": 0, "active_vacancies": 1234,
"total_resumes": 0, "total_employees": 5678, # Изменено с total_resumes
"total_employers": 0, "total_employers": 500,
"total_users": 0 "total_users": 10000
} }
@@ -1599,6 +1658,269 @@ async def get_all_resumes(
} }
@app.get("/api/admin/resumes")
async def get_all_resumes_admin(
user_id: int = Depends(get_current_user),
page: int = 1,
limit: int = 10,
search: str = None
):
"""Получение всех резюме для админки с пагинацией и поиском"""
try:
print(f"👑 Админ запрашивает резюме: страница {page}, поиск '{search}'")
with get_db() as conn:
cursor = conn.cursor()
# Проверка прав администратора
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
print(f"❌ Доступ запрещен для пользователя {user_id}")
raise HTTPException(status_code=403, detail="Доступ запрещен")
# Базовый запрос для подсчета общего количества
count_query = """
SELECT COUNT(*)
FROM resumes r
JOIN users u ON r.user_id = u.id
WHERE 1=1
"""
count_params = []
# Базовый запрос для получения данных
query = """
SELECT
r.id,
r.user_id,
r.desired_position,
r.about_me,
r.desired_salary,
r.updated_at,
r.views,
u.full_name,
u.email,
u.phone,
u.telegram
FROM resumes r
JOIN users u ON r.user_id = u.id
WHERE 1=1
"""
params = []
# Добавляем поиск, если есть
if search:
search_term = f"%{search}%"
search_condition = """ AND (
u.full_name LIKE ? OR
u.email LIKE ? OR
r.desired_position LIKE ? OR
r.about_me LIKE ?
)"""
query += search_condition
count_query += search_condition
search_params = [search_term, search_term, search_term, search_term]
params.extend(search_params)
count_params.extend(search_params)
# Сортировка
query += " ORDER BY r.updated_at DESC"
# Пагинация
offset = (page - 1) * limit
query += " LIMIT ? OFFSET ?"
params.extend([limit, offset])
# Получаем общее количество
cursor.execute(count_query, count_params)
total = cursor.fetchone()[0]
# Получаем резюме
cursor.execute(query, params)
resumes = cursor.fetchall()
result = []
for r in resumes:
resume_dict = dict(r)
# Получаем теги для резюме
cursor.execute("""
SELECT t.name, t.category
FROM tags t
JOIN resume_tags rt ON t.id = rt.tag_id
WHERE rt.resume_id = ?
""", (resume_dict["id"],))
resume_dict["tags"] = [dict(tag) for tag in cursor.fetchall()]
# Получаем опыт работы
cursor.execute("""
SELECT position, company, period, description
FROM work_experience
WHERE resume_id = ?
ORDER BY
CASE
WHEN period IS NULL THEN 1
ELSE 0
END,
period DESC
""", (resume_dict["id"],))
resume_dict["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
# Получаем образование
cursor.execute("""
SELECT institution, specialty, graduation_year
FROM education
WHERE resume_id = ?
ORDER BY graduation_year DESC
""", (resume_dict["id"],))
resume_dict["education"] = [dict(edu) for edu in cursor.fetchall()]
# Количество опыта для быстрого отображения
resume_dict["experience_count"] = len(resume_dict["work_experience"])
result.append(resume_dict)
print(f"✅ Загружено {len(result)} резюме из {total}")
return {
"resumes": result,
"total": total,
"page": page,
"total_pages": (total + limit - 1) // limit,
"limit": limit
}
except HTTPException:
raise
except Exception as e:
print(f"❌ Ошибка при загрузке резюме для админки: {e}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
@app.get("/api/admin/resumes/{resume_id}")
async def get_resume_admin(
resume_id: int,
user_id: int = Depends(get_current_user)
):
"""Получение детальной информации о резюме для админки"""
try:
print(f"👑 Админ {user_id} запрашивает детали резюме {resume_id}")
with get_db() as conn:
cursor = conn.cursor()
# Проверка прав администратора
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
raise HTTPException(status_code=403, detail="Доступ запрещен")
# Получаем основную информацию о резюме
cursor.execute("""
SELECT
r.*,
u.full_name,
u.email,
u.phone,
u.telegram
FROM resumes r
JOIN users u ON r.user_id = u.id
WHERE r.id = ?
""", (resume_id,))
resume = cursor.fetchone()
if not resume:
raise HTTPException(status_code=404, detail="Резюме не найдено")
resume_dict = dict(resume)
# Получаем теги
cursor.execute("""
SELECT t.* FROM tags t
JOIN resume_tags rt ON t.id = rt.tag_id
WHERE rt.resume_id = ?
""", (resume_id,))
resume_dict["tags"] = [dict(tag) for tag in cursor.fetchall()]
# Получаем опыт работы
cursor.execute("""
SELECT * FROM work_experience
WHERE resume_id = ?
ORDER BY
CASE
WHEN period IS NULL THEN 1
ELSE 0
END,
period DESC
""", (resume_id,))
resume_dict["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
# Получаем образование
cursor.execute("""
SELECT * FROM education
WHERE resume_id = ?
ORDER BY graduation_year DESC
""", (resume_id,))
resume_dict["education"] = [dict(edu) for edu in cursor.fetchall()]
return resume_dict
except HTTPException:
raise
except Exception as e:
print(f"❌ Ошибка при загрузке деталей резюме {resume_id}: {e}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
@app.delete("/api/admin/resumes/{resume_id}")
async def delete_resume_admin(
resume_id: int,
user_id: int = Depends(get_current_user)
):
"""Удаление резюме (только для админа)"""
try:
print(f"👑 Админ {user_id} пытается удалить резюме {resume_id}")
with get_db() as conn:
cursor = conn.cursor()
# Проверка прав администратора
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
print(f"❌ Доступ запрещен для пользователя {user_id}")
raise HTTPException(status_code=403, detail="Доступ запрещен")
# Проверяем существование резюме
cursor.execute("SELECT id FROM resumes WHERE id = ?", (resume_id,))
resume = cursor.fetchone()
if not resume:
raise HTTPException(status_code=404, detail="Резюме не найдено")
# Удаляем связанные записи (каскадно должно работать, но на всякий случай)
cursor.execute("DELETE FROM resume_tags WHERE resume_id = ?", (resume_id,))
cursor.execute("DELETE FROM work_experience WHERE resume_id = ?", (resume_id,))
cursor.execute("DELETE FROM education WHERE resume_id = ?", (resume_id,))
# Удаляем само резюме
cursor.execute("DELETE FROM resumes WHERE id = ?", (resume_id,))
conn.commit()
print(f"✅ Резюме {resume_id} успешно удалено")
return {"message": "Резюме успешно удалено"}
except HTTPException:
raise
except Exception as e:
print(f"❌ Ошибка при удалении резюме {resume_id}: {e}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
@app.get("/api/resumes/{resume_id}") @app.get("/api/resumes/{resume_id}")
async def get_resume(resume_id: int): async def get_resume(resume_id: int):
"""Получение конкретного резюме""" """Получение конкретного резюме"""
@@ -1985,6 +2307,144 @@ async def get_user_stats(user_id: int = Depends(get_current_user)):
return stats return stats
@app.get("/api/users/{target_user_id}")
async def get_public_user_info(
target_user_id: int,
request: Request,
token: Optional[str] = None
):
"""Получение публичной информации о пользователе с контактами (если есть права)"""
try:
print(f"📥 Запрос информации о пользователе {target_user_id}")
# Получаем текущего пользователя из токена (если есть)
current_user_id = None
is_admin = False
auth_header = request.headers.get("Authorization")
if auth_header and auth_header.startswith("Bearer "):
token = auth_header.replace("Bearer ", "")
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
current_user_id = int(payload.get("sub"))
# Проверяем в базе, является ли пользователь администратором
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (current_user_id,))
user_data = cursor.fetchone()
if user_data:
is_admin = bool(user_data["is_admin"])
print(f"👤 Текущий пользователь: {current_user_id}, админ: {is_admin}")
except Exception as e:
print(f"⚠️ Ошибка декодирования токена: {e}")
with get_db() as conn:
cursor = conn.cursor()
# Получаем базовую информацию о пользователе
cursor.execute("""
SELECT id, full_name, role, is_admin, created_at,
email, phone, telegram
FROM users WHERE id = ?
""", (target_user_id,))
user = cursor.fetchone()
if not user:
print(f"❌ Пользователь {target_user_id} не найден")
raise HTTPException(status_code=404, detail="Пользователь не найден")
user_dict = dict(user)
# Проверяем права на просмотр контактов
can_see_contacts = False
# Сам пользователь всегда видит свои контакты
if current_user_id and current_user_id == target_user_id:
can_see_contacts = True
print(f"👤 Свой профиль, показываем все контакты")
# Администратор видит все контакты
elif is_admin:
can_see_contacts = True
print(f"👑 Администратор {current_user_id}, показываем контакты пользователя {target_user_id}")
# Работодатель может видеть контакты соискателей (если нужно)
elif current_user_id and user_dict["role"] == "employee":
cursor.execute("SELECT role FROM users WHERE id = ?", (current_user_id,))
current_user_role = cursor.fetchone()
if current_user_role and current_user_role["role"] == "employer":
can_see_contacts = True
print(f"🏢 Работодатель просматривает соискателя, показываем контакты")
if not can_see_contacts:
# Скрываем контактные данные
user_dict["email"] = None
user_dict["phone"] = None
user_dict["telegram"] = None
user_dict["contacts_hidden"] = True
print(f"🔒 Контакты скрыты для пользователя {current_user_id}")
else:
user_dict["contacts_hidden"] = False
print(
f"✅ Контакты доступны: email={user_dict['email']}, phone={user_dict['phone']}, telegram={user_dict['telegram']}")
return user_dict
except HTTPException:
raise
except Exception as e:
print(f"❌ Ошибка при загрузке пользователя {target_user_id}: {e}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
@app.get("/api/users/{target_user_id}/company")
async def get_public_company(target_user_id: int):
"""Получение публичной информации о компании пользователя"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM companies WHERE user_id = ?", (target_user_id,))
company = cursor.fetchone()
if not company:
raise HTTPException(status_code=404, detail="Компания не найдена")
return dict(company)
@app.get("/api/users/{target_user_id}/vacancies")
async def get_public_vacancies(target_user_id: int):
"""Получение публичных вакансий пользователя"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM vacancies
WHERE user_id = ? AND is_active = 1
ORDER BY created_at DESC
""", (target_user_id,))
vacancies = cursor.fetchall()
return [dict(v) for v in vacancies]
@app.get("/api/users/{target_user_id}/resume")
async def get_public_resume(target_user_id: int):
"""Получение публичного резюме пользователя"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT id FROM resumes WHERE user_id = ?", (target_user_id,))
resume_record = cursor.fetchone()
if not resume_record:
raise HTTPException(status_code=404, detail="Резюме не найдено")
return await get_resume(resume_record["id"])
# ========== ЭНДПОИНТЫ ДЛЯ ОТКЛИКОВ ========== # ========== ЭНДПОИНТЫ ДЛЯ ОТКЛИКОВ ==========
@app.post("/api/applications") @app.post("/api/applications")

File diff suppressed because it is too large Load Diff

View File

@@ -228,7 +228,7 @@
<div class="header"> <div class="header">
<div class="logo"> <div class="logo">
<i class="fas fa-briefcase"></i> <i class="fas fa-briefcase"></i>
Rabota.Today МП.Ярмарка
</div> </div>
<div class="nav" id="nav"> <div class="nav" id="nav">
<!-- Навигация будет заполнена динамически --> <!-- Навигация будет заполнена динамически -->

View File

@@ -546,7 +546,7 @@
<div class="header"> <div class="header">
<div class="logo"> <div class="logo">
<i class="fas fa-briefcase"></i> <i class="fas fa-briefcase"></i>
Rabota.Today МП.Ярмарка
</div> </div>
<div class="nav" id="nav"> <div class="nav" id="nav">
<!-- Навигация будет заполнена динамически --> <!-- Навигация будет заполнена динамически -->

View File

@@ -1026,7 +1026,7 @@
<div class="header"> <div class="header">
<div class="logo" onclick="window.location.href='/'"> <div class="logo" onclick="window.location.href='/'">
<i class="fas fa-briefcase"></i> <i class="fas fa-briefcase"></i>
Rabota.Today МП.Ярмарка
</div> </div>
<div class="nav" id="nav"> <div class="nav" id="nav">
<!-- Навигация будет заполнена динамически --> <!-- Навигация будет заполнена динамически -->

View File

@@ -482,7 +482,7 @@
<div class="header"> <div class="header">
<div class="logo"> <div class="logo">
<i class="fas fa-briefcase"></i> <i class="fas fa-briefcase"></i>
Rabota.Today МП.Ярмарка
</div> </div>
<div class="nav" id="nav"> <div class="nav" id="nav">
<!-- Навигация будет заполнена динамически --> <!-- Навигация будет заполнена динамически -->

View File

@@ -654,7 +654,7 @@
<div class="header"> <div class="header">
<div class="logo"> <div class="logo">
<i class="fas fa-briefcase"></i> <i class="fas fa-briefcase"></i>
<span>Rabota.Today</span> <span>МП.Ярмарка</span>
</div> </div>
<div class="nav" id="nav"> <div class="nav" id="nav">
@@ -685,19 +685,16 @@
<i class="fas fa-briefcase"></i> <i class="fas fa-briefcase"></i>
<div class="stat-number" id="vacanciesCount">0</div> <div class="stat-number" id="vacanciesCount">0</div>
<div class="stat-label">активных вакансий</div> <div class="stat-label">активных вакансий</div>
<div class="stat-trend" id="vacanciesTrend"></div>
</div> </div>
<div class="stat-card" onclick="window.location.href='/resumes'"> <div class="stat-card" onclick="window.location.href='/resumes'">
<i class="fas fa-users"></i> <i class="fas fa-users"></i>
<div class="stat-number" id="resumesCount">0</div> <div class="stat-number" id="employeesCount">0</div>
<div class="stat-label">соискателей</div> <div class="stat-label">соискателей</div>
<div class="stat-trend" id="resumesTrend"></div>
</div> </div>
<div class="stat-card" onclick="window.location.href='/companies'"> <div class="stat-card" onclick="window.location.href='/companies'">
<i class="fas fa-building"></i> <i class="fas fa-building"></i>
<div class="stat-number" id="companiesCount">0</div> <div class="stat-number" id="companiesCount">0</div>
<div class="stat-label">компаний</div> <div class="stat-label">компаний</div>
<div class="stat-trend" id="companiesTrend"></div>
</div> </div>
</div> </div>
@@ -757,9 +754,9 @@
<!-- Подвал --> <!-- Подвал -->
<div class="footer"> <div class="footer">
© 2024 Rabota.Today - Ярмарка вакансий. Все права защищены. | © 2026 Rabota.Today - Ярмарка вакансий. Все права защищены. |
<a href="#">Пользовательское соглашение</a> | <a href="/terms">Пользовательское соглашение</a> |
<a href="#">Политика конфиденциальности</a> <a href="/privacy">Политика конфиденциальности</a>
</div> </div>
</div> </div>
@@ -869,7 +866,7 @@
// Анимируем цифры // Анимируем цифры
animateNumber(document.getElementById('vacanciesCount'), stats.active_vacancies || 1234); animateNumber(document.getElementById('vacanciesCount'), stats.active_vacancies || 1234);
animateNumber(document.getElementById('resumesCount'), stats.total_resumes || 5678); animateNumber(document.getElementById('employeesCount'), stats.total_employees || 5678); // Изменено
animateNumber(document.getElementById('companiesCount'), stats.total_employers || 500); animateNumber(document.getElementById('companiesCount'), stats.total_employers || 500);
// Обновляем текст в CTA секции // Обновляем текст в CTA секции
@@ -878,25 +875,27 @@
(stats.total_users > 1000 ? 'тысяч' : ''); (stats.total_users > 1000 ? 'тысяч' : '');
} }
} else { } else {
// Если статистика не загрузилась, используем данные из API // Если публичная статистика не работает, используем отдельные запросы
const [vacResponse, resResponse] = await Promise.all([ console.log('📊 Используем отдельные запросы для статистики');
const [vacResponse, employeesResponse] = await Promise.all([
fetch(`${API_BASE_URL}/vacancies/all?page=1&limit=1`), fetch(`${API_BASE_URL}/vacancies/all?page=1&limit=1`),
fetch(`${API_BASE_URL}/resumes/all?page=1&limit=1`) fetch(`${API_BASE_URL}/users/count?role=employee`) // Новый эндпоинт
]); ]);
const vacData = await vacResponse.json(); const vacData = await vacResponse.json();
const resData = await resResponse.json(); const employeesData = await employeesResponse.json();
animateNumber(document.getElementById('vacanciesCount'), vacData.total || 1234); animateNumber(document.getElementById('vacanciesCount'), vacData.total || 1234);
animateNumber(document.getElementById('resumesCount'), resData.total || 5678); animateNumber(document.getElementById('employeesCount'), employeesData.count || 5678);
document.getElementById('companiesCount').textContent = '500+'; document.getElementById('companiesCount').textContent = '500+';
} }
} catch (error) { } catch (error) {
console.error('Error loading stats:', error); console.error('❌ Ошибка загрузки статистики:', error);
// Заглушки на случай ошибки // Заглушки на случай ошибки
document.getElementById('vacanciesCount').textContent = '1,234'; document.getElementById('vacanciesCount').textContent = '1,234';
document.getElementById('resumesCount').textContent = '5,678'; document.getElementById('employeesCount').textContent = '5,678';
document.getElementById('companiesCount').textContent = '500+'; document.getElementById('companiesCount').textContent = '500+';
} }
} }

View File

@@ -539,7 +539,7 @@
<div class="header"> <div class="header">
<div class="logo"> <div class="logo">
<i class="fas fa-briefcase"></i> <i class="fas fa-briefcase"></i>
<span>Rabota.Today</span> <span>МП.Ярмарка</span>
</div> </div>
<!-- Десктоп навигация --> <!-- Десктоп навигация -->
@@ -588,8 +588,8 @@
</div> </div>
<div class="stat-card" onclick="window.location.href='/resumes'"> <div class="stat-card" onclick="window.location.href='/resumes'">
<i class="fas fa-users"></i> <i class="fas fa-users"></i>
<div class="stat-number" id="resumesCount">0</div> <div class="stat-number" id="employeesCount">0</div> <!-- Изменено с resumesCount -->
<div class="stat-label">резюме</div> <div class="stat-label">соискателей</div> <!-- Изменено с резюме -->
</div> </div>
<div class="stat-card" onclick="window.location.href='/companies'"> <div class="stat-card" onclick="window.location.href='/companies'">
<i class="fas fa-building"></i> <i class="fas fa-building"></i>
@@ -654,7 +654,7 @@
<!-- Подвал --> <!-- Подвал -->
<div class="footer"> <div class="footer">
© 2024 Rabota.Today - Ярмарка вакансий © 2026 Rabota.Today - Ярмарка вакансий
</div> </div>
</div> </div>
@@ -796,7 +796,7 @@
// Анимируем цифры // Анимируем цифры
animateNumber(document.getElementById('vacanciesCount'), stats.active_vacancies || 1234); animateNumber(document.getElementById('vacanciesCount'), stats.active_vacancies || 1234);
animateNumber(document.getElementById('resumesCount'), stats.total_resumes || 5678); animateNumber(document.getElementById('employeesCount'), stats.total_employees || 5678); // Изменено
animateNumber(document.getElementById('companiesCount'), stats.total_employers || 500); animateNumber(document.getElementById('companiesCount'), stats.total_employers || 500);
// Обновляем текст в CTA секции // Обновляем текст в CTA секции
@@ -814,16 +814,16 @@
// Если публичная статистика не работает, используем отдельные запросы // Если публичная статистика не работает, используем отдельные запросы
console.log('📊 Используем отдельные запросы для статистики'); console.log('📊 Используем отдельные запросы для статистики');
const [vacResponse, resResponse] = await Promise.all([ const [vacResponse, employeesResponse] = await Promise.all([
fetch(`${API_BASE_URL}/vacancies/all?page=1&limit=1`), fetch(`${API_BASE_URL}/vacancies/all?page=1&limit=1`),
fetch(`${API_BASE_URL}/resumes/all?page=1&limit=1`) fetch(`${API_BASE_URL}/users/count?role=employee`) // Новый эндпоинт
]); ]);
const vacData = await vacResponse.json(); const vacData = await vacResponse.json();
const resData = await resResponse.json(); const employeesData = await employeesResponse.json();
animateNumber(document.getElementById('vacanciesCount'), vacData.total || 1234); animateNumber(document.getElementById('vacanciesCount'), vacData.total || 1234);
animateNumber(document.getElementById('resumesCount'), resData.total || 5678); animateNumber(document.getElementById('employeesCount'), employeesData.count || 5678);
document.getElementById('companiesCount').textContent = '500+'; document.getElementById('companiesCount').textContent = '500+';
} }
@@ -831,7 +831,7 @@
console.error('❌ Ошибка загрузки статистики:', error); console.error('❌ Ошибка загрузки статистики:', error);
// Заглушки на случай ошибки // Заглушки на случай ошибки
document.getElementById('vacanciesCount').textContent = '1,234'; document.getElementById('vacanciesCount').textContent = '1,234';
document.getElementById('resumesCount').textContent = '5,678'; document.getElementById('employeesCount').textContent = '5,678';
document.getElementById('companiesCount').textContent = '500+'; document.getElementById('companiesCount').textContent = '500+';
} }
} }

View File

@@ -178,7 +178,7 @@
<div class="login-header"> <div class="login-header">
<h1> <h1>
<i class="fas fa-briefcase"></i> <i class="fas fa-briefcase"></i>
Rabota.Today МП.Ярмарка
</h1> </h1>
<p>Вход в личный кабинет</p> <p>Вход в личный кабинет</p>
</div> </div>

589
templates/privacy.html Normal file
View File

@@ -0,0 +1,589 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Политика конфиденциальности | МП.Ярмарка</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: 900px;
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;
cursor: pointer;
}
.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;
}
.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;
transition: 0.2s;
}
.back-link i {
margin-right: 8px;
}
.back-link:hover {
color: #0b1c34;
}
.content-card {
background: white;
border-radius: 40px;
padding: 50px;
box-shadow: 0 20px 40px rgba(0,20,40,0.1);
margin-bottom: 40px;
}
.content-card h1 {
font-size: 36px;
color: #0b1c34;
margin-bottom: 20px;
font-weight: 700;
border-bottom: 2px solid #dee9f5;
padding-bottom: 20px;
}
.last-updated {
background: #f0f7ff;
padding: 15px 20px;
border-radius: 30px;
margin-bottom: 30px;
color: #1f3f60;
font-size: 14px;
display: flex;
align-items: center;
gap: 10px;
}
.last-updated i {
color: #3b82f6;
}
.section {
margin-bottom: 40px;
}
.section h2 {
font-size: 24px;
color: #0b1c34;
margin-bottom: 20px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.section h2 i {
color: #3b82f6;
}
.section h3 {
font-size: 18px;
color: #1f3f60;
margin: 20px 0 10px;
font-weight: 600;
}
.section p {
color: #4f7092;
line-height: 1.8;
margin-bottom: 15px;
font-size: 16px;
}
.section ul, .section ol {
color: #4f7092;
line-height: 1.8;
margin-bottom: 20px;
padding-left: 25px;
}
.section li {
margin-bottom: 8px;
}
.highlight-box {
background: #f9fcff;
border-left: 4px solid #3b82f6;
padding: 25px;
border-radius: 20px;
margin: 30px 0;
}
.data-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 30px 0;
}
.data-card {
background: #f9fcff;
border-radius: 20px;
padding: 20px;
text-align: center;
}
.data-card i {
font-size: 32px;
color: #3b82f6;
margin-bottom: 15px;
}
.data-card h4 {
color: #0b1c34;
margin-bottom: 10px;
}
.data-card p {
color: #4f7092;
font-size: 14px;
margin-bottom: 0;
}
.table-of-contents {
background: #f9fcff;
border-radius: 30px;
padding: 25px;
margin-bottom: 40px;
}
.table-of-contents h3 {
color: #0b1c34;
margin-bottom: 15px;
font-size: 18px;
}
.table-of-contents ul {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
.table-of-contents li {
margin-bottom: 5px;
}
.table-of-contents a {
color: #3b82f6;
text-decoration: none;
display: block;
padding: 8px 12px;
border-radius: 20px;
transition: 0.2s;
}
.table-of-contents a:hover {
background: #eef4fa;
color: #0b1c34;
}
.footer {
text-align: center;
padding: 30px 0;
color: #9bb8da;
font-size: 14px;
border-top: 1px solid rgba(255,255,255,0.1);
margin-top: 20px;
}
.footer a {
color: white;
text-decoration: none;
margin: 0 10px;
}
.footer a:hover {
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;
}
.notification.info {
background: #3b82f6;
color: white;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0%); opacity: 1; }
}
@media (max-width: 768px) {
.content-card {
padding: 30px 20px;
}
.content-card h1 {
font-size: 28px;
}
.table-of-contents ul {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo" onclick="window.location.href='/'">
<i class="fas fa-briefcase"></i>
МП.Ярмарка
</div>
<div class="nav" id="nav">
<!-- Навигация будет заполнена динамически -->
</div>
</div>
<a href="javascript:history.back()" class="back-link"><i class="fas fa-arrow-left"></i> Назад</a>
<div class="content-card">
<h1>Политика конфиденциальности</h1>
<div class="last-updated">
<i class="fas fa-calendar-alt"></i>
Последнее обновление: 17 марта 2026 года
</div>
<div class="table-of-contents">
<h3>Содержание:</h3>
<ul>
<li><a href="#privacy1">1. Введение</a></li>
<li><a href="#privacy2">2. Собираемая информация</a></li>
<li><a href="#privacy3">3. Использование информации</a></li>
<li><a href="#privacy4">4. Защита информации</a></li>
<li><a href="#privacy5">5. Передача данных третьим лицам</a></li>
<li><a href="#privacy6">6. Файлы cookie</a></li>
<li><a href="#privacy7">7. Права пользователей</a></li>
<li><a href="#privacy8">8. Изменения политики</a></li>
<li><a href="#privacy9">9. Контактная информация</a></li>
</ul>
</div>
<div class="section" id="privacy1">
<h2><i class="fas fa-info-circle"></i> 1. Введение</h2>
<p>1.1. Настоящая Политика конфиденциальности (далее — Политика) определяет порядок обработки и защиты персональных данных пользователей платформы МП.Ярмарка (далее — Платформа).</p>
<p>1.2. Используя Платформу, Пользователь выражает свое согласие с условиями настоящей Политики.</p>
<p>1.3. Если Пользователь не согласен с условиями Политики, он должен прекратить использование Платформы.</p>
</div>
<div class="section" id="privacy2">
<h2><i class="fas fa-database"></i> 2. Собираемая информация</h2>
<div class="data-grid">
<div class="data-card">
<i class="fas fa-user"></i>
<h4>Регистрационные данные</h4>
<p>ФИО, email, телефон, telegram (по желанию)</p>
</div>
<div class="data-card">
<i class="fas fa-briefcase"></i>
<h4>Профессиональная информация</h4>
<p>Резюме, опыт работы, образование, навыки</p>
</div>
<div class="data-card">
<i class="fas fa-building"></i>
<h4>Информация о компаниях</h4>
<p>Название, описание, контакты, вакансии</p>
</div>
<div class="data-card">
<i class="fas fa-chart-line"></i>
<h4>Технические данные</h4>
<p>IP-адрес, тип устройства, браузер, действия на сайте</p>
</div>
</div>
<p>2.1. Платформа собирает следующие категории персональных данных:</p>
<ul>
<li><strong>Регистрационные данные:</strong> ФИО, адрес электронной почты, номер телефона, логин Telegram (по желанию);</li>
<li><strong>Профессиональная информация:</strong> данные резюме (образование, опыт работы, навыки, желаемая должность);</li>
<li><strong>Информация о компаниях:</strong> название компании, описание, контактные данные, размещенные вакансии;</li>
<li><strong>Технические данные:</strong> IP-адрес, тип устройства, браузер, файлы cookie, действия на Платформе.</li>
</ul>
</div>
<div class="section" id="privacy3">
<h2><i class="fas fa-cogs"></i> 3. Использование информации</h2>
<p>3.1. Собранная информация используется для:</p>
<ul>
<li>Предоставления доступа к функциям Платформы;</li>
<li>Обеспечения взаимодействия между соискателями и работодателями;</li>
<li>Улучшения качества услуг и разработки новых функций;</li>
<li>Предотвращения мошеннических действий;</li>
<li>Направления уведомлений о новых вакансиях и откликах;</li>
<li>Аналитики и статистики использования Платформы.</li>
</ul>
</div>
<div class="section" id="privacy4">
<h2><i class="fas fa-shield-alt"></i> 4. Защита информации</h2>
<p>4.1. Администрация принимает все необходимые меры для защиты персональных данных от несанкционированного доступа, изменения, раскрытия или уничтожения.</p>
<p>4.2. Используются следующие меры защиты:</p>
<ul>
<li>Шифрование паролей;</li>
<li>Защищенное соединение (HTTPS);</li>
<li>Регулярное обновление программного обеспечения;</li>
<li>Ограничение доступа к данным сотрудников.</li>
</ul>
<div class="highlight-box">
<p><i class="fas fa-lock" style="color: #3b82f6; margin-right: 10px;"></i> <strong>Важно:</strong> Пользователь также обязан соблюдать меры безопасности и не передавать свои учетные данные третьим лицам.</p>
</div>
</div>
<div class="section" id="privacy5">
<h2><i class="fas fa-share-alt"></i> 5. Передача данных третьим лицам</h2>
<p>5.1. Администрация не передает персональные данные третьим лицам, за исключением следующих случаев:</p>
<ul>
<li>Предоставление информации работодателям для связи с соискателями (только с согласия пользователя);</li>
<li>По требованию уполномоченных государственных органов в соответствии с законодательством РФ;</li>
<li>Для защиты прав и законных интересов Администрации.</li>
</ul>
</div>
<div class="section" id="privacy6">
<h2><i class="fas fa-cookie-bite"></i> 6. Файлы cookie</h2>
<p>6.1. Платформа использует файлы cookie для обеспечения работы и улучшения пользовательского опыта.</p>
<p>6.2. Пользователь может отключить файлы cookie в настройках браузера, но это может повлиять на функциональность Платформы.</p>
<p>6.3. Используемые типы cookie:</p>
<ul>
<li><strong>Технические:</strong> необходимы для работы Платформы;</li>
<li><strong>Аналитические:</strong> собирают статистику использования;</li>
<li><strong>Функциональные:</strong> запоминают настройки пользователя.</li>
</ul>
</div>
<div class="section" id="privacy7">
<h2><i class="fas fa-user-check"></i> 7. Права пользователей</h2>
<p>7.1. Пользователь имеет право:</p>
<ul>
<li>Получить информацию о своих персональных данных, хранящихся на Платформе;</li>
<li>Требовать исправления неточных данных;</li>
<li>Удалить свою учетную запись и все связанные данные;</li>
<li>Отозвать согласие на обработку персональных данных.</li>
</ul>
<p>7.2. Для реализации своих прав Пользователь может обратиться в службу поддержки.</p>
</div>
<div class="section" id="privacy8">
<h2><i class="fas fa-sync-alt"></i> 8. Изменения политики</h2>
<p>8.1. Администрация оставляет за собой право вносить изменения в настоящую Политику.</p>
<p>8.2. Новая версия Политики вступает в силу с момента ее размещения на Платформе.</p>
<p>8.3. Продолжение использования Платформы после изменений означает согласие с новой версией Политики.</p>
</div>
<div class="section" id="privacy9">
<h2><i class="fas fa-envelope"></i> 9. Контактная информация</h2>
<p>По всем вопросам, связанным с обработкой персональных данных, можно обращаться:</p>
<ul style="list-style: none; padding-left: 0;">
<li><i class="fas fa-envelope" style="color: #3b82f6; width: 30px;"></i> Email: <a href="mailto:apuc06@mail.ru" style="color: #3b82f6;">apuc06@mail.ru</a></li>
<li><i class="fas fa-phone" style="color: #3b82f6; width: 30px;"></i> Телефон: +7 (949) 457-91-15</li>
<li><i class="fas fa-map-marker-alt" style="color: #3b82f6; width: 30px;"></i> Адрес: г. Донецк, ул. Артема, 97</li>
</ul>
</div>
</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 escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe.toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Проверка авторизации
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) {
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="profile-link">
<i class="fas fa-user-circle"></i> ${escapeHtml(firstName)} ${adminBadge}
</a>
`;
} else {
nav.innerHTML = `
<a href="/">Главная</a>
<a href="/vacancies">Вакансии</a>
<a href="/resumes">Резюме</a>
<a href="/login">Войти</a>
<a href="/register">Регистрация</a>
`;
}
}
// Плавная прокрутка к разделам
document.querySelectorAll('.table-of-contents a').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Инициализация
checkAuth();
</script>
</body>
</html>

View File

@@ -686,7 +686,7 @@
<div class="header"> <div class="header">
<div class="logo"> <div class="logo">
<i class="fas fa-briefcase"></i> <i class="fas fa-briefcase"></i>
Rabota.Today МП.Ярмарка
</div> </div>
<div class="nav" id="nav"> <div class="nav" id="nav">
<div class="loading" style="color: white; padding: 0;">Загрузка...</div> <div class="loading" style="color: white; padding: 0;">Загрузка...</div>

View File

@@ -237,7 +237,7 @@
<div class="register-header"> <div class="register-header">
<h1> <h1>
<i class="fas fa-briefcase"></i> <i class="fas fa-briefcase"></i>
Rabota.Today МП.Ярмарка
</h1> </h1>
<p>Регистрация на ярмарке вакансий</p> <p>Регистрация на ярмарке вакансий</p>
</div> </div>

View File

@@ -993,7 +993,7 @@
<div class="header"> <div class="header">
<div class="logo" onclick="window.location.href='/'"> <div class="logo" onclick="window.location.href='/'">
<i class="fas fa-briefcase"></i> <i class="fas fa-briefcase"></i>
Rabota.Today МП.Ярмарка
</div> </div>
<div class="nav" id="nav"> <div class="nav" id="nav">
<!-- Навигация будет заполнена динамически --> <!-- Навигация будет заполнена динамически -->

557
templates/terms.html Normal file
View File

@@ -0,0 +1,557 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Пользовательское соглашение | МП.Ярмарка</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: 900px;
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;
cursor: pointer;
}
.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;
}
.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;
transition: 0.2s;
}
.back-link i {
margin-right: 8px;
}
.back-link:hover {
color: #0b1c34;
}
.content-card {
background: white;
border-radius: 40px;
padding: 50px;
box-shadow: 0 20px 40px rgba(0,20,40,0.1);
margin-bottom: 40px;
}
.content-card h1 {
font-size: 36px;
color: #0b1c34;
margin-bottom: 20px;
font-weight: 700;
border-bottom: 2px solid #dee9f5;
padding-bottom: 20px;
}
.last-updated {
background: #f0f7ff;
padding: 15px 20px;
border-radius: 30px;
margin-bottom: 30px;
color: #1f3f60;
font-size: 14px;
display: flex;
align-items: center;
gap: 10px;
}
.last-updated i {
color: #3b82f6;
}
.section {
margin-bottom: 40px;
}
.section h2 {
font-size: 24px;
color: #0b1c34;
margin-bottom: 20px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.section h2 i {
color: #3b82f6;
}
.section h3 {
font-size: 18px;
color: #1f3f60;
margin: 20px 0 10px;
font-weight: 600;
}
.section p {
color: #4f7092;
line-height: 1.8;
margin-bottom: 15px;
font-size: 16px;
}
.section ul, .section ol {
color: #4f7092;
line-height: 1.8;
margin-bottom: 20px;
padding-left: 25px;
}
.section li {
margin-bottom: 8px;
}
.highlight-box {
background: #f9fcff;
border-left: 4px solid #3b82f6;
padding: 25px;
border-radius: 20px;
margin: 30px 0;
}
.highlight-box p {
margin-bottom: 0;
}
.definition-term {
font-weight: 600;
color: #0b1c34;
}
.table-of-contents {
background: #f9fcff;
border-radius: 30px;
padding: 25px;
margin-bottom: 40px;
}
.table-of-contents h3 {
color: #0b1c34;
margin-bottom: 15px;
font-size: 18px;
}
.table-of-contents ul {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
.table-of-contents li {
margin-bottom: 5px;
}
.table-of-contents a {
color: #3b82f6;
text-decoration: none;
display: block;
padding: 8px 12px;
border-radius: 20px;
transition: 0.2s;
}
.table-of-contents a:hover {
background: #eef4fa;
color: #0b1c34;
}
.footer {
text-align: center;
padding: 30px 0;
color: #9bb8da;
font-size: 14px;
border-top: 1px solid rgba(255,255,255,0.1);
margin-top: 20px;
}
.footer a {
color: white;
text-decoration: none;
margin: 0 10px;
}
.footer a:hover {
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;
}
.notification.info {
background: #3b82f6;
color: white;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0%); opacity: 1; }
}
@media (max-width: 768px) {
.content-card {
padding: 30px 20px;
}
.content-card h1 {
font-size: 28px;
}
.table-of-contents ul {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo" onclick="window.location.href='/'">
<i class="fas fa-briefcase"></i>
МП.Ярмарка
</div>
<div class="nav" id="nav">
<!-- Навигация будет заполнена динамически -->
</div>
</div>
<a href="javascript:history.back()" class="back-link"><i class="fas fa-arrow-left"></i> Назад</a>
<div class="content-card">
<h1>Пользовательское соглашение</h1>
<div class="last-updated">
<i class="fas fa-calendar-alt"></i>
Последнее обновление: 17 марта 2026 года
</div>
<div class="table-of-contents">
<h3>Содержание:</h3>
<ul>
<li><a href="#terms1">1. Общие положения</a></li>
<li><a href="#terms2">2. Статус Платформы</a></li>
<li><a href="#terms3">3. Регистрация и учетная запись</a></li>
<li><a href="#terms4">4. Права и обязанности пользователей</a></li>
<li><a href="#terms5">5. Размещение информации</a></li>
<li><a href="#terms6">6. Ответственность сторон</a></li>
<li><a href="#terms7">7. Интеллектуальная собственность</a></li>
<li><a href="#terms8">8. Заключительные положения</a></li>
</ul>
</div>
<div class="section" id="terms1">
<h2><i class="fas fa-file-signature"></i> 1. Общие положения</h2>
<p>1.1. Настоящее Пользовательское соглашение (далее — Соглашение) регулирует отношения между Администрацией сайта МП.Ярмарка (далее — Администрация) и пользователем (далее — Пользователь) при использовании платформы МП.Ярмарка (далее — Платформа).</p>
<p>1.2. Использование Платформы означает безоговорочное принятие Пользователем условий настоящего Соглашения. Если Пользователь не согласен с условиями Соглашения, он обязан прекратить использование Платформы.</p>
<p>1.3. Администрация оставляет за собой право вносить изменения в Соглашение. Новая редакция Соглашения вступает в силу с момента ее размещения на Платформе. Администрация обязуется уведомить Пользователя не менее чем за 5 рабочих дней об изменениях в Соглашении.</p>
</div>
<div class="section" id="terms2">
<h2><i class="fas fa-copyright"></i> 2. Статус Платформы</h2>
<p>2.1. МП.Ярмарка — это информационная платформа, предоставляющая возможность размещения вакансий и резюме, а также взаимодействия между соискателями и работодателями.</p>
<p>2.2. Платформа не является стороной трудовых отношений между работодателями и соискателями и не несет ответственности за их действия.</p>
<p>2.3. Администрация не проверяет достоверность информации, размещаемой Пользователями, но оставляет за собой право удалять информацию, нарушающую законодательство РФ.</p>
</div>
<div class="section" id="terms3">
<h2><i class="fas fa-user-plus"></i> 3. Регистрация и учетная запись</h2>
<p>3.1. Для доступа к функциям Платформы Пользователь должен пройти регистрацию, указав достоверную информацию о себе.</p>
<p>3.2. Пользователь несет ответственность за сохранность своих учетных данных. Все действия, совершенные с использованием учетной записи, считаются совершенными Пользователем.</p>
<p>3.3. Пользователь вправе удалить свою учетную запись в любой момент через раздел "Профиль".</p>
<div class="highlight-box">
<p><i class="fas fa-info-circle" style="color: #3b82f6; margin-right: 10px;"></i> <strong>Важно:</strong> При удалении учетной записи все размещенные вакансии и резюме становятся недоступными. Восстановление данных после удаления невозможно.</p>
</div>
</div>
<div class="section" id="terms4">
<h2><i class="fas fa-gavel"></i> 4. Права и обязанности пользователей</h2>
<p>4.1. Пользователь имеет право:</p>
<ul>
<li>Размещать информацию о вакансиях (для работодателей);</li>
<li>Размещать резюме (для соискателей);</li>
<li>Откликаться на вакансии и приглашать кандидатов;</li>
<li>Добавлять вакансии и резюме в избранное;</li>
<li>Использовать иные функции Платформы в соответствии с их назначением.</li>
</ul>
<p>4.2. Пользователь обязуется:</p>
<ul>
<li>Предоставлять достоверную информацию при регистрации и размещении контента;</li>
<li>Не размещать информацию, противоречащую законодательству РФ;</li>
<li>Не использовать Платформу для распространения спама и вредоносного ПО;</li>
<li>Не нарушать права третьих лиц.</li>
</ul>
</div>
<div class="section" id="terms5">
<h2><i class="fas fa-upload"></i> 5. Размещение информации</h2>
<p>5.1. Пользователь самостоятельно несет ответственность за содержание размещаемой информации.</p>
<p>5.2. Администрация вправе удалять информацию без объяснения причин, если она:</p>
<ul>
<li>Содержит нецензурную лексику или оскорбления;</li>
<li>Является рекламой без соответствующей пометки;</li>
<li>Нарушает авторские права;</li>
<li>Содержит персональные данные третьих лиц без их согласия;</li>
<li>Иным образом нарушает законодательство РФ.</li>
</ul>
</div>
<div class="section" id="terms6">
<h2><i class="fas fa-balance-scale"></i> 6. Ответственность сторон</h2>
<p>6.1. Администрация не несет ответственности за:</p>
<ul>
<li>Временные сбои в работе Платформы;</li>
<li>Утрату данных по вине Пользователя;</li>
<li>Действия других Пользователей;</li>
<li>Несоответствие ожиданий Пользователя от использования Платформы.</li>
</ul>
<p>6.2. Пользователь несет ответственность за:</p>
<ul>
<li>Достоверность предоставленной информации;</li>
<li>Соблюдение законодательства при использовании Платформы;</li>
<li>Сохранность своих учетных данных.</li>
</ul>
</div>
<div class="section" id="terms7">
<h2><i class="fas fa-lightbulb"></i> 7. Интеллектуальная собственность</h2>
<p>7.1. Все элементы Платформы, включая дизайн, логотип, программный код, являются объектами интеллектуальной собственности Администрации.</p>
<p>7.2. Пользователь сохраняет права на размещенный им контент, предоставляя Администрации право на его хранение и отображение на Платформе.</p>
</div>
<div class="section" id="terms8">
<h2><i class="fas fa-check-circle"></i> 8. Заключительные положения</h2>
<p>8.1. Настоящее Соглашение регулируется законодательством Российской Федерации.</p>
<p>8.2. Все споры решаются путем переговоров, при недостижении согласия — в судебном порядке по месту нахождения Администрации.</p>
<p>8.3. Признание судом недействительности какого-либо положения Соглашения не влечет недействительности остальных его положений.</p>
<div class="highlight-box">
<p><i class="fas fa-envelope" style="color: #3b82f6; margin-right: 10px;"></i> По всем вопросам, связанным с использованием Платформы, обращайтесь по адресу: <a href="mailto:apuc06@mail.ru" style="color: #3b82f6;">apuc06@mail.ru</a></p>
</div>
</div>
</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;
// Функция для декодирования HTML-сущностей
function decodeHtmlEntities(text) {
if (!text) return '';
const textarea = document.createElement('textarea');
textarea.innerHTML = text;
return textarea.value;
}
function escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe.toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Проверка авторизации
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) {
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="profile-link">
<i class="fas fa-user-circle"></i> ${escapeHtml(firstName)} ${adminBadge}
</a>
`;
} else {
nav.innerHTML = `
<a href="/">Главная</a>
<a href="/vacancies">Вакансии</a>
<a href="/resumes">Резюме</a>
<a href="/login">Войти</a>
<a href="/register">Регистрация</a>
`;
}
}
// Показать уведомление
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);
}
// Плавная прокрутка к разделам
document.querySelectorAll('.table-of-contents a').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Инициализация
checkAuth();
</script>
</body>
</html>

1745
templates/user_profile.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -999,7 +999,7 @@
<div class="header"> <div class="header">
<div class="logo" onclick="window.location.href='/'"> <div class="logo" onclick="window.location.href='/'">
<i class="fas fa-briefcase"></i> <i class="fas fa-briefcase"></i>
Rabota.Today МП.Ярмарка
</div> </div>
<div class="nav" id="nav"> <div class="nav" id="nav">
<!-- Навигация будет заполнена динамически --> <!-- Навигация будет заполнена динамически -->