// 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.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();