@mashka818/exam-de-template 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/exam-guides/COMMENTS_GUIDE.md +53 -0
- package/exam-guides/EXAM_COMMANDS.txt +47 -0
- package/exam-guides/GUIDE_PAGES.md +529 -0
- package/exam-guides/NPM_PACKAGE.md +206 -0
- package/exam-guides/README.md +40 -0
- package/exam-guides/RESPONSIVE.md +224 -0
- package/exam-guides/TECH_STACK.txt +142 -0
- package/exam-guides/THEME_BANQUETAM_NET.md +106 -0
- package/exam-guides/commented-code/README.txt +5 -0
- package/exam-guides/commented-code/client/index.html +14 -0
- package/exam-guides/commented-code/client/package-lock.json +2298 -0
- package/exam-guides/commented-code/client/package.json +21 -0
- package/exam-guides/commented-code/client/public/images/README.txt +26 -0
- package/exam-guides/commented-code/client/public/images/about-cleaning.svg +4 -0
- package/exam-guides/commented-code/client/public/images/admin-banner.svg +1 -0
- package/exam-guides/commented-code/client/public/images/empty-requests.svg +1 -0
- package/exam-guides/commented-code/client/public/images/footer-photo-1.svg +4 -0
- package/exam-guides/commented-code/client/public/images/footer-photo-2.svg +4 -0
- package/exam-guides/commented-code/client/public/images/footer-photo-3.svg +4 -0
- package/exam-guides/commented-code/client/public/images/home-hero.svg +4 -0
- package/exam-guides/commented-code/client/public/images/login-banner.svg +1 -0
- package/exam-guides/commented-code/client/public/images/logo.svg +4 -0
- package/exam-guides/commented-code/client/public/images/new-request-banner.svg +1 -0
- package/exam-guides/commented-code/client/public/images/register-banner.svg +1 -0
- package/exam-guides/commented-code/client/public/images/requests-banner.svg +1 -0
- package/exam-guides/commented-code/client/public/images/slide-1.svg +6 -0
- package/exam-guides/commented-code/client/public/images/slide-2.svg +5 -0
- package/exam-guides/commented-code/client/public/images/slide-3.svg +5 -0
- package/exam-guides/commented-code/client/src/App.jsx +72 -0
- package/exam-guides/commented-code/client/src/api.js +71 -0
- package/exam-guides/commented-code/client/src/components/FormField.jsx +25 -0
- package/exam-guides/commented-code/client/src/components/Layout.jsx +83 -0
- package/exam-guides/commented-code/client/src/components/PageImage.jsx +38 -0
- package/exam-guides/commented-code/client/src/components/ProtectedRoute.jsx +35 -0
- package/exam-guides/commented-code/client/src/components/UserNav.jsx +33 -0
- package/exam-guides/commented-code/client/src/components/landing/HeroSlider.jsx +103 -0
- package/exam-guides/commented-code/client/src/components/landing/LandingLayout.jsx +76 -0
- package/exam-guides/commented-code/client/src/components/landing/SiteFooter.jsx +74 -0
- package/exam-guides/commented-code/client/src/config/images.js +104 -0
- package/exam-guides/commented-code/client/src/constants/services.js +19 -0
- package/exam-guides/commented-code/client/src/context/AuthContext.jsx +72 -0
- package/exam-guides/commented-code/client/src/index.css +73 -0
- package/exam-guides/commented-code/client/src/main.jsx +28 -0
- package/exam-guides/commented-code/client/src/pages/AdminPage.jsx +151 -0
- package/exam-guides/commented-code/client/src/pages/LandingPage.jsx +131 -0
- package/exam-guides/commented-code/client/src/pages/LoginPage.jsx +81 -0
- package/exam-guides/commented-code/client/src/pages/RegisterPage.jsx +117 -0
- package/exam-guides/commented-code/client/src/pages/RequestFormPage.jsx +196 -0
- package/exam-guides/commented-code/client/src/pages/RequestsPage.jsx +112 -0
- package/exam-guides/commented-code/client/src/utils/validation.js +71 -0
- package/exam-guides/commented-code/client/vite.config.js +31 -0
- package/exam-guides/commented-code/server/db/init.js +67 -0
- package/exam-guides/commented-code/server/db/pool.js +23 -0
- package/exam-guides/commented-code/server/db/schema.sql +53 -0
- package/exam-guides/commented-code/server/db/seed.sql +15 -0
- package/exam-guides/commented-code/server/index.js +45 -0
- package/exam-guides/commented-code/server/middleware/auth.js +38 -0
- package/exam-guides/commented-code/server/package-lock.json +1084 -0
- package/exam-guides/commented-code/server/package.json +17 -0
- package/exam-guides/commented-code/server/routes/admin.js +96 -0
- package/exam-guides/commented-code/server/routes/auth.js +128 -0
- package/exam-guides/commented-code/server/routes/requests.js +115 -0
- package/exam-guides/commented-code/server/routes/services.js +31 -0
- package/exam-guides/commented-code/server/utils/validation.js +81 -0
- package/exam-guides/exam-starter/README.txt +22 -0
- package/exam-guides/exam-starter/package.json +13 -0
- package/exam-guides//320/243/320/224/320/220/320/233/320/230/320/242/320/254-/320/237/320/225/320/240/320/225/320/224-/320/241/320/224/320/220/320/247/320/225/320/231.txt +9 -0
- package/exam-project/README.md +16 -0
- package/exam-project/client/index.html +14 -0
- package/exam-project/client/package-lock.json +2298 -0
- package/exam-project/client/package.json +21 -0
- package/exam-project/client/public/images/README.txt +26 -0
- package/exam-project/client/public/images/about-cleaning.svg +4 -0
- package/exam-project/client/public/images/admin-banner.svg +1 -0
- package/exam-project/client/public/images/empty-requests.svg +1 -0
- package/exam-project/client/public/images/footer-photo-1.svg +4 -0
- package/exam-project/client/public/images/footer-photo-2.svg +4 -0
- package/exam-project/client/public/images/footer-photo-3.svg +4 -0
- package/exam-project/client/public/images/home-hero.svg +4 -0
- package/exam-project/client/public/images/login-banner.svg +1 -0
- package/exam-project/client/public/images/logo.svg +4 -0
- package/exam-project/client/public/images/new-request-banner.svg +1 -0
- package/exam-project/client/public/images/register-banner.svg +1 -0
- package/exam-project/client/public/images/requests-banner.svg +1 -0
- package/exam-project/client/public/images/slide-1.svg +6 -0
- package/exam-project/client/public/images/slide-2.svg +5 -0
- package/exam-project/client/public/images/slide-3.svg +5 -0
- package/exam-project/client/src/App.jsx +52 -0
- package/exam-project/client/src/api.js +50 -0
- package/exam-project/client/src/components/FormField.jsx +11 -0
- package/exam-project/client/src/components/Layout.jsx +61 -0
- package/exam-project/client/src/components/PageImage.jsx +22 -0
- package/exam-project/client/src/components/ProtectedRoute.jsx +20 -0
- package/exam-project/client/src/components/UserNav.jsx +20 -0
- package/exam-project/client/src/components/landing/HeroSlider.jsx +89 -0
- package/exam-project/client/src/components/landing/LandingLayout.jsx +61 -0
- package/exam-project/client/src/components/landing/SiteFooter.jsx +61 -0
- package/exam-project/client/src/config/images.js +83 -0
- package/exam-project/client/src/constants/services.js +7 -0
- package/exam-project/client/src/context/AuthContext.jsx +55 -0
- package/exam-project/client/src/index.css +54 -0
- package/exam-project/client/src/main.jsx +17 -0
- package/exam-project/client/src/pages/AdminPage.jsx +128 -0
- package/exam-project/client/src/pages/LandingPage.jsx +115 -0
- package/exam-project/client/src/pages/LoginPage.jsx +63 -0
- package/exam-project/client/src/pages/RegisterPage.jsx +97 -0
- package/exam-project/client/src/pages/RequestFormPage.jsx +178 -0
- package/exam-project/client/src/pages/RequestsPage.jsx +94 -0
- package/exam-project/client/src/utils/validation.js +54 -0
- package/exam-project/client/vite.config.js +17 -0
- package/exam-project/package.json +18 -0
- package/exam-project/scripts/init-project.js +86 -0
- package/exam-project/scripts/prepack.js +27 -0
- package/exam-project/scripts/unpack-template.js +105 -0
- package/exam-project/server/db/init.js +50 -0
- package/exam-project/server/db/pool.js +11 -0
- package/exam-project/server/db/schema.sql +41 -0
- package/exam-project/server/db/seed.sql +12 -0
- package/exam-project/server/index.js +29 -0
- package/exam-project/server/middleware/auth.js +24 -0
- package/exam-project/server/package-lock.json +1084 -0
- package/exam-project/server/package.json +17 -0
- package/exam-project/server/routes/admin.js +76 -0
- package/exam-project/server/routes/auth.js +109 -0
- package/exam-project/server/routes/requests.js +99 -0
- package/exam-project/server/routes/services.js +18 -0
- package/exam-project/server/utils/validation.js +63 -0
- package/package.json +25 -0
- package/scripts/init-project.js +86 -0
- package/scripts/prepack.js +27 -0
- package/scripts/unpack-template.js +105 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* ЛЕНДИНГ (/) — главная для гостей
|
|
4
|
+
* =============================================================================
|
|
5
|
+
* Слайдер, о клининге, услуги, футер с фото-заглушками.
|
|
6
|
+
* Картинки: config/images.js → public/images/
|
|
7
|
+
* БАНКЕТАМ.НЕТ: тексты про банкеты; perks и «Наши услуги» = 4 помещения;
|
|
8
|
+
* LandingLayout brandName → Банкетам.Нет; слайдер 4×3 сек (HeroSlider).
|
|
9
|
+
*
|
|
10
|
+
* СТРУКТУРА: LandingLayout → HeroSlider → секции → SiteFooter
|
|
11
|
+
* perks[] — карточки преимуществ (массив строк, менять под тему)
|
|
12
|
+
* DEFAULT_SERVICE_NAMES — подписи на лендинге (дубль seed.sql; синхронизировать)
|
|
13
|
+
* cabinetPath — куда ведёт CTA для вошедшего пользователя
|
|
14
|
+
*
|
|
15
|
+
* GUIDE_PAGES.md §5 | config/images.js (aboutCleaning, homeHero…)
|
|
16
|
+
* =============================================================================
|
|
17
|
+
*/
|
|
18
|
+
import { Link } from 'react-router-dom';
|
|
19
|
+
import { useAuth } from '../context/AuthContext.jsx';
|
|
20
|
+
import LandingLayout from '../components/landing/LandingLayout.jsx';
|
|
21
|
+
import HeroSlider from '../components/landing/HeroSlider.jsx';
|
|
22
|
+
import SiteFooter from '../components/landing/SiteFooter.jsx';
|
|
23
|
+
import PageImage from '../components/PageImage.jsx';
|
|
24
|
+
import { DEFAULT_SERVICE_NAMES } from '../constants/services.js';
|
|
25
|
+
|
|
26
|
+
const perks = [
|
|
27
|
+
'Опытные клинеры и проверенные средства',
|
|
28
|
+
'Удобная подача заявки онлайн',
|
|
29
|
+
'Наличные или карта — как вам удобнее',
|
|
30
|
+
'Жилые и производственные помещения',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export default function LandingPage() {
|
|
34
|
+
const { user } = useAuth();
|
|
35
|
+
const cabinetPath = user?.role === 'admin' ? '/admin' : '/requests';
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<LandingLayout>
|
|
39
|
+
<HeroSlider />
|
|
40
|
+
|
|
41
|
+
<section className="mx-auto max-w-lg lg:max-w-5xl px-4 lg:px-8 py-10 lg:py-14">
|
|
42
|
+
<div className="lg:grid lg:grid-cols-2 lg:gap-10 lg:items-center animate-fade-up">
|
|
43
|
+
<PageImage
|
|
44
|
+
imageKey="aboutCleaning"
|
|
45
|
+
className="w-full h-48 lg:h-64 object-cover rounded-2xl shadow mb-6 lg:mb-0"
|
|
46
|
+
/>
|
|
47
|
+
<div>
|
|
48
|
+
<h2 className="text-2xl font-bold text-teal-900 mb-3">О нашем клининге</h2>
|
|
49
|
+
<p className="text-slate-600 text-sm leading-relaxed mb-4">
|
|
50
|
+
«Мой Не Сам» — портал для заказа уборки жилых и производственных помещений.
|
|
51
|
+
Вы регистрируетесь, оформляете заявку с адресом и видом услуги, выбираете дату и
|
|
52
|
+
способ оплаты — мы берём задачу в работу.
|
|
53
|
+
</p>
|
|
54
|
+
<p className="text-slate-600 text-sm leading-relaxed">
|
|
55
|
+
Работаем аккуратно, с соблюдением сроков и прозрачными статусами заявки в личном
|
|
56
|
+
кабинете.
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</section>
|
|
61
|
+
|
|
62
|
+
<section className="bg-teal-50 border-y border-teal-100 py-10 lg:py-14">
|
|
63
|
+
<div className="mx-auto max-w-lg lg:max-w-5xl px-4 lg:px-8">
|
|
64
|
+
<h2 className="text-xl font-bold text-teal-900 mb-6 text-center">Наши услуги</h2>
|
|
65
|
+
<ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
66
|
+
{DEFAULT_SERVICE_NAMES.map((name) => (
|
|
67
|
+
<li
|
|
68
|
+
key={name}
|
|
69
|
+
className="bg-white rounded-xl p-4 shadow-sm border border-teal-100 text-center text-sm font-medium text-slate-700 hover:shadow-md transition"
|
|
70
|
+
>
|
|
71
|
+
{name}
|
|
72
|
+
</li>
|
|
73
|
+
))}
|
|
74
|
+
</ul>
|
|
75
|
+
</div>
|
|
76
|
+
</section>
|
|
77
|
+
|
|
78
|
+
<section className="mx-auto max-w-lg lg:max-w-5xl px-4 lg:px-8 py-10">
|
|
79
|
+
<h2 className="text-xl font-bold text-slate-800 mb-4">Почему выбирают нас</h2>
|
|
80
|
+
<ul className="space-y-3">
|
|
81
|
+
{perks.map((p) => (
|
|
82
|
+
<li key={p} className="flex gap-3 text-sm text-slate-600">
|
|
83
|
+
<span className="text-teal-600 font-bold">✓</span>
|
|
84
|
+
{p}
|
|
85
|
+
</li>
|
|
86
|
+
))}
|
|
87
|
+
</ul>
|
|
88
|
+
</section>
|
|
89
|
+
|
|
90
|
+
<section className="mx-auto max-w-lg lg:max-w-5xl px-4 lg:px-8 pb-12">
|
|
91
|
+
<div className="rounded-2xl bg-gradient-to-br from-teal-600 to-cyan-700 p-8 text-center text-white shadow-lg animate-fade-up">
|
|
92
|
+
<h2 className="text-xl font-bold mb-2">
|
|
93
|
+
{user ? 'Добро пожаловать!' : 'Готовы заказать уборку?'}
|
|
94
|
+
</h2>
|
|
95
|
+
<p className="text-teal-100 text-sm mb-6">
|
|
96
|
+
{user
|
|
97
|
+
? 'Перейдите в кабинет, чтобы посмотреть заявки или оформить новую.'
|
|
98
|
+
: 'Зарегистрируйтесь за минуту и оформите первую заявку.'}
|
|
99
|
+
</p>
|
|
100
|
+
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
|
101
|
+
{user ? (
|
|
102
|
+
<Link
|
|
103
|
+
to={cabinetPath}
|
|
104
|
+
className="rounded-xl bg-white text-teal-800 py-3 px-6 font-semibold hover:bg-teal-50 transition"
|
|
105
|
+
>
|
|
106
|
+
{user.role === 'admin' ? 'Панель администратора' : 'Мои заявки'}
|
|
107
|
+
</Link>
|
|
108
|
+
) : (
|
|
109
|
+
<>
|
|
110
|
+
<Link
|
|
111
|
+
to="/register"
|
|
112
|
+
className="rounded-xl bg-white text-teal-800 py-3 px-6 font-semibold hover:bg-teal-50 transition"
|
|
113
|
+
>
|
|
114
|
+
Зарегистрироваться
|
|
115
|
+
</Link>
|
|
116
|
+
<Link
|
|
117
|
+
to="/login"
|
|
118
|
+
className="rounded-xl border border-white/50 py-3 px-6 font-medium hover:bg-white/10 transition"
|
|
119
|
+
>
|
|
120
|
+
Уже есть аккаунт
|
|
121
|
+
</Link>
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</section>
|
|
127
|
+
|
|
128
|
+
<SiteFooter />
|
|
129
|
+
</LandingLayout>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* АВТОРИЗАЦИЯ (/login) — п.2 задания ДЭ
|
|
4
|
+
* =============================================================================
|
|
5
|
+
* ЗАМЕНИТЕ:
|
|
6
|
+
* сообщения об ошибке — уже есть «Неверный логин или пароль»
|
|
7
|
+
* adminka/password — заданы в server/db/init.js (не на этой странице)
|
|
8
|
+
* картинку — login-banner в public/images/
|
|
9
|
+
* Отличительная черта: тёмная шапка (variant="login")
|
|
10
|
+
* БАНКЕТАМ.НЕТ (п.2): ссылка «Еще не зарегистрированы? Регистрация» (сейчас «Создать аккаунт»).
|
|
11
|
+
* БАНКЕТАМ.НЕТ: подсказка админа Admin26 / Demo20 внизу формы.
|
|
12
|
+
*
|
|
13
|
+
* ПОТОК: login({ login, password }) → JWT в localStorage
|
|
14
|
+
* user.role === 'admin' → /admin, иначе → /requests
|
|
15
|
+
* Админ создаётся в server/db/init.js (не на этой странице)
|
|
16
|
+
*
|
|
17
|
+
* GUIDE_PAGES.md §2.2 | server/db/init.js (adminka / Admin26)
|
|
18
|
+
* =============================================================================
|
|
19
|
+
*/
|
|
20
|
+
import { useState } from 'react';
|
|
21
|
+
import { Link, useNavigate } from 'react-router-dom';
|
|
22
|
+
import Layout from '../components/Layout.jsx';
|
|
23
|
+
import FormField from '../components/FormField.jsx';
|
|
24
|
+
import PageImage from '../components/PageImage.jsx';
|
|
25
|
+
import { useAuth } from '../context/AuthContext.jsx';
|
|
26
|
+
|
|
27
|
+
const inputClass =
|
|
28
|
+
'w-full rounded-lg border border-slate-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400';
|
|
29
|
+
|
|
30
|
+
export default function LoginPage() {
|
|
31
|
+
const { login } = useAuth();
|
|
32
|
+
const navigate = useNavigate();
|
|
33
|
+
const [loginVal, setLoginVal] = useState('');
|
|
34
|
+
const [password, setPassword] = useState('');
|
|
35
|
+
const [error, setError] = useState('');
|
|
36
|
+
|
|
37
|
+
const submit = async (e) => {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
setError('');
|
|
40
|
+
if (!loginVal.trim() || !password) {
|
|
41
|
+
setError('Введите логин и пароль');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const user = await login({ login: loginVal, password });
|
|
46
|
+
navigate(user.role === 'admin' ? '/admin' : '/requests');
|
|
47
|
+
} catch (err) {
|
|
48
|
+
setError(err.data?.message || 'Неверный логин или пароль');
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Layout variant="login">
|
|
54
|
+
<PageImage imageKey="loginBanner" className="w-full h-24 object-cover rounded-xl mb-4" />
|
|
55
|
+
|
|
56
|
+
<div className="bg-slate-100 rounded-2xl p-6 shadow animate-slide-in">
|
|
57
|
+
<h2 className="text-xl font-bold text-slate-800 mb-6">Вход в систему</h2>
|
|
58
|
+
<form onSubmit={submit}>
|
|
59
|
+
<FormField label="Логин">
|
|
60
|
+
<input className={inputClass} value={loginVal} onChange={(e) => setLoginVal(e.target.value)} autoComplete="username" />
|
|
61
|
+
</FormField>
|
|
62
|
+
<FormField label="Пароль">
|
|
63
|
+
<input type="password" className={inputClass} value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="current-password" />
|
|
64
|
+
</FormField>
|
|
65
|
+
{error && <p className="field-error mb-4 bg-red-50 border border-red-200 rounded-lg px-3 py-2">{error}</p>}
|
|
66
|
+
<button type="submit" className="w-full rounded-xl bg-slate-800 text-white py-3 font-semibold hover:bg-slate-900 transition">
|
|
67
|
+
Войти
|
|
68
|
+
</button>
|
|
69
|
+
</form>
|
|
70
|
+
{/* БАНКЕТАМ.НЕТ: текст ссылки → «Еще не зарегистрированы? Регистрация» */}
|
|
71
|
+
<p className="text-center text-sm mt-4">
|
|
72
|
+
<Link to="/register" className="text-teal-700 underline">Создать аккаунт</Link>
|
|
73
|
+
</p>
|
|
74
|
+
{/* БАНКЕТАМ.НЕТ: Admin26 / Demo20 */}
|
|
75
|
+
<p className="text-xs text-slate-500 mt-6 text-center">
|
|
76
|
+
Админ: логин <code>adminka</code>, пароль <code>password</code>
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
</Layout>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* РЕГИСТРАЦИЯ (/register) — п.1 задания ДЭ
|
|
4
|
+
* =============================================================================
|
|
5
|
+
* ЗАМЕНИТЕ:
|
|
6
|
+
* label полей — если в теме другие данные пользователя (не ФИО, а «Название фирмы»)
|
|
7
|
+
* валидацию — utils/validation.js (сервер + клиент)
|
|
8
|
+
* API-тело — те же поля уходят в POST /api/auth/register
|
|
9
|
+
* картинку — register-banner в public/images/
|
|
10
|
+
* Отличительная черта: бирюзовая шапка (variant="register")
|
|
11
|
+
* БАНКЕТАМ.НЕТ (п.1): логин латиница+цифры ≥6, пароль ≥8; ссылка «Уже есть аккаунт? Вход».
|
|
12
|
+
* БАНКЕТАМ.НЕТ: brand в Layout → «Банкетам.Нет».
|
|
13
|
+
*
|
|
14
|
+
* ПОТОК: form state → validateRegistration → register() из AuthContext
|
|
15
|
+
* → POST /api/auth/register → navigate('/requests')
|
|
16
|
+
* Ошибки: errors.xxx под полями; serverError — общая строка сверху
|
|
17
|
+
* inputClass — стиль полей; при ошибке добавьте className input-error
|
|
18
|
+
*
|
|
19
|
+
* GUIDE_PAGES.md §2.1 | server/routes/auth.js
|
|
20
|
+
* =============================================================================
|
|
21
|
+
*/
|
|
22
|
+
import { useState } from 'react';
|
|
23
|
+
import { Link, useNavigate } from 'react-router-dom';
|
|
24
|
+
import Layout from '../components/Layout.jsx';
|
|
25
|
+
import FormField from '../components/FormField.jsx';
|
|
26
|
+
import PageImage from '../components/PageImage.jsx';
|
|
27
|
+
import { useAuth } from '../context/AuthContext.jsx';
|
|
28
|
+
import { formatPhoneInput, validateRegistration } from '../utils/validation.js';
|
|
29
|
+
|
|
30
|
+
const inputClass =
|
|
31
|
+
'w-full rounded-lg border border-slate-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-teal-400';
|
|
32
|
+
|
|
33
|
+
export default function RegisterPage() {
|
|
34
|
+
const { register } = useAuth();
|
|
35
|
+
const navigate = useNavigate();
|
|
36
|
+
|
|
37
|
+
// --- Состояние формы: имена полей = ключи для API (login, fullName…) ---
|
|
38
|
+
const [form, setForm] = useState({
|
|
39
|
+
login: '',
|
|
40
|
+
password: '',
|
|
41
|
+
fullName: '',
|
|
42
|
+
phone: '',
|
|
43
|
+
email: '',
|
|
44
|
+
});
|
|
45
|
+
const [errors, setErrors] = useState({});
|
|
46
|
+
const [serverError, setServerError] = useState('');
|
|
47
|
+
|
|
48
|
+
const set = (field) => (e) => {
|
|
49
|
+
let value = e.target.value;
|
|
50
|
+
if (field === 'phone') value = formatPhoneInput(value);
|
|
51
|
+
setForm((f) => ({ ...f, [field]: value }));
|
|
52
|
+
setErrors((err) => ({ ...err, [field]: undefined }));
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const submit = async (e) => {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
setServerError('');
|
|
58
|
+
const localErrors = validateRegistration(form);
|
|
59
|
+
if (Object.keys(localErrors).length) {
|
|
60
|
+
setErrors(localErrors);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const user = await register({
|
|
65
|
+
login: form.login,
|
|
66
|
+
password: form.password,
|
|
67
|
+
fullName: form.fullName,
|
|
68
|
+
phone: form.phone,
|
|
69
|
+
email: form.email,
|
|
70
|
+
});
|
|
71
|
+
navigate(user.role === 'admin' ? '/admin' : '/requests');
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (err.data?.errors) setErrors(err.data.errors);
|
|
74
|
+
else setServerError(err.data?.message || err.message);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Layout variant="register">
|
|
80
|
+
<PageImage imageKey="registerBanner" className="w-full h-24 object-cover rounded-xl mb-4" />
|
|
81
|
+
|
|
82
|
+
<div className="bg-white rounded-2xl shadow-lg p-6 animate-fade-up border border-teal-100">
|
|
83
|
+
<h2 className="text-xl font-bold text-teal-900 mb-1">Регистрация</h2>
|
|
84
|
+
<p className="text-sm text-slate-500 mb-6">Все поля обязательны</p>
|
|
85
|
+
|
|
86
|
+
<form onSubmit={submit} noValidate>
|
|
87
|
+
<FormField label="Логин" error={errors.login}>
|
|
88
|
+
<input className={`${inputClass} ${errors.login ? 'input-error' : ''}`} value={form.login} onChange={set('login')} />
|
|
89
|
+
</FormField>
|
|
90
|
+
<FormField label="Пароль (от 6 символов)" error={errors.password}>
|
|
91
|
+
<input type="password" className={`${inputClass} ${errors.password ? 'input-error' : ''}`} value={form.password} onChange={set('password')} />
|
|
92
|
+
</FormField>
|
|
93
|
+
<FormField label="ФИО" error={errors.fullName}>
|
|
94
|
+
<input className={`${inputClass} ${errors.fullName ? 'input-error' : ''}`} value={form.fullName} onChange={set('fullName')} />
|
|
95
|
+
</FormField>
|
|
96
|
+
<FormField label="Телефон" error={errors.phone}>
|
|
97
|
+
<input placeholder="+7(999)-123-45-67" className={`${inputClass} ${errors.phone ? 'input-error' : ''}`} value={form.phone} onChange={set('phone')} />
|
|
98
|
+
</FormField>
|
|
99
|
+
<FormField label="Email" error={errors.email}>
|
|
100
|
+
<input type="email" className={`${inputClass} ${errors.email ? 'input-error' : ''}`} value={form.email} onChange={set('email')} />
|
|
101
|
+
</FormField>
|
|
102
|
+
|
|
103
|
+
{serverError && <p className="field-error mb-3">{serverError}</p>}
|
|
104
|
+
|
|
105
|
+
<button type="submit" className="w-full rounded-xl bg-teal-600 text-white py-3 font-semibold hover:bg-teal-700 transition">
|
|
106
|
+
Зарегистрироваться
|
|
107
|
+
</button>
|
|
108
|
+
</form>
|
|
109
|
+
|
|
110
|
+
{/* БАНКЕТАМ.НЕТ: на странице регистрации — ссылка «Уже зарегистрированы? Вход» (сейчас «Уже есть аккаунт?») */}
|
|
111
|
+
<p className="text-center text-sm mt-4 text-slate-600">
|
|
112
|
+
Уже есть аккаунт? <Link to="/login" className="text-teal-700 underline">Войти</Link>
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
</Layout>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* п.4 ЗАДАНИЯ — «Страница формирования заявки» (/requests/form)
|
|
4
|
+
* =============================================================================
|
|
5
|
+
* Поля: адрес, контактные данные (телефон), дата/время, вид услуги, оплата.
|
|
6
|
+
* «Иная услуга» — чекбокс + текст (модуль ПУ).
|
|
7
|
+
* После отправки — редирект на п.3 (/requests).
|
|
8
|
+
* БАНКЕТАМ.НЕТ (п.4): select помещение; дата type="text" placeholder ДД.ММ.ГГГГ (не datetime-local).
|
|
9
|
+
* БАНКЕТАМ.НЕТ: убрать блок «Иная услуга» (isCustomService).
|
|
10
|
+
* БАНКЕТАМ.НЕТ: заголовок «Оформление бронирования», label «Помещение».
|
|
11
|
+
*
|
|
12
|
+
* services: api.getServices() → select option (id, name) из service_types
|
|
13
|
+
* submit: validateRequestForm → api.createRequest(form) → navigate('/requests')
|
|
14
|
+
* paymentType: 'cash' | 'card' — значения как в schema CHECK
|
|
15
|
+
* isCustomService — модуль ПУ; для Банкетам.Нет удалить блок и поля в API
|
|
16
|
+
*
|
|
17
|
+
* GUIDE_PAGES.md §2.4 | server/routes/requests.js POST /
|
|
18
|
+
* =============================================================================
|
|
19
|
+
*/
|
|
20
|
+
import { useEffect, useState } from 'react';
|
|
21
|
+
import { Link, useNavigate } from 'react-router-dom';
|
|
22
|
+
import Layout from '../components/Layout.jsx';
|
|
23
|
+
import FormField from '../components/FormField.jsx';
|
|
24
|
+
import PageImage from '../components/PageImage.jsx';
|
|
25
|
+
import { api } from '../api.js';
|
|
26
|
+
import { formatPhoneInput, validateRequestForm } from '../utils/validation.js';
|
|
27
|
+
|
|
28
|
+
const inputClass =
|
|
29
|
+
'w-full rounded-lg border border-slate-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-cyan-400';
|
|
30
|
+
|
|
31
|
+
export default function RequestFormPage() {
|
|
32
|
+
const navigate = useNavigate();
|
|
33
|
+
const [services, setServices] = useState([]);
|
|
34
|
+
const [form, setForm] = useState({
|
|
35
|
+
address: '',
|
|
36
|
+
contactPhone: '',
|
|
37
|
+
scheduledAt: '',
|
|
38
|
+
paymentType: '',
|
|
39
|
+
serviceTypeId: '',
|
|
40
|
+
isCustomService: false,
|
|
41
|
+
customService: '',
|
|
42
|
+
});
|
|
43
|
+
const [errors, setErrors] = useState({});
|
|
44
|
+
const [serverError, setServerError] = useState('');
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
api.getServices().then(setServices).catch(() => {});
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const set = (field) => (e) => {
|
|
51
|
+
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
|
|
52
|
+
let v = value;
|
|
53
|
+
if (field === 'contactPhone') v = formatPhoneInput(value);
|
|
54
|
+
setForm((f) => ({ ...f, [field]: v }));
|
|
55
|
+
setErrors((err) => ({ ...err, [field]: undefined }));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const submit = async (e) => {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
setServerError('');
|
|
61
|
+
const localErrors = validateRequestForm(form);
|
|
62
|
+
if (Object.keys(localErrors).length) {
|
|
63
|
+
setErrors(localErrors);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
await api.createRequest({
|
|
68
|
+
address: form.address,
|
|
69
|
+
contactPhone: form.contactPhone,
|
|
70
|
+
scheduledAt: new Date(form.scheduledAt).toISOString(),
|
|
71
|
+
paymentType: form.paymentType,
|
|
72
|
+
serviceTypeId: form.isCustomService ? null : Number(form.serviceTypeId),
|
|
73
|
+
isCustomService: form.isCustomService,
|
|
74
|
+
customService: form.isCustomService ? form.customService : null,
|
|
75
|
+
});
|
|
76
|
+
navigate('/requests');
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err.data?.errors) setErrors(err.data.errors);
|
|
79
|
+
else setServerError(err.data?.message || err.message);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Layout variant="dashboard">
|
|
85
|
+
<p className="text-white/70 text-xs mb-2">Пункт 4 задания</p>
|
|
86
|
+
<PageImage imageKey="newRequestBanner" className="w-full h-20 object-cover rounded-xl mb-4" />
|
|
87
|
+
|
|
88
|
+
<Link
|
|
89
|
+
to="/requests"
|
|
90
|
+
className="inline-block text-sm text-white/90 underline mb-4 hover:text-white"
|
|
91
|
+
>
|
|
92
|
+
← Назад к истории заявок (п.3)
|
|
93
|
+
</Link>
|
|
94
|
+
|
|
95
|
+
<form
|
|
96
|
+
onSubmit={submit}
|
|
97
|
+
className="bg-white rounded-2xl shadow-lg p-6 animate-fade-up border border-cyan-100"
|
|
98
|
+
noValidate
|
|
99
|
+
>
|
|
100
|
+
{/* БАНКЕТАМ.НЕТ: «Оформление бронирования» */}
|
|
101
|
+
<h1 className="text-xl font-bold text-cyan-900 mb-1">Формирование заявки</h1>
|
|
102
|
+
<p className="text-sm text-slate-500 mb-6">Все поля обязательны</p>
|
|
103
|
+
|
|
104
|
+
<FormField label="Адрес" error={errors.address}>
|
|
105
|
+
<input
|
|
106
|
+
className={`${inputClass} ${errors.address ? 'input-error' : ''}`}
|
|
107
|
+
value={form.address}
|
|
108
|
+
onChange={set('address')}
|
|
109
|
+
placeholder="Адрес выполнения услуги"
|
|
110
|
+
/>
|
|
111
|
+
</FormField>
|
|
112
|
+
|
|
113
|
+
<FormField label="Контактные данные (телефон)" error={errors.contactPhone}>
|
|
114
|
+
<input
|
|
115
|
+
placeholder="+7(999)-123-45-67"
|
|
116
|
+
className={`${inputClass} ${errors.contactPhone ? 'input-error' : ''}`}
|
|
117
|
+
value={form.contactPhone}
|
|
118
|
+
onChange={set('contactPhone')}
|
|
119
|
+
/>
|
|
120
|
+
</FormField>
|
|
121
|
+
|
|
122
|
+
<FormField label="Желаемая дата и время получения услуги" error={errors.scheduledAt}>
|
|
123
|
+
<input
|
|
124
|
+
type="datetime-local"
|
|
125
|
+
className={`${inputClass} ${errors.scheduledAt ? 'input-error' : ''}`}
|
|
126
|
+
value={form.scheduledAt}
|
|
127
|
+
onChange={set('scheduledAt')}
|
|
128
|
+
/>
|
|
129
|
+
</FormField>
|
|
130
|
+
|
|
131
|
+
{!form.isCustomService && (
|
|
132
|
+
<FormField label="Вид услуги" error={errors.serviceTypeId}>
|
|
133
|
+
<select
|
|
134
|
+
className={`${inputClass} ${errors.serviceTypeId ? 'input-error' : ''}`}
|
|
135
|
+
value={form.serviceTypeId}
|
|
136
|
+
onChange={set('serviceTypeId')}
|
|
137
|
+
>
|
|
138
|
+
<option value="">— выберите из списка —</option>
|
|
139
|
+
{services.map((s) => (
|
|
140
|
+
<option key={s.id} value={s.id}>
|
|
141
|
+
{s.name}
|
|
142
|
+
</option>
|
|
143
|
+
))}
|
|
144
|
+
</select>
|
|
145
|
+
<p className="text-xs text-slate-400 mt-1">
|
|
146
|
+
Из задания: общий клининг, генеральная, послестроительная, химчистка
|
|
147
|
+
</p>
|
|
148
|
+
</FormField>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{/* БАНКЕТАМ.НЕТ: удалить весь блок «Иная услуга» — в задании только select помещений */}
|
|
152
|
+
<label className="flex items-center gap-2 mb-4 cursor-pointer">
|
|
153
|
+
<input
|
|
154
|
+
type="checkbox"
|
|
155
|
+
checked={form.isCustomService}
|
|
156
|
+
onChange={set('isCustomService')}
|
|
157
|
+
className="rounded border-slate-300 text-cyan-600"
|
|
158
|
+
/>
|
|
159
|
+
<span className="text-sm">Иная услуга</span>
|
|
160
|
+
</label>
|
|
161
|
+
|
|
162
|
+
{form.isCustomService && (
|
|
163
|
+
<FormField label="Опишите, какая услуга вам необходима" error={errors.customService}>
|
|
164
|
+
<textarea
|
|
165
|
+
rows={3}
|
|
166
|
+
className={`${inputClass} ${errors.customService ? 'input-error' : ''}`}
|
|
167
|
+
value={form.customService}
|
|
168
|
+
onChange={set('customService')}
|
|
169
|
+
/>
|
|
170
|
+
</FormField>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
<FormField label="Предпочтительный тип оплаты" error={errors.paymentType}>
|
|
174
|
+
<select
|
|
175
|
+
className={`${inputClass} ${errors.paymentType ? 'input-error' : ''}`}
|
|
176
|
+
value={form.paymentType}
|
|
177
|
+
onChange={set('paymentType')}
|
|
178
|
+
>
|
|
179
|
+
<option value="">— выберите —</option>
|
|
180
|
+
<option value="cash">Наличные</option>
|
|
181
|
+
<option value="card">Банковская карта</option>
|
|
182
|
+
</select>
|
|
183
|
+
</FormField>
|
|
184
|
+
|
|
185
|
+
{serverError && <p className="field-error mb-3">{serverError}</p>}
|
|
186
|
+
|
|
187
|
+
<button
|
|
188
|
+
type="submit"
|
|
189
|
+
className="w-full rounded-xl bg-cyan-600 text-white py-3 font-semibold hover:bg-cyan-700 transition"
|
|
190
|
+
>
|
|
191
|
+
Отправить заявку
|
|
192
|
+
</button>
|
|
193
|
+
</form>
|
|
194
|
+
</Layout>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* =============================================================================
|
|
3
|
+
* п.3 ЗАДАНИЯ — «Страница создания заявки» (/requests)
|
|
4
|
+
* =============================================================================
|
|
5
|
+
* История заявок + кнопка перехода на /requests/form (п.4)
|
|
6
|
+
* БАНКЕТАМ.НЕТ (п.3): «Личный кабинет» — история бронирований + слайдер (HeroSlider).
|
|
7
|
+
* БАНКЕТАМ.НЕТ: блок отзыва под заявкой, если status !== 'new' (после админа).
|
|
8
|
+
* БАНКЕТАМ.НЕТ: statusColors — подписи «Банкет назначен» / «Банкет завершен».
|
|
9
|
+
*
|
|
10
|
+
* ДАННЫЕ: useEffect → api.getMyRequests() → GET /api/requests/mine
|
|
11
|
+
* statusLabel приходит с сервера (requests.js STATUS_LABELS)
|
|
12
|
+
* Кнопка «Новая заявка» → Link to="/requests/form" (п.4)
|
|
13
|
+
*
|
|
14
|
+
* БАНКЕТАМ.НЕТ (отзывы): под карточкой textarea + кнопка, если status !== 'new'
|
|
15
|
+
* → api.postReview + route на сервере + таблица reviews в schema.sql
|
|
16
|
+
*
|
|
17
|
+
* GUIDE_PAGES.md §2.3 | THEME_BANQUETAM_NET.md
|
|
18
|
+
* =============================================================================
|
|
19
|
+
*/
|
|
20
|
+
import { useEffect, useState } from 'react';
|
|
21
|
+
import { Link } from 'react-router-dom';
|
|
22
|
+
import Layout from '../components/Layout.jsx';
|
|
23
|
+
import PageImage from '../components/PageImage.jsx';
|
|
24
|
+
import { api } from '../api.js';
|
|
25
|
+
|
|
26
|
+
const statusColors = {
|
|
27
|
+
new: 'bg-amber-100 text-amber-800',
|
|
28
|
+
in_progress: 'bg-blue-100 text-blue-800',
|
|
29
|
+
completed: 'bg-emerald-100 text-emerald-800',
|
|
30
|
+
cancelled: 'bg-red-100 text-red-800',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default function RequestsPage() {
|
|
34
|
+
const [requests, setRequests] = useState([]);
|
|
35
|
+
const [loading, setLoading] = useState(true);
|
|
36
|
+
const [error, setError] = useState('');
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
api
|
|
40
|
+
.getMyRequests()
|
|
41
|
+
.then(setRequests)
|
|
42
|
+
.catch((e) => setError(e.message))
|
|
43
|
+
.finally(() => setLoading(false));
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Layout variant="dashboard">
|
|
48
|
+
<div className="animate-fade-up">
|
|
49
|
+
<p className="text-white/70 text-xs mb-1">Пункт 3 задания</p>
|
|
50
|
+
<PageImage imageKey="requestsBanner" className="w-full h-20 object-cover rounded-xl mb-4" />
|
|
51
|
+
|
|
52
|
+
{/* БАНКЕТАМ.НЕТ: заголовок «Личный кабинет» */}
|
|
53
|
+
<h1 className="text-xl font-bold text-white drop-shadow mb-1">Создание заявки</h1>
|
|
54
|
+
<p className="text-white/80 text-sm mb-6">
|
|
55
|
+
История ваших заявок и переход к оформлению новой
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
<Link
|
|
59
|
+
to="/requests/form"
|
|
60
|
+
className="block mb-8 rounded-2xl bg-white p-5 shadow-lg border-2 border-teal-400 hover:border-teal-500 transition group"
|
|
61
|
+
>
|
|
62
|
+
<div className="flex items-center justify-between gap-3">
|
|
63
|
+
<div className="text-left">
|
|
64
|
+
<p className="font-bold text-teal-800 text-lg">Оставить новую заявку</p>
|
|
65
|
+
<p className="text-slate-600 text-sm mt-1">
|
|
66
|
+
Страница формирования заявки: адрес, контакты, услуга, дата, оплата
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
<span className="text-3xl text-teal-600 group-hover:translate-x-1 transition">→</span>
|
|
70
|
+
</div>
|
|
71
|
+
</Link>
|
|
72
|
+
|
|
73
|
+
<h2 className="text-lg font-semibold text-white mb-3">История заявок</h2>
|
|
74
|
+
|
|
75
|
+
{loading && <p className="text-white/80">Загрузка…</p>}
|
|
76
|
+
{error && <p className="text-red-200">{error}</p>}
|
|
77
|
+
|
|
78
|
+
{!loading && requests.length === 0 && (
|
|
79
|
+
<div className="bg-white rounded-xl p-6 text-center text-slate-600 shadow">
|
|
80
|
+
<PageImage imageKey="emptyRequests" className="w-32 h-auto mx-auto mb-3 opacity-80" />
|
|
81
|
+
Заявок пока нет. Нажмите «Оставить новую заявку» выше.
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
<ul className="space-y-3">
|
|
86
|
+
{requests.map((r, i) => (
|
|
87
|
+
<li
|
|
88
|
+
key={r.id}
|
|
89
|
+
className="bg-white rounded-xl p-4 shadow animate-slide-in border-l-4 border-teal-500"
|
|
90
|
+
style={{ animationDelay: `${i * 0.05}s` }}
|
|
91
|
+
>
|
|
92
|
+
<div className="flex justify-between items-start gap-2 mb-2">
|
|
93
|
+
<span className="font-medium text-slate-800">{r.serviceName}</span>
|
|
94
|
+
<span className={`text-xs px-2 py-0.5 rounded-full ${statusColors[r.status]}`}>
|
|
95
|
+
{r.statusLabel}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
<p className="text-sm text-slate-600">Адрес: {r.address}</p>
|
|
99
|
+
<p className="text-sm text-slate-600">Контакты: {r.contactPhone}</p>
|
|
100
|
+
<p className="text-xs text-slate-500 mt-1">
|
|
101
|
+
{new Date(r.scheduledAt).toLocaleString('ru-RU')} · {r.paymentLabel}
|
|
102
|
+
</p>
|
|
103
|
+
{r.cancelReason && (
|
|
104
|
+
<p className="text-xs text-red-600 mt-2">Причина отмены: {r.cancelReason}</p>
|
|
105
|
+
)}
|
|
106
|
+
</li>
|
|
107
|
+
))}
|
|
108
|
+
</ul>
|
|
109
|
+
</div>
|
|
110
|
+
</Layout>
|
|
111
|
+
);
|
|
112
|
+
}
|