// API Service class ApiService { constructor() { this.baseUrl = window.API_URL || '/api'; this.token = localStorage.getItem('newones_token'); } async request(endpoint, options = {}) { const url = `${this.baseUrl}${endpoint}`; const config = { headers: { 'Content-Type': 'application/json', ...(this.token ? { 'Authorization': `Bearer ${this.token}` } : {}), ...options.headers }, ...options }; if (config.body && typeof config.body === 'object') { config.body = JSON.stringify(config.body); } try { const response = await fetch(url, config); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Request failed'); } return data; } catch (error) { console.error('API Error:', error); throw error; } } async get(endpoint) { return this.request(endpoint, { method: 'GET' }); } async post(endpoint, body) { return this.request(endpoint, { method: 'POST', body }); } async put(endpoint, body) { return this.request(endpoint, { method: 'PUT', body }); } async delete(endpoint) { return this.request(endpoint, { method: 'DELETE' }); } setToken(token) { this.token = token; localStorage.setItem('newones_token', token); } clearToken() { this.token = null; localStorage.removeItem('newones_token'); } } // Database Simulation (Fallback mode if API unavailable) class BeautyDatabase { constructor() { this.api = new ApiService(); this.useApi = false; // Set to true when backend is ready this.salons = this.loadData('salons') || this.generateMockSalons(); this.users = this.loadData('users') || []; this.bookings = this.loadData('bookings') || []; this.reviews = this.loadData('reviews') || []; this.currentUser = this.loadData('currentUser') || null; if (!this.loadData('salons')) this.saveData('salons', this.salons); // Try to load user from token on init this.loadUserFromToken(); } async loadUserFromToken() { if (this.api.token) { try { const user = await this.api.get('/auth/me'); this.currentUser = user; this.saveData('currentUser', user); this.useApi = true; } catch (e) { this.api.clearToken(); } } }// Database Simulation class BeautyDatabase { constructor() { this.salons = this.loadData('salons') || this.generateMockSalons(); this.users = this.loadData('users') || []; this.bookings = this.loadData('bookings') || []; this.reviews = this.loadData('reviews') || []; this.currentUser = this.loadData('currentUser') || null; if (!this.loadData('salons')) this.saveData('salons', this.salons); } loadData(key) { const data = localStorage.getItem(`newones_${key}`); return data ? JSON.parse(data) : null; } saveData(key, data) { localStorage.setItem(`newones_${key}`, JSON.stringify(data)); } generateMockSalons() { const services = [ { name: 'Маникюр', price: 1500, duration: 60 }, { name: 'Педикюр', price: 2000, duration: 90 }, { name: 'Стрижка', price: 1200, duration: 45 }, { name: 'Окрашивание', price: 3500, duration: 120 }, { name: 'Макияж', price: 2500, duration: 60 }, { name: 'Коррекция бровей', price: 800, duration: 30 }, { name: 'Наращивание ресниц', price: 3000, duration: 120 }, { name: 'Массаж', price: 2500, duration: 60 }, { name: 'Чистка лица', price: 2800, duration: 90 }, { name: 'Эпиляция', price: 1500, duration: 45 } ]; const salonNames = [ 'Beauty Box', 'Luxe Studio', 'Nail Art Pro', 'Style Icon', 'Glamour Room', 'Chic Beauty', 'Perfect Look', 'Studio One', 'Beauty Lab', 'Elegance', 'Top Style', 'Beauty Zone' ]; const addresses = [ 'ул. Арбат, 10', 'ул. Тверская, 25', 'ул. Новый Арбат, 15', 'Пресненская наб., 2', 'ул. Большая Дмитровка, 8', 'Кутузовский пр., 32', 'ул. Новослободская, 16' ]; return salonNames.map((name, i) => ({ id: i + 1, name: name, type: Math.random() > 0.5 ? 'salon' : 'master', address: addresses[Math.floor(Math.random() * addresses.length)], coords: [55.75 + (Math.random() - 0.5) * 0.1, 37.61 + (Math.random() - 0.5) * 0.1], rating: (3.5 + Math.random() * 1.5).toFixed(1), reviews: Math.floor(Math.random() * 200), image: `https://static.photos/cosmetic/${['640x360', '320x240'][Math.floor(Math.random() * 2)]}/${i + 1}`, services: services.slice(0, 3 + Math.floor(Math.random() * 4)), priceRange: ['budget', 'mid', 'premium'][Math.floor(Math.random() * 3)], isOpen: Math.random() > 0.3, phone: `+7 (999) ${100 + Math.floor(Math.random() * 900)}-${10 + Math.floor(Math.random() * 90)}-${10 + Math.floor(Math.random() * 90)}`, description: 'Профессиональные beauty-услуги премиум класса. Опытные мастера, современное оборудование, атмосфера заботы и комфорта.' })); } // User methods async registerInit(email, password, role, extraData = {}) { // Try API first try { const response = await this.api.post('/auth/register/init', { email, password, role, ...extraData }); this.useApi = true; return { success: true, code: response.code, // Demo only! tempToken: response.temp_token }; } catch (error) { // Fallback to local mode console.warn('API unavailable, using local mode'); if (this.users.find(u => u.email === email)) { return { success: false, error: 'Пользователь с таким email уже существует' }; } // Return demo code for local mode return { success: true, code: role === 'client' ? '123456' : '654321', localMode: true }; } } async registerVerify(email, password, role, code, roleData = {}) { // Try API first if (this.useApi) { try { const response = await this.api.post('/auth/register/verify', { email, password, role, code, roleData }); this.api.setToken(response.token); this.currentUser = response.user; this.saveData('currentUser', response.user); return { success: true, user: response.user }; } catch (error) { return { success: false, error: error.message }; } } // Local fallback mode const expectedCode = role === 'client' ? '123456' : '654321'; if (code !== expectedCode) { return { success: false, error: 'Неверный код подтверждения' }; } const user = { id: Date.now(), email, password, // In real app, hash this! role, firstName: roleData.firstName || email.split('@')[0], lastName: roleData.lastName || '', ...roleData }; this.users.push(user); this.saveData('users', this.users); this.currentUser = user; this.saveData('currentUser', user); // If business, create salon record if (role === 'business' && roleData.companyName) { const newSalon = { id: Date.now(), ownerId: user.id, name: roleData.companyName, type: roleData.businessType || 'salon', address: roleData.address || '', coords: [55.75, 37.61], rating: '0.0', reviews: 0, image: 'https://static.photos/cosmetic/640x360/999', services: [], priceRange: 'mid', isOpen: false, phone: roleData.businessPhone || '', description: roleData.companyDescription || '' }; this.salons.push(newSalon); this.saveData('salons', this.salons); user.salonId = newSalon.id; this.saveData('users', this.users); this.saveData('currentUser', user); } return { success: true, user }; } async login(email, password) { // Try API first try { const response = await this.api.post('/auth/login', { email, password }); this.api.setToken(response.token); this.currentUser = response.user; this.saveData('currentUser', response.user); this.useApi = true; return { success: true, user: response.user }; } catch (error) { // Fallback to local mode const user = this.users.find(u => u.email === email && u.password === password); if (user) { this.currentUser = user; this.saveData('currentUser', user); return { success: true, user }; } return { success: false, error: 'Неверный email или пароль' }; } } logout() { this.currentUser = null; this.api.clearToken(); localStorage.removeItem('newones_currentUser'); localStorage.removeItem('newones_token'); } // Salon methods updateSalon(salonId, data) { const index = this.salons.findIndex(s => s.id === salonId); if (index !== -1) { this.salons[index] = { ...this.salons[index], ...data }; this.saveData('salons', this.salons); return true; } return false; } addBooking(salonId, service, date, time) { if (!this.currentUser) return { success: false, error: 'Необходимо войти' }; const booking = { id: Date.now(), userId: this.currentUser.id, salonId, service, date, time, status: 'pending', createdAt: new Date().toISOString() }; this.bookings.push(booking); this.saveData('bookings', this.bookings); return { success: true, booking }; } toggleFavorite(salonId) { if (!this.currentUser) return { success: false, error: 'Необходимо войти' }; const favorites = this.currentUser.favorites || []; const index = favorites.indexOf(salonId); if (index > -1) { favorites.splice(index, 1); } else { favorites.push(salonId); } this.currentUser.favorites = favorites; const userIndex = this.users.findIndex(u => u.id === this.currentUser.id); this.users[userIndex] = this.currentUser; this.saveData('users', this.users); this.saveData('currentUser', this.currentUser); return { success: true, isFavorite: index === -1 }; } } // Application Logic class App { constructor() { this.db = new BeautyDatabase(); this.currentView = 'list'; this.currentAuthTab = 'login'; this.currentAuthRole = 'client'; this.pendingRegistration = null; // Store reg data during verification this.filters = { type: ['salon', 'master'], price: 'all', rating: 0, service: null, openNow: false, search: '', location: '' }; this.map = null; this.markers = []; this.init(); } init() { this.updateAuthUI(); this.renderSalons(); this.initMap(); // Check URL params for search const params = new URLSearchParams(window.location.search); const search = params.get('search'); if (search) { document.getElementById('searchInput').value = search; this.filters.search = search; this.renderSalons(); } } initMap() { if (this.map) return; this.map = L.map('map').setView([55.75, 37.61], 12); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(this.map); } updateMapMarkers() { if (!this.map) return; // Clear existing markers this.markers.forEach(m => this.map.removeLayer(m)); this.markers = []; const filtered = this.getFilteredSalons(); filtered.forEach(salon => { const marker = L.marker(salon.coords) .addTo(this.map) .bindPopup(`

