modified: main_dc/yalarba/easySite/easySite/app/components/layout/Header.vue
modified: main_dc/yalarba/easySite/easySite/app/composables/useObjects.ts modified: main_dc/yalarba/easySite/easySite/app/pages/auth/login.vue modified: main_dc/yalarba/easySite/easySite/app/pages/auth/register.vue modified: main_dc/yalarba/easySite/easySite/app/pages/index.vue modified: main_dc/yalarba/easySite/easySite/app/pages/objects/[id]/edit.vue modified: main_dc/yalarba/easySite/easySite/app/pages/objects/create.vue modified: main_dc/yalarba/easySite/easySite/app/pages/objects/my-objects.vue modified: main_dc/yalarba/easySite/easySite/app/pages/profile/index.vue modified: main_dc/yalarba/easySite/easySite/nuxt.config.ts set main index page for easysite102.ru with header and search bar
This commit is contained in:
@@ -1,64 +1,162 @@
|
|||||||
<template>
|
<template>
|
||||||
<header class="bg-white shadow-sm sticky top-0 z-50">
|
<header class="header">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="flex justify-between items-center py-4">
|
<div class="header-content">
|
||||||
<!-- Логотип -->
|
<!-- Логотип -->
|
||||||
<NuxtLink to="/" class="flex items-center gap-3">
|
<NuxtLink to="/" class="logo">
|
||||||
<div class="w-10 h-10 bg-primary-500 rounded-lg flex items-center justify-center">
|
<div class="logo-icon">
|
||||||
<span class="text-white font-bold text-lg">T</span>
|
<span class="logo-text">T</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="font-bold text-2xl">TravelEasy</span>
|
<span class="logo-name">TravelEasy</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<!-- Навигация -->
|
<!-- Навигация -->
|
||||||
<nav class="hidden md:flex gap-6">
|
<nav class="nav">
|
||||||
<NuxtLink
|
<NuxtLink to="/profile" class="nav-link">
|
||||||
to="/"
|
👤 Профиль
|
||||||
class="nav-link"
|
|
||||||
:class="{ active: $route.path === '/' }"
|
|
||||||
>
|
|
||||||
Главная
|
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink to="/auth/login" class="nav-link">
|
||||||
to="/objects"
|
🔑 Войти
|
||||||
class="nav-link"
|
|
||||||
:class="{ active: $route.path.startsWith('/objects') }"
|
|
||||||
>
|
|
||||||
Объекты
|
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink
|
<NuxtLink to="/auth/register" class="btn btn-primary nav-button">
|
||||||
to="/objects/my-objects"
|
📝 Регистрация
|
||||||
class="nav-link"
|
|
||||||
>
|
|
||||||
Мои объекты
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
to="/objects/create"
|
|
||||||
class="nav-link"
|
|
||||||
>
|
|
||||||
Добавить объект
|
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Действия пользователя -->
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<NuxtLink to="/objects/create" class="btn btn-primary hidden sm:block">
|
|
||||||
Добавить объект
|
|
||||||
</NuxtLink>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<NuxtLink to="/auth/login" class="btn btn-outline">
|
|
||||||
Войти
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink to="/auth/register" class="btn btn-primary">
|
|
||||||
Регистрация
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// Убрана проверка авторизации - все страницы доступны
|
// Простой хедер без проверки авторизации
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
background: var(--primary-500);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
color: var(--text-inverse);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-name {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
color: var(--primary-600);
|
||||||
|
background: var(--primary-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-content {
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-name {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.logo-name {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link::before {
|
||||||
|
content: attr(data-icon);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,225 +1,39 @@
|
|||||||
import { ref } from 'vue'
|
// В файле composables/useObjects.ts обновляем fetchObjects:
|
||||||
|
|
||||||
export interface ObjectItem {
|
const fetchObjects = async (filters?: any): Promise<ObjectItem[]> => {
|
||||||
id: number
|
loading.value = true
|
||||||
title: string
|
error.value = null
|
||||||
description: string
|
|
||||||
type: 'hotel' | 'apartment' | 'villa' | 'camping' | 'restaurant' | 'attraction'
|
return new Promise((resolve) => {
|
||||||
category: string
|
setTimeout(() => {
|
||||||
address: string
|
let filteredObjects = [...mockObjects]
|
||||||
city: string
|
|
||||||
country: string
|
// Применяем фильтры
|
||||||
price: number
|
if (filters) {
|
||||||
priceUnit: 'per_night' | 'per_person' | 'fixed'
|
if (filters.search) {
|
||||||
amenities: string[]
|
const searchTerm = filters.search.toLowerCase()
|
||||||
images: string[]
|
filteredObjects = filteredObjects.filter(obj =>
|
||||||
contactEmail: string
|
obj.title.toLowerCase().includes(searchTerm) ||
|
||||||
contactPhone: string
|
obj.city.toLowerCase().includes(searchTerm) ||
|
||||||
website?: string
|
obj.description.toLowerCase().includes(searchTerm)
|
||||||
coordinates?: {
|
)
|
||||||
lat: number
|
|
||||||
lng: number
|
|
||||||
}
|
|
||||||
isActive: boolean
|
|
||||||
createdAt: string
|
|
||||||
updatedAt: string
|
|
||||||
userId: number
|
|
||||||
rating?: number
|
|
||||||
reviewCount?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useObjects = () => {
|
|
||||||
const objects = ref<ObjectItem[]>([])
|
|
||||||
const loading = ref(false)
|
|
||||||
const error = ref<string | null>(null)
|
|
||||||
|
|
||||||
// Мок-данные объектов
|
|
||||||
const mockObjects: ObjectItem[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'Отель "Морской бриз"',
|
|
||||||
description: 'Комфортабельный отель с видом на море. Идеальное место для отдыха всей семьей.',
|
|
||||||
type: 'hotel',
|
|
||||||
category: 'accommodation',
|
|
||||||
address: 'ул. Приморская, 15',
|
|
||||||
city: 'Сочи',
|
|
||||||
country: 'Россия',
|
|
||||||
price: 5000,
|
|
||||||
priceUnit: 'per_night',
|
|
||||||
amenities: ['wifi', 'parking', 'breakfast', 'pool', 'spa'],
|
|
||||||
images: ['/images/hotel1.jpg', '/images/hotel2.jpg'],
|
|
||||||
contactEmail: 'hotel@example.com',
|
|
||||||
contactPhone: '+7 999 123-45-67',
|
|
||||||
website: 'https://hotel-example.com',
|
|
||||||
coordinates: { lat: 43.5855, lng: 39.7231 },
|
|
||||||
isActive: true,
|
|
||||||
createdAt: '2024-01-15T10:00:00Z',
|
|
||||||
updatedAt: '2024-01-15T10:00:00Z',
|
|
||||||
userId: 1,
|
|
||||||
rating: 4.5,
|
|
||||||
reviewCount: 23
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'Апартаменты в центре',
|
|
||||||
description: 'Современные апартаменты в историческом центре города.',
|
|
||||||
type: 'apartment',
|
|
||||||
category: 'accommodation',
|
|
||||||
address: 'ул. Центральная, 25',
|
|
||||||
city: 'Москва',
|
|
||||||
country: 'Россия',
|
|
||||||
price: 3500,
|
|
||||||
priceUnit: 'per_night',
|
|
||||||
amenities: ['wifi', 'kitchen', 'washing_machine'],
|
|
||||||
images: ['/images/apartment1.jpg'],
|
|
||||||
contactEmail: 'apart@example.com',
|
|
||||||
contactPhone: '+7 999 765-43-21',
|
|
||||||
isActive: true,
|
|
||||||
createdAt: '2024-01-10T14:30:00Z',
|
|
||||||
updatedAt: '2024-01-12T09:15:00Z',
|
|
||||||
userId: 2,
|
|
||||||
rating: 4.2,
|
|
||||||
reviewCount: 15
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// Получить все объекты
|
|
||||||
const fetchObjects = async (filters?: any): Promise<ObjectItem[]> => {
|
|
||||||
loading.value = true
|
|
||||||
error.value = null
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
let filteredObjects = [...mockObjects]
|
|
||||||
|
|
||||||
// Применяем фильтры
|
|
||||||
if (filters) {
|
|
||||||
if (filters.type) {
|
|
||||||
filteredObjects = filteredObjects.filter(obj => obj.type === filters.type)
|
|
||||||
}
|
|
||||||
if (filters.city) {
|
|
||||||
filteredObjects = filteredObjects.filter(obj =>
|
|
||||||
obj.city.toLowerCase().includes(filters.city.toLowerCase())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (filters.userId) {
|
|
||||||
filteredObjects = filteredObjects.filter(obj => obj.userId === filters.userId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (filters.type) {
|
||||||
objects.value = filteredObjects
|
filteredObjects = filteredObjects.filter(obj => obj.type === filters.type)
|
||||||
loading.value = false
|
|
||||||
resolve(filteredObjects)
|
|
||||||
}, 500)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получить объект по ID
|
|
||||||
const fetchObjectById = async (id: number): Promise<ObjectItem | null> => {
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const object = mockObjects.find(obj => obj.id === id) || null
|
|
||||||
loading.value = false
|
|
||||||
resolve(object)
|
|
||||||
}, 300)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создать новый объект
|
|
||||||
const createObject = async (objectData: Omit<ObjectItem, 'id' | 'createdAt' | 'updatedAt'>): Promise<ObjectItem> => {
|
|
||||||
loading.value = true
|
|
||||||
error.value = null
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
const newObject: ObjectItem = {
|
|
||||||
...objectData,
|
|
||||||
id: Date.now(),
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
mockObjects.push(newObject)
|
|
||||||
objects.value.push(newObject)
|
|
||||||
loading.value = false
|
|
||||||
resolve(newObject)
|
|
||||||
} catch (err) {
|
|
||||||
error.value = 'Ошибка при создании объекта'
|
|
||||||
loading.value = false
|
|
||||||
reject(err)
|
|
||||||
}
|
}
|
||||||
}, 1000)
|
if (filters.city) {
|
||||||
})
|
filteredObjects = filteredObjects.filter(obj =>
|
||||||
}
|
obj.city.toLowerCase().includes(filters.city.toLowerCase())
|
||||||
|
)
|
||||||
// Обновить объект
|
|
||||||
const updateObject = async (id: number, objectData: Partial<ObjectItem>): Promise<ObjectItem> => {
|
|
||||||
loading.value = true
|
|
||||||
error.value = null
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
const index = mockObjects.findIndex(obj => obj.id === id)
|
|
||||||
if (index === -1) {
|
|
||||||
throw new Error('Объект не найден')
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedObject: ObjectItem = {
|
|
||||||
...mockObjects[index],
|
|
||||||
...objectData,
|
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
mockObjects[index] = updatedObject
|
|
||||||
objects.value[index] = updatedObject
|
|
||||||
loading.value = false
|
|
||||||
resolve(updatedObject)
|
|
||||||
} catch (err) {
|
|
||||||
error.value = 'Ошибка при обновлении объекта'
|
|
||||||
loading.value = false
|
|
||||||
reject(err)
|
|
||||||
}
|
}
|
||||||
}, 800)
|
if (filters.userId) {
|
||||||
})
|
filteredObjects = filteredObjects.filter(obj => obj.userId === filters.userId)
|
||||||
}
|
|
||||||
|
|
||||||
// Удалить объект
|
|
||||||
const deleteObject = async (id: number): Promise<void> => {
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const index = mockObjects.findIndex(obj => obj.id === id)
|
|
||||||
if (index !== -1) {
|
|
||||||
mockObjects.splice(index, 1)
|
|
||||||
objects.value.splice(index, 1)
|
|
||||||
}
|
}
|
||||||
loading.value = false
|
}
|
||||||
resolve()
|
|
||||||
}, 500)
|
objects.value = filteredObjects
|
||||||
})
|
loading.value = false
|
||||||
}
|
resolve(filteredObjects)
|
||||||
|
}, 500)
|
||||||
// Получить объекты текущего пользователя
|
})
|
||||||
const fetchMyObjects = async (): Promise<ObjectItem[]> => {
|
|
||||||
// В реальном приложении здесь будет userId из авторизации
|
|
||||||
const currentUserId = 1
|
|
||||||
return fetchObjects({ userId: currentUserId })
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
objects,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
fetchObjects,
|
|
||||||
fetchObjectById,
|
|
||||||
createObject,
|
|
||||||
updateObject,
|
|
||||||
deleteObject,
|
|
||||||
fetchMyObjects
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-50 flex items-center justify-center py-12">
|
<div class="auth-page">
|
||||||
<div class="max-w-md w-full">
|
<div class="auth-container">
|
||||||
<div class="card">
|
<div class="auth-card">
|
||||||
<div class="card-header text-center">
|
<div class="card">
|
||||||
<h1 class="text-2xl font-bold">Вход в систему</h1>
|
<div class="card-header text-center">
|
||||||
<p class="text-gray-600 mt-2">Введите свои учетные данные</p>
|
<h1 class="auth-title">Вход в систему</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<LoginForm />
|
<form @submit.prevent="handleSubmit" class="auth-form">
|
||||||
</div>
|
<div class="form-group">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
<div class="card-footer text-center">
|
<input
|
||||||
<p class="text-gray-600">
|
v-model="form.email"
|
||||||
Нет аккаунта?
|
type="email"
|
||||||
<NuxtLink to="/auth/register" class="text-primary-600 font-medium">
|
class="form-input"
|
||||||
Зарегистрируйтесь
|
placeholder="your@email.com"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Пароль</label>
|
||||||
|
<input
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="Введите пароль"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary auth-button">
|
||||||
|
Войти
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<p class="auth-footer-text">
|
||||||
|
Нет аккаунта?
|
||||||
|
<NuxtLink to="/auth/register" class="auth-link">
|
||||||
|
Зарегистрируйтесь
|
||||||
|
</NuxtLink>
|
||||||
|
</p>
|
||||||
|
<NuxtLink to="/" class="auth-link home-link">
|
||||||
|
← На главную
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,7 +55,71 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
const form = ref({
|
||||||
layout: 'auth'
|
email: '',
|
||||||
|
password: ''
|
||||||
})
|
})
|
||||||
</script>
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
// В демо-режиме просто переходим в профиль
|
||||||
|
alert('Демо-режим: вход выполнен')
|
||||||
|
navigateTo('/profile')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.auth-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-button {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer-text {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-link {
|
||||||
|
color: var(--primary-600);
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-link:hover {
|
||||||
|
color: var(--primary-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,52 +1,76 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex items-center justify-center bg-gray-50">
|
<div class="auth-page">
|
||||||
<div class="max-w-md w-full space-y-8">
|
<div class="auth-container">
|
||||||
<div>
|
<div class="auth-card">
|
||||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
<div class="card">
|
||||||
Регистрация
|
<div class="card-header text-center">
|
||||||
</h2>
|
<h1 class="auth-title">Регистрация</h1>
|
||||||
</div>
|
</div>
|
||||||
<form class="mt-8 space-y-6" @submit.prevent="handleRegister">
|
|
||||||
<div>
|
<div class="card-body">
|
||||||
<input
|
<form @submit.prevent="handleSubmit" class="auth-form">
|
||||||
v-model="form.name"
|
<div class="form-group">
|
||||||
type="text"
|
<label class="form-label">Имя</label>
|
||||||
required
|
<input
|
||||||
class="relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
v-model="form.name"
|
||||||
placeholder="Имя"
|
type="text"
|
||||||
/>
|
class="form-input"
|
||||||
|
placeholder="Ваше имя"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Email</label>
|
||||||
|
<input
|
||||||
|
v-model="form.email"
|
||||||
|
type="email"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="your@email.com"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Пароль</label>
|
||||||
|
<input
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="Придумайте пароль"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Подтверждение пароля</label>
|
||||||
|
<input
|
||||||
|
v-model="form.passwordConfirm"
|
||||||
|
type="password"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="Повторите пароль"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary auth-button">
|
||||||
|
Зарегистрироваться
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<p class="auth-footer-text">
|
||||||
|
Уже есть аккаунт?
|
||||||
|
<NuxtLink to="/auth/login" class="auth-link">
|
||||||
|
Войдите
|
||||||
|
</NuxtLink>
|
||||||
|
</p>
|
||||||
|
<NuxtLink to="/" class="auth-link home-link">
|
||||||
|
← На главную
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
v-model="form.email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
class="relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
|
||||||
placeholder="Email"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
v-model="form.password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
class="relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
|
||||||
placeholder="Пароль"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
||||||
>
|
|
||||||
Зарегистрироваться
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div class="text-center">
|
|
||||||
<NuxtLink to="/auth/login" class="text-indigo-600 hover:text-indigo-500">
|
|
||||||
Уже есть аккаунт? Войдите
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,10 +80,75 @@
|
|||||||
const form = ref({
|
const form = ref({
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: ''
|
password: '',
|
||||||
|
passwordConfirm: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleRegister = async () => {
|
const handleSubmit = () => {
|
||||||
console.log('Register attempt:', form.value)
|
if (form.value.password !== form.value.passwordConfirm) {
|
||||||
|
alert('Пароли не совпадают')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// В демо-режиме просто переходим в профиль
|
||||||
|
alert('Демо-режим: регистрация выполнена')
|
||||||
|
navigateTo('/profile')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.auth-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-button {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer-text {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-link {
|
||||||
|
color: var(--primary-600);
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-link:hover {
|
||||||
|
color: var(--primary-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,86 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-50">
|
<div class="page-wrapper">
|
||||||
|
<!-- Хедер -->
|
||||||
|
<Header />
|
||||||
|
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<section class="hero-section">
|
<section class="hero-section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="relative z-10 py-20">
|
<div class="hero-content">
|
||||||
<div class="max-w-2xl mx-auto text-center">
|
<div class="hero-text">
|
||||||
<h1 class="text-5xl font-bold mb-6">База объектов для путешествий</h1>
|
<h1 class="hero-title">Поиск объектов для путешествий</h1>
|
||||||
<p class="text-xl mb-8 opacity-90">Добавляйте и находите лучшие места для ваших маршрутов</p>
|
<p class="hero-subtitle">Найдите лучшие места для ваших маршрутов</p>
|
||||||
|
|
||||||
<!-- Простой поиск -->
|
<!-- Поисковая строка -->
|
||||||
<div class="max-w-xl mx-auto mb-12">
|
<div class="search-container">
|
||||||
<div class="flex gap-2">
|
<div class="search-box">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Поиск объектов..."
|
placeholder="Введите название, город или описание..."
|
||||||
class="form-input flex-1"
|
class="search-input"
|
||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
>
|
>
|
||||||
<button @click="handleSearch" class="btn btn-primary">
|
<button @click="handleSearch" class="btn btn-primary search-button">
|
||||||
Найти
|
Найти
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Кнопки авторизации -->
|
|
||||||
<div class="flex gap-4 justify-center">
|
|
||||||
<NuxtLink to="/auth/login" class="btn btn-outline text-lg px-8 py-3">
|
|
||||||
Войти
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink to="/auth/register" class="btn btn-primary text-lg px-8 py-3">
|
|
||||||
Регистрация
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Быстрая навигация -->
|
|
||||||
<section class="py-16 bg-white">
|
|
||||||
<div class="container">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-4xl mx-auto">
|
|
||||||
<NuxtLink
|
|
||||||
to="/objects"
|
|
||||||
class="category-card text-center group"
|
|
||||||
>
|
|
||||||
<div class="category-card__icon group-hover:bg-primary-200">
|
|
||||||
🔍
|
|
||||||
</div>
|
|
||||||
<h3 class="font-semibold text-lg mb-2">Все объекты</h3>
|
|
||||||
<p class="text-sm text-gray-600">Просмотр всей базы объектов</p>
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<NuxtLink
|
|
||||||
to="/objects/my-objects"
|
|
||||||
class="category-card text-center group"
|
|
||||||
>
|
|
||||||
<div class="category-card__icon group-hover:bg-primary-200">
|
|
||||||
📝
|
|
||||||
</div>
|
|
||||||
<h3 class="font-semibold text-lg mb-2">Мои объекты</h3>
|
|
||||||
<p class="text-sm text-gray-600">Управление вашими объектами</p>
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<NuxtLink
|
|
||||||
to="/objects/create"
|
|
||||||
class="category-card text-center group"
|
|
||||||
>
|
|
||||||
<div class="category-card__icon group-hover:bg-primary-200">
|
|
||||||
➕
|
|
||||||
</div>
|
|
||||||
<h3 class="font-semibold text-lg mb-2">Добавить объект</h3>
|
|
||||||
<p class="text-sm text-gray-600">Создать новую запись</p>
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import Header from '~/components/layout/Header.vue'
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
@@ -90,4 +45,137 @@ const handleSearch = () => {
|
|||||||
navigateTo('/objects')
|
navigateTo('/objects')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-wrapper {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section {
|
||||||
|
background: linear-gradient(135deg, var(--primary-500) 0%, var(--primary-700) 100%);
|
||||||
|
color: var(--text-inverse);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><polygon fill="%230ea5e9" points="0,1000 1000,0 1000,1000"/></svg>');
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 2rem 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-text {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 0.5rem;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
min-width: 0; /* Важно для правильного сжатия в flex */
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -40,44 +40,44 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
// definePageMeta({
|
||||||
middleware: 'auth'
|
// middleware: 'auth'
|
||||||
})
|
// })
|
||||||
|
|
||||||
const route = useRoute()
|
// const route = useRoute()
|
||||||
const { fetchObjectById, updateObject } = useObjects()
|
// const { fetchObjectById, updateObject } = useObjects()
|
||||||
const object = ref(null)
|
// const object = ref(null)
|
||||||
const loading = ref(true)
|
// const loading = ref(true)
|
||||||
const updating = ref(false)
|
// const updating = ref(false)
|
||||||
|
|
||||||
onMounted(async () => {
|
// onMounted(async () => {
|
||||||
const objectId = parseInt(route.params.id as string)
|
// const objectId = parseInt(route.params.id as string)
|
||||||
if (objectId) {
|
// if (objectId) {
|
||||||
const result = await fetchObjectById(objectId)
|
// const result = await fetchObjectById(objectId)
|
||||||
object.value = result
|
// object.value = result
|
||||||
}
|
// }
|
||||||
loading.value = false
|
// loading.value = false
|
||||||
})
|
// })
|
||||||
|
|
||||||
const handleSubmit = async (formData: any) => {
|
// const handleSubmit = async (formData: any) => {
|
||||||
updating.value = true
|
// updating.value = true
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
const objectId = parseInt(route.params.id as string)
|
// const objectId = parseInt(route.params.id as string)
|
||||||
await updateObject(objectId, formData)
|
// await updateObject(objectId, formData)
|
||||||
|
|
||||||
alert('Объект успешно обновлен!')
|
// alert('Объект успешно обновлен!')
|
||||||
await navigateTo(`/objects/${objectId}`)
|
// await navigateTo(`/objects/${objectId}`)
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error('Error updating object:', error)
|
// console.error('Error updating object:', error)
|
||||||
alert('Ошибка при обновлении объекта')
|
// alert('Ошибка при обновлении объекта')
|
||||||
} finally {
|
// } finally {
|
||||||
updating.value = false
|
// updating.value = false
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const handleCancel = () => {
|
// const handleCancel = () => {
|
||||||
const objectId = route.params.id
|
// const objectId = route.params.id
|
||||||
navigateTo(`/objects/${objectId}`)
|
// navigateTo(`/objects/${objectId}`)
|
||||||
}
|
// }
|
||||||
</script>
|
</script>
|
||||||
@@ -16,37 +16,37 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePageMeta({
|
// definePageMeta({
|
||||||
middleware: 'auth'
|
// middleware: 'auth'
|
||||||
})
|
// })
|
||||||
|
|
||||||
const { createObject } = useObjects()
|
// const { createObject } = useObjects()
|
||||||
const loading = ref(false)
|
// const loading = ref(false)
|
||||||
|
|
||||||
const handleSubmit = async (formData: any) => {
|
// const handleSubmit = async (formData: any) => {
|
||||||
loading.value = true
|
// loading.value = true
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
await createObject({
|
// await createObject({
|
||||||
...formData,
|
// ...formData,
|
||||||
userId: 1, // В реальном приложении из авторизации
|
// userId: 1, // В реальном приложении из авторизации
|
||||||
isActive: true,
|
// isActive: true,
|
||||||
images: formData.images || ['/images/placeholder.jpg'],
|
// images: formData.images || ['/images/placeholder.jpg'],
|
||||||
amenities: formData.amenities || []
|
// amenities: formData.amenities || []
|
||||||
})
|
// })
|
||||||
|
|
||||||
// Показываем уведомление об успехе
|
// // Показываем уведомление об успехе
|
||||||
alert('Объект успешно создан!')
|
// alert('Объект успешно создан!')
|
||||||
await navigateTo('/objects/my-objects')
|
// await navigateTo('/objects/my-objects')
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error('Error creating object:', error)
|
// console.error('Error creating object:', error)
|
||||||
alert('Ошибка при создании объекта')
|
// alert('Ошибка при создании объекта')
|
||||||
} finally {
|
// } finally {
|
||||||
loading.value = false
|
// loading.value = false
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const handleCancel = () => {
|
// const handleCancel = () => {
|
||||||
navigateTo('/objects/my-objects')
|
// navigateTo('/objects/my-objects')
|
||||||
}
|
// }
|
||||||
</script>
|
</script>
|
||||||
@@ -1,36 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-50 py-8">
|
<div class="min-h-screen bg-gray-50 py-8">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- Заголовок и статистика -->
|
<!-- Заголовок -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold">Мои объекты</h1>
|
<h1 class="text-3xl font-bold">Мои объекты</h1>
|
||||||
<p class="text-gray-600 mt-2">Управление вашими добавленными местами</p>
|
<p class="text-gray-600 mt-2">Управление вашими добавленными местами</p>
|
||||||
</div>
|
</div>
|
||||||
<NuxtLink to="/objects/create" class="btn btn-primary">
|
<div class="flex gap-3">
|
||||||
Добавить объект
|
<NuxtLink to="/objects" class="btn btn-outline">
|
||||||
</NuxtLink>
|
🔍 Все объекты
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/objects/create" class="btn btn-primary">
|
||||||
|
Добавить объект
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Статистика -->
|
<!-- Информация -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||||
<div class="card text-center">
|
<p class="text-blue-700">
|
||||||
<div class="text-2xl font-bold text-primary-600">{{ stats.total }}</div>
|
💡 Это демо-режим. В реальном приложении здесь будут отображаться только ваши объекты.
|
||||||
<div class="text-gray-600">Всего объектов</div>
|
</p>
|
||||||
</div>
|
|
||||||
<div class="card text-center">
|
|
||||||
<div class="text-2xl font-bold text-green-600">{{ stats.active }}</div>
|
|
||||||
<div class="text-gray-600">Активных</div>
|
|
||||||
</div>
|
|
||||||
<div class="card text-center">
|
|
||||||
<div class="text-2xl font-bold text-blue-600">{{ stats.rating }}</div>
|
|
||||||
<div class="text-gray-600">Средний рейтинг</div>
|
|
||||||
</div>
|
|
||||||
<div class="card text-center">
|
|
||||||
<div class="text-2xl font-bold text-orange-600">{{ stats.reviews }}</div>
|
|
||||||
<div class="text-gray-600">Отзывов</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -119,64 +111,30 @@
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Навигация -->
|
||||||
|
<div class="mt-8 flex justify-between">
|
||||||
|
<NuxtLink to="/objects" class="btn btn-outline">
|
||||||
|
← Все объекты
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/" class="btn btn-outline">
|
||||||
|
🏠 На главную
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ObjectItem } from '~/composables/useObjects'
|
// import { ObjectItem } from '~/composables/useObjects'
|
||||||
|
|
||||||
definePageMeta({
|
// // Убрали middleware - страница доступна всем
|
||||||
middleware: 'auth'
|
|
||||||
})
|
|
||||||
|
|
||||||
const { fetchMyObjects, deleteObject, objects: myObjects, loading } = useObjects()
|
// const { fetchMyObjects, deleteObject, objects: myObjects, loading } = useObjects()
|
||||||
|
|
||||||
// Статистика (мок-данные)
|
// onMounted(async () => {
|
||||||
const stats = ref({
|
// await fetchMyObjects()
|
||||||
total: 0,
|
// })
|
||||||
active: 0,
|
|
||||||
rating: 4.5,
|
|
||||||
reviews: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
// ... остальные функции без изменений ...
|
||||||
await fetchMyObjects()
|
|
||||||
updateStats()
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateStats = () => {
|
|
||||||
stats.value.total = myObjects.value.length
|
|
||||||
stats.value.active = myObjects.value.filter(obj => obj.isActive).length
|
|
||||||
stats.value.reviews = myObjects.value.reduce((sum, obj) => sum + (obj.reviewCount || 0), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTypeLabel = (type: string) => {
|
|
||||||
const types: Record<string, string> = {
|
|
||||||
hotel: 'Отель',
|
|
||||||
apartment: 'Апартаменты',
|
|
||||||
villa: 'Вилла',
|
|
||||||
camping: 'Кемпинг',
|
|
||||||
restaurant: 'Ресторан',
|
|
||||||
attraction: 'Достопримечательность'
|
|
||||||
}
|
|
||||||
return types[type] || type
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatPrice = (price: number) => {
|
|
||||||
return new Intl.NumberFormat('ru-RU').format(price) + ' ₽'
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
|
||||||
return new Date(dateString).toLocaleDateString('ru-RU')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработка удаления объекта
|
|
||||||
const handleDeleteObject = async (id: number) => {
|
|
||||||
if (confirm('Вы уверены, что хотите удалить этот объект?')) {
|
|
||||||
await deleteObject(id)
|
|
||||||
await fetchMyObjects()
|
|
||||||
updateStats()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
@@ -1,6 +1,300 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="profile-page">
|
||||||
<h1>Страница</h1>
|
<div class="container container-large">
|
||||||
<p>Содержимое страницы</p>
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">Профиль пользователя</h1>
|
||||||
|
<p class="page-subtitle">Информация о вашем аккаунте</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="profile-layout">
|
||||||
|
<!-- Основная информация -->
|
||||||
|
<div class="profile-main">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">Основная информация</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="user-info">
|
||||||
|
<div class="user-avatar-section">
|
||||||
|
<div class="user-avatar">
|
||||||
|
<span class="avatar-text">ИИ</span>
|
||||||
|
</div>
|
||||||
|
<div class="user-details">
|
||||||
|
<div class="user-name">Иван Иванов</div>
|
||||||
|
<div class="user-email">user@example.com</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-number">5</div>
|
||||||
|
<div class="stat-label">Объектов</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-number">12</div>
|
||||||
|
<div class="stat-label">Отзывов</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Быстрые действия -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">Быстрые действия</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="actions-grid">
|
||||||
|
<NuxtLink to="/objects/my-objects" class="btn btn-outline action-btn">
|
||||||
|
📝 Мои объекты
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/objects/create" class="btn btn-primary action-btn">
|
||||||
|
➕ Добавить объект
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/objects" class="btn btn-outline action-btn">
|
||||||
|
🔍 Все объекты
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/" class="btn btn-outline action-btn">
|
||||||
|
🏠 Главная
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Боковая панель -->
|
||||||
|
<div class="profile-sidebar">
|
||||||
|
<!-- Статус -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Статус аккаунта</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-icon">✅</span>
|
||||||
|
<span class="status-text">Активен</span>
|
||||||
|
</div>
|
||||||
|
<p class="status-note">Демо-режим</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Навигация -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Навигация</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="nav-links">
|
||||||
|
<NuxtLink to="/objects" class="nav-link-block">
|
||||||
|
🔍 Все объекты
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/objects/my-objects" class="nav-link-block">
|
||||||
|
📝 Мои объекты
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/objects/create" class="nav-link-block">
|
||||||
|
➕ Добавить объект
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/auth/login" class="nav-link-block">
|
||||||
|
🔑 Войти
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/auth/register" class="nav-link-block">
|
||||||
|
📝 Регистрация
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// Страница профиля доступна всем в демо-режиме
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.profile-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-large {
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 4rem;
|
||||||
|
height: 4rem;
|
||||||
|
background: var(--primary-100);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-text {
|
||||||
|
color: var(--primary-600);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-email {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--success-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-note {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-block {
|
||||||
|
display: block;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-block:hover {
|
||||||
|
color: var(--primary-600);
|
||||||
|
background: var(--primary-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 968px) {
|
||||||
|
.profile-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.user-avatar-section {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -56,6 +56,7 @@ export default defineNuxtConfig({
|
|||||||
css: {
|
css: {
|
||||||
devSourcemap: false
|
devSourcemap: false
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user