@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,76 @@
1
+ /**
2
+ * =============================================================================
3
+ * LandingLayout.jsx — ШАПКА ЛЕНДИНГА (без Layout.jsx)
4
+ * =============================================================================
5
+ * Отдельная шапка для главной / — не путать с Layout на /register, /login…
6
+ * Клик по логотипу → всегда / (даже если пользователь вошёл).
7
+ *
8
+ * Гость: кнопки «Вход» / «Регистрация»
9
+ * user: ссылка в кабинет (/requests или /admin) + «Выйти»
10
+ *
11
+ * ЗАМЕНИТЕ: brandName, cabinetLabel («Мои заявки» → «Личный кабинет»)
12
+ * БАНКЕТАМ.НЕТ: brandName = 'Банкетам.Нет'
13
+ *
14
+ * GUIDE_PAGES.md §5 (лендинг)
15
+ * =============================================================================
16
+ */
17
+ import { Link } from 'react-router-dom';
18
+ import PageImage from '../PageImage.jsx';
19
+ import { useAuth } from '../../context/AuthContext.jsx';
20
+
21
+ const brandName = 'Мой Не Сам'; // БАНКЕТАМ.НЕТ: 'Банкетам.Нет'
22
+
23
+ export default function LandingLayout({ children }) {
24
+ const { user, logout } = useAuth();
25
+
26
+ const cabinetPath = user?.role === 'admin' ? '/admin' : '/requests';
27
+ const cabinetLabel = user?.role === 'admin' ? 'Панель админа' : 'Мои заявки';
28
+
29
+ return (
30
+ <div className="min-h-screen flex flex-col bg-slate-50">
31
+ <header className="bg-white border-b border-slate-200 shadow-sm sticky top-0 z-40">
32
+ <div className="mx-auto w-full max-w-lg lg:max-w-5xl px-4 lg:px-8 py-3 flex items-center justify-between gap-3">
33
+ <Link to="/" className="flex items-center gap-2 min-w-0">
34
+ <PageImage imageKey="logo" className="h-9 lg:h-11 w-auto shrink-0" />
35
+ <span className="font-semibold text-lg text-teal-900 truncate">{brandName}</span>
36
+ </Link>
37
+
38
+ {user ? (
39
+ <div className="flex items-center gap-2 shrink-0 text-sm">
40
+ <Link
41
+ to={cabinetPath}
42
+ className="px-3 py-1.5 rounded-lg bg-teal-600 text-white hover:bg-teal-700 transition"
43
+ >
44
+ {cabinetLabel}
45
+ </Link>
46
+ <button
47
+ type="button"
48
+ onClick={logout}
49
+ className="px-3 py-1.5 rounded-lg text-slate-600 hover:bg-slate-100 transition"
50
+ >
51
+ Выйти
52
+ </button>
53
+ </div>
54
+ ) : (
55
+ <div className="flex gap-2 shrink-0 text-sm">
56
+ <Link
57
+ to="/login"
58
+ className="px-3 py-1.5 rounded-lg text-slate-700 hover:bg-slate-100 transition"
59
+ >
60
+ Вход
61
+ </Link>
62
+ <Link
63
+ to="/register"
64
+ className="px-3 py-1.5 rounded-lg bg-teal-600 text-white hover:bg-teal-700 transition"
65
+ >
66
+ Регистрация
67
+ </Link>
68
+ </div>
69
+ )}
70
+ </div>
71
+ </header>
72
+
73
+ <div className="flex-1 w-full">{children}</div>
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * =============================================================================
3
+ * SiteFooter.jsx — ПОДВАЛ ЛЕНДИНГА
4
+ * =============================================================================
5
+ * Три круглых фото: imageKey footerPhoto1, footerPhoto2, footerPhoto3
6
+ * → PNG в client/public/images/ → записи в config/images.js
7
+ *
8
+ * footerLinks — якоря (сейчас все на /; можно сделать id секций на LandingPage)
9
+ * ЗАМЕНИТЕ: название компании, описание, copyright год
10
+ *
11
+ * БАНКЕТАМ.НЕТ: «Банкетам.Нет», текст про банкетные залы
12
+ * GUIDE_PAGES.md §5.3
13
+ * =============================================================================
14
+ */
15
+ import { Link } from 'react-router-dom';
16
+ import PageImage from '../PageImage.jsx';
17
+
18
+ const footerLinks = [
19
+ { label: 'О компании', to: '/' },
20
+ { label: 'Услуги', to: '/' },
21
+ { label: 'Контакты', to: '/' },
22
+ ];
23
+
24
+ export default function SiteFooter() {
25
+ return (
26
+ <footer className="bg-slate-900 text-slate-300 mt-auto">
27
+ <div className="mx-auto max-w-lg lg:max-w-5xl px-4 lg:px-8 py-10">
28
+ <div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-8">
29
+ <div>
30
+ <p className="text-white font-semibold text-lg mb-2">Мой Не Сам</p>
31
+ <p className="text-sm leading-relaxed max-w-sm">
32
+ Портал заявок на клининговые услуги. Профессиональная уборка жилых и
33
+ производственных помещений — вы отдыхаете, мы наводим порядок.
34
+ </p>
35
+ </div>
36
+
37
+ {/* Фото в футере — замените PNG в public/images/footer-photo-*.png */}
38
+ <div className="flex gap-4 justify-center lg:justify-end">
39
+ <PageImage
40
+ imageKey="footerPhoto1"
41
+ className="w-20 h-20 lg:w-24 lg:h-24 rounded-full object-cover border-2 border-teal-600"
42
+ />
43
+ <PageImage
44
+ imageKey="footerPhoto2"
45
+ className="w-20 h-20 lg:w-24 lg:h-24 rounded-full object-cover border-2 border-teal-600"
46
+ />
47
+ <PageImage
48
+ imageKey="footerPhoto3"
49
+ className="w-20 h-20 lg:w-24 lg:h-24 rounded-full object-cover border-2 border-teal-600"
50
+ />
51
+ </div>
52
+ </div>
53
+
54
+ <nav className="flex flex-wrap gap-4 justify-center lg:justify-start mt-8 text-sm">
55
+ {footerLinks.map((l) => (
56
+ <Link key={l.label} to={l.to} className="hover:text-white transition">
57
+ {l.label}
58
+ </Link>
59
+ ))}
60
+ <Link to="/register" className="hover:text-teal-400 transition">
61
+ Регистрация
62
+ </Link>
63
+ <Link to="/login" className="hover:text-teal-400 transition">
64
+ Вход
65
+ </Link>
66
+ </nav>
67
+
68
+ <p className="text-center lg:text-left text-xs text-slate-500 mt-8 border-t border-slate-700 pt-6">
69
+ © {new Date().getFullYear()} Мой Не Сам · Шаблон для ДЭ
70
+ </p>
71
+ </div>
72
+ </footer>
73
+ );
74
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * =============================================================================
3
+ * КОНФИГУРАЦИЯ ИЗОБРАЖЕНИЙ
4
+ * =============================================================================
5
+ * Все картинки лежат в: client/public/images/
6
+ * В коде подключаются через URL: /images/имя-файла
7
+ *
8
+ * НА ЭКЗАМЕНЕ:
9
+ * 1) Экспорт из Figma → PNG в public/images/
10
+ * 2) Если имя файла другое — поменяйте только этот файл
11
+ * 3) alt — для доступности; замените текст под новую тему
12
+ * БАНКЕТАМ.НЕТ: slide4; alt про банкеты/залы; logo «Банкетам.Нет»
13
+ *
14
+ * Ключ (logo, slide1…) — НЕ переименовывайте без правки PageImage imageKey на страницах
15
+ * file — имя файла в public/images/
16
+ * imageUrl('logo') / imageAlt('logo') — если нужен прямой доступ без PageImage
17
+ *
18
+ * Слайды: slide-1.png … slide-4.png | Футер: footer-photo-1.png …
19
+ *
20
+ * GUIDE_PAGES.md §3 | PageImage.jsx
21
+ * =============================================================================
22
+ */
23
+
24
+ /** Базовый путь. Менять не нужно, если папка называется images */
25
+ export const IMAGES_BASE = '/images';
26
+
27
+ /**
28
+ * Карта картинок по страницам.
29
+ * Ключ (logo, homeHero…) — не трогать, менять только file и alt.
30
+ */
31
+ export const IMAGES = {
32
+ // Шапка на всех страницах → логотип компании / портала
33
+ logo: {
34
+ file: 'logo.svg', // ЗАМЕНИТЕ на logo.png после экспорта из Figma
35
+ alt: 'Логотип «Мой Не Сам»', // → название вашей организации
36
+ },
37
+
38
+ // Главная (/) — иллюстрация услуги
39
+ homeHero: {
40
+ file: 'home-hero.svg',
41
+ alt: 'Клининговые услуги на дому',
42
+ },
43
+
44
+ // Регистрация (/register)
45
+ registerBanner: {
46
+ file: 'register-banner.svg',
47
+ alt: 'Регистрация нового заказчика',
48
+ },
49
+
50
+ // Вход (/login)
51
+ loginBanner: {
52
+ file: 'login-banner.svg',
53
+ alt: 'Вход в личный кабинет',
54
+ },
55
+
56
+ // Список заявок (/requests)
57
+ requestsBanner: {
58
+ file: 'requests-banner.svg',
59
+ alt: 'История заявок на уборку',
60
+ },
61
+
62
+ // Новая заявка (/requests/new)
63
+ newRequestBanner: {
64
+ file: 'new-request-banner.svg',
65
+ alt: 'Оформление заявки на услугу',
66
+ },
67
+
68
+ // Админ-панель (/admin)
69
+ adminBanner: {
70
+ file: 'admin-banner.svg',
71
+ alt: 'Панель администратора',
72
+ },
73
+
74
+ // Пустой список заявок
75
+ emptyRequests: {
76
+ file: 'empty-requests.svg',
77
+ alt: 'Заявок пока нет',
78
+ },
79
+
80
+ // --- Лендинг: слайдер (HeroSlider.jsx) ---
81
+ slide1: { file: 'slide-1.svg', alt: 'Общий клининг' },
82
+ slide2: { file: 'slide-2.svg', alt: 'Генеральная уборка' },
83
+ slide3: { file: 'slide-3.svg', alt: 'Химчистка ковров и мебели' },
84
+
85
+ // Лендинг: блок «О клининге»
86
+ aboutCleaning: { file: 'about-cleaning.svg', alt: 'Профессиональный клининг' },
87
+
88
+ // Лендинг: футер — 3 фото (команда / офис / работы)
89
+ footerPhoto1: { file: 'footer-photo-1.svg', alt: 'Сотрудник 1' },
90
+ footerPhoto2: { file: 'footer-photo-2.svg', alt: 'Сотрудник 2' },
91
+ footerPhoto3: { file: 'footer-photo-3.svg', alt: 'Сотрудник 3' },
92
+ };
93
+
94
+ /** Собрать полный URL для тега <img src="..."> */
95
+ export function imageUrl(imageKey) {
96
+ const item = IMAGES[imageKey];
97
+ if (!item) return '';
98
+ return `${IMAGES_BASE}/${item.file}`;
99
+ }
100
+
101
+ /** Текст alt по ключу */
102
+ export function imageAlt(imageKey) {
103
+ return IMAGES[imageKey]?.alt ?? '';
104
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * =============================================================================
3
+ * constants/services.js — НАЗВАНИЯ УСЛУГ НА ЛЕНДИНГЕ (только отображение)
4
+ * =============================================================================
5
+ * Реальный список для формы п.4 берётся из API GET /api/services (seed.sql).
6
+ * Этот массив — для блока «Наши услуги» на LandingPage.jsx.
7
+ *
8
+ * При смене темы: обновите И seed.sql И этот файл одинаковым списком.
9
+ * БАНКЕТАМ.НЕТ: четыре помещения (зал, ресторан, веранды)
10
+ *
11
+ * GUIDE_PAGES.md §5.2 | server/db/seed.sql
12
+ * =============================================================================
13
+ */
14
+ export const DEFAULT_SERVICE_NAMES = [
15
+ 'Общий клининг', // БАНКЕТАМ.НЕТ: заменить весь массив на 4 помещения
16
+ 'Генеральная уборка',
17
+ 'Послестроительная уборка',
18
+ 'Химчистка ковров и мебели',
19
+ ];
@@ -0,0 +1,72 @@
1
+ /**
2
+ * =============================================================================
3
+ * КОНТЕКСТ АВТОРИЗАЦИИ — user, login, register, logout
4
+ * =============================================================================
5
+ * Хранит JWT в localStorage (api.js setToken).
6
+ * При смене темы логика не меняется; поля user приходят с сервера (fullName, role…).
7
+ *
8
+ * КАК РАБОТАЕТ:
9
+ * login/register → api.js → setToken в localStorage → setUser
10
+ * logout → очистка токена → редирект на /login (на странице)
11
+ * user.role: 'user' | 'admin' — после входа Admin26 попадёт в /admin (см. LoginPage navigate)
12
+ *
13
+ * useAuth() на страницах: const { user, login, register, logout } = useAuth();
14
+ * БАНКЕТАМ.НЕТ: без изменений, если структура users та же.
15
+ *
16
+ * GUIDE_PAGES.md §7 | api.js
17
+ * =============================================================================
18
+ */
19
+ import { createContext, useContext, useEffect, useState } from 'react';
20
+ import { api, getToken, setToken } from '../api.js';
21
+
22
+ const AuthContext = createContext(null);
23
+
24
+ export function AuthProvider({ children }) {
25
+ const [user, setUser] = useState(null);
26
+ const [loading, setLoading] = useState(true);
27
+
28
+ // При загрузке приложения — проверить сохранённый токен
29
+ useEffect(() => {
30
+ const token = getToken();
31
+ if (!token) {
32
+ setLoading(false);
33
+ return;
34
+ }
35
+ api
36
+ .me()
37
+ .then(({ user: u }) => setUser(u))
38
+ .catch(() => setToken(null))
39
+ .finally(() => setLoading(false));
40
+ }, []);
41
+
42
+ const login = async (credentials) => {
43
+ const { user: u, token } = await api.login(credentials);
44
+ setToken(token);
45
+ setUser(u);
46
+ return u;
47
+ };
48
+
49
+ const register = async (data) => {
50
+ const { user: u, token } = await api.register(data);
51
+ setToken(token);
52
+ setUser(u);
53
+ return u;
54
+ };
55
+
56
+ const logout = () => {
57
+ setToken(null);
58
+ setUser(null);
59
+ };
60
+
61
+ return (
62
+ <AuthContext.Provider value={{ user, loading, login, register, logout }}>
63
+ {children}
64
+ </AuthContext.Provider>
65
+ );
66
+ }
67
+
68
+ export function useAuth() {
69
+ const ctx = useContext(AuthContext);
70
+ if (!ctx) throw new Error('useAuth вне AuthProvider');
71
+ return ctx;
72
+ }
@@ -0,0 +1,73 @@
1
+ @import 'tailwindcss';
2
+
3
+ /**
4
+ * =============================================================================
5
+ * ГЛОБАЛЬНЫЕ СТИЛИ
6
+ * =============================================================================
7
+ * @theme — цвета бренда (ЗАМЕНИТЕ --color-brand под новую тему)
8
+ * .animate-* — анимации для модуля №3
9
+ * .field-error / .input-error — ошибки валидации на формах
10
+ *
11
+ * АДАПТИВ: ширина колонки задаётся в Layout.jsx (max-w-lg + lg:max-w-5xl).
12
+ * Подробная инструкция — файл RESPONSIVE.md в корне проекта.
13
+ * БАНКЕТАМ.НЕТ (М3): микроанимации уже есть; цвета @theme можно сменить на «банкетную» палитру.
14
+ *
15
+ * КАК ПОДКЛЮЧИТЬ К СТРАНИЦЕ:
16
+ * animate-fade-up на блоках (Layout header, карточки)
17
+ * .field-error на <p> под полем; .input-error на <input> при ошибке
18
+ * className="input-error" — см. RegisterPage inputClass + errors
19
+ *
20
+ * GUIDE_PAGES.md §3.3 | COMMENTS_GUIDE.md
21
+ * =============================================================================
22
+ */
23
+
24
+ @theme {
25
+ --color-brand: #0d9488;
26
+ --color-brand-dark: #0f766e;
27
+ --font-sans: 'Segoe UI', system-ui, sans-serif;
28
+ }
29
+
30
+ @layer base {
31
+ body {
32
+ @apply min-h-screen bg-slate-50 text-slate-800 antialiased;
33
+ font-family: var(--font-sans);
34
+ }
35
+ }
36
+
37
+ @keyframes fade-up {
38
+ from {
39
+ opacity: 0;
40
+ transform: translateY(12px);
41
+ }
42
+ to {
43
+ opacity: 1;
44
+ transform: translateY(0);
45
+ }
46
+ }
47
+
48
+ @keyframes slide-in {
49
+ from {
50
+ opacity: 0;
51
+ transform: translateX(-8px);
52
+ }
53
+ to {
54
+ opacity: 1;
55
+ transform: translateX(0);
56
+ }
57
+ }
58
+
59
+ .animate-fade-up {
60
+ animation: fade-up 0.45s ease-out both;
61
+ }
62
+
63
+ .animate-slide-in {
64
+ animation: slide-in 0.35s ease-out both;
65
+ }
66
+
67
+ .field-error {
68
+ @apply mt-1 text-sm text-red-600;
69
+ }
70
+
71
+ .input-error {
72
+ @apply border-red-400 ring-red-100;
73
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * =============================================================================
3
+ * main.jsx — ТОЧКА ВХОДА REACT (запуск приложения в браузере)
4
+ * =============================================================================
5
+ * Обычно НЕ МЕНЯЮТ при смене темы.
6
+ *
7
+ * Цепочка: index.html (#root) → main.jsx → AuthProvider → App.jsx (маршруты).
8
+ *
9
+ * Если добавили глобальный контекст (тема, язык) — оберните <App /> ещё одним Provider.
10
+ * Подробнее: GUIDE_PAGES.md §6 (новая страница — маршрут всё равно в App.jsx).
11
+ * =============================================================================
12
+ */
13
+ import { StrictMode } from 'react';
14
+ import { createRoot } from 'react-dom/client';
15
+ import { BrowserRouter } from 'react-router-dom';
16
+ import App from './App.jsx';
17
+ import { AuthProvider } from './context/AuthContext.jsx';
18
+ import './index.css';
19
+
20
+ createRoot(document.getElementById('root')).render(
21
+ <StrictMode>
22
+ <BrowserRouter>
23
+ <AuthProvider>
24
+ <App />
25
+ </AuthProvider>
26
+ </BrowserRouter>
27
+ </StrictMode>
28
+ );
@@ -0,0 +1,151 @@
1
+ /**
2
+ * =============================================================================
3
+ * АДМИН-ПАНЕЛЬ (/admin) — п.5 задания ДЭ
4
+ * =============================================================================
5
+ * ЗАМЕНИТЕ:
6
+ * логин/пароль админа — server/db/init.js (seedAdmin)
7
+ * кнопки статусов — должны совпадать с CHECK в schema.sql
8
+ * API — PATCH /api/admin/requests/:id/status
9
+ * картинку — admin-banner
10
+ * Отличительная черта: фиолетовая шапка (variant="admin")
11
+ * БАНКЕТАМ.НЕТ (п.5): кнопки только «Банкет назначен» / «Банкет завершен»; без «Отменить».
12
+ * БАНКЕТАМ.НЕТ (М2): useState filterStatus, sortBy, page — select + table + «Страница 1 из N».
13
+ * БАНКЕТАМ.НЕТ: toast/alert при успешной смене статуса (message уже есть).
14
+ *
15
+ * load(): adminGetRequests() — таблица всех заявок
16
+ * updateStatus(id, status): PATCH /api/admin/requests/:id/status
17
+ * cancelled + cancelReason — для клининга; БАНКЕТАМ.НЕТ убрать кнопку «Отменить»
18
+ *
19
+ * ФИЛЬТР/СОРТ/СТРАНИЦЫ (М2): готовый пример кода — GUIDE_PAGES.md §8.4
20
+ * filterStatus state → query ?status= на сервере → admin.js WHERE
21
+ *
22
+ * GUIDE_PAGES.md §2.5 | server/routes/admin.js
23
+ * =============================================================================
24
+ */
25
+ import { useEffect, useState } from 'react';
26
+ import Layout from '../components/Layout.jsx';
27
+ import PageImage from '../components/PageImage.jsx';
28
+ import { api } from '../api.js';
29
+
30
+ export default function AdminPage() {
31
+ const [requests, setRequests] = useState([]);
32
+ const [loading, setLoading] = useState(true);
33
+ const [cancelReasons, setCancelReasons] = useState({});
34
+ const [message, setMessage] = useState('');
35
+
36
+ const load = () => {
37
+ setLoading(true);
38
+ api
39
+ .adminGetRequests()
40
+ .then(setRequests)
41
+ .catch((e) => setMessage(e.message))
42
+ .finally(() => setLoading(false));
43
+ };
44
+
45
+ useEffect(() => {
46
+ load();
47
+ }, []);
48
+
49
+ const updateStatus = async (id, status) => {
50
+ setMessage('');
51
+ const cancelReason = cancelReasons[id];
52
+ if (status === 'cancelled' && !cancelReason?.trim()) {
53
+ setMessage('Для отмены укажите причину в поле под заявкой');
54
+ return;
55
+ }
56
+ try {
57
+ await api.adminUpdateStatus(id, { status, cancelReason });
58
+ load();
59
+ } catch (e) {
60
+ setMessage(e.data?.message || e.message);
61
+ }
62
+ };
63
+
64
+ return (
65
+ <Layout variant="admin">
66
+ <div className="animate-fade-up">
67
+ <PageImage imageKey="adminBanner" className="w-full h-20 object-cover rounded-xl mb-4" />
68
+
69
+ {/* БАНКЕТАМ.НЕТ: фильтр/сортировка/пагинация — см. комментарий в шапке файла */}
70
+ <h2 className="text-xl font-bold text-white mb-4">Панель администратора</h2>
71
+ {message && <p className="text-amber-200 text-sm mb-3">{message}</p>}
72
+ {loading && <p className="text-white/70">Загрузка…</p>}
73
+
74
+ <div className="space-y-4">
75
+ {requests.map((r) => (
76
+ <article
77
+ key={r.id}
78
+ className="bg-white rounded-xl p-4 shadow text-sm animate-slide-in"
79
+ >
80
+ <div className="flex justify-between gap-2 mb-2">
81
+ <strong className="text-violet-900">#{r.id}</strong>
82
+ <span className="text-xs bg-slate-100 px-2 py-0.5 rounded">{r.statusLabel}</span>
83
+ </div>
84
+ <p>
85
+ <span className="text-slate-500">Заказчик:</span> {r.userFullName}
86
+ </p>
87
+ <p>
88
+ <span className="text-slate-500">Контакты:</span> {r.contactPhone}, {r.userEmail}
89
+ </p>
90
+ <p>
91
+ <span className="text-slate-500">Адрес:</span> {r.address}
92
+ </p>
93
+ <p>
94
+ <span className="text-slate-500">Услуга:</span> {r.serviceName}
95
+ </p>
96
+ <p>
97
+ <span className="text-slate-500">Когда:</span>{' '}
98
+ {new Date(r.scheduledAt).toLocaleString('ru-RU')} · {r.paymentLabel}
99
+ </p>
100
+
101
+ {/* БАНКЕТАМ.НЕТ: кнопки «Банкет назначен» (in_progress) и «Банкет завершен» (completed); убрать «Отменить» */}
102
+ <div className="mt-3 flex flex-wrap gap-2">
103
+ {r.status === 'new' && (
104
+ <button
105
+ type="button"
106
+ onClick={() => updateStatus(r.id, 'in_progress')}
107
+ className="px-3 py-1.5 rounded-lg bg-blue-600 text-white text-xs hover:bg-blue-700"
108
+ >
109
+ В работе
110
+ </button>
111
+ )}
112
+ {r.status !== 'completed' && r.status !== 'cancelled' && (
113
+ <button
114
+ type="button"
115
+ onClick={() => updateStatus(r.id, 'completed')}
116
+ className="px-3 py-1.5 rounded-lg bg-emerald-600 text-white text-xs hover:bg-emerald-700"
117
+ >
118
+ Выполнено
119
+ </button>
120
+ )}
121
+ {r.status !== 'cancelled' && (
122
+ <button
123
+ type="button"
124
+ onClick={() => updateStatus(r.id, 'cancelled')}
125
+ className="px-3 py-1.5 rounded-lg bg-red-600 text-white text-xs hover:bg-red-700"
126
+ >
127
+ Отменить
128
+ </button>
129
+ )}
130
+ </div>
131
+
132
+ {r.status !== 'cancelled' && (
133
+ <input
134
+ placeholder="Причина отмены (обязательно при отмене)"
135
+ className="mt-2 w-full text-xs border rounded px-2 py-1"
136
+ value={cancelReasons[r.id] || ''}
137
+ onChange={(e) =>
138
+ setCancelReasons((prev) => ({ ...prev, [r.id]: e.target.value }))
139
+ }
140
+ />
141
+ )}
142
+ {r.cancelReason && (
143
+ <p className="text-red-600 text-xs mt-1">Отмена: {r.cancelReason}</p>
144
+ )}
145
+ </article>
146
+ ))}
147
+ </div>
148
+ </div>
149
+ </Layout>
150
+ );
151
+ }