${salon.name}

${salon.address}

★ ${salon.rating}

`); marker.on('click', () => this.openSalonModal(salon.id)); this.markers.push(marker); }); if (filtered.length > 0) { const group = new L.featureGroup(this.markers); this.map.fitBounds(group.getBounds().pad(0.1)); } } getFilteredSalons() { return this.db.salons.filter(salon => { // Type filter if (!this.filters.type.includes(salon.type)) return false; // Price filter if (this.filters.price !== 'all' && salon.priceRange !== this.filters.price) return false; // Rating filter if (parseFloat(salon.rating) < this.filters.rating) return false; // Open now if (this.filters.openNow && !salon.isOpen) return false; // Service filter if (this.filters.service) { const hasService = salon.services.some(s => s.name.toLowerCase().includes(this.filters.service.toLowerCase()) ); if (!hasService) return false; } // Search filter if (this.filters.search) { const searchLower = this.filters.search.toLowerCase(); const matches = salon.name.toLowerCase().includes(searchLower) || salon.services.some(s => s.name.toLowerCase().includes(searchLower)); if (!matches) return false; } // Location filter if (this.filters.location) { if (!salon.address.toLowerCase().includes(this.filters.location.toLowerCase())) return false; } return true; }); } renderSalons() { const container = document.getElementById('listView'); const filtered = this.getFilteredSalons(); document.getElementById('resultsCount').textContent = filtered.length; if (filtered.length === 0) { container.innerHTML = ''; document.getElementById('emptyState').classList.remove('hidden'); return; } document.getElementById('emptyState').classList.add('hidden'); container.innerHTML = filtered.map(salon => this.createSalonCard(salon)).join(''); // Re-init icons setTimeout(() => lucide.createIcons(), 0); } createSalonCard(salon) { const isFav = this.db.currentUser?.favorites?.includes(salon.id); const servicesList = salon.services.slice(0, 3).map(s => s.name).join(' • '); return `
${salon.name}
${salon.isOpen ? 'Открыто' : 'Закрыто'}

