| |
| let appState = { |
| jobs: [], |
| currentUser: null, |
| favorites: [], |
| userJobs: [], |
| notifications: [], |
| messages: [], |
| currentView: 'home' |
| }; |
|
|
| |
| const elements = { |
| |
| homeView: document.getElementById('home-view'), |
| allJobsView: document.getElementById('all-jobs-view'), |
| favoritesView: document.getElementById('favorites-view'), |
| employersView: document.getElementById('employers-view'), |
| postJobView: document.getElementById('post-job-view'), |
|
|
| |
| jobCards: document.getElementById('job-cards'), |
| allJobsCards: document.getElementById('all-jobs-cards'), |
| favoritesCards: document.getElementById('favorites-cards'), |
| employerJobsCards: document.getElementById('employer-jobs-cards'), |
|
|
| |
| emptyFavorites: document.getElementById('empty-favorites'), |
| emptyEmployerJobs: document.getElementById('empty-employer-jobs'), |
|
|
| |
| notificationsView: document.getElementById('notifications-view'), |
| notificationsContainer: document.getElementById('notifications-container'), |
| emptyNotifications: document.getElementById('empty-notifications'), |
| messagesView: document.getElementById('messages-view'), |
| messagesContainer: document.getElementById('messages-container'), |
| emptyMessages: document.getElementById('empty-messages'), |
|
|
| |
| notificationBadge: document.getElementById('notification-badge'), |
| messageBadge: document.getElementById('message-badge'), |
|
|
| |
| loginModal: document.getElementById('login-modal'), |
| registerModal: document.getElementById('register-modal'), |
|
|
| |
| loginForm: document.getElementById('login-form'), |
| registerForm: document.getElementById('register-form'), |
| postJobForm: document.getElementById('post-job-form'), |
|
|
| |
| toast: document.getElementById('toast'), |
| toastMessage: document.getElementById('toast-message'), |
|
|
| |
| jobsCount: document.getElementById('jobs-count'), |
| favoritesCount: document.getElementById('favorites-count') |
| }; |
|
|
| |
| async function loadFromLocalStorage() { |
| try { |
| |
| const savedFavorites = localStorage.getItem('jobHubFavorites'); |
| if (savedFavorites) { |
| appState.favorites = JSON.parse(savedFavorites); |
| } |
|
|
| |
| const savedUserJobs = localStorage.getItem('jobHubUserJobs'); |
| if (savedUserJobs) { |
| appState.userJobs = JSON.parse(savedUserJobs); |
| } |
|
|
| |
| const savedNotifications = localStorage.getItem('jobHubNotifications'); |
| if (savedNotifications) { |
| appState.notifications = JSON.parse(savedNotifications); |
| } |
|
|
| |
| const savedMessages = localStorage.getItem('jobHubMessages'); |
| if (savedMessages) { |
| appState.messages = JSON.parse(savedMessages); |
| } |
|
|
| |
| const savedUser = localStorage.getItem('jobHubCurrentUser'); |
| if (savedUser) { |
| appState.currentUser = JSON.parse(savedUser); |
|
|
| |
| if (appState.currentUser && appState.currentUser.id) { |
| await loadUserFavorites(appState.currentUser.id); |
| if (appState.currentUser.type === 'employer') { |
| await loadUserJobs(appState.currentUser.id); |
| } |
| |
| await loadUserNotifications(appState.currentUser.id); |
| await loadUserMessages(appState.currentUser.id); |
| } |
| } |
|
|
| console.log('Data loaded from localStorage'); |
| } catch (error) { |
| console.error('Error loading from localStorage:', error); |
| } |
| } |
|
|
| |
| function saveToLocalStorage() { |
| try { |
| localStorage.setItem('jobHubFavorites', JSON.stringify(appState.favorites)); |
| localStorage.setItem('jobHubUserJobs', JSON.stringify(appState.userJobs)); |
| localStorage.setItem('jobHubNotifications', JSON.stringify(appState.notifications)); |
| localStorage.setItem('jobHubMessages', JSON.stringify(appState.messages)); |
|
|
| if (appState.currentUser) { |
| localStorage.setItem('jobHubCurrentUser', JSON.stringify(appState.currentUser)); |
| } else { |
| localStorage.removeItem('jobHubCurrentUser'); |
| } |
|
|
| console.log('Data saved to localStorage'); |
| } catch (error) { |
| console.error('Error saving to localStorage:', error); |
| } |
| } |
|
|
| |
| async function initApp() { |
| await loadJobsFromServer(); |
| updateCounters(); |
| updateNotificationBadge(); |
| updateMessageBadge(); |
| switchView('home'); |
| updateAuthButtons(); |
|
|
| |
| if (appState.currentUser) { |
| if (appState.currentUser.id) { |
| await loadUserFavorites(appState.currentUser.id); |
| if (appState.currentUser.type === 'employer') { |
| await loadUserJobs(appState.currentUser.id); |
| } |
| } |
| } |
| } |
|
|
| |
| async function loadJobsFromServer() { |
| try { |
| const response = await fetch('/api/jobs'); |
| if (response.ok) { |
| appState.jobs = await response.json(); |
| } else { |
| console.error('Failed to load jobs from server'); |
| |
| appState.jobs = getSampleJobs(); |
| } |
| } catch (error) { |
| console.error('Error loading jobs:', error); |
| appState.jobs = getSampleJobs(); |
| } |
| } |
|
|
| |
| function getSampleJobs() { |
| return [ |
| { |
| id: 1, |
| title: "Senior Frontend Разработчик", |
| company: "TechNova", |
| location: "Москва", |
| salary: "180 000 - 250 000 ₽", |
| description: "Требуется опытный frontend разработчик для работы с современным стеком: React, TypeScript, Next.js. Опыт работы от 5 лет, знание Redux, GraphQL будет плюсом.", |
| tags: ["React", "TypeScript", "Next.js", "Redux", "GraphQL"], |
| type: "full-time" |
| }, |
| { |
| id: 2, |
| title: "Backend Разработчик (Node.js)", |
| company: "CloudSolutions", |
| location: "Санкт-Петербург", |
| salary: "200 000 - 280 000 ₽", |
| description: "Ищем backend разработчика с опытом работы с Node.js, Express и микросервисной архитектурой. Знание Docker, Kubernetes и AWS обязательно.", |
| tags: ["Node.js", "Express", "Docker", "Kubernetes", "AWS"], |
| type: "full-time" |
| }, |
| { |
| id: 3, |
| title: "Full Stack Разработчик", |
| company: "DigitalAgency Pro", |
| location: "Екатеринбург", |
| salary: "150 000 - 220 000 ₽", |
| description: "Нужен full stack разработчик для работы над веб-приложениями. Опыт с React и Node.js обязателен. Знание TypeScript будет преимуществом.", |
| tags: ["React", "Node.js", "TypeScript", "Full Stack"], |
| type: "full-time" |
| }, |
| { |
| id: 4, |
| title: "Python Разработчик (ML)", |
| company: "DataScience Inc", |
| location: "Новосибирск", |
| salary: "160 000 - 240 000 ₽", |
| description: "Требуется Python разработчик для работы с данными и машинным обучением. Опыт с Django/Flask, TensorFlow/PyTorch обязателен.", |
| tags: ["Python", "Django", "Machine Learning", "TensorFlow"], |
| type: "full-time" |
| }, |
| { |
| id: 5, |
| title: "DevOps Инженер", |
| company: "CloudTech", |
| location: "Казань", |
| salary: "220 000 - 300 000 ₽", |
| description: "Ищем DevOps инженера с опытом работы с AWS, Kubernetes и CI/CD. Знание Terraform и Ansible будет плюсом.", |
| tags: ["DevOps", "AWS", "Kubernetes", "CI/CD"], |
| type: "full-time" |
| }, |
| { |
| id: 6, |
| title: "UI/UX Дизайнер", |
| company: "CreativeMinds", |
| location: "Москва", |
| salary: "120 000 - 180 000 ₽", |
| description: "Требуется UI/UX дизайнер для создания пользовательских интерфейсов. Опыт с Figma, Adobe XD обязателен.", |
| tags: ["UI/UX", "Figma", "Adobe XD", "Design"], |
| type: "full-time" |
| } |
| ]; |
| } |
|
|
| |
| function displayJobs(jobs, container) { |
| container.innerHTML = ''; |
|
|
| if (jobs.length === 0) { |
| container.innerHTML = ` |
| <div class="empty-state"> |
| <div class="empty-state-icon">🔍</div> |
| <h3>Вакансий не найдено</h3> |
| <p>Попробуйте изменить параметры поиска</p> |
| </div> |
| `; |
| return; |
| } |
|
|
| jobs.forEach(job => { |
| const jobCard = document.createElement('div'); |
| jobCard.className = 'job-card'; |
| |
| |
| const isFavorited = appState.favorites.some(fav => fav.id === job.id); |
| |
| jobCard.innerHTML = ` |
| <div class="job-card-header"> |
| <div> |
| <h3 class="job-title">${job.title}</h3> |
| <div class="job-company">${job.company}</div> |
| </div> |
| </div> |
| <div class="job-meta"> |
| <div class="job-location">${job.location}</div> |
| <div class="job-salary">${job.salary}</div> |
| </div> |
| <p class="job-description">${job.description}</p> |
| <div class="job-tags"> |
| ${job.tags.map(tag => `<span class="job-tag">${tag}</span>`).join('')} |
| </div> |
| <div class="job-actions"> |
| <button class="btn-favorite ${isFavorited ? 'active' : ''}" onclick="toggleFavorite(${job.id})"> |
| ${isFavorited ? '❤️' : '🤍'} В избранное |
| </button> |
| <button class="btn-apply" onclick="applyForJob(${job.id})">Откликнуться</button> |
| </div> |
| `; |
| container.appendChild(jobCard); |
| }); |
| } |
|
|
| |
| function switchView(view) { |
| |
| document.querySelectorAll('.view').forEach(v => v.classList.add('hidden')); |
| document.querySelectorAll('nav a').forEach(a => a.classList.remove('active')); |
|
|
| |
| const viewElement = document.getElementById(`${view}-view`); |
| if (viewElement) { |
| viewElement.classList.remove('hidden'); |
| appState.currentView = view; |
|
|
| |
| const navLink = document.querySelector(`nav a[onclick="switchView('${view}')"]`); |
| if (navLink) { |
| navLink.classList.add('active'); |
| } |
|
|
| |
| switch (view) { |
| case 'home': |
| displayJobs(appState.jobs.slice(0, 6), elements.jobCards); |
| break; |
| case 'all-jobs': |
| displayJobs(appState.jobs, elements.allJobsCards); |
| elements.jobsCount.textContent = `${appState.jobs.length} вакансий`; |
| break; |
| case 'favorites': |
| displayFavorites(); |
| break; |
| case 'notifications': |
| displayNotifications(); |
| break; |
| case 'messages': |
| displayMessages(); |
| break; |
| case 'employers': |
| displayEmployerJobs(); |
| break; |
| case 'post-job': |
| |
| elements.postJobForm.reset(); |
| break; |
| } |
| } |
| } |
|
|
| |
| function filterJobs() { |
| const jobSearchTerm = document.getElementById('job-search').value.trim().toLowerCase(); |
| const locationSearchTerm = document.getElementById('location-search').value.trim().toLowerCase(); |
|
|
| let filteredJobs = appState.jobs; |
|
|
| |
| if (jobSearchTerm) { |
| filteredJobs = filteredJobs.filter(job => { |
| return job.title.toLowerCase().includes(jobSearchTerm) || |
| job.description.toLowerCase().includes(jobSearchTerm) || |
| job.tags.some(tag => tag.toLowerCase().includes(jobSearchTerm)); |
| }); |
| } |
|
|
| |
| if (locationSearchTerm) { |
| filteredJobs = filteredJobs.filter(job => { |
| return job.location.toLowerCase().includes(locationSearchTerm); |
| }); |
| } |
|
|
| |
| filteredJobs.sort((a, b) => { |
| const aTitleMatch = a.title.toLowerCase().includes(jobSearchTerm); |
| const bTitleMatch = b.title.toLowerCase().includes(jobSearchTerm); |
| |
| if (aTitleMatch && !bTitleMatch) return -1; |
| if (!aTitleMatch && bTitleMatch) return 1; |
| return 0; |
| }); |
|
|
| if (appState.currentView === 'home') { |
| displayJobs(filteredJobs.slice(0, 6), elements.jobCards); |
| } else if (appState.currentView === 'all-jobs') { |
| displayJobs(filteredJobs, elements.allJobsCards); |
| elements.jobsCount.textContent = `${filteredJobs.length} вакансий`; |
| } |
| } |
|
|
| |
| function advancedFilterJobs(filters) { |
| let filteredJobs = appState.jobs; |
|
|
| |
| if (filters.keyword) { |
| const keyword = filters.keyword.toLowerCase(); |
| filteredJobs = filteredJobs.filter(job => { |
| return job.title.toLowerCase().includes(keyword) || |
| job.description.toLowerCase().includes(keyword) || |
| job.tags.some(tag => tag.toLowerCase().includes(keyword)); |
| }); |
| } |
|
|
| |
| if (filters.location) { |
| const location = filters.location.toLowerCase(); |
| filteredJobs = filteredJobs.filter(job => { |
| return job.location.toLowerCase().includes(location); |
| }); |
| } |
|
|
| |
| if (filters.minSalary || filters.maxSalary) { |
| filteredJobs = filteredJobs.filter(job => { |
| |
| const salaryText = job.salary.replace(/[^\d\s]/g, '').trim(); |
| if (!salaryText) return true; |
| |
| const salaryNumbers = salaryText.match(/\d+/g); |
| if (!salaryNumbers) return true; |
| |
| const avgSalary = salaryNumbers.reduce((sum, num) => sum + parseInt(num), 0) / salaryNumbers.length; |
| |
| if (filters.minSalary && avgSalary < filters.minSalary) return false; |
| if (filters.maxSalary && avgSalary > filters.maxSalary) return false; |
| |
| return true; |
| }); |
| } |
|
|
| |
| if (filters.jobType && filters.jobType !== 'all') { |
| filteredJobs = filteredJobs.filter(job => job.type === filters.jobType); |
| } |
|
|
| |
| if (filters.tags && filters.tags.length > 0) { |
| filteredJobs = filteredJobs.filter(job => { |
| return filters.tags.every(tag => |
| job.tags.some(jobTag => |
| jobTag.toLowerCase().includes(tag.toLowerCase()) |
| ) |
| ); |
| }); |
| } |
|
|
| |
| if (filters.keyword) { |
| filteredJobs.sort((a, b) => { |
| const keyword = filters.keyword.toLowerCase(); |
| const aTitleMatch = a.title.toLowerCase().includes(keyword); |
| const bTitleMatch = b.title.toLowerCase().includes(keyword); |
| |
| if (aTitleMatch && !bTitleMatch) return -1; |
| if (!aTitleMatch && bTitleMatch) return 1; |
| return 0; |
| }); |
| } |
|
|
| return filteredJobs; |
| } |
|
|
| |
| let searchTimeout; |
| function debouncedSearch() { |
| clearTimeout(searchTimeout); |
| searchTimeout = setTimeout(() => { |
| filterJobs(); |
| }, 300); |
| } |
|
|
| |
| function displayFavorites() { |
| if (appState.favorites.length === 0) { |
| elements.favoritesCards.innerHTML = ''; |
| elements.emptyFavorites.classList.remove('hidden'); |
| } else { |
| elements.emptyFavorites.classList.add('hidden'); |
| displayJobs(appState.favorites, elements.favoritesCards); |
| } |
| updateCounters(); |
| } |
|
|
| |
| async function displayNotifications() { |
| |
| if (appState.currentUser && appState.currentUser.id) { |
| try { |
| const response = await fetch(`/api/users/${appState.currentUser.id}/notifications`); |
| if (response.ok) { |
| const serverNotifications = await response.json(); |
| |
| appState.notifications = serverNotifications.map(n => ({ |
| id: n.id, |
| title: n.title, |
| text: n.text, |
| icon: n.icon, |
| timestamp: n.created_at, |
| read: n.read_status |
| })); |
| } |
| } catch (error) { |
| console.error('Error loading notifications from server:', error); |
| } |
| } |
|
|
| elements.notificationsContainer.innerHTML = ''; |
|
|
| if (appState.notifications.length === 0) { |
| elements.emptyNotifications.classList.remove('hidden'); |
| return; |
| } |
|
|
| elements.emptyNotifications.classList.add('hidden'); |
|
|
| appState.notifications.forEach(notification => { |
| const notificationItem = document.createElement('div'); |
| notificationItem.className = `notification-item ${!notification.read ? 'unread' : ''}`; |
| |
| notificationItem.innerHTML = ` |
| <div class="notification-icon">${notification.icon || '🔔'}</div> |
| <div class="notification-content"> |
| <div class="notification-title">${notification.title}</div> |
| <div class="notification-text">${notification.text}</div> |
| <div class="notification-time">${formatTimeAgo(notification.timestamp)}</div> |
| </div> |
| `; |
| |
| |
| if (!notification.read) { |
| notification.read = true; |
| updateNotificationBadge(); |
| |
| |
| if (appState.currentUser && appState.currentUser.id) { |
| fetch(`/api/users/${appState.currentUser.id}/notifications/${notification.id}/read`, { |
| method: 'PUT' |
| }).catch(console.error); |
| } |
| } |
| |
| elements.notificationsContainer.appendChild(notificationItem); |
| }); |
| } |
|
|
| |
| async function displayMessages() { |
| |
| if (appState.currentUser && appState.currentUser.id) { |
| try { |
| const response = await fetch(`/api/users/${appState.currentUser.id}/messages`); |
| if (response.ok) { |
| const serverMessages = await response.json(); |
| |
| appState.messages = serverMessages.map(m => ({ |
| id: m.id, |
| sender: { |
| name: m.sender_name || 'Unknown', |
| avatar: '👤' |
| }, |
| content: m.content, |
| timestamp: m.created_at, |
| read: m.read_status |
| })); |
| } |
| } catch (error) { |
| console.error('Error loading messages from server:', error); |
| } |
| } |
|
|
| elements.messagesContainer.innerHTML = ''; |
|
|
| if (appState.messages.length === 0) { |
| elements.emptyMessages.classList.remove('hidden'); |
| return; |
| } |
|
|
| elements.emptyMessages.classList.add('hidden'); |
|
|
| appState.messages.forEach(message => { |
| const messageItem = document.createElement('div'); |
| messageItem.className = `message-item ${!message.read ? 'unread' : ''}`; |
| |
| messageItem.innerHTML = ` |
| <div class="message-icon">${message.sender.avatar || '👤'}</div> |
| <div class="message-content"> |
| <div class="message-title">${message.sender.name}</div> |
| <div class="message-text">${message.content}</div> |
| <div class="message-time">${formatTimeAgo(message.timestamp)}</div> |
| <div class="message-actions"> |
| <button class="btn btn-secondary" onclick="replyToMessage(${message.id})">Ответить</button> |
| <button class="btn btn-secondary" onclick="deleteMessage(${message.id})">Удалить</button> |
| </div> |
| </div> |
| `; |
| |
| |
| if (!message.read) { |
| message.read = true; |
| updateMessageBadge(); |
| |
| |
| if (appState.currentUser && appState.currentUser.id) { |
| fetch(`/api/users/${appState.currentUser.id}/messages/${message.id}/read`, { |
| method: 'PUT' |
| }).catch(console.error); |
| } |
| } |
| |
| elements.messagesContainer.appendChild(messageItem); |
| }); |
| } |
|
|
| |
| async function addNotification(title, text, icon = '🔔') { |
| const notification = { |
| id: Date.now(), |
| title: title, |
| text: text, |
| icon: icon, |
| timestamp: new Date().toISOString(), |
| read: false |
| }; |
| |
| |
| appState.notifications.unshift(notification); |
| updateNotificationBadge(); |
| saveToLocalStorage(); |
| |
| |
| if (appState.currentUser && appState.currentUser.id) { |
| try { |
| await fetch(`/api/users/${appState.currentUser.id}/notifications`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ title, text, icon }) |
| }); |
| } catch (error) { |
| console.error('Error adding notification to server:', error); |
| } |
| } |
| |
| |
| showToast(text); |
| } |
|
|
| |
| async function addMessage(sender, content) { |
| const message = { |
| id: Date.now(), |
| sender: sender, |
| content: content, |
| timestamp: new Date().toISOString(), |
| read: false |
| }; |
| |
| appState.messages.unshift(message); |
| updateMessageBadge(); |
| saveToLocalStorage(); |
| |
| |
| await addNotification('Новое сообщение', `Сообщение от ${sender.name}`, '💬'); |
| } |
|
|
| |
| function formatTimeAgo(timestamp) { |
| const date = new Date(timestamp); |
| const now = new Date(); |
| const seconds = Math.floor((now - date) / 1000); |
| |
| if (seconds < 60) return 'только что'; |
| if (seconds < 3600) return `${Math.floor(seconds / 60)} мин назад`; |
| if (seconds < 86400) return `${Math.floor(seconds / 3600)} ч назад`; |
| return `${Math.floor(seconds / 86400)} дн назад`; |
| } |
|
|
| |
| function updateNotificationBadge() { |
| const unreadCount = appState.notifications.filter(n => !n.read).length; |
| if (unreadCount > 0) { |
| elements.notificationBadge.textContent = unreadCount; |
| elements.notificationBadge.classList.remove('hidden'); |
| } else { |
| elements.notificationBadge.classList.add('hidden'); |
| } |
| } |
|
|
| |
| function updateMessageBadge() { |
| const unreadCount = appState.messages.filter(m => !m.read).length; |
| if (unreadCount > 0) { |
| elements.messageBadge.textContent = unreadCount; |
| elements.messageBadge.classList.remove('hidden'); |
| } else { |
| elements.messageBadge.classList.add('hidden'); |
| } |
| } |
|
|
| |
| async function clearNotifications() { |
| appState.notifications = []; |
| updateNotificationBadge(); |
| saveToLocalStorage(); |
| |
| |
| if (appState.currentUser && appState.currentUser.id) { |
| try { |
| await fetch(`/api/users/${appState.currentUser.id}/notifications`, { |
| method: 'DELETE' |
| }); |
| } catch (error) { |
| console.error('Error clearing notifications from server:', error); |
| } |
| } |
| |
| displayNotifications(); |
| } |
|
|
| |
| function replyToMessage(messageId) { |
| if (!appState.currentUser) { |
| showToast('Для ответа на сообщение нужно авторизоваться'); |
| showLoginModal(); |
| return; |
| } |
| |
| |
| |
| const recipientId = prompt('Введите ID получателя (для тестирования используйте ID другого пользователя):'); |
| if (recipientId) { |
| const reply = prompt('Введите ваш ответ:'); |
| if (reply) { |
| sendMessageToUser(parseInt(recipientId), reply); |
| } |
| } |
| } |
|
|
| |
| async function sendMessageToUser(receiverId, content) { |
| if (!appState.currentUser || !appState.currentUser.id) { |
| showToast('Для отправки сообщения нужно авторизоваться'); |
| showLoginModal(); |
| return; |
| } |
| |
| try { |
| const response = await fetch('/api/messages', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ |
| sender_id: appState.currentUser.id, |
| receiver_id: receiverId, |
| content: content |
| }) |
| }); |
| |
| if (response.ok) { |
| showToast('Сообщение отправлено'); |
| } else { |
| const errorData = await response.json(); |
| showToast(errorData.error || 'Ошибка отправки сообщения'); |
| } |
| } catch (error) { |
| console.error('Error sending message:', error); |
| showToast('Ошибка отправки сообщения'); |
| } |
| } |
|
|
| |
| async function deleteMessage(messageId) { |
| appState.messages = appState.messages.filter(m => m.id !== messageId); |
| updateMessageBadge(); |
| saveToLocalStorage(); |
| |
| |
| if (appState.currentUser && appState.currentUser.id) { |
| try { |
| await fetch(`/api/users/${appState.currentUser.id}/messages/${messageId}`, { |
| method: 'DELETE' |
| }); |
| } catch (error) { |
| console.error('Error deleting message from server:', error); |
| } |
| } |
| |
| displayMessages(); |
| } |
|
|
| |
| function displayEmployerJobs() { |
| if (appState.userJobs.length === 0) { |
| elements.employerJobsCards.innerHTML = ''; |
| elements.emptyEmployerJobs.classList.remove('hidden'); |
| } else { |
| elements.emptyEmployerJobs.classList.add('hidden'); |
| displayJobs(appState.userJobs, elements.employerJobsCards); |
| } |
| } |
|
|
| |
| function updateCounters() { |
| elements.favoritesCount.textContent = `${appState.favorites.length} избранных`; |
| } |
|
|
| |
| function showLoginModal() { |
| elements.loginModal.classList.add('active'); |
| } |
|
|
| |
| function hideLoginModal() { |
| elements.loginModal.classList.remove('active'); |
| elements.loginForm.reset(); |
| } |
|
|
| |
| function showRegisterModal() { |
| elements.registerModal.classList.add('active'); |
| } |
|
|
| |
| function hideRegisterModal() { |
| elements.registerModal.classList.remove('active'); |
| elements.registerForm.reset(); |
| } |
|
|
| |
| async function handleLogin(e) { |
| e.preventDefault(); |
| const email = document.getElementById('login-email').value; |
| const password = document.getElementById('login-password').value; |
| const userType = document.getElementById('login-type').value; |
|
|
| |
| if (!email || !password) { |
| showToast('Пожалуйста, заполните все поля'); |
| return; |
| } |
|
|
| try { |
| |
| const response = await fetch('/api/login', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ email, password }) |
| }); |
|
|
| if (response.ok) { |
| const userData = await response.json(); |
| appState.currentUser = { |
| id: userData.id, |
| name: userData.name, |
| email: userData.email, |
| type: userData.type |
| }; |
|
|
| |
| await loadUserFavorites(userData.id); |
| if (userData.type === 'employer') { |
| await loadUserJobs(userData.id); |
| } |
| await loadUserNotifications(userData.id); |
| await loadUserMessages(userData.id); |
|
|
| hideLoginModal(); |
| showToast(`Добро пожаловать, ${appState.currentUser.name}!`); |
| updateAuthButtons(); |
| saveToLocalStorage(); |
| } else { |
| |
| appState.currentUser = { |
| name: 'Пользователь', |
| email: email, |
| type: userType |
| }; |
|
|
| hideLoginModal(); |
| showToast(`Добро пожаловать, ${appState.currentUser.name}!`); |
| updateAuthButtons(); |
| saveToLocalStorage(); |
| } |
| } catch (error) { |
| console.error('Login error:', error); |
| |
| appState.currentUser = { |
| name: 'Пользователь', |
| email: email, |
| type: userType |
| }; |
|
|
| hideLoginModal(); |
| showToast(`Добро пожаловать, ${appState.currentUser.name}!`); |
| updateAuthButtons(); |
| saveToLocalStorage(); |
| } |
| } |
|
|
| |
| async function loadUserFavorites(userId) { |
| try { |
| const response = await fetch(`/api/users/${userId}/favorites`); |
| if (response.ok) { |
| appState.favorites = await response.json(); |
| } |
| } catch (error) { |
| console.error('Error loading favorites:', error); |
| } |
| } |
|
|
| |
| async function loadUserJobs(userId) { |
| try { |
| const response = await fetch(`/api/users/${userId}/jobs`); |
| if (response.ok) { |
| appState.userJobs = await response.json(); |
| } |
| } catch (error) { |
| console.error('Error loading user jobs:', error); |
| } |
| } |
|
|
| |
| async function loadUserNotifications(userId) { |
| try { |
| const response = await fetch(`/api/users/${userId}/notifications`); |
| if (response.ok) { |
| const serverNotifications = await response.json(); |
| appState.notifications = serverNotifications.map(n => ({ |
| id: n.id, |
| title: n.title, |
| text: n.text, |
| icon: n.icon, |
| timestamp: n.created_at, |
| read: n.read_status |
| })); |
| updateNotificationBadge(); |
| } |
| } catch (error) { |
| console.error('Error loading user notifications:', error); |
| } |
| } |
|
|
| |
| async function loadUserMessages(userId) { |
| try { |
| const response = await fetch(`/api/users/${userId}/messages`); |
| if (response.ok) { |
| const serverMessages = await response.json(); |
| appState.messages = serverMessages.map(m => ({ |
| id: m.id, |
| sender: { |
| name: m.sender_name || 'Unknown', |
| avatar: '👤' |
| }, |
| content: m.content, |
| timestamp: m.created_at, |
| read: m.read_status |
| })); |
| updateMessageBadge(); |
| } |
| } catch (error) { |
| console.error('Error loading user messages:', error); |
| } |
| } |
|
|
| |
| async function handleRegister(e) { |
| e.preventDefault(); |
| const name = document.getElementById('register-name').value; |
| const email = document.getElementById('register-email').value; |
| const password = document.getElementById('register-password').value; |
| const userType = document.getElementById('register-type').value; |
|
|
| |
| if (!name || !email || !password) { |
| showToast('Пожалуйста, заполните все поля'); |
| return; |
| } |
|
|
| try { |
| |
| const response = await fetch('/api/users', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ name, email, password, type: userType }) |
| }); |
|
|
| if (response.ok) { |
| const userData = await response.json(); |
| appState.currentUser = { |
| id: userData.id, |
| name: userData.name, |
| email: userData.email, |
| type: userData.type |
| }; |
|
|
| |
| appState.notifications = []; |
| appState.messages = []; |
|
|
| hideRegisterModal(); |
| showToast(`Регистрация успешна! Добро пожаловать, ${name}!`); |
| updateAuthButtons(); |
| saveToLocalStorage(); |
| } else { |
| const errorData = await response.json(); |
| showToast(errorData.error || 'Ошибка регистрации'); |
| } |
| } catch (error) { |
| console.error('Registration error:', error); |
| |
| appState.currentUser = { |
| name: name, |
| email: email, |
| type: userType |
| }; |
|
|
| |
| appState.notifications = []; |
| appState.messages = []; |
|
|
| hideRegisterModal(); |
| showToast(`Регистрация успешна! Добро пожаловать, ${name}!`); |
| updateAuthButtons(); |
| saveToLocalStorage(); |
| } |
| } |
|
|
| |
| async function handlePostJob(e) { |
| e.preventDefault(); |
|
|
| if (!appState.currentUser || appState.currentUser.type !== 'employer') { |
| showToast('Только работодатели могут размещать вакансии'); |
| return; |
| } |
|
|
| const job = { |
| title: document.getElementById('job-title').value, |
| company: document.getElementById('job-company').value, |
| location: document.getElementById('job-location').value, |
| salary: document.getElementById('job-salary').value || 'По договоренности', |
| description: document.getElementById('job-description').value, |
| tags: document.getElementById('job-tags').value.split(',').map(tag => tag.trim()).filter(tag => tag), |
| type: document.getElementById('job-type').value, |
| user_id: appState.currentUser.id |
| }; |
|
|
| try { |
| |
| const response = await fetch('/api/jobs', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify(job) |
| }); |
|
|
| if (response.ok) { |
| const newJob = await response.json(); |
|
|
| |
| appState.jobs.unshift(newJob); |
| appState.userJobs.unshift(newJob); |
|
|
| showToast('Вакансия успешно размещена!'); |
| |
| |
| addNotification('Вакансия опубликована', `Ваша вакансия "${job.title}" успешно опубликована`, '✅'); |
| |
| elements.postJobForm.reset(); |
| switchView('employers'); |
| saveToLocalStorage(); |
| } else { |
| const errorData = await response.json(); |
| showToast(errorData.error || 'Ошибка размещения вакансии'); |
| } |
| } catch (error) { |
| console.error('Error posting job:', error); |
| |
| job.id = Date.now(); |
| job.created_at = new Date().toISOString(); |
|
|
| |
| appState.jobs.unshift(job); |
| appState.userJobs.unshift(job); |
|
|
| showToast('Вакансия успешно размещена!'); |
| |
| |
| addNotification('Вакансия опубликована', `Ваша вакансия "${job.title}" успешно опубликована`, '✅'); |
| |
| elements.postJobForm.reset(); |
| switchView('employers'); |
| saveToLocalStorage(); |
| } |
| } |
|
|
| |
| async function toggleFavorite(jobId) { |
| if (!appState.currentUser) { |
| showToast('Для добавления в избранное нужно авторизоваться'); |
| showLoginModal(); |
| return; |
| } |
|
|
| const job = appState.jobs.find(j => j.id === jobId); |
| if (!job) return; |
|
|
| const isFavorite = appState.favorites.some(fav => fav.id === jobId); |
|
|
| try { |
| if (isFavorite) { |
| |
| const response = await fetch(`/api/users/${appState.currentUser.id}/favorites/${jobId}`, { |
| method: 'DELETE' |
| }); |
|
|
| if (response.ok || response.status === 404) { |
| appState.favorites = appState.favorites.filter(fav => fav.id !== jobId); |
| showToast('Вакансия удалена из избранного'); |
| |
| |
| addNotification('Избранное обновлено', `Вакансия "${job.title}" удалена из избранного`, '🗑️'); |
| } else { |
| throw new Error('Failed to remove from favorites'); |
| } |
| } else { |
| |
| const response = await fetch(`/api/users/${appState.currentUser.id}/favorites`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ jobId }) |
| }); |
|
|
| if (response.ok) { |
| appState.favorites.push(job); |
| showToast('Вакансия добавлена в избранное'); |
| |
| |
| addNotification('Избранное обновлено', `Вакансия "${job.title}" добавлена в избранное`, '❤️'); |
| } else { |
| throw new Error('Failed to add to favorites'); |
| } |
| } |
| } catch (error) { |
| console.error('Favorite toggle error:', error); |
| |
| if (isFavorite) { |
| appState.favorites = appState.favorites.filter(fav => fav.id !== jobId); |
| showToast('Вакансия удалена из избранного'); |
| |
| |
| addNotification('Избранное обновлено', `Вакансия "${job.title}" удалена из избранного`, '🗑️'); |
| } else { |
| appState.favorites.push(job); |
| showToast('Вакансия добавлена в избранное'); |
| |
| |
| addNotification('Избранное обновлено', `Вакансия "${job.title}" добавлена в избранное`, '❤️'); |
| } |
| } |
|
|
| |
| if (appState.currentView === 'home') { |
| displayJobs(appState.jobs.slice(0, 6), elements.jobCards); |
| } else if (appState.currentView === 'all-jobs') { |
| displayJobs(appState.jobs, elements.allJobsCards); |
| } else if (appState.currentView === 'favorites') { |
| displayFavorites(); |
| } |
| updateCounters(); |
| saveToLocalStorage(); |
| } |
|
|
| |
| function applyForJob(jobId) { |
| if (!appState.currentUser) { |
| showToast('Для отклика на вакансию нужно авторизоваться'); |
| showLoginModal(); |
| return; |
| } |
|
|
| const job = appState.jobs.find(j => j.id === jobId); |
| if (job) { |
| showToast('Ваш отклик отправлен работодателю!'); |
| |
| |
| addNotification('Отклик отправлен', `Ваш отклик на вакансию "${job.title}" отправлен работодателю`, '📤'); |
| } else { |
| showToast('Ваш отклик отправлен работодателю!'); |
| } |
|
|
| |
| |
| } |
|
|
| |
| function updateAuthButtons() { |
| const authButtons = document.getElementById('auth-buttons'); |
| if (appState.currentUser) { |
| authButtons.innerHTML = ` |
| <div style="color: white; padding: 0.75rem 1.5rem;"> |
| Привет, ${appState.currentUser.name}! (${appState.currentUser.type === 'employer' ? 'Работодатель' : 'Соискатель'}) |
| </div> |
| <button class="btn btn-secondary" onclick="handleLogout()">Выйти</button> |
| `; |
| } else { |
| authButtons.innerHTML = ` |
| <button id="login-btn" class="btn btn-secondary" onclick="showLoginModal()">Войти</button> |
| <button id="register-btn" class="btn btn-primary" onclick="showRegisterModal()">Регистрация</button> |
| `; |
| } |
| } |
|
|
| |
| function handleLogout() { |
| appState.currentUser = null; |
| appState.favorites = []; |
| appState.userJobs = []; |
| appState.notifications = []; |
| appState.messages = []; |
| updateAuthButtons(); |
| updateNotificationBadge(); |
| updateMessageBadge(); |
| showToast('Вы успешно вышли из аккаунта'); |
| switchView('home'); |
| saveToLocalStorage(); |
|
|
| |
| localStorage.removeItem('jobHubCurrentUser'); |
| localStorage.removeItem('jobHubFavorites'); |
| localStorage.removeItem('jobHubUserJobs'); |
| localStorage.removeItem('jobHubNotifications'); |
| localStorage.removeItem('jobHubMessages'); |
| } |
|
|
| |
| function showToast(message) { |
| elements.toastMessage.textContent = message; |
| elements.toast.classList.add('active'); |
|
|
| setTimeout(() => { |
| elements.toast.classList.remove('active'); |
| }, 3000); |
| } |
|
|
| |
| document.addEventListener('DOMContentLoaded', async function() { |
| await loadFromLocalStorage(); |
| await initApp(); |
| |
| |
| document.getElementById('job-search').addEventListener('input', debouncedSearch); |
| document.getElementById('location-search').addEventListener('input', debouncedSearch); |
| |
| |
| document.getElementById('job-search').addEventListener('keypress', function(e) { |
| if (e.key === 'Enter') { |
| filterJobs(); |
| } |
| }); |
|
|
| document.getElementById('location-search').addEventListener('keypress', function(e) { |
| if (e.key === 'Enter') { |
| filterJobs(); |
| } |
| }); |
| }); |