Files
yarmarka/templates/profile.html
2026-03-12 19:23:54 +03:00

820 lines
30 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, #eef5fa 0%, #e0eaf5 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
background: #0b1c34;
color: white;
padding: 20px 40px;
border-radius: 40px;
margin-bottom: 40px;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 28px;
font-weight: 700;
display: flex;
align-items: center;
gap: 15px;
}
.logo i {
color: #3b82f6;
background: rgba(255,255,255,0.1);
padding: 12px;
border-radius: 20px;
}
.nav {
display: flex;
gap: 15px;
align-items: center;
}
.nav a {
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 30px;
}
.nav a:hover {
background: rgba(255,255,255,0.1);
}
.nav .logout-btn {
background: rgba(255,255,255,0.1);
cursor: pointer;
}
.profile-grid {
display: grid;
grid-template-columns: 300px 1fr;
gap: 30px;
}
.profile-sidebar {
background: white;
border-radius: 40px;
padding: 30px;
text-align: center;
box-shadow: 0 20px 40px rgba(0,20,40,0.1);
height: fit-content;
}
.avatar {
width: 120px;
height: 120px;
background: #eef4fa;
border-radius: 60px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
font-size: 48px;
color: #3b82f6;
}
.profile-name {
font-size: 24px;
font-weight: 700;
color: #0b1c34;
margin-bottom: 5px;
}
.profile-role {
color: #3b82f6;
font-weight: 600;
margin-bottom: 20px;
}
.profile-stats {
display: flex;
justify-content: center;
gap: 20px;
margin: 20px 0;
padding: 20px 0;
border-top: 1px solid #dee9f5;
border-bottom: 1px solid #dee9f5;
}
.stat {
text-align: center;
}
.stat-value {
font-size: 20px;
font-weight: 700;
color: #0b1c34;
}
.stat-label {
font-size: 12px;
color: #4f7092;
}
.contact-info {
text-align: left;
margin-top: 20px;
}
.contact-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
color: #1f3f60;
}
.contact-item i {
color: #3b82f6;
width: 20px;
}
.profile-content {
background: white;
border-radius: 40px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0,20,40,0.1);
}
.content-tabs {
display: flex;
gap: 20px;
margin-bottom: 30px;
border-bottom: 2px solid #dee9f5;
padding-bottom: 15px;
}
.content-tab {
padding: 10px 20px;
cursor: pointer;
font-weight: 600;
color: #4f7092;
border-radius: 30px;
transition: 0.2s;
}
.content-tab.active {
background: #eef4fa;
color: #0b1c34;
}
.content-pane {
display: none;
}
.content-pane.active {
display: block;
}
.btn {
padding: 12px 24px;
border-radius: 30px;
border: none;
font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: 0.2s;
}
.btn-primary {
background: #0b1c34;
color: white;
}
.btn-primary:hover {
background: #1b3f6b;
}
.btn-outline {
background: transparent;
border: 2px solid #3b82f6;
color: #0b1c34;
}
.btn-outline:hover {
background: #eef4fa;
}
.vacancies-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.vacancy-card {
background: #f9fcff;
border: 1px solid #dee9f5;
border-radius: 30px;
padding: 25px;
transition: 0.2s;
}
.vacancy-card:hover {
background: white;
box-shadow: 0 10px 30px rgba(0,20,40,0.1);
}
.vacancy-card h3 {
color: #0b1c34;
margin-bottom: 10px;
}
.vacancy-salary {
color: #3b82f6;
font-weight: 700;
font-size: 18px;
margin: 10px 0;
}
.resume-section {
background: #f9fcff;
border-radius: 30px;
padding: 30px;
}
.resume-field {
margin-bottom: 20px;
}
.resume-field label {
display: block;
font-weight: 600;
color: #1f3f60;
margin-bottom: 5px;
}
.resume-field input,
.resume-field textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid #dee9f5;
border-radius: 20px;
font-size: 16px;
}
.resume-field input:focus,
.resume-field textarea:focus {
border-color: #3b82f6;
outline: none;
}
.exp-item, .edu-item {
background: white;
border-radius: 20px;
padding: 20px;
margin-bottom: 15px;
border: 1px solid #dee9f5;
}
.loading {
text-align: center;
padding: 40px;
color: #4f7092;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">
<i class="fas fa-briefcase"></i>
Rabota.Today
</div>
<div class="nav">
<a href="/">Главная</a>
<a href="/vacancies">Вакансии</a>
<a href="/resumes">Резюме</a>
<a href="/profile" style="background: #3b82f6;">Профиль</a>
<span class="logout-btn" onclick="logout()">
<i class="fas fa-sign-out-alt"></i> Выйти
</span>
</div>
</div>
<div class="profile-grid">
<!-- Боковая панель -->
<div class="profile-sidebar">
<div class="avatar">
<i class="fas fa-user-circle"></i>
</div>
<div class="profile-name" id="profileName">Загрузка...</div>
<div class="profile-role" id="profileRole"></div>
<div class="profile-stats">
<div class="stat">
<div class="stat-value" id="profileViews">0</div>
<div class="stat-label">просмотров</div>
</div>
<div class="stat">
<div class="stat-value" id="profileFavorites">0</div>
<div class="stat-label">в избранном</div>
</div>
</div>
<div class="contact-info">
<div class="contact-item">
<i class="fas fa-envelope"></i>
<span id="profileEmail"></span>
</div>
<div class="contact-item">
<i class="fas fa-phone"></i>
<span id="profilePhone"></span>
</div>
<div class="contact-item">
<i class="fab fa-telegram"></i>
<span id="profileTelegram"></span>
</div>
</div>
</div>
<!-- Основной контент -->
<div class="profile-content">
<div class="content-tabs">
<div class="content-tab active" onclick="switchContentTab('main')">Главное</div>
<div class="content-tab" id="employerTab" onclick="switchContentTab('vacancies')">Вакансии</div>
<div class="content-tab" id="employeeTab" onclick="switchContentTab('resume')">Резюме</div>
</div>
<!-- Главная вкладка -->
<div class="content-pane active" id="mainPane">
<h2 style="margin-bottom: 20px;">Добро пожаловать!</h2>
<p style="color: #4f7092; margin-bottom: 30px;">Здесь будет ваша активность и статистика</p>
<div style="background: #f9fcff; border-radius: 30px; padding: 30px;">
<h3>Быстрые действия</h3>
<div style="display: flex; gap: 15px; margin-top: 20px;">
<button class="btn btn-primary" onclick="window.location.href='/vacancies'">
<i class="fas fa-search"></i> Найти вакансии
</button>
<button class="btn btn-outline" onclick="window.location.href='/resumes'">
<i class="fas fa-users"></i> Найти сотрудников
</button>
</div>
</div>
</div>
<!-- Вкладка с вакансиями -->
<div class="content-pane" id="vacanciesPane">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>Мои вакансии</h2>
<button class="btn btn-primary" onclick="openVacancyModal()">
<i class="fas fa-plus"></i> Создать вакансию
</button>
</div>
<div id="vacanciesList" class="vacancies-grid">
<div class="loading">Загрузка вакансий...</div>
</div>
</div>
<!-- Вкладка с резюме -->
<div class="content-pane" id="resumePane">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>Мое резюме</h2>
<button class="btn btn-primary" onclick="saveResume()">
<i class="fas fa-save"></i> Сохранить
</button>
</div>
<div class="resume-section">
<div class="resume-field">
<label>Желаемая должность</label>
<input type="text" id="desiredPosition" placeholder="Например: Frontend-разработчик">
</div>
<div class="resume-field">
<label>О себе</label>
<textarea id="aboutMe" rows="4" placeholder="Расскажите о своем опыте"></textarea>
</div>
<div class="resume-field">
<label>Желаемая зарплата</label>
<input type="text" id="desiredSalary" placeholder="от 200 000 ₽">
</div>
<!-- 👇 ДОБАВЬТЕ ЭТО ПОЛЕ ДЛЯ ТЕГОВ 👇 -->
<div class="resume-field">
<label>Навыки (теги через запятую)</label>
<input type="text" id="resumeTags" placeholder="Python, JavaScript, React, SQL">
</div>
<!-- 👆 КОНЕЦ ДОБАВЛЕНИЯ 👆 -->
<h3 style="margin: 30px 0 20px;">Опыт работы</h3>
<div id="experienceContainer"></div>
<button class="btn btn-outline" onclick="addExperience()" style="margin-bottom: 30px;">
<i class="fas fa-plus"></i> Добавить опыт
</button>
<h3 style="margin: 20px 0;">Образование</h3>
<div id="educationContainer"></div>
<button class="btn btn-outline" onclick="addEducation()">
<i class="fas fa-plus"></i> Добавить образование
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Модальное окно создания вакансии -->
<div id="vacancyModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); align-items: center; justify-content: center; z-index: 1000;">
<div style="background: white; max-width: 500px; width: 90%; border-radius: 40px; padding: 40px;">
<h3 style="margin-bottom: 20px;">Новая вакансия</h3>
<div class="resume-field">
<label>Название</label>
<input type="text" id="vacancyTitle" placeholder="Менеджер по продажам">
</div>
<div class="resume-field">
<label>Зарплата</label>
<input type="text" id="vacancySalary" placeholder="от 100 000 ₽">
</div>
<!-- 👇 ВОТ СЮДА ВСТАВЬТЕ КОД С ТЕГАМИ 👇 -->
<div class="resume-field">
<label>Теги (через запятую)</label>
<input type="text" id="vacancyTags" placeholder="Python, JavaScript, React">
</div>
<!-- 👆 КОНЕЦ ВСТАВКИ 👆 -->
<div class="resume-field">
<label>Описание</label>
<textarea id="vacancyDescription" rows="4" placeholder="Обязанности, требования..."></textarea>
</div>
<div class="resume-field">
<label>Контакт</label>
<input type="text" id="vacancyContact" placeholder="@telegram">
</div>
<div style="display: flex; gap: 10px; margin-top: 30px;">
<button class="btn btn-primary" onclick="createVacancy()" style="flex: 1;">Создать</button>
<button class="btn btn-outline" onclick="closeVacancyModal()" style="flex: 1;">Отмена</button>
</div>
</div>
</div>
<script>
const API_BASE_URL = 'http://localhost:8000/api';
let currentUser = null;
// Проверка авторизации
const token = localStorage.getItem('accessToken');
if (!token) {
window.location.href = '/login';
}
// Загрузка данных профиля
async function loadProfile() {
try {
const response = await fetch(`${API_BASE_URL}/user`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) throw new Error('Ошибка загрузки');
currentUser = await response.json();
document.getElementById('profileName').textContent = currentUser.full_name;
document.getElementById('profileEmail').textContent = currentUser.email;
document.getElementById('profilePhone').textContent = currentUser.phone;
document.getElementById('profileTelegram').textContent = currentUser.telegram || '—';
document.getElementById('profileRole').textContent = currentUser.role === 'employee' ? 'Соискатель' : 'Работодатель';
// Показываем нужные вкладки
if (currentUser.role === 'employer') {
document.getElementById('employerTab').style.display = 'block';
document.getElementById('employeeTab').style.display = 'none';
loadVacancies();
} else {
document.getElementById('employerTab').style.display = 'none';
document.getElementById('employeeTab').style.display = 'block';
loadResume();
}
} catch (error) {
console.error('Error loading profile:', error);
logout();
}
}
// Загрузка вакансий
async function loadVacancies() {
try {
const response = await fetch(`${API_BASE_URL}/vacancies`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const vacancies = await response.json();
const container = document.getElementById('vacanciesList');
if (vacancies.length === 0) {
container.innerHTML = '<div class="loading">У вас пока нет вакансий</div>';
return;
}
container.innerHTML = vacancies.map(v => `
<div class="vacancy-card">
<h3>${escapeHtml(v.title)}</h3>
<div class="vacancy-salary">${escapeHtml(v.salary || 'з/п не указана')}</div>
<p>${escapeHtml(v.description || '')}</p>
<div style="margin-top: 15px; color: #4f7092;">
<i class="fab fa-telegram"></i> ${escapeHtml(v.contact || '—')}
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading vacancies:', error);
}
}
// Загрузка резюме
async function loadResume() {
try {
const response = await fetch(`${API_BASE_URL}/resume`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.status === 404) {
// Резюме не найдено - показываем пустую форму
addExperience();
addEducation();
return;
}
const resume = await response.json();
document.getElementById('desiredPosition').value = resume.desired_position || '';
document.getElementById('aboutMe').value = resume.about_me || '';
document.getElementById('desiredSalary').value = resume.desired_salary || '';
// 👇 ЗАГРУЖАЕМ ТЕГИ 👇
if (resume.tags && resume.tags.length > 0) {
document.getElementById('resumeTags').value = resume.tags.map(t => t.name).join(', ');
}
// Загрузка опыта
document.getElementById('experienceContainer').innerHTML = '';
resume.work_experience.forEach(exp => {
addExperience(exp.position, exp.company, exp.period);
});
// Загрузка образования
document.getElementById('educationContainer').innerHTML = '';
resume.education.forEach(edu => {
addEducation(edu.institution, edu.specialty, edu.graduation_year);
});
} catch (error) {
console.error('Error loading resume:', error);
}
}
// Добавление опыта
function addExperience(position = '', company = '', period = '') {
const container = document.getElementById('experienceContainer');
const item = document.createElement('div');
item.className = 'exp-item';
item.innerHTML = `
<div class="resume-field">
<label>Должность</label>
<input type="text" class="exp-position" value="${escapeHtml(position)}">
</div>
<div class="resume-field">
<label>Компания</label>
<input type="text" class="exp-company" value="${escapeHtml(company)}">
</div>
<div class="resume-field">
<label>Период</label>
<input type="text" class="exp-period" value="${escapeHtml(period)}">
</div>
<button class="btn btn-outline" onclick="this.closest('.exp-item').remove()" style="margin-top: 10px;">
<i class="fas fa-trash"></i> Удалить
</button>
`;
container.appendChild(item);
}
// Добавление образования
function addEducation(institution = '', specialty = '', year = '') {
const container = document.getElementById('educationContainer');
const item = document.createElement('div');
item.className = 'edu-item';
item.innerHTML = `
<div class="resume-field">
<label>Учебное заведение</label>
<input type="text" class="edu-institution" value="${escapeHtml(institution)}">
</div>
<div class="resume-field">
<label>Специальность</label>
<input type="text" class="edu-specialty" value="${escapeHtml(specialty)}">
</div>
<div class="resume-field">
<label>Год окончания</label>
<input type="text" class="edu-year" value="${escapeHtml(year)}">
</div>
<button class="btn btn-outline" onclick="this.closest('.edu-item').remove()" style="margin-top: 10px;">
<i class="fas fa-trash"></i> Удалить
</button>
`;
container.appendChild(item);
}
// Сохранение резюме
async function saveResume() {
const workExperience = [];
document.querySelectorAll('.exp-item').forEach(item => {
workExperience.push({
position: item.querySelector('.exp-position')?.value || '',
company: item.querySelector('.exp-company')?.value || '',
period: item.querySelector('.exp-period')?.value || null
});
});
const education = [];
document.querySelectorAll('.edu-item').forEach(item => {
education.push({
institution: item.querySelector('.edu-institution')?.value || '',
specialty: item.querySelector('.edu-specialty')?.value || null,
graduation_year: item.querySelector('.edu-year')?.value || null
});
});
// Получаем теги
const tagsInput = document.getElementById('resumeTags')?.value || '';
const tags = tagsInput
.split(',')
.map(t => t.trim())
.filter(t => t.length > 0);
const resumeData = {
desired_position: document.getElementById('desiredPosition').value || null,
about_me: document.getElementById('aboutMe').value || null,
desired_salary: document.getElementById('desiredSalary').value || null,
work_experience: workExperience,
education: education,
tags: tags // добавляем теги
};
try {
const response = await fetch(`${API_BASE_URL}/resume`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(resumeData)
});
if (!response.ok) throw new Error('Ошибка сохранения');
alert('Резюме сохранено!');
} catch (error) {
alert(error.message);
}
}
// Создание вакансии
async function createVacancy() {
// Получаем теги из поля ввода
const tagsInput = document.getElementById('vacancyTags')?.value || '';
const tags = tagsInput
.split(',')
.map(t => t.trim())
.filter(t => t.length > 0);
const vacancyData = {
title: document.getElementById('vacancyTitle').value,
salary: document.getElementById('vacancySalary').value || null,
description: document.getElementById('vacancyDescription').value || null,
contact: document.getElementById('vacancyContact').value || null,
tags: tags // добавляем теги в данные
};
if (!vacancyData.title) {
alert('Введите название вакансии');
return;
}
try {
const response = await fetch(`${API_BASE_URL}/vacancies`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(vacancyData)
});
if (!response.ok) throw new Error('Ошибка создания');
closeVacancyModal();
loadVacancies();
// Очищаем форму
document.getElementById('vacancyTitle').value = '';
document.getElementById('vacancySalary').value = '';
document.getElementById('vacancyTags').value = ''; // очищаем теги
document.getElementById('vacancyDescription').value = '';
document.getElementById('vacancyContact').value = '';
alert('Вакансия создана!');
} catch (error) {
alert(error.message);
}
}
// Переключение вкладок
function switchContentTab(tab) {
document.querySelectorAll('.content-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.content-pane').forEach(p => p.classList.remove('active'));
if (tab === 'main') {
document.querySelectorAll('.content-tab')[0].classList.add('active');
document.getElementById('mainPane').classList.add('active');
} else if (tab === 'vacancies') {
document.querySelectorAll('.content-tab')[1].classList.add('active');
document.getElementById('vacanciesPane').classList.add('active');
} else if (tab === 'resume') {
document.querySelectorAll('.content-tab')[2].classList.add('active');
document.getElementById('resumePane').classList.add('active');
}
}
// Модальное окно вакансии
function openVacancyModal() {
document.getElementById('vacancyModal').style.display = 'flex';
}
function closeVacancyModal() {
document.getElementById('vacancyModal').style.display = 'none';
}
// Выход
function logout() {
localStorage.removeItem('accessToken');
localStorage.removeItem('userId');
localStorage.removeItem('userRole');
localStorage.removeItem('userName');
window.location.href = '/';
}
// Экранирование 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;");
}
// Загрузка при старте
loadProfile();
// Закрытие модалки по клику вне окна
window.onclick = function(event) {
const modal = document.getElementById('vacancyModal');
if (event.target === modal) {
modal.style.display = 'none';
}
};
</script>
</body>
</html>