${salon.name}

${salon.address}

${salon.rating}

${servicesList}

${salon.services.slice(0, 2).map(s => ` от ${s.price} ₽ `).join('')}
`; } // View handlers toggleView(view) { this.currentView = view; const listView = document.getElementById('listView'); const mapView = document.getElementById('mapView'); if (view === 'map') { listView.classList.add('hidden'); mapView.classList.remove('hidden'); setTimeout(() => { this.map.invalidateSize(); this.updateMapMarkers(); }, 100); } else { listView.classList.remove('hidden'); mapView.classList.add('hidden'); this.renderSalons(); } } // Filter handlers handleSearch(value) { this.filters.search = value; this.debounceRender(); } handleLocationSearch(value) { this.filters.location = value; this.debounceRender(); } performSearch() { this.renderSalons(); if (this.currentView === 'map') { this.updateMapMarkers(); } } debounceRender() { clearTimeout(this.searchTimeout); this.searchTimeout = setTimeout(() => this.performSearch(), 300); } filterByService(service) { this.filters.service = this.filters.service === service ? null : service; // Update UI document.querySelectorAll('.service-tag').forEach(tag => { if (tag.textContent.toLowerCase().includes(service)) { tag.classList.toggle('active', this.filters.service === service); } else { tag.classList.remove('active'); } }); this.renderSalons(); } filterByType(type) { const index = this.filters.type.indexOf(type); if (index > -1) { this.filters.type.splice(index, 1); } else { this.filters.type.push(type); } this.renderSalons(); } filterByPrice(price) { this.filters.price = price; this.renderSalons(); } filterByRating(rating) { this.filters.rating = rating; this.renderSalons(); } toggleOpenNow(checked) { this.filters.openNow = checked; this.renderSalons(); } resetFilters() { this.filters = { type: ['salon', 'master'], price: 'all', rating: 0, service: null, openNow: false, search: '', location: '' }; document.getElementById('searchInput').value = ''; document.getElementById('locationInput').value = ''; document.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = true); document.querySelectorAll('.service-tag').forEach(tag => tag.classList.remove('active')); this.renderSalons(); } // Auth handlers openAuth(role = 'client') { this.currentAuthRole = role; document.getElementById('authModal').classList.remove('hidden'); document.getElementById('businessFields').classList.toggle('hidden', role !== 'business'); lucide.createIcons(); } closeAuth() { document.getElementById('authModal').classList.add('hidden'); } switchAuthTab(tab) { this.currentAuthTab = tab; document.getElementById('tabLogin').className = tab === 'login' ? 'flex-1 py-2 text-sm font-medium rounded-md bg-white text-gray-900 shadow-sm transition' : 'flex-1 py-2 text-sm font-medium rounded-md text-gray-600 hover:text-gray-900 transition'; document.getElementById('tabRegister').className = tab === 'register' ? 'flex-1 py-2 text-sm font-medium rounded-md bg-white text-gray-900 shadow-sm transition' : 'flex-1 py-2 text-sm font-medium rounded-md text-gray-600 hover:text-gray-900 transition'; document.getElementById('authTitle').textContent = tab === 'login' ? 'Вход' : 'Регистрация'; document.getElementById('authButtonText').textContent = tab === 'login' ? 'Войти' : 'Зарегистрироваться'; document.getElementById('authFooterText').textContent = tab === 'login' ? 'Нет аккаунта?' : 'Уже есть аккаунт?'; document.getElementById('authFooterLink').textContent = tab === 'login' ? 'Зарегистрироваться' : 'Войти'; } async handleAuth(e) { e.preventDefault(); const formData = new FormData(e.target); const email = formData.get('email'); const password = formData.get('password'); if (this.currentAuthTab === 'login') { const result = await this.db.login(email, password); if (result.success) { this.closeAuth(); this.updateAuthUI(); alert('Добро пожаловать!'); } else { alert(result.error); } } else { // Registration with verification const roleData = {}; if (this.currentAuthRole === 'client') { roleData.firstName = formData.get('firstName') || ''; roleData.lastName = formData.get('lastName') || ''; roleData.phone = formData.get('phone') || ''; } else { roleData.companyName = formData.get('businessName') || ''; roleData.businessType = formData.get('businessType') || 'salon'; roleData.companyDescription = ''; // Will fill in dashboard roleData.address = formData.get('businessAddress') || ''; roleData.businessPhone = formData.get('businessPhone') || ''; roleData.inn = formData.get('inn') || ''; } // Store pending data this.pendingRegistration = { email, password, role: this.currentAuthRole, roleData }; // Init registration (get verification code) const result = await this.db.registerInit(email, password, this.currentAuthRole, roleData); if (result.success) { this.showVerificationStep(result.code); } else { alert(result.error); } } } updateAuthUI() { const authButtons = document.getElementById('authButtons'); const userMenu = document.getElementById('userMenu'); if (this.db.currentUser) { authButtons.classList.add('hidden'); userMenu.classList.remove('hidden'); userMenu.classList.add('flex'); document.getElementById('userName').textContent = this.db.currentUser.name || this.db.currentUser.email; } else { authButtons.classList.remove('hidden'); userMenu.classList.add('hidden'); userMenu.classList.remove('flex'); } } logout() { this.db.logout(); this.updateAuthUI(); } // Dashboard showDashboard() { if (!this.db.currentUser) return; document.getElementById('dashboardModal').classList.remove('hidden'); if (this.db.currentUser.role === 'client') { document.getElementById('clientDashboard').classList.remove('hidden'); document.getElementById('businessDashboard').classList.add('hidden'); document.getElementById('dashboardSubtitle').textContent = 'Личный кабинет клиента'; this.loadClientDashboard(); } else { document.getElementById('clientDashboard').classList.add('hidden'); document.getElementById('businessDashboard').classList.remove('hidden'); document.getElementById('dashboardSubtitle').textContent = 'Управление бизнесом'; this.loadBusinessDashboard(); } lucide.createIcons(); } closeDashboard() { document.getElementById('dashboardModal').classList.add('hidden'); } loadClientDashboard() { const userBookings = this.db.bookings.filter(b => b.userId === this.db.currentUser.id); const favorites = this.db.currentUser.favorites || []; document.getElementById('clientBookingsCount').textContent = userBookings.length; document.getElementById('clientFavoritesCount').textContent = favorites.length; const bookingsList = document.getElementById('clientBookingsList'); if (userBookings.length === 0) { bookingsList.innerHTML = '

У вас пока нет записей

'; } else { bookingsList.innerHTML = userBookings.map(b => { const salon = this.db.salons.find(s => s.id === b.salonId); return `

