@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.
Files changed (131) hide show
  1. package/exam-guides/COMMENTS_GUIDE.md +53 -0
  2. package/exam-guides/EXAM_COMMANDS.txt +47 -0
  3. package/exam-guides/GUIDE_PAGES.md +529 -0
  4. package/exam-guides/NPM_PACKAGE.md +206 -0
  5. package/exam-guides/README.md +40 -0
  6. package/exam-guides/RESPONSIVE.md +224 -0
  7. package/exam-guides/TECH_STACK.txt +142 -0
  8. package/exam-guides/THEME_BANQUETAM_NET.md +106 -0
  9. package/exam-guides/commented-code/README.txt +5 -0
  10. package/exam-guides/commented-code/client/index.html +14 -0
  11. package/exam-guides/commented-code/client/package-lock.json +2298 -0
  12. package/exam-guides/commented-code/client/package.json +21 -0
  13. package/exam-guides/commented-code/client/public/images/README.txt +26 -0
  14. package/exam-guides/commented-code/client/public/images/about-cleaning.svg +4 -0
  15. package/exam-guides/commented-code/client/public/images/admin-banner.svg +1 -0
  16. package/exam-guides/commented-code/client/public/images/empty-requests.svg +1 -0
  17. package/exam-guides/commented-code/client/public/images/footer-photo-1.svg +4 -0
  18. package/exam-guides/commented-code/client/public/images/footer-photo-2.svg +4 -0
  19. package/exam-guides/commented-code/client/public/images/footer-photo-3.svg +4 -0
  20. package/exam-guides/commented-code/client/public/images/home-hero.svg +4 -0
  21. package/exam-guides/commented-code/client/public/images/login-banner.svg +1 -0
  22. package/exam-guides/commented-code/client/public/images/logo.svg +4 -0
  23. package/exam-guides/commented-code/client/public/images/new-request-banner.svg +1 -0
  24. package/exam-guides/commented-code/client/public/images/register-banner.svg +1 -0
  25. package/exam-guides/commented-code/client/public/images/requests-banner.svg +1 -0
  26. package/exam-guides/commented-code/client/public/images/slide-1.svg +6 -0
  27. package/exam-guides/commented-code/client/public/images/slide-2.svg +5 -0
  28. package/exam-guides/commented-code/client/public/images/slide-3.svg +5 -0
  29. package/exam-guides/commented-code/client/src/App.jsx +72 -0
  30. package/exam-guides/commented-code/client/src/api.js +71 -0
  31. package/exam-guides/commented-code/client/src/components/FormField.jsx +25 -0
  32. package/exam-guides/commented-code/client/src/components/Layout.jsx +83 -0
  33. package/exam-guides/commented-code/client/src/components/PageImage.jsx +38 -0
  34. package/exam-guides/commented-code/client/src/components/ProtectedRoute.jsx +35 -0
  35. package/exam-guides/commented-code/client/src/components/UserNav.jsx +33 -0
  36. package/exam-guides/commented-code/client/src/components/landing/HeroSlider.jsx +103 -0
  37. package/exam-guides/commented-code/client/src/components/landing/LandingLayout.jsx +76 -0
  38. package/exam-guides/commented-code/client/src/components/landing/SiteFooter.jsx +74 -0
  39. package/exam-guides/commented-code/client/src/config/images.js +104 -0
  40. package/exam-guides/commented-code/client/src/constants/services.js +19 -0
  41. package/exam-guides/commented-code/client/src/context/AuthContext.jsx +72 -0
  42. package/exam-guides/commented-code/client/src/index.css +73 -0
  43. package/exam-guides/commented-code/client/src/main.jsx +28 -0
  44. package/exam-guides/commented-code/client/src/pages/AdminPage.jsx +151 -0
  45. package/exam-guides/commented-code/client/src/pages/LandingPage.jsx +131 -0
  46. package/exam-guides/commented-code/client/src/pages/LoginPage.jsx +81 -0
  47. package/exam-guides/commented-code/client/src/pages/RegisterPage.jsx +117 -0
  48. package/exam-guides/commented-code/client/src/pages/RequestFormPage.jsx +196 -0
  49. package/exam-guides/commented-code/client/src/pages/RequestsPage.jsx +112 -0
  50. package/exam-guides/commented-code/client/src/utils/validation.js +71 -0
  51. package/exam-guides/commented-code/client/vite.config.js +31 -0
  52. package/exam-guides/commented-code/server/db/init.js +67 -0
  53. package/exam-guides/commented-code/server/db/pool.js +23 -0
  54. package/exam-guides/commented-code/server/db/schema.sql +53 -0
  55. package/exam-guides/commented-code/server/db/seed.sql +15 -0
  56. package/exam-guides/commented-code/server/index.js +45 -0
  57. package/exam-guides/commented-code/server/middleware/auth.js +38 -0
  58. package/exam-guides/commented-code/server/package-lock.json +1084 -0
  59. package/exam-guides/commented-code/server/package.json +17 -0
  60. package/exam-guides/commented-code/server/routes/admin.js +96 -0
  61. package/exam-guides/commented-code/server/routes/auth.js +128 -0
  62. package/exam-guides/commented-code/server/routes/requests.js +115 -0
  63. package/exam-guides/commented-code/server/routes/services.js +31 -0
  64. package/exam-guides/commented-code/server/utils/validation.js +81 -0
  65. package/exam-guides/exam-starter/README.txt +22 -0
  66. package/exam-guides/exam-starter/package.json +13 -0
  67. 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
  68. package/exam-project/README.md +16 -0
  69. package/exam-project/client/index.html +14 -0
  70. package/exam-project/client/package-lock.json +2298 -0
  71. package/exam-project/client/package.json +21 -0
  72. package/exam-project/client/public/images/README.txt +26 -0
  73. package/exam-project/client/public/images/about-cleaning.svg +4 -0
  74. package/exam-project/client/public/images/admin-banner.svg +1 -0
  75. package/exam-project/client/public/images/empty-requests.svg +1 -0
  76. package/exam-project/client/public/images/footer-photo-1.svg +4 -0
  77. package/exam-project/client/public/images/footer-photo-2.svg +4 -0
  78. package/exam-project/client/public/images/footer-photo-3.svg +4 -0
  79. package/exam-project/client/public/images/home-hero.svg +4 -0
  80. package/exam-project/client/public/images/login-banner.svg +1 -0
  81. package/exam-project/client/public/images/logo.svg +4 -0
  82. package/exam-project/client/public/images/new-request-banner.svg +1 -0
  83. package/exam-project/client/public/images/register-banner.svg +1 -0
  84. package/exam-project/client/public/images/requests-banner.svg +1 -0
  85. package/exam-project/client/public/images/slide-1.svg +6 -0
  86. package/exam-project/client/public/images/slide-2.svg +5 -0
  87. package/exam-project/client/public/images/slide-3.svg +5 -0
  88. package/exam-project/client/src/App.jsx +52 -0
  89. package/exam-project/client/src/api.js +50 -0
  90. package/exam-project/client/src/components/FormField.jsx +11 -0
  91. package/exam-project/client/src/components/Layout.jsx +61 -0
  92. package/exam-project/client/src/components/PageImage.jsx +22 -0
  93. package/exam-project/client/src/components/ProtectedRoute.jsx +20 -0
  94. package/exam-project/client/src/components/UserNav.jsx +20 -0
  95. package/exam-project/client/src/components/landing/HeroSlider.jsx +89 -0
  96. package/exam-project/client/src/components/landing/LandingLayout.jsx +61 -0
  97. package/exam-project/client/src/components/landing/SiteFooter.jsx +61 -0
  98. package/exam-project/client/src/config/images.js +83 -0
  99. package/exam-project/client/src/constants/services.js +7 -0
  100. package/exam-project/client/src/context/AuthContext.jsx +55 -0
  101. package/exam-project/client/src/index.css +54 -0
  102. package/exam-project/client/src/main.jsx +17 -0
  103. package/exam-project/client/src/pages/AdminPage.jsx +128 -0
  104. package/exam-project/client/src/pages/LandingPage.jsx +115 -0
  105. package/exam-project/client/src/pages/LoginPage.jsx +63 -0
  106. package/exam-project/client/src/pages/RegisterPage.jsx +97 -0
  107. package/exam-project/client/src/pages/RequestFormPage.jsx +178 -0
  108. package/exam-project/client/src/pages/RequestsPage.jsx +94 -0
  109. package/exam-project/client/src/utils/validation.js +54 -0
  110. package/exam-project/client/vite.config.js +17 -0
  111. package/exam-project/package.json +18 -0
  112. package/exam-project/scripts/init-project.js +86 -0
  113. package/exam-project/scripts/prepack.js +27 -0
  114. package/exam-project/scripts/unpack-template.js +105 -0
  115. package/exam-project/server/db/init.js +50 -0
  116. package/exam-project/server/db/pool.js +11 -0
  117. package/exam-project/server/db/schema.sql +41 -0
  118. package/exam-project/server/db/seed.sql +12 -0
  119. package/exam-project/server/index.js +29 -0
  120. package/exam-project/server/middleware/auth.js +24 -0
  121. package/exam-project/server/package-lock.json +1084 -0
  122. package/exam-project/server/package.json +17 -0
  123. package/exam-project/server/routes/admin.js +76 -0
  124. package/exam-project/server/routes/auth.js +109 -0
  125. package/exam-project/server/routes/requests.js +99 -0
  126. package/exam-project/server/routes/services.js +18 -0
  127. package/exam-project/server/utils/validation.js +63 -0
  128. package/package.json +25 -0
  129. package/scripts/init-project.js +86 -0
  130. package/scripts/prepack.js +27 -0
  131. 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
+ }