first
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, FormEvent } from 'react'
|
||||
import AdminHeader from '@/components/AdminHeader'
|
||||
import {
|
||||
AdminContainer,
|
||||
AdminContent,
|
||||
PageTitle,
|
||||
LoadingSpinner,
|
||||
Spinner,
|
||||
RoomCard,
|
||||
RoomCardHeader,
|
||||
RoomCardTitle,
|
||||
FilterInput,
|
||||
DeleteButton
|
||||
} from "@/styles/admin.styled"
|
||||
import styled from 'styled-components'
|
||||
import { whiteColor, modalColor, lightGreenColor, blackColor } from '@/styles/colors'
|
||||
|
||||
const ContactCard = styled.div`
|
||||
background: rgba(15, 15, 23, 0.6);
|
||||
border: 1px solid rgba(109, 103, 148, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(76, 230, 94, 0.3);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
`
|
||||
|
||||
const ContactInfo = styled.div`
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const ContactType = styled.div`
|
||||
font-size: 0.875rem;
|
||||
color: ${modalColor};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 0.5rem;
|
||||
`
|
||||
|
||||
const ContactValue = styled.div`
|
||||
font-size: 1.125rem;
|
||||
color: ${whiteColor};
|
||||
word-break: break-word;
|
||||
`
|
||||
|
||||
const EditButton = styled.button`
|
||||
background: rgba(76, 230, 94, 0.2);
|
||||
border: 1px solid rgba(76, 230, 94, 0.4);
|
||||
color: ${lightGreenColor};
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
margin-left: 1rem;
|
||||
|
||||
&:hover {
|
||||
background: rgba(76, 230, 94, 0.3);
|
||||
border-color: ${lightGreenColor};
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
`
|
||||
|
||||
const ModalOverlay = styled.div`
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
const ModalContent = styled.form`
|
||||
background: rgba(15, 15, 23, 0.95);
|
||||
border: 1px solid rgba(109, 103, 148, 0.3);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const ModalHeader = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
`
|
||||
|
||||
const ModalTitle = styled.h3`
|
||||
font-size: 1.5rem;
|
||||
color: ${whiteColor};
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const CloseButton = styled.button`
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${modalColor};
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(109, 103, 148, 0.2);
|
||||
color: ${whiteColor};
|
||||
}
|
||||
`
|
||||
|
||||
const ModalBody = styled.div`
|
||||
margin-bottom: 1.5rem;
|
||||
`
|
||||
|
||||
const FormLabel = styled.label`
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: ${modalColor};
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
`
|
||||
|
||||
const FormInput = styled(FilterInput)`
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const ModalFooter = styled.div`
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
const CancelButton = styled.button`
|
||||
background: rgba(109, 103, 148, 0.2);
|
||||
border: 1px solid rgba(109, 103, 148, 0.4);
|
||||
color: ${modalColor};
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(109, 103, 148, 0.3);
|
||||
border-color: ${modalColor};
|
||||
}
|
||||
`
|
||||
|
||||
const SaveButton = styled.button`
|
||||
background: rgba(76, 230, 94, 0.2);
|
||||
border: 1px solid rgba(76, 230, 94, 0.4);
|
||||
color: ${lightGreenColor};
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(76, 230, 94, 0.3);
|
||||
border-color: ${lightGreenColor};
|
||||
}
|
||||
`
|
||||
|
||||
interface Contact {
|
||||
id: number
|
||||
type: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export default function AdminContactsPage() {
|
||||
const [contacts, setContacts] = useState<Contact[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [editRow, setEditRow] = useState<Contact | null>(null)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchContacts()
|
||||
}, [])
|
||||
|
||||
const fetchContacts = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/admin/contacts')
|
||||
const data: Contact[] = await res.json()
|
||||
setContacts(data)
|
||||
} catch {
|
||||
setError('Ошибка при загрузке контактов')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditClick = (row: Contact) => {
|
||||
setEditRow(row)
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const handleSave = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
const form = e.currentTarget
|
||||
const formData = new FormData(form)
|
||||
const updatedValue = formData.get('value') as string
|
||||
|
||||
const res = await fetch('/api/admin/contacts', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: editRow?.type, value: updatedValue })
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
await fetchContacts()
|
||||
setShowModal(false)
|
||||
setEditRow(null)
|
||||
} else {
|
||||
alert('Ошибка при сохранении')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminContainer>
|
||||
<AdminHeader />
|
||||
<AdminContent>
|
||||
<PageTitle>Контакты</PageTitle>
|
||||
|
||||
{loading ? (
|
||||
<LoadingSpinner>
|
||||
<Spinner />
|
||||
</LoadingSpinner>
|
||||
) : error ? (
|
||||
<div style={{ color: '#dc3545', padding: '2rem' }}>{error}</div>
|
||||
) : (
|
||||
<div>
|
||||
{contacts.map((row) => (
|
||||
<ContactCard key={row.id}>
|
||||
<ContactInfo>
|
||||
<ContactType>{row.type}</ContactType>
|
||||
<ContactValue>{row.value}</ContactValue>
|
||||
</ContactInfo>
|
||||
<EditButton onClick={() => handleEditClick(row)}>
|
||||
Редактировать
|
||||
</EditButton>
|
||||
</ContactCard>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showModal && editRow && (
|
||||
<ModalOverlay onClick={() => setShowModal(false)}>
|
||||
<ModalContent onClick={(e) => e.stopPropagation()} onSubmit={handleSave}>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Редактировать контакт</ModalTitle>
|
||||
<CloseButton type="button" onClick={() => setShowModal(false)}>
|
||||
×
|
||||
</CloseButton>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<FormLabel>Тип: {editRow.type}</FormLabel>
|
||||
<FormLabel style={{ marginTop: '1rem' }}>Значение</FormLabel>
|
||||
<FormInput
|
||||
name="value"
|
||||
type="text"
|
||||
defaultValue={editRow.value}
|
||||
required
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<CancelButton type="button" onClick={() => setShowModal(false)}>
|
||||
Отмена
|
||||
</CancelButton>
|
||||
<SaveButton type="submit">
|
||||
Сохранить
|
||||
</SaveButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</ModalOverlay>
|
||||
)}
|
||||
</AdminContent>
|
||||
</AdminContainer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, FormEvent } from 'react'
|
||||
import AdminHeader from '@/components/AdminHeader'
|
||||
|
||||
interface Faq {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export default function AdminFaqPage() {
|
||||
const [faqs, setFaqs] = useState<Faq[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [editingFaq, setEditingFaq] = useState<Faq | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetchFaqs()
|
||||
}, [])
|
||||
|
||||
const fetchFaqs = async () => {
|
||||
setLoading(true)
|
||||
const res = await fetch('/api/admin/faq')
|
||||
const data: Faq[] = await res.json()
|
||||
setFaqs(data)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('Удалить вопрос?')) return
|
||||
const res = await fetch(`/api/admin/faq?id=${id}`, { method: 'DELETE' })
|
||||
if (res.ok) fetchFaqs()
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
const form = e.currentTarget
|
||||
const formData = new FormData(form)
|
||||
const payload = Object.fromEntries(formData.entries()) as {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const method = editingFaq ? 'PUT' : 'POST'
|
||||
const res = await fetch('/api/admin/faq', {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(editingFaq ? { ...editingFaq, ...payload } : payload),
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
fetchFaqs()
|
||||
setShowModal(false)
|
||||
setEditingFaq(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminHeader />
|
||||
<div className="container py-5 text-light">
|
||||
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 className="fs-1">Часто задаваемые вопросы</h2>
|
||||
<button className="btn btn-success" onClick={() => setShowModal(true)}>
|
||||
+ Добавить вопрос
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p>Загрузка...</p>
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-dark table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Заголовок</th>
|
||||
<th>Описание</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{faqs.map((faq) => (
|
||||
<tr key={faq.id}>
|
||||
<td>{faq.title}</td>
|
||||
<td>{faq.description}</td>
|
||||
<td>
|
||||
<button
|
||||
className="btn btn-warning btn-sm me-2"
|
||||
onClick={() => {
|
||||
setEditingFaq(faq)
|
||||
setShowModal(true)
|
||||
}}
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(faq.id)}>
|
||||
✖
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showModal && (
|
||||
<div className="modal d-block bg-dark bg-opacity-75" tabIndex={-1}>
|
||||
<div className="modal-dialog">
|
||||
<form className="modal-content bg-dark text-white" onSubmit={handleSubmit}>
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">{editingFaq ? 'Редактировать' : 'Добавить'} вопрос</h5>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close btn-close-white"
|
||||
onClick={() => {
|
||||
setShowModal(false)
|
||||
setEditingFaq(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Заголовок</label>
|
||||
<input
|
||||
name="title"
|
||||
defaultValue={editingFaq?.title || ''}
|
||||
className="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Описание</label>
|
||||
<textarea
|
||||
name="description"
|
||||
defaultValue={editingFaq?.description || ''}
|
||||
className="form-control"
|
||||
required
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => {
|
||||
setShowModal(false)
|
||||
setEditingFaq(null)
|
||||
}}
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button type="submit" className="btn btn-success">
|
||||
Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
'use client'
|
||||
|
||||
import { FormEvent, useEffect, useState } from 'react'
|
||||
import AdminHeader from "@/components/AdminHeader"
|
||||
import Image from "next/image"
|
||||
|
||||
type Game = {
|
||||
id: number
|
||||
title: string
|
||||
image_url: string
|
||||
platform: string
|
||||
}
|
||||
|
||||
export default function AdminGamesPage() {
|
||||
const [games, setGames] = useState<Game[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchGames()
|
||||
}, [])
|
||||
|
||||
const fetchGames = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/admin/games')
|
||||
const data: Game[] = await res.json()
|
||||
setGames(data)
|
||||
} catch {
|
||||
setError('Ошибка при загрузке игр')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
const confirmed = confirm('Удалить игру?')
|
||||
if (!confirmed) return
|
||||
|
||||
const res = await fetch(`/api/admin/games?id=${id}`, { method: 'DELETE' })
|
||||
if (res.ok) {
|
||||
setGames((prev) => prev.filter((g) => g.id !== id))
|
||||
} else {
|
||||
alert('Ошибка при удалении')
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddGame = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
const form = e.currentTarget
|
||||
const formData = new FormData(form)
|
||||
|
||||
const res = await fetch('/api/admin/games/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
const added: Game = await res.json()
|
||||
setGames((prev) => [...prev, added])
|
||||
setShowModal(false)
|
||||
} else {
|
||||
alert('Ошибка при добавлении игры')
|
||||
}
|
||||
}
|
||||
|
||||
const imageLoader = ({ src }: { src: string }) => `/api/uploads/games/${src}`
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminHeader />
|
||||
<div className="container py-5 text-light">
|
||||
<div className="text-center mb-5">
|
||||
<button className="btn btn-primary" onClick={() => setShowModal(true)}>
|
||||
Добавить новую игру
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2 className="mb-4 fs-1 text-center">Список игр</h2>
|
||||
|
||||
{loading && <p>Загрузка...</p>}
|
||||
{error && <p className="text-danger">{error}</p>}
|
||||
|
||||
<div className="row row-cols-1 row-cols-md-4 g-4">
|
||||
{games.map((game) => (
|
||||
<div key={game.id} className="col">
|
||||
<div className="card bg-dark text-white h-100">
|
||||
{game.image_url && (
|
||||
<Image
|
||||
loader={() => imageLoader({ src: game.image_url })}
|
||||
src={`/api/uploads/games/${game.image_url}`}
|
||||
alt={game.title}
|
||||
className="card-img-top"
|
||||
width={400}
|
||||
height={160}
|
||||
style={{ objectFit: 'cover', width: '100%', height: 160 }}
|
||||
/>
|
||||
)}
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">{game.title}</h5>
|
||||
<p className="card-text">Платформа: {game.platform}</p>
|
||||
<button className="btn btn-danger" onClick={() => handleDelete(game.id)}>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showModal && (
|
||||
<div className="modal d-block bg-black bg-opacity-75" tabIndex={-1} role="dialog">
|
||||
<div className="modal-dialog modal-dialog-centered" role="document">
|
||||
<form className="modal-content bg-dark text-white" onSubmit={handleAddGame} encType="multipart/form-data">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Новая игра</h5>
|
||||
<button type="button" className="btn-close btn-close-white" onClick={() => setShowModal(false)} />
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Название</label>
|
||||
<input name="title" type="text" className="form-control" required />
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Платформа</label>
|
||||
<select name="platform" className="form-select" defaultValue="pc">
|
||||
<option value="pc">PC</option>
|
||||
<option value="ps">PS</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Изображение</label>
|
||||
<input name="image" type="file" accept="image/*" className="form-control" required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={() => setShowModal(false)}>
|
||||
Отмена
|
||||
</button>
|
||||
<button type="submit" className="btn btn-success">
|
||||
Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, FormEvent } from 'react'
|
||||
import AdminHeader from '@/components/AdminHeader'
|
||||
|
||||
const fields = ['name', 'description', 'cpu', 'ram', 'gpu', 'mouse', 'keyboard', 'headphones', 'monitor', 'bracket', 'chair'] as const
|
||||
|
||||
type Room = {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
cpu?: string
|
||||
ram?: string
|
||||
gpu?: string
|
||||
mouse?: string
|
||||
keyboard?: string
|
||||
headphones?: string
|
||||
monitor?: string
|
||||
bracket?: string
|
||||
chair?: string
|
||||
}
|
||||
|
||||
export default function AdminRoomsPage() {
|
||||
const [rooms, setRooms] = useState<Room[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [editingRoom, setEditingRoom] = useState<Room | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetchRooms()
|
||||
}, [])
|
||||
|
||||
const fetchRooms = async () => {
|
||||
setLoading(true)
|
||||
const res = await fetch('/api/admin/rooms')
|
||||
const data: Room[] = await res.json()
|
||||
setRooms(data)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('Удалить комнату?')) return
|
||||
const res = await fetch(`/api/admin/rooms?id=${id}`, { method: 'DELETE' })
|
||||
if (res.ok) fetchRooms()
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
const form = e.currentTarget
|
||||
const formData = new FormData(form)
|
||||
const payload = Object.fromEntries(formData.entries())
|
||||
|
||||
const method = editingRoom ? 'PUT' : 'POST'
|
||||
|
||||
const res = await fetch('/api/admin/rooms', {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(editingRoom ? { ...editingRoom, ...payload } : payload)
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
fetchRooms()
|
||||
setShowModal(false)
|
||||
setEditingRoom(null)
|
||||
}
|
||||
}
|
||||
|
||||
const standardRooms = rooms.filter(r => r.name.toLowerCase() !== 'ps')
|
||||
const psRooms = rooms.filter(r => r.name.toLowerCase() === 'ps')
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminHeader />
|
||||
<div className="container py-5 text-light">
|
||||
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 className="fs-1">Железо</h2>
|
||||
<button className="btn btn-success" onClick={() => setShowModal(true)}>
|
||||
+ Добавить комнату
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? <p>Загрузка...</p> : (
|
||||
<>
|
||||
<div className="table-responsive mb-5">
|
||||
<h4 className="mb-3">Стандартные комнаты</h4>
|
||||
<table className="table table-dark table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
{fields.map(f => <th key={f}>{f}</th>)}
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{standardRooms.map((room) => (
|
||||
<tr key={room.id}>
|
||||
{fields.map(f => <td key={f}>{room[f]}</td>)}
|
||||
<td>
|
||||
<button className="btn btn-warning btn-sm me-2" onClick={() => { setEditingRoom(room); setShowModal(true) }}>✎</button>
|
||||
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(room.id)}>✖</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{psRooms.length > 0 && (
|
||||
<div className="table-responsive">
|
||||
<h4 className="mb-3">Консольные комнаты (PS)</h4>
|
||||
<table className="table table-dark table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th>attachment</th>
|
||||
<th>gamepad</th>
|
||||
<th>tv</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{psRooms.map((room) => (
|
||||
<tr key={room.id}>
|
||||
<td>{room.name}</td>
|
||||
<td>{room.description}</td>
|
||||
<td>{room.cpu}</td>
|
||||
<td>{room.ram}</td>
|
||||
<td>{room.gpu}</td>
|
||||
<td>
|
||||
<button className="btn btn-warning btn-sm me-2" onClick={() => { setEditingRoom(room); setShowModal(true) }}>✎</button>
|
||||
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(room.id)}>✖</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{showModal && (
|
||||
<div className="modal d-block bg-dark bg-opacity-75" tabIndex={-1}>
|
||||
<div className="modal-dialog modal-dialog-scrollable">
|
||||
<form className="modal-content bg-dark text-white" onSubmit={handleSubmit}>
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">{editingRoom ? 'Редактировать' : 'Добавить'} комнату</h5>
|
||||
<button type="button" className="btn-close btn-close-white" onClick={() => { setShowModal(false); setEditingRoom(null) }} />
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{fields.map(field => (
|
||||
<div className="mb-3" key={field}>
|
||||
<label className="form-label">{field}</label>
|
||||
<input
|
||||
name={field}
|
||||
defaultValue={editingRoom?.[field] || ''}
|
||||
className="form-control"
|
||||
required={field === 'name'}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={() => { setShowModal(false); setEditingRoom(null) }}>
|
||||
Отмена
|
||||
</button>
|
||||
<button type="submit" className="btn btn-success">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export default function AdminLoginPage() {
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const router = useRouter()
|
||||
|
||||
const handleLogin = async () => {
|
||||
const res = await fetch('/api/admin-login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password }),
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
router.push('/admin')
|
||||
} else {
|
||||
const data = await res.json()
|
||||
setError(data.error || 'Ошибка авторизации')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-vh-100 d-flex justify-content-center align-items-center bg-black text-light px-3">
|
||||
<div className="card bg-dark text-white shadow-lg border-0" style={{ width: '100%', maxWidth: 400 }}>
|
||||
<div className="card-body p-4">
|
||||
<h3 className="card-title text-center mb-4">Админ-панель</h3>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor="password" className="form-label">
|
||||
Пароль
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-control bg-secondary border-0 text-white"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Введите пароль"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <div className="alert alert-danger py-2">{error}</div>}
|
||||
|
||||
<button className="btn btn-primary w-100 mt-2" onClick={handleLogin}>
|
||||
Войти
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import AdminHeader from "@/components/AdminHeader"
|
||||
import {
|
||||
AdminContainer,
|
||||
AdminContent,
|
||||
PageTitle,
|
||||
FiltersContainer,
|
||||
FilterGroup,
|
||||
FilterLabel,
|
||||
FilterInput,
|
||||
FilterSelect,
|
||||
RoomCard,
|
||||
RoomCardHeader,
|
||||
RoomCardTitle,
|
||||
RoomCapacity,
|
||||
TimeGrid,
|
||||
TimeSlot,
|
||||
TimeSlotHeader,
|
||||
TimeSlotTime,
|
||||
TimeSlotCount,
|
||||
TimeSlotBookings,
|
||||
BookingBadge,
|
||||
BookingsList,
|
||||
BookingCard,
|
||||
BookingCardHeader,
|
||||
BookingCardInfo,
|
||||
BookingCardTitle,
|
||||
BookingCardMeta,
|
||||
BookingCardActions,
|
||||
StatusSelect,
|
||||
DeleteButton,
|
||||
EmptyState,
|
||||
EmptyStateIcon,
|
||||
EmptyStateText,
|
||||
LoadingSpinner,
|
||||
Spinner,
|
||||
SectionTitle,
|
||||
AddButton,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
FormRow,
|
||||
FormLabel,
|
||||
FormInput as StyledFormInput,
|
||||
FormSelect as StyledFormSelect,
|
||||
ModalFooter,
|
||||
CancelButton,
|
||||
SubmitButton,
|
||||
ErrorMessage
|
||||
} from "@/styles/admin.styled"
|
||||
|
||||
type Booking = {
|
||||
id: number
|
||||
type: string
|
||||
room: string
|
||||
date_time: string
|
||||
time_slot: string
|
||||
name: string
|
||||
game: string | null
|
||||
contact: string
|
||||
contact_method: string | null
|
||||
status: 'pending' | 'confirmed' | 'cancelled'
|
||||
created_at: string
|
||||
}
|
||||
|
||||
const ROOM_NAMES: Record<string, string> = {
|
||||
'solo': 'Solo',
|
||||
'duo': 'Duo',
|
||||
'trio': 'Trio',
|
||||
'bootcamp 5': 'Bootcamp 5',
|
||||
'bootcamp 6': 'Bootcamp 6',
|
||||
}
|
||||
|
||||
const ROOM_CAPACITY: Record<string, number> = {
|
||||
'solo': 5,
|
||||
'duo': 4,
|
||||
'trio': 3,
|
||||
'bootcamp 5': 2,
|
||||
'bootcamp 6': 1,
|
||||
}
|
||||
|
||||
const STATUS_NAMES: Record<string, string> = {
|
||||
'pending': 'Ожидает',
|
||||
'confirmed': 'Подтверждено',
|
||||
'cancelled': 'Отменено',
|
||||
}
|
||||
|
||||
export default function AdminHomePage() {
|
||||
const [selectedDate, setSelectedDate] = useState<string>(new Date().toISOString().split('T')[0])
|
||||
const [selectedRoom, setSelectedRoom] = useState<string>('all')
|
||||
const [bookings, setBookings] = useState<Booking[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showAddModal, setShowAddModal] = useState(false)
|
||||
const [formError, setFormError] = useState<string>('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const [newBooking, setNewBooking] = useState({
|
||||
type: 'pc',
|
||||
room: 'solo',
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
time: '12:00',
|
||||
name: '',
|
||||
game: '',
|
||||
contact: '',
|
||||
contactMethod: 'telegram',
|
||||
status: 'pending'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
fetchBookings()
|
||||
}, [selectedDate, selectedRoom])
|
||||
|
||||
const fetchBookings = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = new URLSearchParams({ date: selectedDate })
|
||||
if (selectedRoom !== 'all') {
|
||||
params.append('room', selectedRoom)
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/admin/bookings?${params}`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setBookings(data.bookings || [])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке бронирований:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const updateStatus = async (id: number, status: string) => {
|
||||
try {
|
||||
const res = await fetch('/api/admin/bookings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, status }),
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
fetchBookings()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при обновлении статуса:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteBooking = async (id: number) => {
|
||||
if (!confirm('Вы уверены, что хотите удалить это бронирование?')) return
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/admin/bookings?id=${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
fetchBookings()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при удалении бронирования:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddBooking = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setFormError('')
|
||||
setSubmitting(true)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/admin/bookings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newBooking),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
if (res.ok) {
|
||||
setShowAddModal(false)
|
||||
setNewBooking({
|
||||
type: 'pc',
|
||||
room: 'solo',
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
time: '12:00',
|
||||
name: '',
|
||||
game: '',
|
||||
contact: '',
|
||||
contactMethod: 'telegram',
|
||||
status: 'pending'
|
||||
})
|
||||
fetchBookings()
|
||||
} else {
|
||||
setFormError(data.error || 'Ошибка при создании бронирования')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка при создании бронирования:', error)
|
||||
setFormError('Ошибка сети. Попробуйте еще раз.')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Группируем бронирования по комнатам и времени
|
||||
const bookingsByRoomAndTime: Record<string, Record<string, Booking[]>> = {}
|
||||
bookings.forEach(booking => {
|
||||
if (booking.status === 'cancelled') return
|
||||
if (!bookingsByRoomAndTime[booking.room]) {
|
||||
bookingsByRoomAndTime[booking.room] = {}
|
||||
}
|
||||
if (!bookingsByRoomAndTime[booking.room][booking.time_slot]) {
|
||||
bookingsByRoomAndTime[booking.room][booking.time_slot] = []
|
||||
}
|
||||
bookingsByRoomAndTime[booking.room][booking.time_slot].push(booking)
|
||||
})
|
||||
|
||||
// Генерируем все часы
|
||||
const allHours = Array.from({ length: 24 }, (_, i) => {
|
||||
return `${i.toString().padStart(2, '0')}:00`
|
||||
})
|
||||
|
||||
const rooms = ['solo', 'duo', 'trio', 'bootcamp 5', 'bootcamp 6']
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminContainer>
|
||||
<AdminHeader/>
|
||||
|
||||
<AdminContent>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem', flexWrap: 'wrap', gap: '1rem' }}>
|
||||
<PageTitle style={{ margin: 0 }}>Управление бронированиями</PageTitle>
|
||||
<AddButton onClick={() => setShowAddModal(true)}>
|
||||
<span>+</span>
|
||||
<span>Добавить бронирование</span>
|
||||
</AddButton>
|
||||
</div>
|
||||
|
||||
<FiltersContainer>
|
||||
<FilterGroup>
|
||||
<FilterLabel>Дата</FilterLabel>
|
||||
<FilterInput
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={(e) => setSelectedDate(e.target.value)}
|
||||
/>
|
||||
</FilterGroup>
|
||||
<FilterGroup>
|
||||
<FilterLabel>Тип комнаты</FilterLabel>
|
||||
<FilterSelect
|
||||
value={selectedRoom}
|
||||
onChange={(e) => setSelectedRoom(e.target.value)}
|
||||
>
|
||||
<option value="all">Все комнаты</option>
|
||||
{rooms.map(room => (
|
||||
<option key={room} value={room}>{ROOM_NAMES[room]}</option>
|
||||
))}
|
||||
</FilterSelect>
|
||||
</FilterGroup>
|
||||
</FiltersContainer>
|
||||
|
||||
{loading ? (
|
||||
<LoadingSpinner>
|
||||
<Spinner />
|
||||
</LoadingSpinner>
|
||||
) : (
|
||||
<>
|
||||
{/* Расписание по часам */}
|
||||
{selectedRoom === 'all' ? (
|
||||
rooms.map(room => {
|
||||
const roomBookings = bookingsByRoomAndTime[room] || {}
|
||||
const capacity = ROOM_CAPACITY[room]
|
||||
|
||||
return (
|
||||
<RoomCard key={room}>
|
||||
<RoomCardHeader>
|
||||
<RoomCardTitle>{ROOM_NAMES[room]}</RoomCardTitle>
|
||||
<RoomCapacity>{capacity} мест</RoomCapacity>
|
||||
</RoomCardHeader>
|
||||
<TimeGrid>
|
||||
{allHours.map(hour => {
|
||||
const hourBookings = roomBookings[hour] || []
|
||||
const booked = hourBookings.length
|
||||
const isFull = booked >= capacity
|
||||
|
||||
return (
|
||||
<TimeSlot
|
||||
key={hour}
|
||||
$available={!isFull}
|
||||
$booked={booked}
|
||||
$capacity={capacity}
|
||||
>
|
||||
<TimeSlotHeader>
|
||||
<TimeSlotTime>{hour}</TimeSlotTime>
|
||||
<TimeSlotCount $full={isFull}>
|
||||
{booked}/{capacity}
|
||||
</TimeSlotCount>
|
||||
</TimeSlotHeader>
|
||||
<TimeSlotBookings>
|
||||
{hourBookings.length > 0 ? (
|
||||
hourBookings.map(booking => (
|
||||
<BookingBadge
|
||||
key={booking.id}
|
||||
$status={booking.status}
|
||||
title={`${booking.name} - ${booking.contact}`}
|
||||
>
|
||||
{booking.name}
|
||||
</BookingBadge>
|
||||
))
|
||||
) : (
|
||||
<span style={{ fontSize: '0.75rem', color: 'rgba(109, 103, 148, 0.5)' }}>
|
||||
Свободно
|
||||
</span>
|
||||
)}
|
||||
</TimeSlotBookings>
|
||||
</TimeSlot>
|
||||
)
|
||||
})}
|
||||
</TimeGrid>
|
||||
</RoomCard>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
(() => {
|
||||
const roomBookings = bookingsByRoomAndTime[selectedRoom] || {}
|
||||
const capacity = ROOM_CAPACITY[selectedRoom]
|
||||
|
||||
return (
|
||||
<RoomCard>
|
||||
<RoomCardHeader>
|
||||
<RoomCardTitle>{ROOM_NAMES[selectedRoom]}</RoomCardTitle>
|
||||
<RoomCapacity>{capacity} мест</RoomCapacity>
|
||||
</RoomCardHeader>
|
||||
<TimeGrid>
|
||||
{allHours.map(hour => {
|
||||
const hourBookings = roomBookings[hour] || []
|
||||
const booked = hourBookings.length
|
||||
const isFull = booked >= capacity
|
||||
|
||||
return (
|
||||
<TimeSlot
|
||||
key={hour}
|
||||
$available={!isFull}
|
||||
$booked={booked}
|
||||
$capacity={capacity}
|
||||
>
|
||||
<TimeSlotHeader>
|
||||
<TimeSlotTime>{hour}</TimeSlotTime>
|
||||
<TimeSlotCount $full={isFull}>
|
||||
{booked}/{capacity}
|
||||
</TimeSlotCount>
|
||||
</TimeSlotHeader>
|
||||
<TimeSlotBookings>
|
||||
{hourBookings.length > 0 ? (
|
||||
hourBookings.map(booking => (
|
||||
<BookingBadge
|
||||
key={booking.id}
|
||||
$status={booking.status}
|
||||
title={`${booking.name} - ${booking.contact}`}
|
||||
>
|
||||
{booking.name}
|
||||
</BookingBadge>
|
||||
))
|
||||
) : (
|
||||
<span style={{ fontSize: '0.75rem', color: 'rgba(109, 103, 148, 0.5)' }}>
|
||||
Свободно
|
||||
</span>
|
||||
)}
|
||||
</TimeSlotBookings>
|
||||
</TimeSlot>
|
||||
)
|
||||
})}
|
||||
</TimeGrid>
|
||||
</RoomCard>
|
||||
)
|
||||
})()
|
||||
)}
|
||||
|
||||
{/* Список всех бронирований */}
|
||||
<SectionTitle>
|
||||
Все бронирования на {formatDate(selectedDate)}
|
||||
</SectionTitle>
|
||||
|
||||
{bookings.length === 0 ? (
|
||||
<EmptyState>
|
||||
<EmptyStateIcon>📅</EmptyStateIcon>
|
||||
<EmptyStateText>Нет бронирований на эту дату</EmptyStateText>
|
||||
</EmptyState>
|
||||
) : (
|
||||
<BookingsList>
|
||||
{bookings.map(booking => (
|
||||
<BookingCard key={booking.id} $status={booking.status}>
|
||||
<BookingCardHeader>
|
||||
<BookingCardInfo>
|
||||
<BookingCardTitle>{booking.name}</BookingCardTitle>
|
||||
<BookingCardMeta>
|
||||
<span>🕐 {booking.time_slot}</span>
|
||||
<span>🖥 {ROOM_NAMES[booking.room] || booking.room}</span>
|
||||
{booking.game && <span>🎮 {booking.game}</span>}
|
||||
<span>📞 {booking.contact}</span>
|
||||
{booking.contact_method && (
|
||||
<span>💬 {booking.contact_method}</span>
|
||||
)}
|
||||
</BookingCardMeta>
|
||||
</BookingCardInfo>
|
||||
<BookingCardActions>
|
||||
<StatusSelect
|
||||
$status={booking.status}
|
||||
value={booking.status}
|
||||
onChange={(e) => updateStatus(booking.id, e.target.value)}
|
||||
>
|
||||
<option value="pending">Ожидает</option>
|
||||
<option value="confirmed">Подтверждено</option>
|
||||
<option value="cancelled">Отменено</option>
|
||||
</StatusSelect>
|
||||
<DeleteButton onClick={() => deleteBooking(booking.id)}>
|
||||
Удалить
|
||||
</DeleteButton>
|
||||
</BookingCardActions>
|
||||
</BookingCardHeader>
|
||||
</BookingCard>
|
||||
))}
|
||||
</BookingsList>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Модальное окно для добавления бронирования */}
|
||||
{showAddModal && (
|
||||
<ModalOverlay onClick={() => !submitting && setShowAddModal(false)}>
|
||||
<ModalContent onClick={(e) => e.stopPropagation()} onSubmit={handleAddBooking}>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Добавить бронирование</ModalTitle>
|
||||
<ModalCloseButton
|
||||
type="button"
|
||||
onClick={() => !submitting && setShowAddModal(false)}
|
||||
>
|
||||
×
|
||||
</ModalCloseButton>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<FormRow>
|
||||
<div>
|
||||
<FormLabel>Тип активности</FormLabel>
|
||||
<StyledFormSelect
|
||||
value={newBooking.type}
|
||||
onChange={(e) => setNewBooking({ ...newBooking, type: e.target.value })}
|
||||
required
|
||||
>
|
||||
<option value="pc">Играть на ПК</option>
|
||||
<option value="ps">Играть на PS</option>
|
||||
<option value="tv">Смотреть трансляции</option>
|
||||
<option value="zayvka">Заявка</option>
|
||||
</StyledFormSelect>
|
||||
</div>
|
||||
<div>
|
||||
<FormLabel>Тип комнаты</FormLabel>
|
||||
<StyledFormSelect
|
||||
value={newBooking.room}
|
||||
onChange={(e) => setNewBooking({ ...newBooking, room: e.target.value })}
|
||||
required
|
||||
>
|
||||
<option value="solo">Solo (5 мест)</option>
|
||||
<option value="duo">Duo (4 места)</option>
|
||||
<option value="trio">Trio (3 места)</option>
|
||||
<option value="bootcamp 5">Bootcamp 5 (2 места)</option>
|
||||
<option value="bootcamp 6">Bootcamp 6 (1 место)</option>
|
||||
</StyledFormSelect>
|
||||
</div>
|
||||
</FormRow>
|
||||
|
||||
<FormRow>
|
||||
<div>
|
||||
<FormLabel>Дата</FormLabel>
|
||||
<StyledFormInput
|
||||
type="date"
|
||||
value={newBooking.date}
|
||||
onChange={(e) => setNewBooking({ ...newBooking, date: e.target.value })}
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormLabel>Время</FormLabel>
|
||||
<StyledFormSelect
|
||||
value={newBooking.time}
|
||||
onChange={(e) => setNewBooking({ ...newBooking, time: e.target.value })}
|
||||
required
|
||||
>
|
||||
{Array.from({ length: 24 }, (_, i) => {
|
||||
const hour = i.toString().padStart(2, '0')
|
||||
return (
|
||||
<option key={hour} value={`${hour}:00`}>
|
||||
{hour}:00
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</StyledFormSelect>
|
||||
</div>
|
||||
</FormRow>
|
||||
|
||||
<div>
|
||||
<FormLabel>Имя клиента</FormLabel>
|
||||
<StyledFormInput
|
||||
type="text"
|
||||
value={newBooking.name}
|
||||
onChange={(e) => setNewBooking({ ...newBooking, name: e.target.value })}
|
||||
placeholder="Введите имя"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FormLabel>Игра (необязательно)</FormLabel>
|
||||
<StyledFormInput
|
||||
type="text"
|
||||
value={newBooking.game}
|
||||
onChange={(e) => setNewBooking({ ...newBooking, game: e.target.value })}
|
||||
placeholder="Название игры"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FormLabel>Контактный номер</FormLabel>
|
||||
<StyledFormInput
|
||||
type="text"
|
||||
value={newBooking.contact}
|
||||
onChange={(e) => setNewBooking({ ...newBooking, contact: e.target.value })}
|
||||
placeholder="+7 (777) 123-45-67"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormRow>
|
||||
<div>
|
||||
<FormLabel>Способ связи</FormLabel>
|
||||
<StyledFormSelect
|
||||
value={newBooking.contactMethod}
|
||||
onChange={(e) => setNewBooking({ ...newBooking, contactMethod: e.target.value })}
|
||||
>
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="whatsapp">WhatsApp</option>
|
||||
<option value="phone">Телефон</option>
|
||||
</StyledFormSelect>
|
||||
</div>
|
||||
<div>
|
||||
<FormLabel>Статус</FormLabel>
|
||||
<StyledFormSelect
|
||||
value={newBooking.status}
|
||||
onChange={(e) => setNewBooking({ ...newBooking, status: e.target.value })}
|
||||
>
|
||||
<option value="pending">Ожидает</option>
|
||||
<option value="confirmed">Подтверждено</option>
|
||||
<option value="cancelled">Отменено</option>
|
||||
</StyledFormSelect>
|
||||
</div>
|
||||
</FormRow>
|
||||
|
||||
{formError && <ErrorMessage>{formError}</ErrorMessage>}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<CancelButton
|
||||
type="button"
|
||||
onClick={() => setShowAddModal(false)}
|
||||
disabled={submitting}
|
||||
>
|
||||
Отмена
|
||||
</CancelButton>
|
||||
<SubmitButton type="submit" disabled={submitting}>
|
||||
{submitting ? 'Создание...' : 'Создать бронирование'}
|
||||
</SubmitButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</ModalOverlay>
|
||||
)}
|
||||
</AdminContent>
|
||||
</AdminContainer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
'use client'
|
||||
|
||||
import { FormEvent, useEffect, useState } from 'react'
|
||||
import AdminHeader from '@/components/AdminHeader'
|
||||
|
||||
type Price = {
|
||||
id: number
|
||||
game_type: 'ПК' | 'PS'
|
||||
room_type: string
|
||||
day_type: string
|
||||
hour_1: number
|
||||
hour_3: number
|
||||
hour_5: number
|
||||
night: number
|
||||
}
|
||||
|
||||
export default function AdminPricesPage() {
|
||||
const [prices, setPrices] = useState<Price[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [editRow, setEditRow] = useState<Price | null>(null)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchPrices()
|
||||
}, [])
|
||||
|
||||
const fetchPrices = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/admin/price')
|
||||
const data: Price[] = await res.json()
|
||||
setPrices(data)
|
||||
} catch {
|
||||
setError('Ошибка при загрузке тарифов')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEditClick = (row: Price) => {
|
||||
setEditRow(row)
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const handleSave = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
const form = e.currentTarget
|
||||
const formData = new FormData(form)
|
||||
const updated = Object.fromEntries(formData.entries()) as Record<string, string>
|
||||
|
||||
const res = await fetch('/api/admin/price', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...editRow, ...updated })
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
await fetchPrices()
|
||||
setShowModal(false)
|
||||
setEditRow(null)
|
||||
} else {
|
||||
alert('Ошибка при сохранении')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminHeader />
|
||||
<div className="container py-5 text-light">
|
||||
<h2 className="mb-4 fs-1 text-center">Цены на ПК</h2>
|
||||
|
||||
{loading && <p>Загрузка...</p>}
|
||||
{error && <p className="text-danger">{error}</p>}
|
||||
|
||||
<div className="table-responsive">
|
||||
<table className="table table-dark table-bordered align-middle text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Тип игры</th>
|
||||
<th>Тип комнаты</th>
|
||||
<th>Тип дня</th>
|
||||
<th>1 час</th>
|
||||
<th>3 часа</th>
|
||||
<th>5 часов</th>
|
||||
<th>Ночь</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{prices.map((row, idx) =>
|
||||
row.game_type === 'ПК' ? (
|
||||
<tr key={idx}>
|
||||
<td>{row.game_type}</td>
|
||||
<td>{row.room_type}</td>
|
||||
<td>{row.day_type}</td>
|
||||
<td>{row.hour_1}</td>
|
||||
<td>{row.hour_3}</td>
|
||||
<td>{row.hour_5}</td>
|
||||
<td>{row.night}</td>
|
||||
<td>
|
||||
<button className="btn btn-sm btn-warning" onClick={() => handleEditClick(row)}>
|
||||
✎ Редактировать
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
) : null
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 className="mb-4 fs-1 text-center">Цены на PS</h2>
|
||||
|
||||
<div className="table-responsive">
|
||||
<table className="table table-dark table-bordered align-middle text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Тип игры</th>
|
||||
<th>Тип комнаты</th>
|
||||
<th>Тип дня</th>
|
||||
<th>1 час</th>
|
||||
<th>3 часа</th>
|
||||
<th>5 часов</th>
|
||||
<th>Ночь</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{prices.map((row, idx) =>
|
||||
row.game_type === 'PS' ? (
|
||||
<tr key={idx}>
|
||||
<td>{row.game_type}</td>
|
||||
<td>{row.room_type}</td>
|
||||
<td>{row.day_type}</td>
|
||||
<td>{row.hour_1}</td>
|
||||
<td>{row.hour_3}</td>
|
||||
<td>{row.hour_5}</td>
|
||||
<td>{row.night}</td>
|
||||
<td>
|
||||
<button className="btn btn-sm btn-warning" onClick={() => handleEditClick(row)}>
|
||||
✎ Редактировать
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
) : null
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{showModal && editRow && (
|
||||
<div className="modal d-block bg-dark bg-opacity-75" tabIndex={-1}>
|
||||
<div className="modal-dialog modal-dialog-centered">
|
||||
<form className="modal-content bg-dark text-white" onSubmit={handleSave}>
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Редактировать тариф</h5>
|
||||
<button type="button" className="btn-close btn-close-white" onClick={() => setShowModal(false)} />
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{['hour_1', 'hour_3', 'hour_5', 'night'].map((field) => (
|
||||
<div className="mb-3" key={field}>
|
||||
<label className="form-label">{field.replace('_', ' ')}:</label>
|
||||
<input
|
||||
name={field}
|
||||
type="number"
|
||||
step="0.01"
|
||||
defaultValue={editRow[field as keyof Price]?.toString() || ''}
|
||||
className="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button className="btn btn-secondary" type="button" onClick={() => setShowModal(false)}>
|
||||
Отмена
|
||||
</button>
|
||||
<button className="btn btn-success" type="submit">
|
||||
Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const ADMIN_HASHED_PASSWORD = "$2b$10$JQm79m.C64TWTwc7qmbJLOW2UnDLCUsL5e34eFb1MhRPqGBBxw0QK";
|
||||
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { password } = await req.json();
|
||||
|
||||
try {
|
||||
const isValid = await bcrypt.compare(password, ADMIN_HASHED_PASSWORD);
|
||||
|
||||
if (!isValid) {
|
||||
return NextResponse.json({ error: 'Неверный пароль' }, { status: 401 });
|
||||
}
|
||||
|
||||
const token = jwt.sign({ role: 'admin' }, process.env.JWT_SECRET!, { expiresIn: "1h" });
|
||||
|
||||
const res = NextResponse.json({ success: true });
|
||||
|
||||
res.cookies.set('token', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
maxAge: 3600
|
||||
});
|
||||
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при выполнении запроса:', error);
|
||||
return NextResponse.json({ error: 'Произошла ошибка при обработке запроса.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { bd } from '@/lib/bd';
|
||||
import { verifyAdminToken } from '@/lib/verifyAdmin';
|
||||
|
||||
// GET - получить все бронирования для админки
|
||||
export async function GET(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value;
|
||||
if (!verifyAdminToken(token)) {
|
||||
return NextResponse.json({ error: 'Нет доступа' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const date = searchParams.get('date');
|
||||
const room = searchParams.get('room');
|
||||
|
||||
let query = 'SELECT * FROM bookings WHERE 1=1';
|
||||
const params: any[] = [];
|
||||
|
||||
if (date) {
|
||||
query += ' AND DATE(date_time) = ?';
|
||||
params.push(date);
|
||||
}
|
||||
|
||||
if (room) {
|
||||
query += ' AND room = ?';
|
||||
params.push(room);
|
||||
}
|
||||
|
||||
query += ' ORDER BY date_time DESC, time_slot ASC';
|
||||
|
||||
const [bookings] = await bd.query(query, params);
|
||||
|
||||
return NextResponse.json({ bookings });
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении бронирований:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Ошибка сервера' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT - обновить статус бронирования
|
||||
export async function PUT(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value;
|
||||
if (!verifyAdminToken(token)) {
|
||||
return NextResponse.json({ error: 'Нет доступа' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { id, status } = await req.json();
|
||||
|
||||
if (!id || !status) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Необходимы id и status' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await bd.query(
|
||||
'UPDATE bookings SET status = ? WHERE id = ?',
|
||||
[status, id]
|
||||
);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Ошибка при обновлении бронирования:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Ошибка сервера' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST - создать новое бронирование (для админки)
|
||||
export async function POST(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value;
|
||||
if (!verifyAdminToken(token)) {
|
||||
return NextResponse.json({ error: 'Нет доступа' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await req.json();
|
||||
const { type, room, date, time, name, game, contact, contactMethod, status } = data;
|
||||
|
||||
if (!type || !room || !date || !time || !name || !contact) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Не все обязательные поля заполнены' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Формируем datetime из date и time
|
||||
const dateTime = `${date} ${time}:00`;
|
||||
|
||||
// Проверяем доступность слота перед созданием (если статус не cancelled)
|
||||
if (status !== 'cancelled') {
|
||||
const [existingBookings] = await bd.query(
|
||||
`SELECT COUNT(*) as count FROM bookings
|
||||
WHERE DATE(date_time) = ? AND time_slot = ? AND room = ? AND status != 'cancelled'`,
|
||||
[date, time, room]
|
||||
) as any[];
|
||||
|
||||
const bookedCount = existingBookings[0]?.count || 0;
|
||||
|
||||
// Получаем максимальную вместимость для типа комнаты
|
||||
const ROOM_CAPACITY: Record<string, number> = {
|
||||
'solo': 5,
|
||||
'duo': 4,
|
||||
'trio': 3,
|
||||
'bootcamp 5': 2,
|
||||
'bootcamp 6': 1,
|
||||
};
|
||||
|
||||
const maxCapacity = ROOM_CAPACITY[room] || 0;
|
||||
|
||||
if (bookedCount >= maxCapacity) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Этот временной слот уже занят' },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем бронирование
|
||||
const [result] = await bd.query(
|
||||
`INSERT INTO bookings (type, room, date_time, time_slot, name, game, contact, contact_method, status, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
|
||||
[type, room, dateTime, time, name, game || null, contact, contactMethod || null, status || 'pending']
|
||||
) as any[];
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
id: result.insertId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка при создании бронирования:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Ошибка сервера' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE - удалить бронирование
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value;
|
||||
if (!verifyAdminToken(token)) {
|
||||
return NextResponse.json({ error: 'Нет доступа' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const id = searchParams.get('id');
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Необходим id' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await bd.query('DELETE FROM bookings WHERE id = ?', [id]);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Ошибка при удалении бронирования:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Ошибка сервера' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { bd } from '@/lib/bd'
|
||||
import { verifyAdminToken } from '@/lib/verifyAdmin'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value
|
||||
const isAdmin = verifyAdminToken(token)
|
||||
|
||||
if (!isAdmin) {
|
||||
return NextResponse.json({ error: 'Доступ запрещён' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const [rows] = await bd.query(
|
||||
'SELECT value, type FROM contacts'
|
||||
)
|
||||
return NextResponse.json(rows)
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Ошибка БД' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value;
|
||||
const isAdmin = verifyAdminToken(token);
|
||||
|
||||
if (!isAdmin) {
|
||||
return NextResponse.json({ error: 'Доступ запрещён' }, { status: 401 });
|
||||
}
|
||||
|
||||
const data = await req.json();
|
||||
const { value, type } = data;
|
||||
|
||||
if (!value || !type) {
|
||||
return NextResponse.json({ error: 'value и type обязательны' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
await bd.query(
|
||||
`UPDATE contacts SET value = ? WHERE type = ?`,
|
||||
[value, type]
|
||||
);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ error: 'Ошибка при обновлении контакта' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { bd } from '@/lib/bd'
|
||||
import { verifyAdminToken } from '@/lib/verifyAdmin'
|
||||
import type { ResultSetHeader, FieldPacket } from 'mysql2'
|
||||
|
||||
// GET — получить все записи
|
||||
export async function GET(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value
|
||||
if (!verifyAdminToken(token)) return NextResponse.json({ error: 'Нет доступа' }, { status: 401 })
|
||||
|
||||
const [rows] = await bd.query('SELECT * FROM faq ORDER BY id DESC')
|
||||
return NextResponse.json(rows)
|
||||
}
|
||||
|
||||
// POST — создать новую запись
|
||||
export async function POST(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value
|
||||
if (!verifyAdminToken(token)) return NextResponse.json({ error: 'Нет доступа' }, { status: 401 })
|
||||
|
||||
const { title, description } = await req.json()
|
||||
if (!title || !description) {
|
||||
return NextResponse.json({ error: 'Заполните все поля' }, { status: 400 })
|
||||
}
|
||||
|
||||
const [result]: [ResultSetHeader, FieldPacket[]] = await bd.query(
|
||||
'INSERT INTO faq (title, description) VALUES (?, ?)',
|
||||
[title, description]
|
||||
)
|
||||
|
||||
return NextResponse.json({ id: result.insertId, title, description })
|
||||
}
|
||||
|
||||
// PUT — редактировать существующую
|
||||
export async function PUT(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value
|
||||
if (!verifyAdminToken(token)) return NextResponse.json({ error: 'Нет доступа' }, { status: 401 })
|
||||
|
||||
const { id, title, description } = await req.json()
|
||||
if (!id || !title || !description) {
|
||||
return NextResponse.json({ error: 'Заполните все поля' }, { status: 400 })
|
||||
}
|
||||
|
||||
await bd.query('UPDATE faq SET title = ?, description = ? WHERE id = ?', [
|
||||
title, description, id
|
||||
])
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
|
||||
// DELETE — удалить запись
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value
|
||||
if (!verifyAdminToken(token)) return NextResponse.json({ error: 'Нет доступа' }, { status: 401 })
|
||||
|
||||
const { searchParams } = new URL(req.url)
|
||||
const id = searchParams.get('id')
|
||||
if (!id) return NextResponse.json({ error: 'ID не указан' }, { status: 400 })
|
||||
|
||||
await bd.query('DELETE FROM faq WHERE id = ?', [id])
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { bd } from '@/lib/bd'
|
||||
import { verifyAdminToken } from '@/lib/verifyAdmin'
|
||||
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value
|
||||
const isAdmin = verifyAdminToken(token)
|
||||
if (!isAdmin) {
|
||||
return NextResponse.json({ error: 'Доступ запрещён' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(req.url)
|
||||
const id = searchParams.get('id')
|
||||
if (!id) return NextResponse.json({ error: 'ID не указан' }, { status: 400 })
|
||||
|
||||
try {
|
||||
await bd.query('DELETE FROM games WHERE id = ?', [id])
|
||||
return NextResponse.json({ success: true })
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Ошибка при удалении' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value
|
||||
const isAdmin = verifyAdminToken(token)
|
||||
|
||||
if (!isAdmin) {
|
||||
return NextResponse.json({ error: 'Доступ запрещён' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const [rows] = await bd.query('SELECT id, title, image_url, platform FROM games')
|
||||
return NextResponse.json(rows)
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Ошибка БД' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { verifyAdminToken } from '@/lib/verifyAdmin';
|
||||
import { bd } from '@/lib/bd';
|
||||
import type { ResultSetHeader } from 'mysql2';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value;
|
||||
if (!verifyAdminToken(token)) {
|
||||
return NextResponse.json({ error: 'Доступ запрещён' }, { status: 401 });
|
||||
}
|
||||
|
||||
const formData = await req.formData();
|
||||
|
||||
const title = formData.get('title');
|
||||
const platform = formData.get('platform');
|
||||
const image = formData.get('image') as File | null;
|
||||
|
||||
if (
|
||||
typeof title !== 'string' ||
|
||||
typeof platform !== 'string' ||
|
||||
!image ||
|
||||
typeof image !== 'object'
|
||||
) {
|
||||
return NextResponse.json({ error: 'Заполните все поля корректно' }, { status: 400 });
|
||||
}
|
||||
|
||||
const bytes = await image.arrayBuffer();
|
||||
const buffer = Buffer.from(bytes);
|
||||
const fileName = `${Date.now()}-${image.name.replace(/\s+/g, '_')}`;
|
||||
const uploadDir = path.join(process.cwd(), 'uploads/games');
|
||||
const filePath = path.join(uploadDir, fileName);
|
||||
|
||||
try {
|
||||
// создаём директорию если её нет
|
||||
await fs.mkdir(uploadDir, { recursive: true });
|
||||
|
||||
// сохраняем файл
|
||||
await fs.writeFile(filePath, buffer);
|
||||
|
||||
const [result] = await bd.query<ResultSetHeader>(
|
||||
'INSERT INTO games (title, image_url, platform) VALUES (?, ?, ?)',
|
||||
[title, fileName, platform]
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
id: result.insertId,
|
||||
title,
|
||||
platform,
|
||||
image_url: fileName,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Ошибка при сохранении файла или в базе данных:', err);
|
||||
return NextResponse.json({ error: 'Ошибка сервера' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { bd } from '@/lib/bd'
|
||||
import { verifyAdminToken } from '@/lib/verifyAdmin'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value
|
||||
const isAdmin = verifyAdminToken(token)
|
||||
|
||||
if (!isAdmin) {
|
||||
return NextResponse.json({ error: 'Доступ запрещён' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const [rows] = await bd.query(
|
||||
'SELECT game_type, room_type, day_type, hour_1, hour_3, hour_5, night FROM prices'
|
||||
)
|
||||
return NextResponse.json(rows)
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Ошибка БД' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value
|
||||
const isAdmin = verifyAdminToken(token)
|
||||
|
||||
if (!isAdmin) {
|
||||
return NextResponse.json({ error: 'Доступ запрещён' }, { status: 401 })
|
||||
}
|
||||
|
||||
const data = await req.json()
|
||||
const {
|
||||
game_type,
|
||||
room_type,
|
||||
day_type,
|
||||
hour_1,
|
||||
hour_3,
|
||||
hour_5,
|
||||
night,
|
||||
} = data
|
||||
|
||||
if (!game_type || !room_type || !day_type) {
|
||||
return NextResponse.json({ error: 'Ключевые поля отсутствуют' }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
await bd.query(
|
||||
`UPDATE prices
|
||||
SET hour_1 = ?, hour_3 = ?, hour_5 = ?, night = ?
|
||||
WHERE game_type = ? AND room_type = ? AND day_type = ?`,
|
||||
[hour_1, hour_3, hour_5, night, game_type, room_type, day_type]
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return NextResponse.json({ error: 'Ошибка при обновлении' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { bd } from '@/lib/bd'
|
||||
import { verifyAdminToken } from '@/lib/verifyAdmin'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value
|
||||
if (!verifyAdminToken(token)) return NextResponse.json({ error: 'Нет доступа' }, { status: 401 })
|
||||
|
||||
const [rows] = await bd.query('SELECT * FROM rooms')
|
||||
return NextResponse.json(rows)
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value
|
||||
if (!verifyAdminToken(token)) return NextResponse.json({ error: 'Нет доступа' }, { status: 401 })
|
||||
|
||||
const data = await req.json()
|
||||
const fields = ['name', 'description', 'cpu', 'ram', 'gpu', 'mouse', 'keyboard', 'headphones', 'monitor', 'bracket', 'chair']
|
||||
const values = fields.map(f => data[f])
|
||||
|
||||
await bd.query(
|
||||
`INSERT INTO rooms (${fields.join(', ')}) VALUES (${fields.map(() => '?').join(', ')})`,
|
||||
values
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value
|
||||
if (!verifyAdminToken(token)) return NextResponse.json({ error: 'Нет доступа' }, { status: 401 })
|
||||
|
||||
const data = await req.json()
|
||||
const fields = ['name', 'description', 'cpu', 'ram', 'gpu', 'mouse', 'keyboard', 'headphones', 'monitor', 'bracket', 'chair']
|
||||
const values = fields.map(f => data[f])
|
||||
values.push(data.id)
|
||||
|
||||
await bd.query(
|
||||
`UPDATE rooms SET ${fields.map(f => `${f} = ?`).join(', ')} WHERE id = ?`,
|
||||
values
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const token = req.cookies.get('token')?.value
|
||||
if (!verifyAdminToken(token)) return NextResponse.json({ error: 'Нет доступа' }, { status: 401 })
|
||||
|
||||
const { searchParams } = new URL(req.url)
|
||||
const id = searchParams.get('id')
|
||||
if (!id) return NextResponse.json({ error: 'Нет ID' }, { status: 400 })
|
||||
|
||||
await bd.query('DELETE FROM rooms WHERE id = ?', [id])
|
||||
return NextResponse.json({ success: true })
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { bd } from '@/lib/bd';
|
||||
|
||||
// Количество компьютеров по типам комнат
|
||||
const ROOM_CAPACITY: Record<string, number> = {
|
||||
'solo': 5,
|
||||
'duo': 4,
|
||||
'trio': 3,
|
||||
'bootcamp 5': 2,
|
||||
'bootcamp 6': 1,
|
||||
};
|
||||
|
||||
// Генерируем все часы дня (0-23)
|
||||
function generateAllHours(): string[] {
|
||||
return Array.from({ length: 24 }, (_, i) => {
|
||||
const hour = i.toString().padStart(2, '0');
|
||||
return `${hour}:00`;
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const date = searchParams.get('date');
|
||||
const room = searchParams.get('room');
|
||||
|
||||
if (!date || !room) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Необходимы параметры date и room' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Получаем все бронирования на эту дату и для этого типа комнаты
|
||||
const [bookings] = await bd.query(
|
||||
`SELECT time_slot, room FROM bookings
|
||||
WHERE DATE(date_time) = ? AND room = ? AND status != 'cancelled'`,
|
||||
[date, room]
|
||||
) as any[];
|
||||
|
||||
// Подсчитываем количество бронирований на каждый час
|
||||
const bookingsByHour: Record<string, number> = {};
|
||||
bookings.forEach((booking: any) => {
|
||||
const hour = booking.time_slot;
|
||||
bookingsByHour[hour] = (bookingsByHour[hour] || 0) + 1;
|
||||
});
|
||||
|
||||
// Получаем максимальное количество компьютеров для этого типа комнаты
|
||||
const maxCapacity = ROOM_CAPACITY[room] || 0;
|
||||
|
||||
// Генерируем все часы и проверяем доступность
|
||||
const allHours = generateAllHours();
|
||||
const availableSlots = allHours.map(hour => {
|
||||
const bookedCount = bookingsByHour[hour] || 0;
|
||||
const isAvailable = bookedCount < maxCapacity;
|
||||
return {
|
||||
time: hour,
|
||||
available: isAvailable,
|
||||
booked: bookedCount,
|
||||
total: maxCapacity,
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({ slots: availableSlots });
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении доступных слотов:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Ошибка сервера' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { bd } from '@/lib/bd';
|
||||
|
||||
// POST - создать новое бронирование
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const data = await req.json();
|
||||
const { type, room, date, time, name, game, contact, contactMethod } = data;
|
||||
|
||||
if (!type || !room || !date || !time || !name || !contact) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Не все обязательные поля заполнены' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Формируем datetime из date и time
|
||||
const dateTime = `${date} ${time}:00`;
|
||||
|
||||
// Проверяем доступность слота перед созданием
|
||||
const [existingBookings] = await bd.query(
|
||||
`SELECT COUNT(*) as count FROM bookings
|
||||
WHERE DATE(date_time) = ? AND time_slot = ? AND room = ? AND status != 'cancelled'`,
|
||||
[date, time, room]
|
||||
) as any[];
|
||||
|
||||
const bookedCount = existingBookings[0]?.count || 0;
|
||||
|
||||
// Получаем максимальную вместимость для типа комнаты
|
||||
const ROOM_CAPACITY: Record<string, number> = {
|
||||
'solo': 5,
|
||||
'duo': 4,
|
||||
'trio': 3,
|
||||
'bootcamp 5': 2,
|
||||
'bootcamp 6': 1,
|
||||
};
|
||||
|
||||
const maxCapacity = ROOM_CAPACITY[room] || 0;
|
||||
|
||||
if (bookedCount >= maxCapacity) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Этот временной слот уже занят' },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// Создаем бронирование
|
||||
const [result] = await bd.query(
|
||||
`INSERT INTO bookings (type, room, date_time, time_slot, name, game, contact, contact_method, status, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', NOW())`,
|
||||
[type, room, dateTime, time, name, game || null, contact, contactMethod || null]
|
||||
) as any[];
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
id: result.insertId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка при создании бронирования:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Ошибка сервера' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// GET - получить все бронирования (для админки)
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const date = searchParams.get('date');
|
||||
const room = searchParams.get('room');
|
||||
|
||||
let query = 'SELECT * FROM bookings WHERE 1=1';
|
||||
const params: any[] = [];
|
||||
|
||||
if (date) {
|
||||
query += ' AND DATE(date_time) = ?';
|
||||
params.push(date);
|
||||
}
|
||||
|
||||
if (room) {
|
||||
query += ' AND room = ?';
|
||||
params.push(room);
|
||||
}
|
||||
|
||||
query += ' ORDER BY date_time DESC, time_slot ASC';
|
||||
|
||||
const [bookings] = await bd.query(query, params);
|
||||
|
||||
return NextResponse.json({ bookings });
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении бронирований:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Ошибка сервера' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { bd } from '@/lib/bd'
|
||||
|
||||
export async function GET() {
|
||||
const [rows] = await bd.query('SELECT * FROM contacts')
|
||||
return NextResponse.json(rows)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { bd } from '@/lib/bd'
|
||||
|
||||
export async function GET() {
|
||||
const [rows] = await bd.query('SELECT * FROM faq ORDER BY id DESC')
|
||||
return NextResponse.json(rows)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import {bd} from "@/lib/bd";
|
||||
import {NextResponse} from "next/server";
|
||||
|
||||
export async function GET(request: Request){
|
||||
const {searchParams} = new URL(request.url)
|
||||
const platform = searchParams.get('platform') || 'pc';
|
||||
try {
|
||||
const [rows] = await bd.query(
|
||||
`SELECT id, title, image_url, platform FROM games WHERE platform = ?`,
|
||||
[platform]
|
||||
)
|
||||
return NextResponse.json(rows)
|
||||
}catch (error){
|
||||
console.error("DB error", error)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import {bd} from "@/lib/bd";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const gameType = searchParams.get('gameType');
|
||||
const roomType = searchParams.get('roomType');
|
||||
|
||||
const [rows] = await bd.execute(
|
||||
'SELECT day_type, hour_1, hour_3, hour_5, night FROM prices WHERE game_type = ? AND room_type = ?',
|
||||
[gameType, roomType]
|
||||
);
|
||||
|
||||
return NextResponse.json(rows);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import {bd} from '@/lib/bd';
|
||||
import {NextResponse} from "next/server";
|
||||
|
||||
export async function GET(){
|
||||
try {
|
||||
const [rows] = await bd.query('SELECT * FROM rooms');
|
||||
return NextResponse.json(rows);
|
||||
}catch (error){
|
||||
return NextResponse.json({message: 'Ошибка запроса', error: error}, {status:500})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const data = await req.json();
|
||||
|
||||
const token = process.env.TELEGRAM_BOT_TOKEN;
|
||||
const chatId = process.env.TELEGRAM_CHAT_ID;
|
||||
|
||||
if (!token || !chatId) {
|
||||
return NextResponse.json({ error: 'Missing env vars' }, { status: 500 });
|
||||
}
|
||||
console.log('gr')
|
||||
|
||||
const text = `
|
||||
<b>📬 Новая заявка</b>
|
||||
|
||||
🎮 Действие: ${data.type || '—'}
|
||||
🖥 Кабина: ${data.room || '—'}
|
||||
📅 Дата: ${data.date || '—'}
|
||||
⏰ Время: ${data.time || '—'}
|
||||
🧑 Имя: ${data.name || '—'}
|
||||
🎮 Игра: ${data.game || '—'}
|
||||
📞 Контакт: ${data.contact || '—'}
|
||||
💬 Связь: ${data.contactMethod || '—'}
|
||||
`;
|
||||
|
||||
const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
text,
|
||||
parse_mode: 'HTML',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json({ error: 'Failed to send' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||
const pathname = req.nextUrl.pathname; // /api/uploads/games/image.png
|
||||
const relativePath = pathname.replace(/^\/api\/uploads\//, ''); // games/image.png
|
||||
|
||||
const filePath = path.join(process.cwd(), 'uploads', ...relativePath.split('/'));
|
||||
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
const fileBuffer = await fs.readFile(filePath);
|
||||
|
||||
const ext = path.extname(filePath).slice(1).toLowerCase();
|
||||
const contentTypes: Record<string, string> = {
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
webp: 'image/webp',
|
||||
gif: 'image/gif',
|
||||
};
|
||||
|
||||
return new NextResponse(fileBuffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': contentTypes[ext] || 'application/octet-stream',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Файл не найден:', err);
|
||||
return new NextResponse('Not found', { status: 404 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
@import "bootstrap/dist/css/bootstrap.min.css";
|
||||
@font-face {
|
||||
font-family: 'TradeGothic';
|
||||
src: url('/fonts/TradeGothicPro.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #0E0E17;
|
||||
font-family: 'TradeGothic', sans-serif;
|
||||
text-transform: uppercase;
|
||||
color: #fff;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import "../styles/reset.css";
|
||||
import React from "react";
|
||||
import StartupLoader from "@/components/StartupLoader";
|
||||
import Script from "next/script";
|
||||
|
||||
export const viewport = {
|
||||
themeColor: "#00ff90",
|
||||
colorScheme: "dark",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://strike-arena.kz"),
|
||||
title: "Strike Arena — Киберклуб нового уровня",
|
||||
description: "Киберклуб Strike Arena: современные ПК и PS5, VIP-кабины, быстрая бронь, турниры и вознаграждения. Бронируй онлайн и выигрывай чаще.",
|
||||
keywords: ["Strike Arena", "киберклуб", "компьютерный клуб", "игровые залы", "бронь ПК", "PlayStation", "турниры", "игры", "e-sports"],
|
||||
applicationName: "Strike Arena",
|
||||
generator: "Next.js",
|
||||
manifest: "/site.webmanifest",
|
||||
authors: [{ name: "Strike Arena Team", url: "https://strike-arena.kz" }],
|
||||
creator: "Strike Arena",
|
||||
publisher: "Strike Arena",
|
||||
openGraph: {
|
||||
title: "Strike Arena — Игровой клуб нового поколения",
|
||||
description: "Современный киберклуб с бронью мест, турнирами, призами и VIP-комфортом.",
|
||||
url: "https://strike-arena.kz",
|
||||
siteName: "Strike Arena",
|
||||
images: [
|
||||
{
|
||||
url: "/og-image.jpg",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "Strike Arena — киберклуб нового уровня",
|
||||
},
|
||||
],
|
||||
locale: "ru_RU",
|
||||
type: "website",
|
||||
},
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/favicons/favicon.ico", sizes: "any" },
|
||||
{ url: "/favicons/apple-touch-icon.png", sizes: "180x180" },
|
||||
{ url: "/favicons/android-chrome-192x192.png", sizes: "192x192" },
|
||||
{ url: "/favicons/android-chrome-512x512.png", sizes: "512x512" },
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Strike Arena — Киберклуб нового уровня",
|
||||
description: "Современный клуб с лучшими условиями для геймеров. Бронируй — играй — побеждай.",
|
||||
images: ["/og-image.jpg"],
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="ru">
|
||||
<body>
|
||||
{children}
|
||||
<StartupLoader />
|
||||
<Script
|
||||
id="binotel-getcall"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function(d, w, s) {
|
||||
var widgetHash = 'j2he8ic2huirt42fsxdj', gcw = d.createElement(s);
|
||||
gcw.type = 'text/javascript'; gcw.async = true;
|
||||
gcw.src = '//widgets.binotel.com/getcall/widgets/' + widgetHash + '.js';
|
||||
var sn = d.getElementsByTagName(s)[0]; sn.parentNode.insertBefore(gcw, sn);
|
||||
})(document, window, 'script');
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,704 @@
|
||||
'use client'
|
||||
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
blackColor,
|
||||
darkGreenColor,
|
||||
inactiveBorderColor,
|
||||
inactiveTextColor,
|
||||
lightGreenColor,
|
||||
whiteColor
|
||||
} from "@/styles/colors";
|
||||
|
||||
|
||||
// General
|
||||
export const MainPage = styled.div`
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
`
|
||||
export const Section = styled.section`
|
||||
max-width: 1220px;
|
||||
position: relative;
|
||||
margin: 220px auto 0;
|
||||
-webkit-transform: translateZ(0);
|
||||
will-change: transform;
|
||||
h2{
|
||||
text-align: center;
|
||||
font-size: 40px;
|
||||
span{
|
||||
background: linear-gradient(to right, ${lightGreenColor}, ${darkGreenColor});
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
@media(max-width: 1400px) {
|
||||
font-size: 32px;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@media (max-width: 1060px) {
|
||||
font-size: clamp(24px, 5vw,36px);
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
&.roomSection::after{
|
||||
content: "";
|
||||
width:300px;
|
||||
height: 200px;
|
||||
position: absolute;
|
||||
top: 300px;
|
||||
right: -400px;
|
||||
border-radius: 50%;
|
||||
background: ${lightGreenColor};
|
||||
filter: blur(120px);
|
||||
}
|
||||
`
|
||||
export const PriceHeader_btn = styled.button`
|
||||
font-size: 64px;
|
||||
text-transform: uppercase;
|
||||
color: ${inactiveTextColor};
|
||||
margin-left: 170px;
|
||||
position: relative;
|
||||
&::after{
|
||||
content: "";
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: -18px;
|
||||
background: ${inactiveBorderColor};
|
||||
position: absolute;
|
||||
width: 120%;
|
||||
height: 7px;
|
||||
}
|
||||
&.active{
|
||||
background: linear-gradient(to right, ${lightGreenColor}, ${darkGreenColor});
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
&.active::after{
|
||||
background: linear-gradient(to right, ${lightGreenColor}, ${darkGreenColor});
|
||||
}
|
||||
}
|
||||
@media (max-width: 1400px) {
|
||||
font-size: 52px;
|
||||
margin-left: 100px;
|
||||
line-height: 1.1;
|
||||
&::after{
|
||||
bottom: -16px;
|
||||
height: 6px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1300px) {
|
||||
margin-left: 100px;
|
||||
}
|
||||
@media (max-width: 1080px) {
|
||||
margin-left: 0;
|
||||
font-size: 40px;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
font-size: 32px;
|
||||
&::after{
|
||||
bottom: -14px;
|
||||
height: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 420px) {
|
||||
width: 100%;
|
||||
&::after{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// start
|
||||
export const StartBlock = styled.section`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
max-width: 1920px;
|
||||
padding: 0 60px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 70vh;
|
||||
&::after{
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -200px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100vw;
|
||||
height: 1144px;
|
||||
background-image: url("/images/startBg.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
z-index: -10;
|
||||
}
|
||||
img{
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
img:nth-child(1){
|
||||
left: 100px;
|
||||
}
|
||||
img:nth-child(4){
|
||||
right: 150px;
|
||||
}
|
||||
h1{
|
||||
font-size: 64px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
max-width: 648px;
|
||||
margin-bottom: 40px;
|
||||
span {
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
${lightGreenColor} 0%,
|
||||
${lightGreenColor} 40%,
|
||||
${darkGreenColor} 110%,
|
||||
${darkGreenColor} 140%
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1800px) {
|
||||
img:nth-child(1){
|
||||
left: 50px;
|
||||
}
|
||||
img:nth-child(4){
|
||||
right: 75px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1600px) {
|
||||
img:nth-child(4){
|
||||
right: 40px;
|
||||
width: 362px;
|
||||
}
|
||||
img:nth-child(1){
|
||||
left: 10px;
|
||||
width: 402px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1400px) {
|
||||
height: 75vh;
|
||||
img:nth-child(4){
|
||||
width: 360px;
|
||||
}
|
||||
img:nth-child(1){
|
||||
width: 380px;
|
||||
}
|
||||
h1{
|
||||
font-size: 50px;
|
||||
max-width: 508px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
button{
|
||||
font-size: 32px;
|
||||
padding: 16px 38px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1300px) {
|
||||
img:nth-child(4){
|
||||
right: 10px;
|
||||
}
|
||||
img:nth-child(1){
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1180px) {
|
||||
img:nth-child(4){
|
||||
width: 280px;
|
||||
right: 10px;
|
||||
}
|
||||
img:nth-child(1){
|
||||
width: 340px;
|
||||
left: -50px;
|
||||
}
|
||||
h1{
|
||||
font-size: 50px;
|
||||
max-width: 508px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
button{
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1040px) {
|
||||
height: 80vh;
|
||||
img:nth-child(4){
|
||||
width: 280px;
|
||||
bottom: -20%;
|
||||
right: 10%;
|
||||
}
|
||||
img:nth-child(1){
|
||||
width: 340px;
|
||||
bottom: -20%;
|
||||
left: 10%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 850px) {
|
||||
img:nth-child(4){
|
||||
width: 280px;
|
||||
bottom: -20%;
|
||||
right: 4%;
|
||||
}
|
||||
img:nth-child(1){
|
||||
width: 340px;
|
||||
bottom: -20%;
|
||||
left: 4%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
padding: 0 20px;
|
||||
margin-top: 60px;
|
||||
justify-content: start;
|
||||
height: 50vh;
|
||||
img:nth-child(4){
|
||||
width: 240px;
|
||||
bottom: -26%;
|
||||
right: 4%;
|
||||
}
|
||||
img:nth-child(1){
|
||||
width: 280px;
|
||||
bottom: -26%;
|
||||
left: 4%;
|
||||
}
|
||||
h1{
|
||||
font-size: 40px;
|
||||
max-width: 400px;
|
||||
}
|
||||
button{
|
||||
font-size: 26px;
|
||||
padding: 12px 32px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
height: 40vh;
|
||||
h1{
|
||||
font-size: 30px;
|
||||
max-width: 300px;
|
||||
}
|
||||
img:nth-child(4){
|
||||
width: 220px;
|
||||
bottom: -34%;
|
||||
right: 5%;
|
||||
}
|
||||
img:nth-child(1){
|
||||
width: 220px;
|
||||
bottom: -34%;
|
||||
left: 5%;
|
||||
}
|
||||
button{
|
||||
font-size: 26px;
|
||||
padding: 12px 32px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
img:nth-child(4){
|
||||
width: 220px;
|
||||
bottom: -34%;
|
||||
right: 2%;
|
||||
}
|
||||
img:nth-child(1){
|
||||
width: 220px;
|
||||
bottom: -34%;
|
||||
left: 2%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 418px) {
|
||||
img:nth-child(4){
|
||||
width: 200px;
|
||||
bottom: -40%;
|
||||
right: 2%;
|
||||
}
|
||||
img:nth-child(1){
|
||||
width: 200px;
|
||||
bottom: -40%;
|
||||
left: -4%;
|
||||
}
|
||||
h1{
|
||||
font-size: 30px;
|
||||
max-width: 300px;
|
||||
}
|
||||
button{
|
||||
font-size: 26px;
|
||||
padding: 12px 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
img:nth-child(4){
|
||||
width: 180px;
|
||||
bottom: -24%;
|
||||
right: 2%;
|
||||
}
|
||||
img:nth-child(1){
|
||||
width: 180px;
|
||||
bottom: -24%;
|
||||
left: -4%;
|
||||
}
|
||||
}
|
||||
@media (max-height: 740px) {
|
||||
img:nth-child(4){
|
||||
bottom: -44%;
|
||||
}
|
||||
img:nth-child(1){
|
||||
bottom: -44%;
|
||||
}
|
||||
}
|
||||
@media (max-height: 690px) {
|
||||
img:nth-child(4){
|
||||
bottom: -56%;
|
||||
}
|
||||
img:nth-child(1){
|
||||
bottom: -56%;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
// App
|
||||
export const AppSection = styled.section`
|
||||
background: ${blackColor};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 90%;
|
||||
margin: 140px auto 80px;
|
||||
border-radius: 40px;
|
||||
position: relative;
|
||||
&::after{
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -14%;
|
||||
left: -20%;
|
||||
background: ${lightGreenColor};
|
||||
width: 500px;
|
||||
height: 400px;
|
||||
border-radius: 30%;
|
||||
filter: blur(200px);
|
||||
opacity: .5;
|
||||
z-index: -1;
|
||||
}
|
||||
@media (max-width: 1250px) {
|
||||
padding: 60px 0;
|
||||
.phoneApp{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
export const AppText = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
width: 706px;
|
||||
h2{
|
||||
font-size: 40px;
|
||||
margin: 20px 0 24px;
|
||||
}
|
||||
p{
|
||||
font-size: 24px;
|
||||
margin-bottom: 24px;
|
||||
span{
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
div{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
}
|
||||
button{
|
||||
cursor: auto;
|
||||
}
|
||||
@media (max-width: 850px) {
|
||||
width: 600px;
|
||||
}
|
||||
@media (max-width: 650px) {
|
||||
width: 90%;
|
||||
h2{
|
||||
font-size: 30px;
|
||||
}
|
||||
p{
|
||||
font-size: 18px;
|
||||
}
|
||||
div{
|
||||
gap: 10px;
|
||||
img{
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
h2{
|
||||
font-size: 30px;
|
||||
}
|
||||
p{
|
||||
font-size: 18px;
|
||||
}
|
||||
div{
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
button{
|
||||
font-size: 20px;
|
||||
padding: 8px 28px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// faq
|
||||
export const FaqAccordion = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
&::after{
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: ${lightGreenColor};
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
border-radius: 50%;
|
||||
filter: blur(200px);
|
||||
opacity: .25;
|
||||
z-index: -1;
|
||||
}
|
||||
&::before{
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
right: -33%;
|
||||
transform: rotate(-30deg);
|
||||
background: ${lightGreenColor};
|
||||
width: 900px;
|
||||
height: 500px;
|
||||
border-radius: 40%;
|
||||
filter: blur(200px);
|
||||
opacity: .6;
|
||||
z-index: -1;
|
||||
}
|
||||
`
|
||||
export const FaqTitle = styled.div`
|
||||
text-align: center;
|
||||
h2{
|
||||
font-size: 40px;
|
||||
}
|
||||
h3{
|
||||
font-size: 20px;
|
||||
margin: 12px 0 50px;
|
||||
}
|
||||
`
|
||||
|
||||
//footer
|
||||
export const HeaderPhone = styled.div`
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
margin-left: 33px;
|
||||
h4{
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
p{
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
`
|
||||
export const HeaderBlock = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.logo{
|
||||
margin-right: 56px;
|
||||
}
|
||||
.gis2Link{
|
||||
margin-right: 15px;
|
||||
}
|
||||
.tgLink{
|
||||
margin: 0 12px 0 8px;
|
||||
}
|
||||
.footerPhone{
|
||||
|
||||
h4{
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Footer = styled.footer`
|
||||
display: flex;
|
||||
max-width: 1500px;
|
||||
margin: 200px auto 0;
|
||||
height: 140px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 218px;
|
||||
position: relative;
|
||||
|
||||
.policy{
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
color:${whiteColor};
|
||||
font-size: 20px;
|
||||
}
|
||||
.inst{
|
||||
margin-right: 8px;
|
||||
}
|
||||
&::after{
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -66%;
|
||||
left: 24%;
|
||||
background: ${lightGreenColor};
|
||||
width: 600px;
|
||||
height: 90px;
|
||||
filter: blur(80px);
|
||||
border-radius: 20%;
|
||||
z-index: -1;
|
||||
}
|
||||
&::before{
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -90%;
|
||||
left: -12%;
|
||||
background: ${lightGreenColor};
|
||||
width: 200px;
|
||||
height: 120px;
|
||||
filter: blur(80px);
|
||||
border-radius: 20%;
|
||||
z-index: -1;
|
||||
}
|
||||
@media (max-width: 1260px) {
|
||||
width: 90%;
|
||||
margin: 200px auto 0;
|
||||
justify-content: space-between;
|
||||
gap: 0;
|
||||
}
|
||||
@media (max-width: 920px) {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
column-gap: 64px;
|
||||
height: max-content;
|
||||
.footerBlock{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
column-gap: 64px;
|
||||
margin: 32px 0 24px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
margin: 160px auto 0;
|
||||
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
justify-content: center;
|
||||
margin-top: 100px;
|
||||
.footerBlock{
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin: 20px 0 20px;
|
||||
}
|
||||
.logo{
|
||||
order: 1;
|
||||
}
|
||||
.footerBlock{
|
||||
order: 2;
|
||||
}
|
||||
.footerPhone{
|
||||
margin-left:0;
|
||||
align-items: center;
|
||||
}
|
||||
.policy{
|
||||
order: 3;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
}
|
||||
`
|
||||
// Policy
|
||||
export const PolicyContent = styled.div`
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
max-width: 994px;
|
||||
margin: 0 auto 150px;
|
||||
font-size: 32px;
|
||||
line-height: 1.4;
|
||||
position: relative;
|
||||
padding: 0 20px;
|
||||
|
||||
h1 {
|
||||
margin: 70px 0 55px;
|
||||
font-size: 64px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -10%;
|
||||
right: -60%;
|
||||
background: ${lightGreenColor};
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
opacity: 0.6;
|
||||
filter: blur(200px);
|
||||
border-radius: 20%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -20%;
|
||||
left: -10%;
|
||||
background: ${lightGreenColor};
|
||||
width: 500px;
|
||||
height: 200px;
|
||||
opacity: 0.6;
|
||||
filter: blur(200px);
|
||||
border-radius: 20%;
|
||||
z-index: -4;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 24px;
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: 20px;
|
||||
margin-bottom: 100px;
|
||||
h1 {
|
||||
font-size: 38px;
|
||||
margin: 50px 0 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 370px) {
|
||||
font-size: 18px;
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,182 @@
|
||||
'use client'
|
||||
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
AppSection, AppText, FaqAccordion, FaqTitle, Footer,
|
||||
MainPage,
|
||||
HeaderBlock, HeaderPhone,
|
||||
Section,
|
||||
StartBlock
|
||||
} from "@/app/main.styled";
|
||||
import StyledButton from "@/components/StyledButton";
|
||||
import RoomList from "@/components/RoomsList";
|
||||
import PriceList from "@/components/PriceList";
|
||||
import FeaturesList from "@/components/FeaturesList";
|
||||
import GameList from "@/components/GameList";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import Accordion from "@/components/Accordion";
|
||||
import HeaderPage from "@/components/HeaderPage";
|
||||
import {useBronStore} from "@/store/useBronStore";
|
||||
import BronModal from "@/components/BronModal";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
type Faq = {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
type Contacts = {
|
||||
id: number
|
||||
type: string
|
||||
value: string
|
||||
}
|
||||
|
||||
|
||||
export default function Home() {
|
||||
const open = useBronStore((s) => s.open);
|
||||
const [faqs, setFaqs] = useState<Faq[]>([])
|
||||
const [contacts, setContacts] = useState<Contacts[]>([])
|
||||
|
||||
const inst = contacts.find((c)=> c.type ==='inst');
|
||||
const phone = contacts.find((c)=> c.type ==='phone');
|
||||
const tg = contacts.find((c)=> c.type ==='tg');
|
||||
const gis2 = contacts.find((c)=> c.type ==='gis2');
|
||||
const whatsapp = contacts.find((c)=> c.type ==='whatsapp');
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchFaq() {
|
||||
const res = await fetch(`/api/faq`);
|
||||
const data = await res.json();
|
||||
setFaqs(data);
|
||||
}
|
||||
async function fetchContact() {
|
||||
const res = await fetch(`/api/contacts`);
|
||||
const data = await res.json();
|
||||
setContacts(data);
|
||||
}
|
||||
|
||||
fetchFaq();
|
||||
fetchContact();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MainPage>
|
||||
<HeaderPage whatsapp={whatsapp?.value} tg={tg?.value} phone={phone?.value} inst={inst?.value} gis2={gis2?.value} />
|
||||
<main>
|
||||
<StartBlock>
|
||||
|
||||
<Image src={'/images/startLeft.png'} alt={'startLeft'} width={482} height={440} />
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.5 }}
|
||||
>
|
||||
Выигрывай чаще и получай удовольствие <span>от компьютерных игр</span>
|
||||
</motion.h1>
|
||||
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.8, delay: 0.8 }}
|
||||
>
|
||||
<StyledButton onClick={() => open('start')} label={'Забронировать место'} padding={'14px 46px'} fontSize={'42px'} />
|
||||
</motion.div>
|
||||
|
||||
|
||||
<Image src={'/images/startRight.png'} alt={'startRight'} width={422} height={410} quality={100} />
|
||||
</StartBlock>
|
||||
<Section className={'roomSection'}>
|
||||
<h2>В нашем клубе мы предлагаем только <span>современное и надёжное</span> оборудование, которое обеспечит вам <span>комфортную игру</span></h2>
|
||||
<RoomList />
|
||||
</Section>
|
||||
<Section>
|
||||
<PriceList />
|
||||
</Section>
|
||||
<Section>
|
||||
<FeaturesList />
|
||||
</Section>
|
||||
<Section>
|
||||
<GameList tg={tg?.value} whatsapp={whatsapp?.value} />
|
||||
</Section>
|
||||
<AppSection>
|
||||
<AppText>
|
||||
<StyledButton label={'Приложение'} padding={'12px 38px'} fontSize={'24px'} />
|
||||
<h2>Загрузи и бронируй заранее.</h2>
|
||||
<p>Следи за событиями, прокачивай профиль и получай вознаграждения. Пополнение счёта c <span>Kaspi</span> — в пару кликов.</p>
|
||||
<div>
|
||||
<Link href={'https://play.google.com/store/apps/details?id=com.f5computers.langame_aggregator'}>
|
||||
<Image src={'/icons/appGooglePlay.png'} alt={'google play'} width={220} height={84} loading={"lazy"} />
|
||||
</Link>
|
||||
<Link href={'https://apps.apple.com/us/app/langame/id1642484175?l=ru'}>
|
||||
<Image src={'/icons/appAppStore.png'} alt={'appStore'} width={235} height={84} loading={"lazy"} />
|
||||
</Link>
|
||||
</div>
|
||||
</AppText>
|
||||
<Image className={'phoneApp'} src={'/images/phone.png'} alt={'phone'} width={412} height={414} loading={"lazy"} />
|
||||
</AppSection>
|
||||
<FaqAccordion>
|
||||
<FaqTitle>
|
||||
<h2>FAQ</h2>
|
||||
<h3>Часто задаваемые вопросы</h3>
|
||||
</FaqTitle>
|
||||
{faqs.map((faqBlock)=>(
|
||||
<Accordion key={faqBlock.id} title={faqBlock.title}>
|
||||
{faqBlock.description}
|
||||
</Accordion>
|
||||
))}
|
||||
</FaqAccordion>
|
||||
</main>
|
||||
<Footer>
|
||||
<Link href='/' className={'logo'}>
|
||||
<Image src={'/icons/logo.png'} alt={'logo'} width={226} height={60} loading={'lazy'} />
|
||||
</Link>
|
||||
<Link href='/policy' className={'policy'}>
|
||||
Политика<br/> конфиденциальности
|
||||
</Link>
|
||||
<HeaderBlock className={'footerBlock'}>
|
||||
<div>{inst ? (
|
||||
<Link href={inst.value} target="_blank">
|
||||
<Image src="/icons/inst.png" alt="inst" width={60} height={60} loading="lazy" />
|
||||
</Link>
|
||||
) : (
|
||||
<Image src="/icons/inst.png" alt="inst" width={60} height={60} loading="lazy" />
|
||||
)}
|
||||
|
||||
{whatsapp ? (
|
||||
<Link href={whatsapp.value} target="_blank">
|
||||
<Image src="/icons/whatsapp.png" alt="whatsapp" width={58} height={58} loading="lazy" />
|
||||
</Link>
|
||||
) : (
|
||||
<Image src="/icons/whatsapp.png" alt="whatsapp" width={58} height={58} loading="lazy" />
|
||||
)}
|
||||
|
||||
{tg ? (
|
||||
<Link href={tg.value} className="tgLink" target="_blank">
|
||||
<Image src="/icons/tg.png" alt="tg" width={48} height={48} loading="lazy" />
|
||||
</Link>
|
||||
) : (
|
||||
<Image src="/icons/tg.png" alt="tg" width={48} height={48} loading="lazy" />
|
||||
)}
|
||||
|
||||
{gis2 ? (
|
||||
<Link href={gis2.value} className="gis2Link headerImage" target="_blank">
|
||||
<Image src="/icons/location.svg" alt="geo" width={37} height={50} loading="lazy" />
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={'/'} className="gis2Link headerImage" target="_blank">
|
||||
<Image src="/icons/location.svg" alt="geo" width={37} height={50} loading="lazy" />
|
||||
</Link>
|
||||
)}</div>
|
||||
<HeaderPhone className={'footerPhone'}>
|
||||
<p style={{ marginBottom: 0 }}>{phone?.value}</p>
|
||||
<h4 onClick={() => open('zayvka-game')}>Заказать звонок</h4>
|
||||
</HeaderPhone>
|
||||
</HeaderBlock>
|
||||
</Footer>
|
||||
<BronModal />
|
||||
</MainPage>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
import {MainPage, PolicyContent} from "@/app/main.styled";
|
||||
import HeaderPage from "@/components/HeaderPage";
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
type Contacts = {
|
||||
id: number
|
||||
type: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export default function AboutPage() {
|
||||
const [contacts, setContacts] = useState<Contacts[]>([])
|
||||
const phone = contacts.find((c)=> c.type ==='phone');
|
||||
const inst = contacts.find((c)=> c.type ==='inst');
|
||||
const tg = contacts.find((c)=> c.type ==='tg');
|
||||
const gis2 = contacts.find((c)=> c.type ==='gis2');
|
||||
const whatsapp = contacts.find((c)=> c.type ==='whatsapp');
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchContact() {
|
||||
const res = await fetch(`/api/contacts`);
|
||||
const data = await res.json();
|
||||
setContacts(data);
|
||||
}
|
||||
fetchContact();
|
||||
}, []);
|
||||
return (
|
||||
<MainPage>
|
||||
<HeaderPage whatsapp={whatsapp?.value} tg={tg?.value} phone={phone?.value} inst={inst?.value} gis2={gis2?.value} />
|
||||
<PolicyContent>
|
||||
<h1>Политика конфиденциальности</h1>
|
||||
<p>1. Общие положения Данная Политика конфиденциальности (далее — "Политика") определяет порядок
|
||||
обработки и защиты персональных данных посетителей сайта и клиентов Компьютерного клуба Strike
|
||||
Arena, принадлежащего ИП Медина. Информация о организации: ИП Медина Адрес: г. Астана, улица
|
||||
Сығанак, 54, 120 БИН: 930805402161 Банк: АО «Kaspi Bank» КБе: 19 БИК: CASPKZKA Номер счёта:
|
||||
KZ77722S000034244500 <br/>
|
||||
2. Какие данные собираются<br/>•Персональные данные: ФИО, телефон.<br/>•Технические
|
||||
данные: IP-адрес, тип устройства, cookies.<br/>•Данные о посещении сайта: запись взаимодействий
|
||||
(страницы, поведение). <br/>
|
||||
3. Цели сбора данных<br/>•Для бронирования времени в клубе.<br/>•Для улучшения качества
|
||||
оказываемых услуг. <br/>
|
||||
4. Передача данных третьим лицам Передача данных третьим лицам возможна только в
|
||||
целях обработки платежей. Данные не продаются и не передаются другим третьим лицам. <br/>
|
||||
5. Защита персональных данных Мы принимаем все разумные меры для защиты данных, однако передача информации
|
||||
через интернет не может быть абсолютно безопасной. <br/>
|
||||
6. Права пользователей•Право на доступ к своим
|
||||
данным.•Право на изменение и удаление личных данных.<br/>
|
||||
7. Контактные данные Компьютерный клуб Strike<br/>
|
||||
Arena Телефон: +7-775-260-85-59 Email: strikearena.astana@gmail.com</p>
|
||||
</PolicyContent>
|
||||
</MainPage>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface AccordionProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const AccordionContainer = styled.div`
|
||||
padding: 30px 62px;
|
||||
background: #15241E;
|
||||
border-radius: 20px;
|
||||
width: 878px;
|
||||
margin-bottom: 25px;
|
||||
cursor: pointer;
|
||||
@media (max-width: 970px) {
|
||||
width: 90%;
|
||||
padding: 25px 40px;
|
||||
}
|
||||
@media (max-width: 750px) {
|
||||
padding: 20px 25px;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
padding: 15px 20px;
|
||||
width: 95%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
const AccordionTitle = styled.div`
|
||||
font-size: 40px;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
line-height: 1;
|
||||
width: 100%;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
text-align: start;
|
||||
h3 {
|
||||
max-width: 580px;
|
||||
margin: 0;
|
||||
}
|
||||
@media (max-width: 970px) {
|
||||
font-size: 32px;
|
||||
gap: 12px;
|
||||
}
|
||||
@media (max-width: 750px) {
|
||||
font-size: 28px;
|
||||
gap: 10px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
font-size: 22px;
|
||||
gap: 8px;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
h3 {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const AccordionContentWrapper = styled.div<{ $maxHeight: number }>`
|
||||
overflow: hidden;
|
||||
transition: max-height 0.4s ease;
|
||||
max-height: ${({ $maxHeight }) => $maxHeight}px;
|
||||
`;
|
||||
|
||||
const AccordionContent = styled.div.attrs({})<{ $isOpen: boolean }>`
|
||||
opacity: ${({ $isOpen }) => ($isOpen ? 1 : 0)};
|
||||
transform: ${({ $isOpen }) => ($isOpen ? 'translateY(0)' : 'translateY(-10px)')};
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
|
||||
p {
|
||||
font-size: 20px;
|
||||
text-align: start;
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
text-transform: none;
|
||||
max-width: 520px;
|
||||
}
|
||||
@media (max-width: 750px) {
|
||||
p {
|
||||
font-size: 18px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
p {
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const PlusIcon = styled.span<{ $isOpen: boolean }>`
|
||||
transition: transform 0.3s ease;
|
||||
font-size: 40px;
|
||||
transform: ${({ $isOpen }) => ($isOpen ? 'rotate(45deg)' : 'rotate(0)')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@media (max-width: 750px) {
|
||||
font-size: 32px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
font-size: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Accordion = ({ title, children }: AccordionProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [height, setHeight] = useState(0);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
setHeight(isOpen ? contentRef.current.scrollHeight+20 : 0);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<AccordionContainer onClick={() => setIsOpen(!isOpen)}>
|
||||
<AccordionTitle>
|
||||
<h3>{title}</h3>
|
||||
<PlusIcon $isOpen={isOpen}>+</PlusIcon>
|
||||
</AccordionTitle>
|
||||
<AccordionContentWrapper $maxHeight={height}>
|
||||
<AccordionContent $isOpen={isOpen} ref={contentRef}>
|
||||
<p>{children}</p>
|
||||
</AccordionContent>
|
||||
</AccordionContentWrapper>
|
||||
</AccordionContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Accordion;
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react';
|
||||
import Link from "next/link";
|
||||
import { usePathname } from 'next/navigation';
|
||||
import {
|
||||
AdminHeaderStyled,
|
||||
AdminHeaderContent,
|
||||
AdminLogo,
|
||||
AdminNav,
|
||||
AdminNavLink
|
||||
} from "@/styles/admin.styled";
|
||||
|
||||
const AdminHeader = () => {
|
||||
const pathname = usePathname();
|
||||
|
||||
const navItems = [
|
||||
{ href: "/admin", label: "Бронирования" },
|
||||
{ href: "/admin/contacts", label: "Контакты" },
|
||||
{ href: "/admin/hardware", label: "Железо" },
|
||||
{ href: "/admin/price", label: "Прайс" },
|
||||
{ href: "/admin/games", label: "Игры" },
|
||||
{ href: "/admin/faq", label: "FAQ" },
|
||||
];
|
||||
|
||||
return (
|
||||
<AdminHeaderStyled>
|
||||
<AdminHeaderContent>
|
||||
<AdminLogo href="/admin">STRIKE ARENA</AdminLogo>
|
||||
<AdminNav>
|
||||
{navItems.map((item) => (
|
||||
<li key={item.href}>
|
||||
<AdminNavLink
|
||||
href={item.href}
|
||||
$active={pathname === item.href}
|
||||
as={Link}
|
||||
>
|
||||
{item.label}
|
||||
</AdminNavLink>
|
||||
</li>
|
||||
))}
|
||||
</AdminNav>
|
||||
</AdminHeaderContent>
|
||||
</AdminHeaderStyled>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminHeader;
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import React from "react";
|
||||
|
||||
const containerVariants = {
|
||||
hidden: {},
|
||||
show: {
|
||||
transition: {
|
||||
staggerChildren: 0.8,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 100 },
|
||||
show: { opacity: 1, y: 0, transition: { duration: 0.6, ease: "easeOut" } },
|
||||
};
|
||||
|
||||
export default function AnimatedSection({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<motion.section
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="show"
|
||||
viewport={{ once: true, amount: 0.4 }}
|
||||
>
|
||||
{React.Children.map(children, (child) => (
|
||||
<motion.div variants={itemVariants}>{child}</motion.div>
|
||||
))}
|
||||
</motion.section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,640 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useBronStore } from '@/store/useBronStore';
|
||||
import StyledButton from "@/components/StyledButton";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
ModalWrapper,
|
||||
Overlay,
|
||||
CloseButton,
|
||||
SectionTitle,
|
||||
Input,
|
||||
ChoiceBtn,
|
||||
TwoColumns,
|
||||
TitleModal,
|
||||
StepsModal,
|
||||
BackBtn,
|
||||
BronBtn,
|
||||
MaskedInput, InputLabel,
|
||||
TimeSlotGrid,
|
||||
TimeSlotBtn
|
||||
} from "@/styles/modal.styled";
|
||||
import {gradient} from "@/styles/colors";
|
||||
|
||||
export default function BronModal() {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const { isOpen, screen, close, goTo, formData, setFormData } = useBronStore();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [shouldAnimateIn, setShouldAnimateIn] = useState(false);
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState<string>(today);
|
||||
const [selectedTime, setSelectedTime] = useState<string | null>(null);
|
||||
const [availableSlots, setAvailableSlots] = useState<Array<{time: string, available: boolean, booked: number, total: number}>>([]);
|
||||
const [loadingSlots, setLoadingSlots] = useState(false);
|
||||
|
||||
const [choice, setChoice] = useState<string|null>(null)
|
||||
const options = [
|
||||
{label:"Играть на пк", value:'pc'},
|
||||
{label:"Играть на ps", value:'ps'},
|
||||
{label:"Смотреть трансляции", value:'tv'},
|
||||
{label:"Пока не определился", value:'zayvka'},
|
||||
]
|
||||
|
||||
const [choiceRoom, setChoiceRoom] = useState<string|null>(null)
|
||||
const rooms = [
|
||||
{title:"solo", label:"VIP кабины на одного игрока"},
|
||||
{title:"duo", label:"vip кабины на двоих игроков"},
|
||||
{title:"trio", label:"vip кабины на три игрока"},
|
||||
{title:"bootcamp 5", label:"vip кабина на пять игроков"},
|
||||
{title:"bootcamp 6", label:"vip кабина на шесть игроков"},
|
||||
{title:"Пока не определился", label:""},
|
||||
]
|
||||
|
||||
const [transRoom, setTransRoom] = useState<string|null>(null)
|
||||
const transRooms = [
|
||||
{title:"PS Big vip", label:"Большие VIP кабины. Компанией до 8 человек"},
|
||||
{title:"PS Medium", label:"Средние VIP кабины. Компанией до 4 человек"},
|
||||
{title:"PS лаундж", label:"PS зона в общем холле. На 2-4 человека"},
|
||||
]
|
||||
|
||||
const contactOptions: { type: "telegram" | "whatsapp" | "phone"; icon: string }[] = [
|
||||
{ type: 'telegram', icon: '/icons/tg.png' },
|
||||
{ type: 'whatsapp', icon: '/icons/whatsapp.png' },
|
||||
{ type: 'phone', icon: '/icons/phone.png' },
|
||||
];
|
||||
|
||||
|
||||
const fetchAvailableSlots = async (date: string, room: string) => {
|
||||
if (!date || !room) return;
|
||||
|
||||
setLoadingSlots(true);
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/available-slots?date=${date}&room=${room}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setAvailableSlots(data.slots || []);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Ошибка при получении доступных слотов', e);
|
||||
} finally {
|
||||
setLoadingSlots(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSend = async (): Promise<boolean> => {
|
||||
try {
|
||||
// Сначала сохраняем в БД
|
||||
if (formData.date && formData.time && formData.room) {
|
||||
const bookingRes = await fetch('/api/bookings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...formData,
|
||||
date: formData.date,
|
||||
time: formData.time,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!bookingRes.ok) {
|
||||
const error = await bookingRes.json();
|
||||
alert(error.error || 'Ошибка при создании бронирования');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Затем отправляем в Telegram
|
||||
const res = await fetch('/api/send-to-telegram', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...formData,
|
||||
time: formData.time || '—',
|
||||
}),
|
||||
});
|
||||
|
||||
return res.ok;
|
||||
} catch (e) {
|
||||
console.error('Ошибка сети', e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setIsVisible(true);
|
||||
document.body.style.overflow = 'hidden';
|
||||
requestAnimationFrame(() => {
|
||||
setShouldAnimateIn(true);
|
||||
});
|
||||
} else {
|
||||
setChoice(null);
|
||||
setChoiceRoom(null);
|
||||
setTransRoom(null);
|
||||
setSelectedDate(today);
|
||||
setSelectedTime(null);
|
||||
setAvailableSlots([]);
|
||||
|
||||
document.body.style.overflow = '';
|
||||
setShouldAnimateIn(false);
|
||||
const timeout = setTimeout(() => setIsVisible(false), 300);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Загружаем доступные слоты при изменении даты или комнаты
|
||||
useEffect(() => {
|
||||
if (formData.date && formData.room && (screen === 'pc3' || screen === 'pc3-2')) {
|
||||
fetchAvailableSlots(formData.date, formData.room);
|
||||
}
|
||||
}, [formData.date, formData.room, screen]);
|
||||
|
||||
|
||||
|
||||
if (!isOpen && !isVisible) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{(isOpen || isVisible) && (
|
||||
<Overlay>
|
||||
<ModalWrapper $animateIn={shouldAnimateIn}>
|
||||
<CloseButton onClick={close}>×</CloseButton>
|
||||
{screen === 'start' && (
|
||||
<>
|
||||
<TitleModal>забронируй место в <span>strike arena</span></TitleModal>
|
||||
<StepsModal>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<p>2</p>
|
||||
<p>3</p>
|
||||
<p>4</p>
|
||||
</StepsModal>
|
||||
<SectionTitle>Выбери чем ты хочешь заняться в клубе</SectionTitle>
|
||||
<TwoColumns>
|
||||
{options.map((option, index) => (
|
||||
<ChoiceBtn
|
||||
className={'big_btn'}
|
||||
key={index}
|
||||
$selected={choice===option.value}
|
||||
onClick={()=> {
|
||||
setChoice(option.value)
|
||||
setFormData({type: option.value})
|
||||
}}
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
</ChoiceBtn>
|
||||
))}
|
||||
</TwoColumns>
|
||||
<StyledButton
|
||||
disabled={!choice}
|
||||
onClick={()=>{
|
||||
if(choice==='pc') goTo('pc2');
|
||||
else if(choice==='ps') goTo('playstation');
|
||||
else if(choice==='tv') goTo('televis');
|
||||
else if(choice==='zayvka') goTo('zayvka');
|
||||
}}
|
||||
label={"Далее"} padding={"8px 46px"} fontSize={"32px"} />
|
||||
<BronBtn onClick={()=>{goTo('zayvka')}}>Забронировать</BronBtn>
|
||||
</>
|
||||
)}
|
||||
|
||||
{screen === 'pc2' && (
|
||||
<>
|
||||
<TitleModal>забронируй место в <span>strike arena</span></TitleModal>
|
||||
<StepsModal>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<p>3</p>
|
||||
<p>4</p>
|
||||
</StepsModal>
|
||||
<SectionTitle className={"marginTitle"} style={{marginTop:"140px"}}>в каком зале планируешь играть?</SectionTitle>
|
||||
<TwoColumns>
|
||||
{rooms.map((room, index) => (
|
||||
<ChoiceBtn
|
||||
key={index}
|
||||
$selected={choiceRoom===room.title}
|
||||
onClick={()=>setChoiceRoom(room.title)}
|
||||
>
|
||||
<h3>{room.title}</h3>
|
||||
<span>{room.label}</span>
|
||||
</ChoiceBtn>
|
||||
))}
|
||||
</TwoColumns>
|
||||
<StyledButton
|
||||
disabled={!choiceRoom}
|
||||
onClick={() => {
|
||||
goTo("pc3");
|
||||
setFormData({ room: choiceRoom ?? undefined });
|
||||
}}
|
||||
|
||||
label={"Далее"} padding={"8px 46px"} fontSize={"32px"} />
|
||||
<BronBtn onClick={()=>{goTo('zayvka')}}>Забронировать</BronBtn>
|
||||
<BackBtn onClick={()=>{goTo('start')}}>Назад</BackBtn>
|
||||
</>
|
||||
)}
|
||||
|
||||
{screen === 'pc3' && (
|
||||
<>
|
||||
<TitleModal>забронируй место в <span>strike arena</span></TitleModal>
|
||||
<StepsModal>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<p>4</p>
|
||||
</StepsModal>
|
||||
<SectionTitle>выбери дату и время</SectionTitle>
|
||||
<Input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
min={today}
|
||||
style={{ width: "260px", marginBottom: "30px" }}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSelectedDate(value);
|
||||
setSelectedTime(null);
|
||||
setFormData({ date: value, time: undefined });
|
||||
}}
|
||||
/>
|
||||
{formData.date && (
|
||||
<>
|
||||
<InputLabel style={{ marginBottom: "20px" }}>
|
||||
{loadingSlots ? 'Загрузка доступных слотов...' : 'Выбери время'}
|
||||
</InputLabel>
|
||||
<TimeSlotGrid>
|
||||
{availableSlots.map((slot) => (
|
||||
<TimeSlotBtn
|
||||
key={slot.time}
|
||||
$selected={selectedTime === slot.time}
|
||||
$available={slot.available}
|
||||
disabled={!slot.available}
|
||||
onClick={() => {
|
||||
if (slot.available) {
|
||||
setSelectedTime(slot.time);
|
||||
setFormData({ time: slot.time });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{slot.time}
|
||||
</TimeSlotBtn>
|
||||
))}
|
||||
</TimeSlotGrid>
|
||||
{availableSlots.length > 0 && (
|
||||
<InputLabel style={{ fontSize: "14px", marginTop: "10px" }}>
|
||||
Занято: {availableSlots.filter(s => !s.available).length} / {availableSlots.length} слотов
|
||||
</InputLabel>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<StyledButton
|
||||
disabled={!selectedDate || !selectedTime}
|
||||
onClick={() => {
|
||||
goTo("pc4")
|
||||
setFormData({ date: selectedDate, time: selectedTime || undefined });
|
||||
}}
|
||||
label="Далее"
|
||||
padding="8px 46px"
|
||||
fontSize="32px"
|
||||
/>
|
||||
<BronBtn onClick={()=>{goTo('zayvka')}}>Забронировать</BronBtn>
|
||||
<BackBtn onClick={()=>{goTo('pc2')}}>Назад</BackBtn>
|
||||
</>
|
||||
)}
|
||||
{screen === 'pc3-2' && (
|
||||
<>
|
||||
<TitleModal>забронируй место в <span>strike arena</span></TitleModal>
|
||||
<StepsModal>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<p>4</p>
|
||||
</StepsModal>
|
||||
<SectionTitle>выбери дату и время</SectionTitle>
|
||||
<Input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
min={today}
|
||||
style={{ width: "260px", marginBottom: "30px" }}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSelectedDate(value);
|
||||
setSelectedTime(null);
|
||||
setFormData({ date: value, time: undefined });
|
||||
}}
|
||||
/>
|
||||
{formData.date && formData.room && (
|
||||
<>
|
||||
<InputLabel style={{ marginBottom: "20px" }}>
|
||||
{loadingSlots ? 'Загрузка доступных слотов...' : 'Выбери время'}
|
||||
</InputLabel>
|
||||
<TimeSlotGrid>
|
||||
{availableSlots.map((slot) => (
|
||||
<TimeSlotBtn
|
||||
key={slot.time}
|
||||
$selected={selectedTime === slot.time}
|
||||
$available={slot.available}
|
||||
disabled={!slot.available}
|
||||
onClick={() => {
|
||||
if (slot.available) {
|
||||
setSelectedTime(slot.time);
|
||||
setFormData({ time: slot.time });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{slot.time}
|
||||
</TimeSlotBtn>
|
||||
))}
|
||||
</TimeSlotGrid>
|
||||
{availableSlots.length > 0 && (
|
||||
<InputLabel style={{ fontSize: "14px", marginTop: "10px" }}>
|
||||
Занято: {availableSlots.filter(s => !s.available).length} / {availableSlots.length} слотов
|
||||
</InputLabel>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<StyledButton
|
||||
disabled={!selectedDate || !selectedTime}
|
||||
onClick={() => {
|
||||
goTo("trans")
|
||||
setFormData({ date: selectedDate, time: selectedTime || undefined });
|
||||
}}
|
||||
label="Далее"
|
||||
padding="8px 46px"
|
||||
fontSize="32px"
|
||||
/>
|
||||
<BronBtn onClick={()=>{goTo('zayvka')}}>Забронировать</BronBtn>
|
||||
<BackBtn onClick={()=>{goTo('playstation')}}>Назад</BackBtn>
|
||||
</>
|
||||
)}
|
||||
|
||||
{screen === 'pc4' && (
|
||||
<>
|
||||
<TitleModal>забронируй место в <span>strike arena</span></TitleModal>
|
||||
<StepsModal>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
</StepsModal>
|
||||
<InputLabel className={"marginTitle"} style={{ marginTop: '100px' }}>Свяжемся с тобой для подтверждения брони и уточнения времени</InputLabel>
|
||||
<Input
|
||||
placeholder={"Введи свое имя"}
|
||||
onChange={(e) => {
|
||||
setFormData({ name: e.target.value })
|
||||
}}
|
||||
/>
|
||||
<InputLabel>Во что планируешь играть?</InputLabel>
|
||||
<Input
|
||||
placeholder={"Введи игру"}
|
||||
onChange={(e) => {
|
||||
setFormData({ game: e.target.value })
|
||||
}}
|
||||
/>
|
||||
<InputLabel>Введите номер телефона</InputLabel>
|
||||
<MaskedInput
|
||||
mask="+0 (000) 000-00-00"
|
||||
placeholder="+_ (___) ___-__-__"
|
||||
value={formData.contact || ''}
|
||||
onAccept={(value) => setFormData({ contact: value })}
|
||||
unmask={false}
|
||||
/>
|
||||
<InputLabel>Выбери удобный способ для связи</InputLabel>
|
||||
<div className={'contacts'} style={{ display: 'flex', gap: '20px', margin: '0px 0 40px' }}>
|
||||
{contactOptions.map((opt) => (
|
||||
<button
|
||||
key={opt.type}
|
||||
onClick={() => setFormData({ contactMethod: opt.type})}
|
||||
style={{
|
||||
borderRadius: '12px',
|
||||
padding: '10px',
|
||||
background: formData.contactMethod === opt.type ? gradient : 'transparent',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<Image src={opt.icon} alt={opt.type} width={48} height={48} loading={'lazy'} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<StyledButton
|
||||
disabled={!formData.name || !formData.contact || !formData.contactMethod}
|
||||
onClick={async () => {
|
||||
const success = await handleSend();
|
||||
if(success) {
|
||||
goTo("ready");
|
||||
}
|
||||
}}
|
||||
label="Далее"
|
||||
padding="8px 46px"
|
||||
fontSize="32px"
|
||||
/>
|
||||
<BronBtn onClick={()=>{goTo('zayvka')}}>Забронировать</BronBtn>
|
||||
<BackBtn onClick={()=>{goTo('pc3')}}>Назад</BackBtn>
|
||||
</>
|
||||
)}
|
||||
|
||||
{screen === 'trans' && (
|
||||
<>
|
||||
<TitleModal>забронируй место в <span>strike arena</span></TitleModal>
|
||||
<StepsModal>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
</StepsModal>
|
||||
<InputLabel style={{ marginTop: '100px' }}>Свяжемся с тобой для подтверждения брони и уточнения времени</InputLabel>
|
||||
<Input
|
||||
placeholder={"Введи свое имя"}
|
||||
onChange={(e) => {
|
||||
setFormData({ name: e.target.value })
|
||||
}}
|
||||
/>
|
||||
<InputLabel>Введите номер телефона</InputLabel>
|
||||
<MaskedInput
|
||||
mask="+0 (000) 000-00-00"
|
||||
placeholder="+_ (___) ___-__-__"
|
||||
value={formData.contact || ''}
|
||||
onAccept={(value) => setFormData({ contact: value })}
|
||||
unmask={false}
|
||||
/>
|
||||
<InputLabel>Выбери удобный способ для связи</InputLabel>
|
||||
<div style={{ display: 'flex', gap: '20px', margin: '0px 0 40px' }}>
|
||||
{contactOptions.map((opt) => (
|
||||
<button
|
||||
key={opt.type}
|
||||
onClick={() => setFormData({ contactMethod: opt.type })}
|
||||
style={{
|
||||
borderRadius: '12px',
|
||||
padding: '10px',
|
||||
background: formData.contactMethod === opt.type ? gradient : 'transparent',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<Image src={opt.icon} alt={opt.type} width={48} height={48} loading={'lazy'} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<StyledButton
|
||||
disabled={!formData.name || !formData.contact || !formData.contactMethod}
|
||||
onClick={async () => {
|
||||
const success = await handleSend();
|
||||
if (success) {
|
||||
goTo("ready");
|
||||
}
|
||||
}}
|
||||
label="Далее"
|
||||
padding="8px 46px"
|
||||
fontSize="32px"
|
||||
/>
|
||||
<BronBtn onClick={()=>{goTo('zayvka')}}>Забронировать</BronBtn>
|
||||
<BackBtn onClick={()=>{goTo('pc3-2')}}>Назад</BackBtn>
|
||||
</>
|
||||
)}
|
||||
|
||||
{screen === 'playstation' && (
|
||||
<>
|
||||
<TitleModal>забронируй место в <span>strike arena</span></TitleModal>
|
||||
<StepsModal>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<p>3</p>
|
||||
<p>4</p>
|
||||
</StepsModal>
|
||||
<SectionTitle className={"marginTitle"} style={{marginTop:"140px"}}>в каком зале планируешь играть?</SectionTitle>
|
||||
{transRooms.map((room, index) => (
|
||||
<ChoiceBtn
|
||||
key={index}
|
||||
$selected={transRoom===room.title}
|
||||
onClick={()=>setTransRoom(room.title)}
|
||||
style={{marginBottom:"18px", maxWidth:"510px", width:"80%"}}
|
||||
>
|
||||
<h3>{room.title}</h3>
|
||||
<span>{room.label}</span>
|
||||
</ChoiceBtn>
|
||||
))}
|
||||
<StyledButton
|
||||
disabled={!transRoom}
|
||||
onClick={() => {
|
||||
goTo("pc3");
|
||||
setFormData({ room: transRoom ?? undefined});
|
||||
}}
|
||||
label={"Далее"} padding={"8px 46px"} fontSize={"32px"} />
|
||||
<BronBtn onClick={()=>{goTo('zayvka')}}>Забронировать</BronBtn>
|
||||
<BackBtn onClick={()=>{goTo('start')}}>Назад</BackBtn>
|
||||
</>
|
||||
)}
|
||||
{screen === 'televis' && (
|
||||
<>
|
||||
<TitleModal>забронируй место в <span>strike arena</span></TitleModal>
|
||||
<StepsModal>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<span> <Image src={"/icons/tick.svg"} alt={'tick'} width={40.5} height={29.5} loading={"lazy"} /> </span>
|
||||
<p>3</p>
|
||||
<p>4</p>
|
||||
</StepsModal>
|
||||
<SectionTitle className={"marginTitle"} style={{marginTop:"140px"}}>в каком зале планируешь играть?</SectionTitle>
|
||||
{transRooms.map((room, index) => (
|
||||
<ChoiceBtn
|
||||
key={index}
|
||||
$selected={transRoom===room.title}
|
||||
onClick={()=>setTransRoom(room.title)}
|
||||
style={{marginBottom:"18px", maxWidth:"510px", width:"80%"}}
|
||||
>
|
||||
<h3>{room.title}</h3>
|
||||
<span>{room.label}</span>
|
||||
</ChoiceBtn>
|
||||
))}
|
||||
<StyledButton
|
||||
disabled={!transRoom}
|
||||
onClick={() => {
|
||||
goTo("pc3-2");
|
||||
setFormData({ room: transRoom ?? undefined});
|
||||
}}
|
||||
label={"Далее"} padding={"8px 46px"} fontSize={"32px"} />
|
||||
<BronBtn onClick={()=>{goTo('zayvka')}}>Забронировать</BronBtn>
|
||||
<BackBtn onClick={()=>{goTo('start')}}>Назад</BackBtn>
|
||||
</>
|
||||
)}
|
||||
|
||||
{screen === 'zayvka' && (
|
||||
<>
|
||||
<TitleModal style={{position:"relative", top:"20px"}}>забронируй место в <span>strike arena</span></TitleModal>
|
||||
<InputLabel style={{marginBottom:"50px"}}>Оставь заявку и мы тебе перезвоним поможем забронировать место</InputLabel>
|
||||
<Input
|
||||
placeholder={"Введи свое имя"}
|
||||
onChange={(e) => {
|
||||
setFormData({ name: e.target.value })
|
||||
}}
|
||||
/>
|
||||
<MaskedInput
|
||||
mask="+0 (000) 000-00-00"
|
||||
placeholder="+_ (___) ___-__-__"
|
||||
value={formData.contact || ''}
|
||||
onAccept={(value) => setFormData({ contact: value })}
|
||||
unmask={false}
|
||||
style={{marginBottom:"50px"}}
|
||||
/>
|
||||
<StyledButton
|
||||
disabled={!formData.name || !formData.contact}
|
||||
onClick={async () => {
|
||||
const success = await handleSend();
|
||||
if (success) {
|
||||
goTo("ready");
|
||||
}
|
||||
}}
|
||||
label="Оставить заявку"
|
||||
padding="8px 46px"
|
||||
fontSize="24px"
|
||||
/>
|
||||
<BackBtn onClick={()=>{goTo('start')}}>Назад</BackBtn>
|
||||
</>
|
||||
)}
|
||||
{screen === 'zayvka-game' && (
|
||||
<>
|
||||
<TitleModal style={{position:"relative", top:"20px"}}>забронируй место в <span>strike arena</span></TitleModal>
|
||||
<InputLabel style={{marginBottom:"50px"}}>Оставь заявку и мы тебе перезвоним поможем забронировать место</InputLabel>
|
||||
<Input
|
||||
placeholder={"Введи свое имя"}
|
||||
onChange={(e) => {
|
||||
setFormData({ name: e.target.value })
|
||||
}}
|
||||
/>
|
||||
<MaskedInput
|
||||
mask="+0 (000) 000-00-00"
|
||||
placeholder="+_ (___) ___-__-__"
|
||||
value={formData.contact || ''}
|
||||
onAccept={(value) => setFormData({ contact: value })}
|
||||
unmask={false}
|
||||
style={{marginBottom:"50px"}}
|
||||
/>
|
||||
<StyledButton
|
||||
disabled={!formData.name || !formData.contact}
|
||||
onClick={async () => {
|
||||
const success = await handleSend();
|
||||
if (success) {
|
||||
goTo("ready");
|
||||
}
|
||||
}}
|
||||
label="Оставить заявку"
|
||||
padding="8px 46px"
|
||||
fontSize="24px"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{screen === 'ready' && (
|
||||
<>
|
||||
<Image style={{marginBottom:"30px",}} src={"/icons/tick.svg"} alt={"tick"} width={160} height={122} loading={"lazy"} />
|
||||
<TitleModal className={'readyTitle'} style={{position:"relative", top:0}}><span>{formData.name}, спасибо,</span><br/> Мы приняли твою заявку<br/> на бронь</TitleModal>
|
||||
<InputLabel className={'readyText'} style={{fontSize:"24px",marginBottom:"50px"}}>Aдмин клуба STRIKE ARENA свяжется с тобой в ближайшее время для уточнения времени и подтверждения твоей заявки</InputLabel>
|
||||
<StyledButton
|
||||
onClick={close}
|
||||
label="Назад"
|
||||
padding="8px 46px"
|
||||
fontSize="32px"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ModalWrapper>
|
||||
</Overlay>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import styled from "styled-components";
|
||||
import Image from "next/image";
|
||||
import {blackColor, gradient, lightGreenColor} from "@/styles/colors";
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: 'stairs.svg',
|
||||
text: '2 игровых этажа',
|
||||
image: '1.png'
|
||||
},
|
||||
{
|
||||
icon: 'vip.svg',
|
||||
text: 'Все VIP кабины',
|
||||
image: '2.png'
|
||||
},
|
||||
{
|
||||
icon: 'energy.svg',
|
||||
text: 'Мощное железо',
|
||||
image: '3.png'
|
||||
},
|
||||
{
|
||||
icon: 'wc.svg',
|
||||
text: 'WC для девочек',
|
||||
image: '4.png'
|
||||
},
|
||||
{
|
||||
icon: 'smoke.png',
|
||||
text: 'Дымный Lounge',
|
||||
image: '5.png'
|
||||
},
|
||||
{
|
||||
icon: 'price.svg',
|
||||
text: 'Выгодные пакеты',
|
||||
image: '6.png'
|
||||
},
|
||||
{
|
||||
icon: 'group.svg',
|
||||
text: '3 VIP Bootcamp',
|
||||
image: '7.png'
|
||||
},
|
||||
{
|
||||
icon: 'vent.svg',
|
||||
text: 'Хорошая вентиляция',
|
||||
image: '8.png'
|
||||
},
|
||||
{
|
||||
icon: 'room.svg',
|
||||
text: 'Звукоизоляция',
|
||||
image: '9.png'
|
||||
},
|
||||
{
|
||||
icon: 'guard.svg',
|
||||
text: 'Безопасность',
|
||||
image: '10.png'
|
||||
},
|
||||
{
|
||||
icon: 'food.svg',
|
||||
text: 'Ассортимент еды',
|
||||
image: '11.png'
|
||||
},
|
||||
{
|
||||
icon: 'game.svg',
|
||||
text: '5 Зон с PS 5 Pro',
|
||||
image: '12.png'
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
const FeaturesContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
&::after{
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 400px;
|
||||
height: 600px;
|
||||
top: 80px;
|
||||
left: -500px;
|
||||
transform: rotate(40deg);
|
||||
border-radius: 50%;
|
||||
filter: blur(100px);
|
||||
opacity: .5;
|
||||
background: ${lightGreenColor};
|
||||
}
|
||||
@media (max-width: 1260px) {
|
||||
justify-content: center;
|
||||
gap: 40px;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
gap: 30px;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
flex-direction: column;
|
||||
margin-top: 30px;
|
||||
gap: 30px;
|
||||
}
|
||||
`
|
||||
const TitleBlock = styled.h2`
|
||||
line-height: 1;
|
||||
width: 100%;
|
||||
@media (max-width: 1400px) {
|
||||
width: 100% !important;
|
||||
margin: 0 auto;
|
||||
font-size: 40px !important;
|
||||
}
|
||||
@media (max-width: 1260px) {
|
||||
width: 90% !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
font-size: 40px !important;
|
||||
text-align: center !important;
|
||||
margin-bottom: 32px !important;
|
||||
}
|
||||
@media (max-width: 470px) {
|
||||
font-size: 32px !important;
|
||||
}
|
||||
`
|
||||
const FeaturesGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 140px);
|
||||
gap: 12px 16px;
|
||||
@media (max-width: 1100px) {
|
||||
grid-template-columns: repeat(4, 120px);
|
||||
gap: 10px 12px;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
grid-template-columns: repeat(3, 130px);
|
||||
gap: 12px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
grid-template-columns: repeat(3, 100px);
|
||||
gap: 10px;
|
||||
}
|
||||
@media (max-width: 350px) {
|
||||
grid-template-columns: repeat(3, 90px);
|
||||
gap: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
const FeaturesImage = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
const ImageWrapper = styled.div`
|
||||
position: relative;
|
||||
width: 439px;
|
||||
height: 618px;
|
||||
@media (max-width: 1100px) {
|
||||
width: 380px;
|
||||
height: 534px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
width: 380px;
|
||||
height: 534px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
width: 320px;
|
||||
height: 350px;
|
||||
}
|
||||
@media (max-width: 350px) {
|
||||
width: 280px;
|
||||
height: 304px;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const FeaturesBlock = styled.div<{ $active?: boolean }>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
text-align: center;
|
||||
background: ${blackColor};
|
||||
border-radius: 20px;
|
||||
padding: 18px 0;
|
||||
position: relative;
|
||||
.highlight {
|
||||
position: absolute;
|
||||
width: 104%;
|
||||
height: 104%;
|
||||
top: -2%;
|
||||
left: -2%;
|
||||
background: ${gradient};
|
||||
border-radius: 20px;
|
||||
z-index: -1;
|
||||
transition: transform .3s ease;
|
||||
transform: ${({ $active }) => ($active ? 'scale(1)' : 'scale(.9)')};
|
||||
}
|
||||
img{
|
||||
height: 70px;
|
||||
}
|
||||
p{
|
||||
cursor: default;
|
||||
width: 120px;
|
||||
font-size: 19px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
padding: 12px 0;
|
||||
img {
|
||||
height: 45px;
|
||||
}
|
||||
p {
|
||||
font-size: 16px;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
padding: 14px 0;
|
||||
img {
|
||||
height: 55px;
|
||||
}
|
||||
p {
|
||||
font-size: 16px;
|
||||
width: 100px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
padding: 12px 0;
|
||||
img {
|
||||
height: 45px;
|
||||
}
|
||||
p {
|
||||
font-size: 14px;
|
||||
width: 90px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 350px) {
|
||||
padding: 10px 0;
|
||||
img {
|
||||
height: 40px;
|
||||
}
|
||||
p {
|
||||
font-size: 13px;
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
const FeaturesList = () => {
|
||||
const [activeIndex, setActiveIndex] = useState<number>(0)
|
||||
|
||||
const useIsMobile = () => {
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const check = () => setIsMobile(window.innerWidth <= 1024)
|
||||
check()
|
||||
window.addEventListener('resize', check)
|
||||
return () => window.removeEventListener('resize', check)
|
||||
}, [])
|
||||
|
||||
return isMobile
|
||||
}
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TitleBlock style={{ textAlign: 'start' }}>
|
||||
<span>Твое топовое</span><br />
|
||||
игровое пространство
|
||||
</TitleBlock>
|
||||
<FeaturesContent>
|
||||
<FeaturesGrid>
|
||||
{features.map((feature, index) => (
|
||||
<FeaturesBlock
|
||||
key={index}
|
||||
$active={activeIndex === index}
|
||||
onMouseEnter={!isMobile ? () => setActiveIndex(index) : undefined}
|
||||
onClick={isMobile ? () => setActiveIndex(index) : undefined}
|
||||
>
|
||||
<div className="highlight" />
|
||||
<Image src={'/icons/' + feature.icon} alt={'startLeft'} width={40} height={40} loading={'lazy'} />
|
||||
<p>{feature.text}</p>
|
||||
</FeaturesBlock>
|
||||
))}
|
||||
</FeaturesGrid>
|
||||
<FeaturesImage>
|
||||
<ImageWrapper>
|
||||
{features.map((f, i) => (
|
||||
<Image
|
||||
key={i}
|
||||
src={`/images/features/${f.image}`}
|
||||
alt={'feature'}
|
||||
width={439}
|
||||
height={618}
|
||||
className={activeIndex === i ? 'visible' : ''}
|
||||
style={{ zIndex: activeIndex === i ? 1 : 0 }}
|
||||
/>
|
||||
))}
|
||||
</ImageWrapper>
|
||||
</FeaturesImage>
|
||||
</FeaturesContent>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesList;
|
||||
@@ -0,0 +1,294 @@
|
||||
'use client'
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import styled from "styled-components";
|
||||
import {PriceHeader_btn} from "@/app/main.styled";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import {lightGreenColor} from "@/styles/colors";
|
||||
import {useBronStore} from "@/store/useBronStore";
|
||||
|
||||
const GameHeader = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 170px;
|
||||
align-items: center;
|
||||
button{
|
||||
margin-left: 0;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
gap: 40px;
|
||||
flex-direction: row;
|
||||
button {
|
||||
font-size: 42px;
|
||||
width: auto;
|
||||
padding: 0 20px;
|
||||
&::after {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
gap: 20px;
|
||||
button {
|
||||
font-size: 36px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 400px) {
|
||||
button {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
`
|
||||
const GameContent = styled.div`
|
||||
display: grid;
|
||||
margin-top: 100px;
|
||||
grid-template-columns: repeat(4, 270px);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 45px;
|
||||
position: relative;
|
||||
-webkit-transform: translateZ(0);
|
||||
will-change: transform;
|
||||
&::after{
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: -30%;
|
||||
background: ${lightGreenColor};
|
||||
width: 200px;
|
||||
height: 400px;
|
||||
filter: blur(120px);
|
||||
opacity: .6;
|
||||
z-index: -1;
|
||||
}
|
||||
@media (max-width: 1300px) {
|
||||
grid-template-columns: repeat(3, 270px);
|
||||
}
|
||||
@media (max-width: 1000px) {
|
||||
grid-template-columns: repeat(3, 200px);
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
grid-template-columns: repeat(2, 240px);
|
||||
gap: 30px;
|
||||
margin-top: 60px;
|
||||
}
|
||||
@media (max-width: 550px) {
|
||||
grid-template-columns: repeat(2, 180px);
|
||||
gap: 20px;
|
||||
}
|
||||
@media (max-width: 400px) {
|
||||
grid-template-columns: repeat(2, 150px);
|
||||
gap: 15px;
|
||||
}
|
||||
`
|
||||
const GameBlock = styled.div`
|
||||
cursor: pointer;
|
||||
p{
|
||||
font-size: 30px;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
}
|
||||
img {
|
||||
width: 300px;
|
||||
height: 155px;
|
||||
border-radius: 15px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
img {
|
||||
width: 100%;
|
||||
height: 130px;
|
||||
}
|
||||
p {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 550px) {
|
||||
img {
|
||||
height: 100px;
|
||||
}
|
||||
p {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 400px) {
|
||||
img {
|
||||
height: 85px;
|
||||
}
|
||||
p {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
`
|
||||
const GameText = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
margin-top: 80px;
|
||||
div{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
}
|
||||
h3{
|
||||
line-height: 1;
|
||||
font-size: 36px;
|
||||
text-align: center;
|
||||
text-transform: none;
|
||||
span{
|
||||
background: #8921ff;
|
||||
text-transform: uppercase;
|
||||
padding: 4px 18px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
margin-top: 60px;
|
||||
h3 {
|
||||
font-size: 32px;
|
||||
max-width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
div {
|
||||
gap: 25px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 550px) {
|
||||
gap: 20px;
|
||||
margin-top: 50px;
|
||||
h3 {
|
||||
font-size: 28px;
|
||||
max-width: 90%;
|
||||
}
|
||||
div {
|
||||
gap: 20px;
|
||||
img {
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 400px) {
|
||||
gap: 15px;
|
||||
margin-top: 40px;
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
}
|
||||
div {
|
||||
gap: 15px;
|
||||
img {
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
interface Game{
|
||||
id: number;
|
||||
title: string;
|
||||
image_url: string;
|
||||
platform: string;
|
||||
}
|
||||
type Links = {
|
||||
tg?: string;
|
||||
whatsapp?: string;
|
||||
}
|
||||
|
||||
const GameList:React.FC<Links> = ({tg, whatsapp}) => {
|
||||
const [platform, setPlatform] = useState<'pc'|'ps'>('pc')
|
||||
const [games, setGames] = useState<Game[]>([]);
|
||||
const open = useBronStore((s) => s.open);
|
||||
|
||||
useEffect(()=>{
|
||||
fetch(`/api/games?platform=${platform}`)
|
||||
.then(res=>res.json())
|
||||
.then(setGames)
|
||||
}, [platform])
|
||||
|
||||
const [showAll, setShowAll] = useState(false)
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth <= 760)
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
return () => window.removeEventListener('resize', checkMobile)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GameHeader>
|
||||
<PriceHeader_btn onClick={()=> setPlatform('pc')} className={platform==='pc' ? 'active' : ''}>
|
||||
Пк игры:
|
||||
</PriceHeader_btn>
|
||||
<PriceHeader_btn onClick={()=> setPlatform('ps')} className={platform==='ps' ? 'active' : ''}>
|
||||
Ps игры:
|
||||
</PriceHeader_btn>
|
||||
</GameHeader>
|
||||
<GameContent>
|
||||
{(isMobile && !showAll ? games.slice(0, 8) : games).map((game) => (
|
||||
<GameBlock key={game.id} onClick={() => open('zayvka-game')}>
|
||||
<p>{game.title}</p>
|
||||
<Image
|
||||
src={`/api/uploads/games/${game.image_url}`}
|
||||
alt={game.title}
|
||||
width={270}
|
||||
height={140}
|
||||
loading="lazy"
|
||||
/>
|
||||
</GameBlock>
|
||||
))}
|
||||
</GameContent>
|
||||
{isMobile && games.length >8 && (
|
||||
<div style={{ textAlign: 'center', marginTop: '42px', width:"100%"}}>
|
||||
<button
|
||||
onClick={() => setShowAll(prev => !prev)}
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
padding: '12px 24px',
|
||||
borderRadius: '12px',
|
||||
border: 'none',
|
||||
background: lightGreenColor,
|
||||
color: '#000',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
{showAll ? 'Скрыть' : 'Показать все'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<GameText>
|
||||
<h3><span>Не нашел нужную игру?</span><br/>Напиши нам и мы ее скачаем!</h3>
|
||||
<div>
|
||||
{tg ? (
|
||||
<Link href={tg} target={"_blank"}>
|
||||
<Image src={'/icons/tg_primary.png'} alt={'tg'} width={78} height={78} loading={"lazy"} />
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={'/'}>
|
||||
<Image src={'/icons/tg_primary.png'} alt={'tg'} width={78} height={78} loading={"lazy"} />
|
||||
</Link>
|
||||
)}
|
||||
{whatsapp ? (
|
||||
<Link href={whatsapp} target={"_blank"}>
|
||||
<Image src={'/icons/wa_primary.png'} alt={'whatsapp'} width={81} height={81} loading={"lazy"} />
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={'/'}>
|
||||
<Image src={'/icons/wa_primary.png'} alt={'whatsapp'} width={81} height={81} loading={"lazy"} />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</GameText>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GameList;
|
||||
@@ -0,0 +1,268 @@
|
||||
import React, {useState} from 'react';
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import {useBronStore} from "@/store/useBronStore";
|
||||
import styled from "styled-components";
|
||||
|
||||
type HeaderPageProps = {
|
||||
whatsapp?: string;
|
||||
tg?: string;
|
||||
inst?: string;
|
||||
phone?: string;
|
||||
gis2?: string;
|
||||
}
|
||||
|
||||
const Header = styled.header`
|
||||
margin: 0 auto;
|
||||
max-width: 1480px;
|
||||
height: 148px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
&::after{
|
||||
content: "";
|
||||
width: 100vw;
|
||||
height:100%;
|
||||
border-radius: 0 0 20px 20px;
|
||||
background-color: rgba(14,14,23,.7);
|
||||
backdrop-filter: blur(100%);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: -1;
|
||||
}
|
||||
@media (max-width: 1500px) {
|
||||
height: 120px;
|
||||
padding: 0 40px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
height: 100px;
|
||||
}
|
||||
`
|
||||
const HeaderSchedule = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
h3{
|
||||
font-size: 48px;
|
||||
}
|
||||
p{
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
const HeaderPhone = styled.div`
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
margin-left: 33px;
|
||||
h4{
|
||||
font-size: 18px;
|
||||
margin-top: 5px;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
}
|
||||
p{
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
`
|
||||
const HeaderBlock = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.logo{
|
||||
margin-right: 56px;
|
||||
}
|
||||
.gis2Link{
|
||||
margin-right: 15px;
|
||||
}
|
||||
.tgLink{
|
||||
margin: 0 12px 0 8px;
|
||||
}
|
||||
&.desktopOnly {
|
||||
@media (max-width: 900px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.footerPhone{
|
||||
h4{
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.headerImage img{
|
||||
height: 40px;
|
||||
}
|
||||
.gis2Link{
|
||||
margin-right: 2px;
|
||||
}
|
||||
.tgLink{
|
||||
margin: 0 6px 0 -2px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
width: 100%;
|
||||
justify-content:space-between;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.headerImage img{
|
||||
height: 40px;
|
||||
}
|
||||
.gis2Link{
|
||||
margin-right: 2px;
|
||||
}
|
||||
.tgLink{
|
||||
margin: 0 6px 0 -2px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.logo img{
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
`
|
||||
const MenuButton = styled.button`
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
@media (max-width: 900px) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
`
|
||||
const MobileMenu = styled.div<{ $isOpenMenu: boolean }>`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: ${({ $isOpenMenu }) => ($isOpenMenu ? '0' : '-100%')};
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: rgba(14, 14, 23, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 999;
|
||||
transition: right 0.3s ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
a {
|
||||
color: white;
|
||||
font-size: 28px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
@media (min-width: 901px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
const HeaderPage: React.FC<HeaderPageProps> = ({whatsapp, tg, inst, gis2, phone}) => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
const open = useBronStore((s) => s.open);
|
||||
return (
|
||||
<Header>
|
||||
<HeaderBlock>
|
||||
<Link href='/' className={'logo'}>
|
||||
<Image src={'/icons/logo.png'} alt={'logo'} width={196} height={52} loading={'lazy'} />
|
||||
</Link>
|
||||
<HeaderSchedule>
|
||||
<h3>24/7</h3>
|
||||
<p>Пиши нам<br />мы на связи</p>
|
||||
</HeaderSchedule>
|
||||
<MenuButton onClick={() => setIsMenuOpen(true)}>
|
||||
<Image src="/icons/menu.svg" alt="menu" width={40} height={40} loading="lazy" />
|
||||
</MenuButton>
|
||||
</HeaderBlock>
|
||||
<HeaderBlock className={"desktopOnly"}>
|
||||
{gis2 ? (
|
||||
<Link href={gis2} className="gis2Link headerImage" target="_blank">
|
||||
<Image src="/icons/location.svg" alt="geo" width={37} height={50} loading="lazy" />
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={'/'} className="gis2Link headerImage" target="_blank">
|
||||
<Image src="/icons/location.svg" alt="geo" width={37} height={50} loading="lazy" />
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{whatsapp ? (
|
||||
<Link href={whatsapp} className="headerImage" target="_blank">
|
||||
<Image src="/icons/whatsapp.png" alt="whatsapp" width={58} height={58} loading="lazy" />
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={'/'} className="headerImage" target="_blank">
|
||||
<Image src="/icons/whatsapp.png" alt="whatsapp" width={58} height={58} loading="lazy" />
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{tg ? (
|
||||
<Link href={tg} className="tgLink headerImage" target="_blank">
|
||||
<Image src="/icons/tg.png" alt="tg" width={48} height={48} loading="lazy" />
|
||||
</Link>
|
||||
) : (
|
||||
<Link href={'/'} className="tgLink headerImage" target="_blank">
|
||||
<Image src="/icons/tg.png" alt="tg" width={48} height={48} loading="lazy" />
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{inst ? (
|
||||
<Link href={inst} target="_blank">
|
||||
<Image src="/icons/inst.png" alt="inst" width={60} height={60} loading="lazy" />
|
||||
</Link>
|
||||
) : (
|
||||
<Image src="/icons/inst.png" alt="inst" width={60} height={60} loading="lazy" />
|
||||
)}
|
||||
|
||||
<HeaderPhone>
|
||||
<p style={{ marginBottom: 0 }}>{phone || '+7 775 260 85 59'}</p>
|
||||
<h4 onClick={() => open('zayvka-game')}>Заказать звонок</h4>
|
||||
</HeaderPhone>
|
||||
</HeaderBlock>
|
||||
<MobileMenu $isOpenMenu={isMenuOpen}>
|
||||
<button onClick={() => setIsMenuOpen(false)} style={{fontSize: "32px"}}>
|
||||
Close
|
||||
</button>
|
||||
|
||||
{gis2 && <Link href={gis2} target="_blank">2 ГИС</Link>}
|
||||
{whatsapp && <Link href={whatsapp} target="_blank">WhatsApp</Link>}
|
||||
{tg && <Link href={tg} target="_blank">Telegram</Link>}
|
||||
{phone && <Link href={`tel:${phone}`}>{phone}</Link>}
|
||||
|
||||
</MobileMenu>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderPage;
|
||||
@@ -0,0 +1,570 @@
|
||||
'use client'
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import styled from "styled-components";
|
||||
import {blackColor, darkGreenColor, lightGreenColor} from "@/styles/colors";
|
||||
import {PriceHeader_btn} from "@/app/main.styled";
|
||||
|
||||
type PriceRow = {
|
||||
day_type: string
|
||||
hour_1: string
|
||||
hour_3: string
|
||||
hour_5: string
|
||||
night: string
|
||||
}
|
||||
|
||||
const PriceHeader = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
h2{
|
||||
font-size: 64px;
|
||||
margin: 0;
|
||||
width: max-content;
|
||||
line-height: 1;
|
||||
text-align: start;
|
||||
}
|
||||
@media (max-width: 1400px) {
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@media (max-width: 1080px) {
|
||||
width: 80%;
|
||||
h2 {
|
||||
font-size: 40px;
|
||||
}
|
||||
div {
|
||||
display: flex;
|
||||
margin-top: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
h2{
|
||||
font-size: 40px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
h2 {
|
||||
font-size: 42px;
|
||||
}
|
||||
div {
|
||||
gap: 1px;
|
||||
button {
|
||||
font-size: 24px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 420px) {
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
h2 {
|
||||
font-size: 36px;
|
||||
}
|
||||
div {
|
||||
gap: 0px;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
button {
|
||||
font-size: 24px;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const PriceControl = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 80px auto;
|
||||
button{
|
||||
margin-left: 0;
|
||||
font-size: 40px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
&::after{
|
||||
width: 100%;
|
||||
left: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@media (max-width: 1400px) {
|
||||
width: 90%;
|
||||
button{
|
||||
font-size: 34px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1080px) {
|
||||
width: 80%;
|
||||
margin: 60px auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
button {
|
||||
font-size: 24px;
|
||||
flex: 1;
|
||||
width: max-content;
|
||||
white-space: nowrap;
|
||||
padding: 12px 0 0;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&::after{
|
||||
width: 130%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
gap: 8px;
|
||||
button {
|
||||
font-size: 20px;
|
||||
padding: 0;
|
||||
&::after{
|
||||
width: 110%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
gap: 6px;
|
||||
button {
|
||||
font-size: 16px;
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
gap: 6px;
|
||||
margin: 50px auto 40px;
|
||||
button {
|
||||
font-size: 16px;
|
||||
padding: 6px 10px;
|
||||
min-width: 100px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 400px) {
|
||||
gap: 4px;
|
||||
button {
|
||||
font-size: 16px;
|
||||
padding: 6px 10px;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
`
|
||||
const PriceContent = styled.div`
|
||||
position: relative;
|
||||
font-size: 40px;
|
||||
th{
|
||||
width: 300px;
|
||||
height:150px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
background: transparent;
|
||||
font-weight: normal;
|
||||
}
|
||||
td{
|
||||
width: 300px;
|
||||
height:150px;
|
||||
background: ${blackColor};
|
||||
text-align: center;
|
||||
}
|
||||
.tableBottom{
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
|
||||
&::after{
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(to right, ${lightGreenColor}, ${darkGreenColor});
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
border: none;
|
||||
border-radius: 0 0 20px 0px;
|
||||
}
|
||||
.customTable {
|
||||
all: unset;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
th, td {
|
||||
min-width: 180px;
|
||||
height: 150px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
border: 1px solid ${darkGreenColor};
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
td{
|
||||
border: 1px solid ${darkGreenColor};
|
||||
padding: 16px;
|
||||
}
|
||||
thead th:nth-child(1){
|
||||
border: none;
|
||||
}
|
||||
thead th{
|
||||
border-left: 1px solid ${darkGreenColor};
|
||||
}
|
||||
tbody th{
|
||||
border-top: 1px solid ${darkGreenColor};
|
||||
}
|
||||
}
|
||||
@media(max-width: 1400px) {
|
||||
display: flex;
|
||||
justify-content:center;
|
||||
font-size: 34px;
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
th{
|
||||
width: 220px;
|
||||
height:100px;
|
||||
}
|
||||
td{
|
||||
width: 220px;
|
||||
height:100px;
|
||||
}
|
||||
.customTable {
|
||||
th, td {
|
||||
height: 130px;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 1080px) {
|
||||
font-size: 28px;
|
||||
width: 80%;
|
||||
.customTable {
|
||||
th, td {
|
||||
min-width: 160px;
|
||||
height: 110px;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 1000px) {
|
||||
font-size: 24px;
|
||||
.customTable{
|
||||
th, td {
|
||||
min-width: 100px;
|
||||
height: 90px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
font-size: 20px;
|
||||
.customTable{
|
||||
th, td {
|
||||
min-width: 80px;
|
||||
height: 90px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
font-size: 16px;
|
||||
.customTable{
|
||||
th, td {
|
||||
|
||||
height: 70px;
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 400px) {
|
||||
font-size: 16px;
|
||||
.customTable{
|
||||
th, td {
|
||||
min-width: 80px;
|
||||
height: 60px;
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const TableWrapper = styled.div`
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
|
||||
table {
|
||||
min-width: 600px;
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: ${darkGreenColor};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 40px;
|
||||
background: linear-gradient(to right, transparent, rgba(0,0,0,0.2));
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
&::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
&::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
const PriceList = () => {
|
||||
const [gameType, setGameType] = useState('ПК')
|
||||
const [roomType, setRoomType] = useState('solo')
|
||||
const [prices, setPrices] = useState<PriceRow[]>([])
|
||||
|
||||
const [isMobileView, setIsMobileView] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobileView(window.innerWidth < 500);
|
||||
};
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
|
||||
return () => window.removeEventListener("resize", checkMobile);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchPrices() {
|
||||
const res = await fetch(`/api/prices?gameType=${gameType}&roomType=${roomType}`);
|
||||
const data = await res.json();
|
||||
setPrices(data);
|
||||
}
|
||||
|
||||
fetchPrices();
|
||||
}, [gameType, roomType]);
|
||||
return (
|
||||
<div>
|
||||
<PriceHeader>
|
||||
<h2>Прайс</h2>
|
||||
<div>
|
||||
<PriceHeader_btn
|
||||
onClick={() => {
|
||||
setGameType('ПК')
|
||||
setRoomType('solo')
|
||||
}}
|
||||
className={gameType === 'ПК' ? 'active' : ''}
|
||||
>
|
||||
Пк игры:
|
||||
</PriceHeader_btn>
|
||||
<PriceHeader_btn
|
||||
onClick={() => {
|
||||
setGameType('PS')
|
||||
setRoomType('lounge')
|
||||
}}
|
||||
className={gameType === 'PS' ? 'active' : ''}
|
||||
>
|
||||
PS игры:
|
||||
</PriceHeader_btn>
|
||||
</div>
|
||||
</PriceHeader>
|
||||
<PriceControl>
|
||||
{gameType === 'ПК' ? (
|
||||
<>
|
||||
<PriceHeader_btn
|
||||
onClick={() => setRoomType('solo')}
|
||||
className={roomType === 'solo' ? 'active' : ''}
|
||||
>
|
||||
room solo
|
||||
</PriceHeader_btn>
|
||||
<PriceHeader_btn
|
||||
onClick={() => setRoomType('duo')}
|
||||
className={roomType === 'duo' ? 'active' : ''}
|
||||
>
|
||||
room duo
|
||||
</PriceHeader_btn>
|
||||
<PriceHeader_btn
|
||||
onClick={() => setRoomType('trio')}
|
||||
className={roomType === 'trio' ? 'active' : ''}
|
||||
>
|
||||
room trio
|
||||
</PriceHeader_btn>
|
||||
<PriceHeader_btn
|
||||
onClick={() => setRoomType('five')}
|
||||
className={roomType === 'five' ? 'active' : ''}
|
||||
>
|
||||
room five
|
||||
</PriceHeader_btn>
|
||||
<PriceHeader_btn
|
||||
onClick={() => setRoomType('six')}
|
||||
className={roomType === 'six' ? 'active' : ''}
|
||||
>
|
||||
room six
|
||||
</PriceHeader_btn>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PriceHeader_btn
|
||||
onClick={() => setRoomType('lounge')}
|
||||
className={roomType === 'lounge' ? 'active' : ''}
|
||||
>
|
||||
playstation<br/>lounge
|
||||
</PriceHeader_btn>
|
||||
<PriceHeader_btn
|
||||
onClick={() => setRoomType('vip_normal')}
|
||||
className={roomType === 'vip_normal' ? 'active' : ''}
|
||||
>
|
||||
playstation<br/>vip средний
|
||||
</PriceHeader_btn>
|
||||
<PriceHeader_btn
|
||||
onClick={() => setRoomType('vip_big')}
|
||||
className={roomType === 'vip_big' ? 'active' : ''}
|
||||
>
|
||||
playstation<br/>vip большой
|
||||
</PriceHeader_btn>
|
||||
</>
|
||||
)}
|
||||
</PriceControl>
|
||||
<PriceContent>
|
||||
<TableWrapper>
|
||||
{isMobileView ?
|
||||
(<table className="customTable">
|
||||
<thead>
|
||||
{gameType === 'ПК' && (
|
||||
<tr>
|
||||
<th></th>
|
||||
{prices.map((row, idx) => (
|
||||
<th key={idx}>{row.day_type}</th>
|
||||
))}
|
||||
</tr>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{gameType === 'ПК' ? (
|
||||
<>
|
||||
<tr>
|
||||
<th>1 ЧАС</th>
|
||||
{prices.map((row, idx) => (
|
||||
<td key={idx}>{row.hour_1}</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>3 ЧАСА</th>
|
||||
{prices.map((row, idx) => (
|
||||
<td key={idx}>{row.hour_3}</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>5 ЧАСОВ</th>
|
||||
{prices.map((row, idx) => (
|
||||
<td key={idx}>{row.hour_5}</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>НОЧЬ</th>
|
||||
{prices.map((row, idx) => (
|
||||
<td key={idx}>{row.night}</td>
|
||||
))}
|
||||
</tr>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<tr>
|
||||
<th>1 ЧАС</th>
|
||||
{prices.map((row, idx) => (
|
||||
<td key={idx}>{row.day_type}</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>3 ЧАСА</th>
|
||||
{prices.map((row, idx) => (
|
||||
<td key={idx}>{row.hour_1}</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>3 ЧАСА + КАЛЬЯН</th>
|
||||
{prices.map((row, idx) => (
|
||||
<td key={idx}>{row.hour_3}</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>5 ЧАСОВ</th>
|
||||
{prices.map((row, idx) => (
|
||||
<td key={idx}>{row.hour_5}</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>5 ЧАСОВ + КАЛЬЯН</th>
|
||||
{prices.map((row, idx) => (
|
||||
<td key={idx} className={idx === prices.length - 1 ? 'tableBottom' : ''}>{row.night}</td>
|
||||
))}
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
</tbody>
|
||||
</table>)
|
||||
:
|
||||
(<table className="customTable">
|
||||
<thead>
|
||||
{gameType === 'ПК' ? (
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>1 ЧАС</th>
|
||||
<th>3 ЧАСА</th>
|
||||
<th>5 ЧАСОВ</th>
|
||||
<th>НОЧЬ</th>
|
||||
</tr>
|
||||
) : (
|
||||
<tr>
|
||||
<th>1 ЧАС</th>
|
||||
<th>3 ЧАСА</th>
|
||||
<th>3 ЧАСА + Кальян</th>
|
||||
<th>5 ЧАСОВ</th>
|
||||
<th>5 ЧАСОВ + Кальян</th>
|
||||
</tr>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{prices.map((row, idx) => (
|
||||
<tr key={idx}>
|
||||
{gameType === 'ПК' ? (<th>{row.day_type}</th>) : (<td>{row.day_type}</td>)}
|
||||
<td>{row.hour_1}</td>
|
||||
<td>{row.hour_3}</td>
|
||||
<td>{row.hour_5}</td>
|
||||
<td className={idx === prices.length - 1 ? 'tableBottom' : ''}>{row.night}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>)
|
||||
}
|
||||
</TableWrapper>
|
||||
</PriceContent>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PriceList;
|
||||
@@ -0,0 +1,494 @@
|
||||
'use client'
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import styled from "styled-components";
|
||||
import {blackColor, darkGreenColor, lightGreenColor} from "@/styles/colors";
|
||||
import {AnimatePresence, motion} from "framer-motion";
|
||||
|
||||
type Room = {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
cpu: string;
|
||||
gpu: string;
|
||||
ram: string;
|
||||
monitor: string;
|
||||
mouse: string;
|
||||
keyboard: string;
|
||||
headphones: string;
|
||||
chair: string;
|
||||
bracket: string;
|
||||
};
|
||||
|
||||
const RoomButton = styled.button`
|
||||
width: 472px;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 24px 40px 16px;
|
||||
background-color: ${blackColor};
|
||||
border-radius: 20px;
|
||||
color: white;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
transition: all 0.3s ease;
|
||||
.room-text{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(to right, ${lightGreenColor}, ${darkGreenColor});
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
width: 100%;
|
||||
padding: 20px 30px 14px;
|
||||
p {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
padding: 20px 20px 18px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 70px;
|
||||
p {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
padding: 14px 20px 10px;
|
||||
min-height: 70px;
|
||||
p {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const RoomTitle = styled.div`
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
line-height: 1;
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
margin: 74px auto 0;
|
||||
padding: 0 20px;
|
||||
gap: 32px;
|
||||
|
||||
.roomsList {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 50%;
|
||||
.gradient-border {
|
||||
width: 100%;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(to right, ${lightGreenColor}, ${darkGreenColor});
|
||||
background-origin: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
width: 90%;
|
||||
gap: 32px;
|
||||
.roomsList {
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
margin-top: 30px;
|
||||
.roomsList {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
padding: 0 12px;
|
||||
margin-top: 30px;
|
||||
gap: 30px;
|
||||
}
|
||||
`;
|
||||
|
||||
const RoomInfo = styled.div`
|
||||
background: ${blackColor};
|
||||
padding: 32px 52px;
|
||||
height: 100%;
|
||||
border-radius: 20px;
|
||||
h4 {
|
||||
background: linear-gradient(to right, ${lightGreenColor}, ${darkGreenColor});
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-transform: uppercase;
|
||||
font-size: 36px;
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
padding: 28px 20px;
|
||||
h4 {
|
||||
font-size: 32px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1050px) {
|
||||
padding:20px 36px;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
padding: 26px 32px 42px;
|
||||
|
||||
h4 {
|
||||
line-height: 1;
|
||||
font-size: 40px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
padding: 20px 12px 32px;
|
||||
h4 {
|
||||
font-size: 32px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const RoomInfo_block = styled.div`
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 24px auto 0;
|
||||
h5 {
|
||||
background: linear-gradient(to right, ${lightGreenColor}, ${darkGreenColor});
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
font-size: 24px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
margin: 20px auto 0;
|
||||
h5 {
|
||||
font-size: 22px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
margin: 16px auto 0;
|
||||
h5 {
|
||||
font-size: 22px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
margin: 32px auto 0;
|
||||
h5 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const RoomBorder = styled.div`
|
||||
border: 2px solid transparent;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(to right, ${lightGreenColor}, ${darkGreenColor});
|
||||
background-origin: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
width: 550px;
|
||||
}
|
||||
@media (max-width: 1050px) {
|
||||
width: 450px;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
width: 380px;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
`;
|
||||
|
||||
const RoomDetail = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 24px;
|
||||
text-transform: uppercase;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.value {
|
||||
text-align: right;
|
||||
max-width: 50%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
font-size: 22px;
|
||||
.value {
|
||||
max-width: 45%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 2px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.label {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.value {
|
||||
max-width: 100%;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
const RoomDescription = styled.div`
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-top: 10px;
|
||||
line-height: 1.3;
|
||||
text-transform: none;
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
font-size: 15px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
const RoomList = () => {
|
||||
const [rooms, setRooms] = useState<Room[]>([]);
|
||||
const [selectedRoom, setSelectedRoom] = useState<Room | null>(null);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/rooms')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setRooms(data)
|
||||
setSelectedRoom(data[0])
|
||||
})
|
||||
}, []);
|
||||
return (
|
||||
<Wrapper>
|
||||
<div className={'roomsList'}>
|
||||
{rooms.map((room)=>(
|
||||
<div key={room.id} className={'gradient-border'}>
|
||||
<RoomButton
|
||||
className={room.id === selectedRoom?.id ? 'active' : ''}
|
||||
onClick={() => setSelectedRoom(room)}
|
||||
>
|
||||
<div className="room-text">
|
||||
<RoomTitle>{room.name}</RoomTitle>
|
||||
<RoomDescription>{room.description}</RoomDescription>
|
||||
</div>
|
||||
</RoomButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<AnimatePresence mode="wait">
|
||||
<div style={{width: '100%'}}>
|
||||
{selectedRoom && (
|
||||
selectedRoom.name === 'ps' ? (
|
||||
<motion.div
|
||||
key={selectedRoom.id}
|
||||
initial={{opacity: 0, y: 50}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
exit={{opacity: 0, y: -50}}
|
||||
transition={{duration: 0.4, ease: "easeOut"}}
|
||||
>
|
||||
<RoomBorder>
|
||||
<RoomInfo>
|
||||
<motion.h4
|
||||
initial={{opacity: 0, y: -50}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.4, ease: "easeOut", delay: 0.1}}
|
||||
>
|
||||
{selectedRoom.name}
|
||||
</motion.h4>
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 50}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.4, ease: "easeOut", delay: 0.2}}
|
||||
>
|
||||
<RoomInfo_block>
|
||||
<h5>Cистемный блок</h5>
|
||||
<RoomDetail>
|
||||
<span className="label">Приставки</span>
|
||||
<span className="value">{selectedRoom.cpu}</span>
|
||||
</RoomDetail>
|
||||
<RoomDetail>
|
||||
<span className="label">Геймпады</span>
|
||||
<span className="value">{selectedRoom.gpu}</span>
|
||||
</RoomDetail>
|
||||
<RoomDetail>
|
||||
<span className="label">Телевизор</span>
|
||||
<span className="value">{selectedRoom.ram}</span>
|
||||
</RoomDetail>
|
||||
<RoomDetail>
|
||||
<span className="label">Удобынй большой диван и стол</span>
|
||||
</RoomDetail>
|
||||
</RoomInfo_block>
|
||||
</motion.div>
|
||||
</RoomInfo>
|
||||
</RoomBorder>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key={selectedRoom.id}
|
||||
initial={{opacity: 0, y: 50}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
exit={{opacity: 0, y: -50}}
|
||||
transition={{duration: 0.4, ease: "easeOut"}}
|
||||
>
|
||||
<RoomBorder>
|
||||
<RoomInfo>
|
||||
<motion.h4
|
||||
initial={{opacity: 0, y: -50}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.4, ease: "easeOut", delay: 0.1}}
|
||||
>
|
||||
{selectedRoom.name}
|
||||
</motion.h4>
|
||||
<motion.div
|
||||
initial={{opacity: 0, y: 50}}
|
||||
animate={{opacity: 1, y: 0}}
|
||||
transition={{duration: 0.4, ease: "easeOut", delay: 0.2}}
|
||||
>
|
||||
<RoomInfo_block>
|
||||
<h5>Cистемный блок</h5>
|
||||
<RoomDetail>
|
||||
<span className="label">Процессор</span>
|
||||
<span className="value">{selectedRoom.cpu}</span>
|
||||
</RoomDetail>
|
||||
<RoomDetail>
|
||||
<span className="label">Озу</span>
|
||||
<span className="value">{selectedRoom.ram}</span>
|
||||
</RoomDetail>
|
||||
<RoomDetail>
|
||||
<span className="label">Видеокарта</span>
|
||||
<span className="value">{selectedRoom.gpu}</span>
|
||||
</RoomDetail>
|
||||
</RoomInfo_block>
|
||||
<RoomInfo_block>
|
||||
<h5>Периферия</h5>
|
||||
<RoomDetail>
|
||||
<span className="label">Мышка</span>
|
||||
<span className="value">{selectedRoom.mouse}</span>
|
||||
</RoomDetail>
|
||||
<RoomDetail>
|
||||
<span className="label">Клавиатура</span>
|
||||
<span className="value">{selectedRoom.keyboard}</span>
|
||||
</RoomDetail>
|
||||
<RoomDetail>
|
||||
<span className="label">Наушники</span>
|
||||
<span className="value">{selectedRoom.headphones}</span>
|
||||
</RoomDetail>
|
||||
</RoomInfo_block>
|
||||
<RoomInfo_block>
|
||||
<h5>Монитор+Кронштейн</h5>
|
||||
<RoomDetail>
|
||||
<span className="label">Монитор</span>
|
||||
<span className="value">{selectedRoom.monitor}</span>
|
||||
</RoomDetail>
|
||||
<RoomDetail>
|
||||
<span className="label">Кронштейн</span>
|
||||
<span className="value">{selectedRoom.bracket}</span>
|
||||
</RoomDetail>
|
||||
</RoomInfo_block>
|
||||
<RoomInfo_block>
|
||||
<RoomDetail>
|
||||
<h5>Кресло</h5>
|
||||
<span className="value">{selectedRoom.chair}</span>
|
||||
</RoomDetail>
|
||||
</RoomInfo_block>
|
||||
</motion.div>
|
||||
</RoomInfo>
|
||||
</RoomBorder>
|
||||
</motion.div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</AnimatePresence>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoomList;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// components/StartupLoader.tsx
|
||||
'use client'
|
||||
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function StartupLoader() {
|
||||
const [showLoader, setShowLoader] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setShowLoader(false), 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{showLoader && (
|
||||
<motion.div
|
||||
initial={{ opacity: 1 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 9999,
|
||||
backgroundColor: 'black',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<motion.h1
|
||||
initial={{ scale: 1.2 }}
|
||||
animate={{ scale: 1 }}
|
||||
exit={{ scale: 0.8 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
style={{ fontSize: '32px' }}
|
||||
>
|
||||
Загрузка...
|
||||
</motion.h1>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import styled from "styled-components";
|
||||
import {gradient, lightGreenColor, whiteColor} from "@/styles/colors";
|
||||
|
||||
interface ButtonProps {
|
||||
label: string,
|
||||
padding: string,
|
||||
fontSize: string,
|
||||
onClick?: () => void,
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
interface StyledProps {
|
||||
$padding: string,
|
||||
$fontSize: string,
|
||||
}
|
||||
|
||||
const StyleButton = styled.button<StyledProps>`
|
||||
background: ${gradient};
|
||||
padding: ${props => props.$padding};
|
||||
font-size: ${props => props.$fontSize};
|
||||
color: ${whiteColor};
|
||||
text-transform: uppercase;
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: ${lightGreenColor};
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
opacity: 0;
|
||||
transition: all 0.4s ease;
|
||||
border-radius: 10%;
|
||||
filter: blur(20px);
|
||||
z-index: -1;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
&:hover::before {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: .6;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledButton = ({label, padding, fontSize, onClick, disabled}: ButtonProps) => {
|
||||
return (
|
||||
<StyleButton disabled={disabled} $padding={padding} $fontSize={fontSize} onClick={onClick}>
|
||||
{label}
|
||||
</StyleButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default StyledButton;
|
||||
@@ -0,0 +1,129 @@
|
||||
// components/StyledMotion.ts
|
||||
'use client'
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { motion } from 'framer-motion';
|
||||
import {darkGreenColor, lightGreenColor} from "@/styles/colors";
|
||||
|
||||
export const MotionStartBlock = styled(motion.section)`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
max-width: 1920px;
|
||||
padding: 0 60px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 70vh;
|
||||
&::after{
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -200px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100vw;
|
||||
height: 1144px;
|
||||
background-image: url("/images/startBg.png");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
z-index: -10;
|
||||
}
|
||||
img{
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
img:nth-child(1){
|
||||
left: -80px;
|
||||
}
|
||||
img:nth-child(4){
|
||||
right: 40px;
|
||||
|
||||
}
|
||||
h1{
|
||||
font-size: 64px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
max-width: 648px;
|
||||
margin-bottom: 40px;
|
||||
span{
|
||||
background: linear-gradient(to right, ${lightGreenColor}, ${darkGreenColor});
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1600px) {
|
||||
img:nth-child(4){
|
||||
right: 0;
|
||||
width: 422px;
|
||||
|
||||
}
|
||||
img:nth-child(1){
|
||||
width: 452px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1400px) {
|
||||
img:nth-child(4){
|
||||
width: 360px;
|
||||
|
||||
}
|
||||
img:nth-child(1){
|
||||
width: 380px;
|
||||
}
|
||||
h1{
|
||||
font-size: 50px;
|
||||
max-width: 508px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
button{
|
||||
font-size: 32px;
|
||||
padding: 16px 38px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1180px) {
|
||||
img:nth-child(4){
|
||||
width: 300px;
|
||||
}
|
||||
img:nth-child(1){
|
||||
width: 340px;
|
||||
}
|
||||
h1{
|
||||
font-size: 50px;
|
||||
max-width: 508px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
button{
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1040px) {
|
||||
img:nth-child(4),
|
||||
img:nth-child(1){
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
padding: 0 20px;
|
||||
height: 80vh;
|
||||
h1{
|
||||
font-size: 40px;
|
||||
max-width: 400px;
|
||||
}
|
||||
button{
|
||||
font-size: 26px;
|
||||
padding: 12px 32px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 418px) {
|
||||
h1{
|
||||
font-size: 30px;
|
||||
max-width: 300px;
|
||||
}
|
||||
button{
|
||||
font-size: 26px;
|
||||
padding: 12px 32px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,9 @@
|
||||
import mysql from 'mysql2/promise'
|
||||
|
||||
export const bd = mysql.createPool({
|
||||
host: process.env.MYSQL_HOST,
|
||||
user: process.env.MYSQL_USER,
|
||||
password: process.env.MYSQL_PASSWORD,
|
||||
database: process.env.MYSQL_DATABASE,
|
||||
charset: 'utf8mb4',
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
interface AdminPayload {
|
||||
role: string
|
||||
}
|
||||
|
||||
export function verifyAdminToken(token: string | undefined): boolean {
|
||||
if (!token) return false
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as AdminPayload
|
||||
return decoded.role === 'admin'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { jwtVerify } from "jose";
|
||||
|
||||
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET);
|
||||
|
||||
export async function middleware(req: NextRequest) {
|
||||
const token = req.cookies.get("token")?.value;
|
||||
const url = req.nextUrl.clone();
|
||||
|
||||
if (req.nextUrl.pathname.startsWith("/admin") && req.nextUrl.pathname !== "/admin/login") {
|
||||
if (!token) {
|
||||
url.pathname = "/admin/login";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, JWT_SECRET);
|
||||
if (payload.role !== "admin") throw new Error();
|
||||
} catch {
|
||||
url.pathname = "/admin/login";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/admin", "/admin/:path*"],
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
type ScreenType =
|
||||
| 'start'
|
||||
| 'pc2'
|
||||
| 'pc3'
|
||||
| 'pc3-2'
|
||||
| 'pc4'
|
||||
| 'trans'
|
||||
| 'televis'
|
||||
| 'zayvka'
|
||||
| 'zayvka-game'
|
||||
| 'playstation'
|
||||
| 'ready'
|
||||
|
||||
interface FormData {
|
||||
type?: string
|
||||
room?: string
|
||||
date?: string
|
||||
time?: string
|
||||
name?: string
|
||||
game?: string
|
||||
contact?: string
|
||||
contactMethod?: 'telegram' | 'whatsapp' | 'phone'
|
||||
}
|
||||
|
||||
interface BronStore {
|
||||
isOpen: boolean
|
||||
screen: ScreenType
|
||||
formData: FormData
|
||||
open: (screen: ScreenType) => void
|
||||
close: () => void
|
||||
goTo: (screen: ScreenType) => void
|
||||
setFormData: (data: Partial<FormData>) => void
|
||||
}
|
||||
|
||||
export const useBronStore = create<BronStore>((set) => ({
|
||||
isOpen: false,
|
||||
screen: 'start',
|
||||
formData: {},
|
||||
open: (screen) =>
|
||||
set((state) => ({
|
||||
...state,
|
||||
isOpen: true,
|
||||
screen,
|
||||
})),
|
||||
close: () => set({ isOpen: false, screen: 'start', formData: {}, }),
|
||||
goTo: (screen) => set({ screen }),
|
||||
setFormData: (data) =>
|
||||
set((state) => ({ formData: { ...state.formData, ...data } })),
|
||||
}))
|
||||
@@ -0,0 +1,664 @@
|
||||
import styled from 'styled-components';
|
||||
import { lightGreenColor, darkGreenColor, blackColor, modalColor, whiteColor } from './colors';
|
||||
|
||||
export const AdminContainer = styled.div`
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 100%);
|
||||
color: ${whiteColor};
|
||||
padding: 0;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AdminContent = styled.div`
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AdminHeaderStyled = styled.nav`
|
||||
background: rgba(15, 15, 23, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(109, 103, 148, 0.2);
|
||||
padding: 1rem 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
`;
|
||||
|
||||
export const AdminHeaderContent = styled.div`
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0 1rem;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AdminLogo = styled.a`
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(90deg, ${lightGreenColor}, ${darkGreenColor});
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-decoration: none;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
`;
|
||||
|
||||
export const AdminNav = styled.ul`
|
||||
display: flex;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AdminNavLink = styled.a<{ $active?: boolean }>`
|
||||
color: ${({ $active }) => ($active ? lightGreenColor : modalColor)};
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
color: ${lightGreenColor};
|
||||
background: rgba(76, 230, 94, 0.1);
|
||||
}
|
||||
|
||||
${({ $active }) => $active && `
|
||||
background: rgba(76, 230, 94, 0.15);
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60%;
|
||||
height: 2px;
|
||||
background: ${lightGreenColor};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const PageTitle = styled.h1`
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 2rem;
|
||||
background: linear-gradient(90deg, ${lightGreenColor}, ${darkGreenColor});
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FiltersContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FilterGroup = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
min-width: 200px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FilterLabel = styled.label`
|
||||
font-size: 0.875rem;
|
||||
color: ${modalColor};
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
`;
|
||||
|
||||
export const FilterInput = styled.input`
|
||||
background: rgba(15, 15, 23, 0.8);
|
||||
border: 1px solid rgba(109, 103, 148, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1rem;
|
||||
color: ${whiteColor};
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: ${lightGreenColor};
|
||||
background: rgba(15, 15, 23, 0.95);
|
||||
box-shadow: 0 0 0 3px rgba(76, 230, 94, 0.1);
|
||||
}
|
||||
|
||||
&::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FilterSelect = styled.select`
|
||||
background: rgba(15, 15, 23, 0.8);
|
||||
border: 1px solid rgba(109, 103, 148, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1rem;
|
||||
color: ${whiteColor};
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: ${lightGreenColor};
|
||||
background: rgba(15, 15, 23, 0.95);
|
||||
box-shadow: 0 0 0 3px rgba(76, 230, 94, 0.1);
|
||||
}
|
||||
|
||||
option {
|
||||
background: ${blackColor};
|
||||
color: ${whiteColor};
|
||||
}
|
||||
`;
|
||||
|
||||
export const RoomCard = styled.div`
|
||||
background: rgba(15, 15, 23, 0.6);
|
||||
border: 1px solid rgba(109, 103, 148, 0.2);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(76, 230, 94, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
`;
|
||||
|
||||
export const RoomCardHeader = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid rgba(109, 103, 148, 0.2);
|
||||
`;
|
||||
|
||||
export const RoomCardTitle = styled.h2`
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: ${whiteColor};
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
export const RoomCapacity = styled.div`
|
||||
font-size: 0.875rem;
|
||||
color: ${modalColor};
|
||||
background: rgba(109, 103, 148, 0.2);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
export const TimeGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 0.75rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TimeSlot = styled.div<{ $available: boolean; $booked: number; $capacity: number }>`
|
||||
background: ${({ $available, $booked, $capacity }) => {
|
||||
if ($booked === 0) return 'rgba(76, 230, 94, 0.1)';
|
||||
if ($booked >= $capacity) return 'rgba(220, 53, 69, 0.2)';
|
||||
return 'rgba(255, 193, 7, 0.15)';
|
||||
}};
|
||||
border: 1px solid ${({ $available, $booked, $capacity }) => {
|
||||
if ($booked === 0) return 'rgba(76, 230, 94, 0.3)';
|
||||
if ($booked >= $capacity) return 'rgba(220, 53, 69, 0.4)';
|
||||
return 'rgba(255, 193, 7, 0.4)';
|
||||
}};
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
`;
|
||||
|
||||
export const TimeSlotHeader = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
`;
|
||||
|
||||
export const TimeSlotTime = styled.div`
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: ${whiteColor};
|
||||
`;
|
||||
|
||||
export const TimeSlotCount = styled.div<{ $full: boolean }>`
|
||||
font-size: 0.75rem;
|
||||
color: ${({ $full }) => ($full ? '#dc3545' : '#4ce65e')};
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
export const TimeSlotBookings = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
min-height: 40px;
|
||||
`;
|
||||
|
||||
export const BookingBadge = styled.span<{ $status: string }>`
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
background: ${({ $status }) => {
|
||||
if ($status === 'confirmed') return 'rgba(40, 167, 69, 0.3)';
|
||||
if ($status === 'cancelled') return 'rgba(220, 53, 69, 0.3)';
|
||||
return 'rgba(255, 193, 7, 0.3)';
|
||||
}};
|
||||
color: ${({ $status }) => {
|
||||
if ($status === 'confirmed') return '#28a745';
|
||||
if ($status === 'cancelled') return '#dc3545';
|
||||
return '#ffc107';
|
||||
}};
|
||||
border: 1px solid ${({ $status }) => {
|
||||
if ($status === 'confirmed') return 'rgba(40, 167, 69, 0.5)';
|
||||
if ($status === 'cancelled') return 'rgba(220, 53, 69, 0.5)';
|
||||
return 'rgba(255, 193, 7, 0.5)';
|
||||
}};
|
||||
`;
|
||||
|
||||
export const BookingsList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
`;
|
||||
|
||||
export const BookingCard = styled.div<{ $status: string }>`
|
||||
background: rgba(15, 15, 23, 0.6);
|
||||
border: 1px solid ${({ $status }) => {
|
||||
if ($status === 'confirmed') return 'rgba(40, 167, 69, 0.3)';
|
||||
if ($status === 'cancelled') return 'rgba(220, 53, 69, 0.3)';
|
||||
return 'rgba(255, 193, 7, 0.3)';
|
||||
}};
|
||||
border-left: 4px solid ${({ $status }) => {
|
||||
if ($status === 'confirmed') return '#28a745';
|
||||
if ($status === 'cancelled') return '#dc3545';
|
||||
return '#ffc107';
|
||||
}};
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
`;
|
||||
|
||||
export const BookingCardHeader = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
`;
|
||||
|
||||
export const BookingCardInfo = styled.div`
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
`;
|
||||
|
||||
export const BookingCardTitle = styled.h3`
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: ${whiteColor};
|
||||
margin: 0 0 0.5rem 0;
|
||||
`;
|
||||
|
||||
export const BookingCardMeta = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: ${modalColor};
|
||||
margin-top: 0.5rem;
|
||||
`;
|
||||
|
||||
export const BookingCardActions = styled.div`
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
export const StatusSelect = styled.select<{ $status: string }>`
|
||||
background: rgba(15, 15, 23, 0.8);
|
||||
border: 1px solid ${({ $status }) => {
|
||||
if ($status === 'confirmed') return 'rgba(40, 167, 69, 0.5)';
|
||||
if ($status === 'cancelled') return 'rgba(220, 53, 69, 0.5)';
|
||||
return 'rgba(255, 193, 7, 0.5)';
|
||||
}};
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 1rem;
|
||||
color: ${whiteColor};
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: ${lightGreenColor};
|
||||
box-shadow: 0 0 0 3px rgba(76, 230, 94, 0.1);
|
||||
}
|
||||
|
||||
option {
|
||||
background: ${blackColor};
|
||||
color: ${whiteColor};
|
||||
}
|
||||
`;
|
||||
|
||||
export const DeleteButton = styled.button`
|
||||
background: rgba(220, 53, 69, 0.2);
|
||||
border: 1px solid rgba(220, 53, 69, 0.4);
|
||||
color: #dc3545;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(220, 53, 69, 0.3);
|
||||
border-color: #dc3545;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
`;
|
||||
|
||||
export const EmptyState = styled.div`
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
color: ${modalColor};
|
||||
`;
|
||||
|
||||
export const EmptyStateIcon = styled.div`
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
`;
|
||||
|
||||
export const EmptyStateText = styled.p`
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
export const LoadingSpinner = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 4rem 2rem;
|
||||
`;
|
||||
|
||||
export const Spinner = styled.div`
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid rgba(109, 103, 148, 0.2);
|
||||
border-top-color: ${lightGreenColor};
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`;
|
||||
|
||||
export const SectionTitle = styled.h2`
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: ${whiteColor};
|
||||
margin: 2rem 0 1rem 0;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid rgba(109, 103, 148, 0.2);
|
||||
`;
|
||||
|
||||
export const AddButton = styled.button`
|
||||
background: linear-gradient(90deg, ${lightGreenColor}, ${darkGreenColor});
|
||||
border: none;
|
||||
color: ${blackColor};
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(76, 230, 94, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
|
||||
export const ModalOverlay = styled.div`
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
`;
|
||||
|
||||
export const ModalContent = styled.form`
|
||||
background: rgba(15, 15, 23, 0.95);
|
||||
border: 1px solid rgba(109, 103, 148, 0.3);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(109, 103, 148, 0.5);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ModalHeader = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid rgba(109, 103, 148, 0.2);
|
||||
`;
|
||||
|
||||
export const ModalTitle = styled.h3`
|
||||
font-size: 1.5rem;
|
||||
color: ${whiteColor};
|
||||
margin: 0;
|
||||
background: linear-gradient(90deg, ${lightGreenColor}, ${darkGreenColor});
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
`;
|
||||
|
||||
export const ModalCloseButton = styled.button`
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${modalColor};
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(109, 103, 148, 0.2);
|
||||
color: ${whiteColor};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ModalBody = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
`;
|
||||
|
||||
export const FormRow = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FormLabel = styled.label`
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: ${modalColor};
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
export const FormInput = styled(FilterInput)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const FormSelect = styled(FilterSelect)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const ModalFooter = styled.div`
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid rgba(109, 103, 148, 0.2);
|
||||
`;
|
||||
|
||||
export const CancelButton = styled.button`
|
||||
background: rgba(109, 103, 148, 0.2);
|
||||
border: 1px solid rgba(109, 103, 148, 0.4);
|
||||
color: ${modalColor};
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(109, 103, 148, 0.3);
|
||||
border-color: ${modalColor};
|
||||
}
|
||||
`;
|
||||
|
||||
export const SubmitButton = styled.button`
|
||||
background: linear-gradient(90deg, ${lightGreenColor}, ${darkGreenColor});
|
||||
border: none;
|
||||
color: ${blackColor};
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(76, 230, 94, 0.3);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ErrorMessage = styled.div`
|
||||
background: rgba(220, 53, 69, 0.2);
|
||||
border: 1px solid rgba(220, 53, 69, 0.4);
|
||||
color: #dc3545;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.5rem;
|
||||
`;
|
||||
@@ -0,0 +1,9 @@
|
||||
export const lightGreenColor = '#4CE65E';
|
||||
export const darkGreenColor = '#2D5920';
|
||||
export const whiteColor = '#fff';
|
||||
export const inactiveTextColor = '#afafaf';
|
||||
export const inactiveBorderColor = '#3C3C3C';
|
||||
export const blackColor = '#0E0E17';
|
||||
export const modalColor = '#6D6794';
|
||||
|
||||
export const gradient = `linear-gradient(90deg, ${lightGreenColor}, ${darkGreenColor})`
|
||||
@@ -0,0 +1,474 @@
|
||||
import {blackColor, darkGreenColor, gradient, lightGreenColor, modalColor, whiteColor} from "@/styles/colors";
|
||||
import styled from "styled-components";
|
||||
import {IMaskInput} from "react-imask";
|
||||
|
||||
export const Overlay = styled.div`
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
export const ModalWrapper = styled.div<{ $animateIn: boolean }>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
background: ${blackColor};
|
||||
color: white;
|
||||
padding: 32px;
|
||||
border-radius: 20px;
|
||||
width: 70vw;
|
||||
height: 94vh;
|
||||
position: relative;
|
||||
opacity: ${({ $animateIn }) => ($animateIn ? 1 : 0)};
|
||||
transform: ${({ $animateIn }) =>
|
||||
$animateIn ? 'translateY(0)' : 'translateY(-20px)'};
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
@media (max-height: 950px) {
|
||||
height: 100vh;
|
||||
width: 80vw;
|
||||
.marginTitle{
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
height: 100vh;
|
||||
width:100vw;
|
||||
border-radius: 0;
|
||||
.marginTitle{
|
||||
margin-top: 16px !important;
|
||||
}
|
||||
}
|
||||
@media(max-width: 450px) {
|
||||
.marginTitle{
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.contacts{
|
||||
gap: 4px;
|
||||
margin: 10px 0 20px;
|
||||
img{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
.readyText{
|
||||
font-size: 20px !important;
|
||||
margin-bottom: 30px !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CloseButton = styled.button`
|
||||
position: absolute;
|
||||
top: 3.5%;
|
||||
right: 3.5%;
|
||||
font-size: 42px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${modalColor};
|
||||
cursor: pointer;
|
||||
@media (max-width: 800px) {
|
||||
top: 0;
|
||||
right: 2%;
|
||||
}
|
||||
@media (max-height: 685px) {
|
||||
top: 24px;
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
top: 18px;
|
||||
right: 4%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const SectionTitle = styled.h2`
|
||||
font-size: 26px;
|
||||
margin-bottom: 20px;
|
||||
@media (max-width: 900px) {
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
width: 80%;
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
font-size: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Input = styled.input`
|
||||
border-radius: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 2px solid ${modalColor};
|
||||
color: ${modalColor};
|
||||
padding: 0 25px;
|
||||
max-width: 400px;
|
||||
width: 40%;
|
||||
height: 60px;
|
||||
font-size: 24px;
|
||||
text-transform: uppercase;
|
||||
line-height: 60px;
|
||||
@media (max-width: 580px) {
|
||||
width: 90%;
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
height: 50px;
|
||||
font-size: 18px;
|
||||
}
|
||||
`;
|
||||
export const MaskedInput = styled(IMaskInput)`
|
||||
border-radius: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 2px solid ${modalColor};
|
||||
color: ${modalColor};
|
||||
padding: 0 25px;
|
||||
max-width: 400px;
|
||||
width: 40%;
|
||||
height: 60px;
|
||||
font-size: 24px;
|
||||
text-transform: uppercase;
|
||||
background: transparent;
|
||||
@media (max-width: 580px) {
|
||||
width: 90%;
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
font-size: 18px;
|
||||
height: 50px;
|
||||
}
|
||||
`;
|
||||
export const InputLabel = styled.p`
|
||||
text-transform: none;
|
||||
color: ${whiteColor};
|
||||
max-width: 590px;
|
||||
font-size: 20px;
|
||||
margin:10px 0 14px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
@media (max-width: 560px) {
|
||||
max-width: 90%;
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
font-size: 18px;
|
||||
margin:14px 0 8px;
|
||||
}
|
||||
`
|
||||
|
||||
export const ChoiceBtn = styled.button<{$selected?: boolean}>`
|
||||
width: 402px;
|
||||
max-height: 120px;
|
||||
padding: 30px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
text-transform: uppercase;
|
||||
color:${modalColor};
|
||||
position: relative;
|
||||
border-radius: 15px;
|
||||
background: ${blackColor};
|
||||
h3{
|
||||
font-size: 40px;
|
||||
background: linear-gradient(to right, ${lightGreenColor}, ${darkGreenColor});
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
max-width: 300px;
|
||||
}
|
||||
span{
|
||||
font-size: 24px;
|
||||
background: ${({$selected})=> ($selected ? gradient : modalColor)};
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
&::after{
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -3%;
|
||||
left: -0.5%;
|
||||
width: 101%;
|
||||
height: 106%;
|
||||
background: ${({$selected})=>($selected ? gradient : modalColor)};
|
||||
z-index: -1;
|
||||
border-radius: 15px;
|
||||
}
|
||||
@media (max-width: 1220px) {
|
||||
width: 300px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
width: 96%;
|
||||
padding: 16px 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
width: 96%;
|
||||
padding: 10px 8px;
|
||||
margin-top: 4px;
|
||||
h3{
|
||||
font-size: 24px;
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
span{
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
`
|
||||
export const TwoColumns = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 400px);
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
gap: 30px 28px;
|
||||
margin: 20px 0 50px;
|
||||
@media (max-width: 1220px) {
|
||||
grid-template-columns: repeat(2, 300px);
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 90%;
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #999;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
margin: 20px 0 20px;
|
||||
gap: 16px 28px;
|
||||
height: 300px;
|
||||
.big_btn{
|
||||
span{
|
||||
padding: 12px 14px;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
export const TitleModal = styled.h2`
|
||||
font-size: 48px;
|
||||
text-align: center;
|
||||
color: ${whiteColor};
|
||||
line-height: 1;
|
||||
margin-bottom: 24px;
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
span{
|
||||
color: ${lightGreenColor};
|
||||
}
|
||||
@media (max-height: 950px) {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
font-size: 38px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
font-size: 24px;
|
||||
margin-top: 42px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
`
|
||||
export const StepsModal = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 80px;
|
||||
margin-bottom: 60px;
|
||||
position: absolute;
|
||||
top: 130px;
|
||||
p{
|
||||
color: ${modalColor};
|
||||
font-size: 32px;
|
||||
position: relative;
|
||||
width: 40.5px;
|
||||
text-align: center;
|
||||
&::after{
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -30%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: ${modalColor};
|
||||
height:6px;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
span{
|
||||
position: relative;
|
||||
&::after{
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -56%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: ${lightGreenColor};
|
||||
height:6px;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
@media (max-height: 950px) {
|
||||
position: relative;
|
||||
margin-bottom: 40px;
|
||||
top: 0;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
gap: 50px;
|
||||
p{
|
||||
font-size: 28px;
|
||||
width: 30px;
|
||||
&::after{
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
img{
|
||||
width: 30px;
|
||||
}
|
||||
span{
|
||||
&::after{
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
export const BackBtn = styled.button`
|
||||
position: absolute;
|
||||
top: 4%;
|
||||
left: 4%;
|
||||
font-size: 24px;
|
||||
color: ${modalColor};
|
||||
text-transform: uppercase;
|
||||
@media (max-width: 800px) {
|
||||
top: 2%;
|
||||
left: 2%;
|
||||
}
|
||||
@media (max-height: 685px) {
|
||||
top: 44px;
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
top: 32px;
|
||||
left: 4%;
|
||||
}
|
||||
`
|
||||
export const BronBtn = styled.button`
|
||||
position: absolute;
|
||||
bottom: 4%;
|
||||
left: 4%;
|
||||
font-size: 24px;
|
||||
color: ${whiteColor};
|
||||
padding: 16px 50px;
|
||||
background: linear-gradient(to right, #6C6693, #272535);
|
||||
text-transform: uppercase;
|
||||
border-radius: 15px;
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 90%;
|
||||
height: 50%;
|
||||
background: ${whiteColor};
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
opacity: 0;
|
||||
transition: all 0.4s ease;
|
||||
border-radius: 50%;
|
||||
filter: blur(20px);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export const TimeSlotGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #999;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 450px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 6px;
|
||||
max-height: 300px;
|
||||
}
|
||||
`
|
||||
|
||||
export const TimeSlotBtn = styled.button<{$selected?: boolean; $available?: boolean}>`
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid ${({$available}) => ($available ? modalColor : inactiveBorderColor)};
|
||||
background: ${({$selected, $available}) =>
|
||||
$selected ? gradient :
|
||||
$available ? 'transparent' :
|
||||
'rgba(60, 60, 60, 0.3)'};
|
||||
color: ${({$selected, $available}) =>
|
||||
$selected ? whiteColor :
|
||||
$available ? modalColor :
|
||||
inactiveTextColor};
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
cursor: ${({$available}) => ($available ? 'pointer' : 'not-allowed')};
|
||||
transition: all 0.2s ease;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:hover {
|
||||
${({$available}) => $available && `
|
||||
transform: scale(1.05);
|
||||
border-color: ${lightGreenColor};
|
||||
`}
|
||||
}
|
||||
|
||||
@media (max-width: 450px) {
|
||||
padding: 10px 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,90 @@
|
||||
/* Reset and base styles */
|
||||
* {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
|
||||
a, a:link, a:visited {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Common */
|
||||
|
||||
aside, nav, footer, header, section, main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, p {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
ul, ul li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
img, svg {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
address {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
|
||||
input, textarea, button, select {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
textarea:focus, input:focus {outline: none;}
|
||||
|
||||
button, input[type="submit"] {
|
||||
display: inline-block;
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input:focus, input:active,
|
||||
button:focus, button:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
legend {
|
||||
display: block;
|
||||
}
|
||||
Reference in New Issue
Block a user