${salon?.name || 'Неизвестно'}

${b.service} • ${b.date} ${b.time}

${b.status === 'confirmed' ? 'Подтверждено' : 'Ожидание'}
`; }).join(''); } } loadBusinessDashboard() { const salon = this.db.salons.find(s => s.ownerId === this.db.currentUser.id); if (!salon) return; document.getElementById('editBusinessName').value = salon.name; document.getElementById('editBusinessPhone').value = salon.phone; document.getElementById('editBusinessAddress').value = salon.address; document.getElementById('editBusinessDesc').value = salon.description; // Services const servicesList = document.getElementById('servicesList'); servicesList.innerHTML = salon.services.map((s, i) => `
`).join(''); // Stats document.getElementById('statViews').textContent = Math.floor(Math.random() * 1000); document.getElementById('statCalls').textContent = Math.floor(Math.random() * 50); document.getElementById('statBookings').textContent = this.db.bookings.filter(b => b.salonId === salon.id).length; } switchDashboardTab(tab) { ['Profile', 'Services', 'Bookings', 'Stats'].forEach(t => { document.getElementById(`dashTab${t}`).className = t.toLowerCase() === tab ? 'pb-4 px-2 border-b-2 border-pink-500 text-pink-600 font-medium' : 'pb-4 px-2 border-b-2 border-transparent text-gray-500 hover:text-gray-700 font-medium'; document.getElementById(`dashContent${t}`).classList.toggle('hidden', t.toLowerCase() !== tab); }); } saveBusinessProfile() { const salon = this.db.salons.find(s => s.ownerId === this.db.currentUser.id); if (salon) { this.db.updateSalon(salon.id, { name: document.getElementById('editBusinessName').value, phone: document.getElementById('editBusinessPhone').value, address: document.getElementById('editBusinessAddress').value, description: document.getElementById('editBusinessDesc').value }); alert('Изменения сохранены!'); this.renderSalons(); } } addService() { const salon = this.db.salons.find(s => s.ownerId === this.db.currentUser.id); if (salon) { salon.services.push({ name: 'Новая услуга', price: 1000, duration: 60 }); this.db.saveData('salons', this.db.salons); this.loadBusinessDashboard(); lucide.createIcons(); } } updateService(index, field, value) { const salon = this.db.salons.find(s => s.ownerId === this.db.currentUser.id); if (salon) { salon.services[index][field] = field === 'price' ? parseInt(value) : value; this.db.saveData('salons', this.db.salons); } } removeService(index) { const salon = this.db.salons.find(s => s.ownerId === this.db.currentUser.id); if (salon) { salon.services.splice(index, 1); this.db.saveData('salons', this.db.salons); this.loadBusinessDashboard(); lucide.createIcons(); } } // Salon Detail openSalonModal(salonId) { const salon = this.db.salons.find(s => s.id === salonId); if (!salon) return; const isFav = this.db.currentUser?.favorites?.includes(salonId); document.getElementById('salonModalContent').innerHTML = `
${salon.type === 'salon' ? 'Салон красоты' : 'Индивидуальный мастер'}

${salon.name}

${salon.address} ${salon.rating} (${salon.reviews} отзывов)
${salon.isOpen ? 'Открыто' : 'Закрыто'}

${salon.description}

Услуги

${salon.services.map(s => `

${s.name}

${s.duration} мин

${s.price} ₽
`).join('')}
Позвонить
`; document.getElementById('salonModal').classList.remove('hidden'); lucide.createIcons(); } closeSalonModal() { document.getElementById('salonModal').classList.add('hidden'); } bookService(salonId, serviceName = '') { if (!this.db.currentUser) { alert('Для записи необходимо войти в аккаунт'); this.openAuth('client'); return; } const date = prompt('Введите дату (ДД.ММ.ГГГГ):', '25.12.2024'); const time = prompt('Введите время (ЧЧ:ММ):', '14:00'); if (date && time) { const result = this.db.addBooking(salonId, serviceName || 'Консультация', date, time); if (result.success) { alert('Запись успешно создана! Мастер свяжется с вами для подтверждения.'); this.closeSalonModal(); } } } toggleFavorite(salonId) { if (!this.db.currentUser) { this.openAuth('client'); return; } const result = this.db.toggleFavorite(salonId); if (result.success) { this.renderSalons(); if (!document.getElementById('salonModal').classList.contains('hidden')) { this.openSalonModal(salonId); } } } showFavorites() { if (!this.db.currentUser || !this.db.currentUser.favorites?.length) { alert('У вас пока нет избранных заведений'); return; } // Filter to show only favorites const favIds = this.db.currentUser.favorites; const container = document.getElementById('listView'); const favSalons = this.db.salons.filter(s => favIds.includes(s.id)); document.getElementById('resultsTitle').textContent = 'Избранное'; document.getElementById('resultsCount').textContent = favSalons.length; document.getElementById('listView').classList.remove('hidden'); document.getElementById('mapView').classList.add('hidden'); if (favSalons.length === 0) { container.innerHTML = ''; document.getElementById('emptyState').classList.remove('hidden'); } else { document.getElementById('emptyState').classList.add('hidden'); container.innerHTML = favSalons.map(salon => this.createSalonCard(salon)).join(''); lucide.createIcons(); } } goHome() { document.getElementById('resultsTitle').textContent = 'Все заведения'; this.resetFilters(); window.scrollTo({ top: 0, behavior: 'smooth' }); } } // Initialize const app = new App();