@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,21 @@
1
+ {
2
+ "name": "exam-template-client",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "react": "^19.1.0",
12
+ "react-dom": "^19.1.0",
13
+ "react-router-dom": "^7.6.1"
14
+ },
15
+ "devDependencies": {
16
+ "@tailwindcss/vite": "^4.1.7",
17
+ "@vitejs/plugin-react": "^4.5.0",
18
+ "tailwindcss": "^4.1.7",
19
+ "vite": "^6.3.5"
20
+ }
21
+ }
@@ -0,0 +1,26 @@
1
+ ПАПКА КАРТИНОК ДЛЯ ФРОНТА (экзамен / Figma)
2
+ ============================================
3
+
4
+ 1. Экспортируйте макеты из Figma в PNG (или оставьте SVG-заглушки).
5
+ 2. Кладите файлы СЮДА: client/public/images/
6
+ 3. В браузере они доступны как: /images/имя-файла.png
7
+
8
+ Список файлов (имена менять можно, но тогда правьте client/src/config/images.js):
9
+
10
+ logo.png — логотип в шапке (или logo.svg)
11
+ home-hero.png — главная страница
12
+ register-banner.png — регистрация
13
+ login-banner.png — вход
14
+ requests-banner.png — список заявок
15
+ new-request-banner.png — форма новой заявки
16
+ admin-banner.png — панель администратора
17
+ empty-requests.png — пустой список заявок
18
+
19
+ ЛЕНДИНГ (/):
20
+ slide-1.png … slide-3.png — слайдер
21
+ about-cleaning.png — блок «О клининге»
22
+ footer-photo-1.png … 3.png — круглые фото в футере
23
+
24
+ При смене темы: замените картинки на свои, подписи alt — в config/images.js
25
+
26
+ БАНКЕТАМ.НЕТ: slide-1…slide-4.png (4 слайда); тексты банкетов в HeroSlider.jsx
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="400" height="280" viewBox="0 0 400 280">
2
+ <rect width="400" height="280" rx="16" fill="#e0f2f1"/>
3
+ <text x="200" y="145" text-anchor="middle" fill="#0f766e" font-family="sans-serif" font-size="13">about-cleaning.png</text>
4
+ </svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="360" height="80" viewBox="0 0 360 80"><rect width="360" height="80" rx="12" fill="#c4b5fd"/><text x="180" y="48" text-anchor="middle" fill="#4c1d95" font-size="12" font-family="sans-serif">admin-banner</text></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="200" height="120" viewBox="0 0 200 120"><rect width="200" height="120" rx="8" fill="#f1f5f9"/><text x="100" y="65" text-anchor="middle" fill="#64748b" font-size="10" font-family="sans-serif">=5B 70O2>:</text></svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">
2
+ <rect width="120" height="120" rx="60" fill="#cbd5e1"/>
3
+ <text x="60" y="65" text-anchor="middle" fill="#64748b" font-size="9" font-family="sans-serif">D>B> 1</text>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">
2
+ <rect width="120" height="120" rx="60" fill="#cbd5e1"/>
3
+ <text x="60" y="65" text-anchor="middle" fill="#64748b" font-size="9" font-family="sans-serif">D>B> 2</text>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">
2
+ <rect width="120" height="120" rx="60" fill="#cbd5e1"/>
3
+ <text x="60" y="65" text-anchor="middle" fill="#64748b" font-size="9" font-family="sans-serif">D>B> 3</text>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="360" height="160" viewBox="0 0 360 160">
2
+ <rect width="360" height="160" rx="16" fill="#ccfbf1"/>
3
+ <text x="180" y="85" text-anchor="middle" fill="#0f766e" font-family="sans-serif" font-size="14">home-hero  70<5=8B5 PNG 87 Figma</text>
4
+ </svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="360" height="100" viewBox="0 0 360 100"><rect width="360" height="100" rx="12" fill="#cbd5e1"/><text x="180" y="55" text-anchor="middle" fill="#1e293b" font-size="12" font-family="sans-serif">login-banner</text></svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="120" height="40" viewBox="0 0 120 40">
2
+ <rect width="120" height="40" rx="8" fill="#0d9488"/>
3
+ <text x="60" y="26" text-anchor="middle" fill="white" font-family="sans-serif" font-size="11" font-weight="bold">>9 5 !0<</text>
4
+ </svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="360" height="80" viewBox="0 0 360 80"><rect width="360" height="80" rx="12" fill="#a5f3fc"/><text x="180" y="48" text-anchor="middle" fill="#155e75" font-size="12" font-family="sans-serif">new-request-banner</text></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="360" height="100" viewBox="0 0 360 100"><rect width="360" height="100" rx="12" fill="#99f6e4"/><text x="180" y="55" text-anchor="middle" fill="#134e4a" font-size="12" font-family="sans-serif">register-banner</text></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="360" height="80" viewBox="0 0 360 80"><rect width="360" height="80" rx="12" fill="#5eead4"/><text x="180" y="48" text-anchor="middle" fill="#115e59" font-size="12" font-family="sans-serif">requests-banner</text></svg>
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="800" height="400" viewBox="0 0 800 400">
2
+ <defs><linearGradient id="g1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#0d9488"/><stop offset="100%" style="stop-color:#0891b2"/></linearGradient></defs>
3
+ <rect width="800" height="400" fill="url(#g1)"/>
4
+ <text x="400" y="200" text-anchor="middle" fill="white" font-family="sans-serif" font-size="22" font-weight="bold">1I89 :;8=8=3</text>
5
+ <text x="400" y="230" text-anchor="middle" fill="#ccfbf1" font-size="14">slide-1.png  70<5=8B5 87 Figma</text>
6
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="800" height="400" viewBox="0 0 800 400">
2
+ <rect width="800" height="400" fill="#0f766e"/>
3
+ <text x="400" y="200" text-anchor="middle" fill="white" font-family="sans-serif" font-size="22" font-weight="bold">5=5@0;L=0O C1>@:0</text>
4
+ <text x="400" y="230" text-anchor="middle" fill="#99f6e4" font-size="14">slide-2.png</text>
5
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="800" height="400" viewBox="0 0 800 400">
2
+ <rect width="800" height="400" fill="#115e59"/>
3
+ <text x="400" y="200" text-anchor="middle" fill="white" font-family="sans-serif" font-size="22" font-weight="bold">%8<G8AB:0 :>2@>2 8 <515;8</text>
4
+ <text x="400" y="230" text-anchor="middle" fill="#a7f3d0" font-size="14">slide-3.png</text>
5
+ </svg>
@@ -0,0 +1,72 @@
1
+ /**
2
+ * =============================================================================
3
+ * App.jsx — МАРШРУТЫ REACT (react-router-dom)
4
+ * =============================================================================
5
+ * Каждый пункт задания ДЭ = отдельный path + страница в pages/
6
+ *
7
+ * п.1 /register → RegisterPage.jsx
8
+ * п.2 /login → LoginPage.jsx
9
+ * п.3 /requests → RequestsPage.jsx (история + кнопка на форму)
10
+ * п.4 /requests/form → RequestFormPage.jsx (форма заявки)
11
+ * п.5 /admin → AdminPage.jsx (только role=admin)
12
+ * / LandingPage.jsx — лендинг для всех (гость и после входа по логотипу)
13
+ *
14
+ * ProtectedRoute role="user"|"admin" — без токена редирект на /login
15
+ * /requests/new → редирект на /requests/form (старый путь, можно не трогать)
16
+ * path="*" → на главную /
17
+ *
18
+ * НОВАЯ СТРАНИЦА: import + <Route path="/about" element={...} /> — см. GUIDE_PAGES.md §6
19
+ * БАНКЕТАМ.НЕТ: пути те же; тексты и логика — в pages/, не здесь
20
+ * =============================================================================
21
+ */
22
+ import { Routes, Route, Navigate } from 'react-router-dom';
23
+ import ProtectedRoute from './components/ProtectedRoute.jsx';
24
+ import LandingPage from './pages/LandingPage.jsx';
25
+ import RegisterPage from './pages/RegisterPage.jsx';
26
+ import LoginPage from './pages/LoginPage.jsx';
27
+ import RequestsPage from './pages/RequestsPage.jsx';
28
+ import RequestFormPage from './pages/RequestFormPage.jsx';
29
+ import AdminPage from './pages/AdminPage.jsx';
30
+ export default function App() {
31
+ return (
32
+ <Routes>
33
+ {/* Лендинг для всех; по клику «Мой Не Сам» — сюда же */}
34
+ <Route path="/" element={<LandingPage />} />
35
+ <Route path="/register" element={<RegisterPage />} />
36
+ <Route path="/login" element={<LoginPage />} />
37
+
38
+ {/* п.3 — страница создания заявки */}
39
+ <Route
40
+ path="/requests"
41
+ element={
42
+ <ProtectedRoute role="user">
43
+ <RequestsPage />
44
+ </ProtectedRoute>
45
+ }
46
+ />
47
+
48
+ {/* п.4 — страница формирования заявки */}
49
+ <Route
50
+ path="/requests/form"
51
+ element={
52
+ <ProtectedRoute role="user">
53
+ <RequestFormPage />
54
+ </ProtectedRoute>
55
+ }
56
+ />
57
+
58
+ {/* старый путь — редирект на п.4 */}
59
+ <Route path="/requests/new" element={<Navigate to="/requests/form" replace />} />
60
+
61
+ <Route
62
+ path="/admin"
63
+ element={
64
+ <ProtectedRoute role="admin">
65
+ <AdminPage />
66
+ </ProtectedRoute>
67
+ }
68
+ />
69
+ <Route path="*" element={<Navigate to="/" replace />} />
70
+ </Routes>
71
+ );
72
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * =============================================================================
3
+ * HTTP-ЗАПРОСЫ К API (все вызовы бэкенда в одном файле)
4
+ * =============================================================================
5
+ * Пути /api/... проксируются Vite на localhost:3001 (vite.config.js).
6
+ * При смене темы endpoints чаще всего те же; меняют тела запросов в routes на сервере.
7
+ *
8
+ * TOKEN_KEY — ключ в localStorage; можно не менять.
9
+ * БАНКЕТАМ.НЕТ: добавить postReview(requestId, { text }) → POST /api/requests/:id/review
10
+ *
11
+ * КАК ДОБАВИТЬ НОВЫЙ ЗАПРОС:
12
+ * 1) Метод в server/routes/...
13
+ * 2) Строка в export const api = { ... }
14
+ * 3) Вызов на странице: await api.myMethod(...)
15
+ *
16
+ * Ошибки с сервера: catch (err) { err.data?.errors } — объект полей для формы
17
+ * err.data?.message — одна строка (логин неверный)
18
+ *
19
+ * GUIDE_PAGES.md §8 | THEME_BANQUETAM_NET.md
20
+ * =============================================================================
21
+ */
22
+
23
+ const TOKEN_KEY = 'exam_token';
24
+
25
+ export function getToken() {
26
+ return localStorage.getItem(TOKEN_KEY);
27
+ }
28
+
29
+ export function setToken(token) {
30
+ if (token) localStorage.setItem(TOKEN_KEY, token);
31
+ else localStorage.removeItem(TOKEN_KEY);
32
+ }
33
+
34
+ async function request(path, options = {}) {
35
+ const headers = { 'Content-Type': 'application/json', ...options.headers };
36
+ const token = getToken();
37
+ if (token) headers.Authorization = `Bearer ${token}`;
38
+
39
+ const res = await fetch(`/api${path}`, { ...options, headers });
40
+ const data = await res.json().catch(() => ({}));
41
+
42
+ if (!res.ok) {
43
+ const err = new Error(data.message || 'Ошибка запроса');
44
+ err.status = res.status;
45
+ err.data = data;
46
+ throw err;
47
+ }
48
+ return data;
49
+ }
50
+
51
+ export const api = {
52
+ // --- Авторизация → server/routes/auth.js (п.1 register, п.2 login) ---
53
+ // body register: { login, password, fullName, phone, email }
54
+ register: (body) => request('/auth/register', { method: 'POST', body: JSON.stringify(body) }),
55
+ login: (body) => request('/auth/login', { method: 'POST', body: JSON.stringify(body) }),
56
+ me: () => request('/auth/me'), // проверка токена при F5
57
+
58
+ // --- Справочник → server/routes/services.js + db/seed.sql (помещения/услуги) ---
59
+ getServices: () => request('/services'),
60
+
61
+ // --- Заявки пользователя → server/routes/requests.js (п.3 mine, п.4 POST) ---
62
+ getMyRequests: () => request('/requests/mine'),
63
+ createRequest: (body) => request('/requests', { method: 'POST', body: JSON.stringify(body) }),
64
+ // БАНКЕТАМ.НЕТ: postReview: (id, body) => request(`/requests/${id}/review`, { method: 'POST', body: JSON.stringify(body) }),
65
+
66
+ // --- Админ → server/routes/admin.js (п.5) ---
67
+ // БАНКЕТАМ.НЕТ: adminGetRequests: (params) => request(`/admin/requests?status=${params.status}`)
68
+ adminGetRequests: () => request('/admin/requests'),
69
+ adminUpdateStatus: (id, body) =>
70
+ request(`/admin/requests/${id}/status`, { method: 'PATCH', body: JSON.stringify(body) }),
71
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * =============================================================================
3
+ * FormField.jsx — ПОЛЕ ФОРМЫ (label + input + ошибка)
4
+ * =============================================================================
5
+ * САМ КОМПОНЕНТ не меняют. Меняют на страницах (RegisterPage, RequestFormPage):
6
+ * <FormField label="ФИО" error={errors.fullName}>
7
+ * <input ... />
8
+ * </FormField>
9
+ *
10
+ * error={errors.xxx} — текст с validateRegistration / validateRequestForm (validation.js).
11
+ * Добавить новое поле: label + children (input/select/textarea) + ключ в errors на странице.
12
+ *
13
+ * GUIDE_PAGES.md §2.4, §7.1
14
+ * =============================================================================
15
+ */
16
+ export default function FormField({ label, error, children, className = '' }) {
17
+ return (
18
+ <label className={`block mb-4 ${className}`}>
19
+ {/* Подпись поля — задаётся на странице (RegisterPage, NewRequestPage…) */}
20
+ <span className="block text-sm font-medium text-slate-700 mb-1">{label}</span>
21
+ {children}
22
+ {error && <p className="field-error">{error}</p>}
23
+ </label>
24
+ );
25
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * =============================================================================
3
+ * LAYOUT — общая обёртка всех страниц (шапка + контент)
4
+ * =============================================================================
5
+ * ЗАМЕНИТЕ при другой теме:
6
+ * brandName — название портала («Мой Не Сам» → «Автосервис», «Курсы»…)
7
+ * brandTagline — короткий слоган под логотипом
8
+ * headerStyles — цвета шапки по разделам (каждый раздел — своя «черта» по заданию)
9
+ * logo — картинка в config/images.js → file: 'logo.png'
10
+ *
11
+ * АДАПТИВ (мобильный / ПК):
12
+ * Сейчас: max-w-lg — узкая колонка (~390–512px), как макет 390×844.
13
+ * Для ПК добавьте lg:max-w-5xl lg:px-8 к header и main (см. RESPONSIVE.md).
14
+ * БАНКЕТАМ.НЕТ: brandName = 'Банкетам.Нет'; brandTagline = 'Бронирование банкетных залов'.
15
+ *
16
+ * variant со страницы: register | login | dashboard | admin | default
17
+ * → разный цвет шапки (отличительные черты по заданию)
18
+ * UserNav — только user + variant dashboard (п.3/п.4 в шапке)
19
+ * Link to="/" на логотипе — после входа возврат на лендинг
20
+ *
21
+ * GUIDE_PAGES.md §2 | RESPONSIVE.md (max-w-lg / lg:max-w-5xl)
22
+ * =============================================================================
23
+ */
24
+ import { Link, useNavigate } from 'react-router-dom';
25
+ import { useAuth } from '../context/AuthContext.jsx';
26
+ import PageImage from './PageImage.jsx';
27
+ import UserNav from './UserNav.jsx';
28
+
29
+ // --- Тексты бренда (менять на экзамене в первую очередь) ---
30
+ const brandName = 'Мой Не Сам'; // БАНКЕТАМ.НЕТ: 'Банкетам.Нет'
31
+ const brandTagline = 'Клининг без хлопот'; // БАНКЕТАМ.НЕТ: 'Бронирование банкетных залов'
32
+
33
+ // --- Стили шапки: variant передаётся со страницы (register, login, dashboard, admin) ---
34
+ const headerStyles = {
35
+ default: 'bg-white border-b border-slate-200 text-slate-800',
36
+ register: 'bg-teal-700 text-white',
37
+ login: 'bg-slate-800 text-white',
38
+ dashboard: 'bg-gradient-to-r from-teal-600 to-cyan-600 text-white',
39
+ admin: 'bg-violet-900 text-white',
40
+ };
41
+
42
+ export default function Layout({ children, variant = 'default' }) {
43
+ const { user, logout } = useAuth();
44
+ const navigate = useNavigate();
45
+
46
+ return (
47
+ <div className="min-h-screen flex flex-col">
48
+ {/* === ШАПКА: логотип-картинка + название === */}
49
+ <header className={`${headerStyles[variant]} shadow-sm animate-fade-up`}>
50
+ <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">
51
+ <Link to="/" className="flex items-center gap-2 min-w-0">
52
+ {/* Картинка: client/public/images/ + ключ logo в config/images.js */}
53
+ <PageImage imageKey="logo" className="h-9 lg:h-11 w-auto shrink-0" />
54
+ <span className="font-semibold text-lg tracking-tight truncate">{brandName}</span>
55
+ </Link>
56
+
57
+ {user ? (
58
+ <div className="flex items-center gap-2 shrink-0">
59
+ {user.role === 'user' && variant === 'dashboard' && <UserNav />}
60
+ <button
61
+ type="button"
62
+ onClick={() => {
63
+ logout();
64
+ navigate('/login');
65
+ }}
66
+ className="text-sm opacity-90 hover:opacity-100"
67
+ >
68
+ Выйти
69
+ </button>
70
+ </div>
71
+ ) : (
72
+ <span className="text-xs opacity-80 shrink-0 hidden sm:inline">{brandTagline}</span>
73
+ )}
74
+ </div>
75
+ </header>
76
+
77
+ {/* max-w-lg — мобильный макет; lg:max-w-5xl — шире на ПК (см. RESPONSIVE.md) */}
78
+ <main className="flex-1 mx-auto w-full max-w-lg lg:max-w-5xl px-4 lg:px-8 py-6 lg:py-10">
79
+ {children}
80
+ </main>
81
+ </div>
82
+ );
83
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * =============================================================================
3
+ * PageImage.jsx — КАРТИНКА НА СТРАНИЦЕ (единый способ)
4
+ * =============================================================================
5
+ * КАК МЕНЯТЬ КАРТИНКУ:
6
+ * 1) Файл в client/public/images/ (например banner.png)
7
+ * 2) Запись в client/src/config/images.js: myKey: { file: 'banner.png', alt: '...' }
8
+ * 3) На странице: <PageImage imageKey="myKey" className="w-full h-40 object-cover" />
9
+ *
10
+ * НЕ пишите <img src="/images/..."> напрямую в pages — иначе на экзамене запутаетесь.
11
+ *
12
+ * className — размер: h-9 (логотип), h-40 (баннер), rounded-full (аватар в футере).
13
+ * onError — скрывает битую картинку; при отладке временно закомментируйте.
14
+ *
15
+ * GUIDE_PAGES.md §3 | БАНКЕТАМ.НЕТ: slide-1…4, footer-photo-1…3
16
+ * =============================================================================
17
+ */
18
+ import { imageAlt, imageUrl } from '../config/images.js';
19
+
20
+ export default function PageImage({ imageKey, className = 'w-full h-auto rounded-xl object-cover' }) {
21
+ const src = imageUrl(imageKey);
22
+ const alt = imageAlt(imageKey);
23
+
24
+ if (!src) return null;
25
+
26
+ return (
27
+ <img
28
+ src={src}
29
+ alt={alt}
30
+ className={className}
31
+ loading="lazy"
32
+ /* при ошибке загрузки (забыли положить PNG) — скрываем, чтобы не ломать вёрстку */
33
+ onError={(e) => {
34
+ e.currentTarget.style.display = 'none';
35
+ }}
36
+ />
37
+ );
38
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * =============================================================================
3
+ * ЗАЩИТА МАРШРУТОВ — только для авторизованных
4
+ * =============================================================================
5
+ * role="user" — обычный заказчик (/requests)
6
+ * role="admin" — панель админа (/admin)
7
+ * При другой теме роли те же: user | admin (server/db/schema.sql users.role)
8
+ * БАНКЕТАМ.НЕТ: user → /requests (личный кабинет), admin → /admin
9
+ *
10
+ * loading — пока читается токен из localStorage и вызывается /api/auth/me
11
+ * !user — гость → /login
12
+ * user.role !== role — админ зашёл на /requests → редирект на /admin и наоборот
13
+ *
14
+ * Менять редко. Новый раздел «только для менеджера» — добавить role в БД + сюда.
15
+ * =============================================================================
16
+ */
17
+ import { Navigate } from 'react-router-dom';
18
+ import { useAuth } from '../context/AuthContext.jsx';
19
+
20
+ export default function ProtectedRoute({ children, role }) {
21
+ const { user, loading } = useAuth();
22
+
23
+ // Пока AuthContext проверяет токен — не редиректим преждевременно
24
+ if (loading) {
25
+ return <p className="text-center text-slate-500 animate-pulse py-12">Загрузка…</p>;
26
+ }
27
+
28
+ if (!user) return <Navigate to="/login" replace />;
29
+
30
+ if (role && user.role !== role) {
31
+ return <Navigate to={user.role === 'admin' ? '/admin' : '/requests'} replace />;
32
+ }
33
+
34
+ return children;
35
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * =============================================================================
3
+ * UserNav.jsx — ВКЛАДКИ В ШАПКЕ (только заказчик, variant="dashboard")
4
+ * =============================================================================
5
+ * Показывается в Layout.jsx когда user.role === 'user' и variant === 'dashboard'.
6
+ * NavLink подсвечивает активный раздел (isActive → bg-white/30).
7
+ *
8
+ * ЗАМЕНИТЕ: текст ссылок и title (подсказка при наведении).
9
+ * Пути /requests и /requests/form — по заданию ДЭ, обычно не меняют.
10
+ *
11
+ * БАНКЕТАМ.НЕТ: «Личный кабинет» → /requests; «Новое бронирование» → /requests/form
12
+ * GUIDE_PAGES.md §2.3
13
+ * =============================================================================
14
+ */
15
+ import { NavLink } from 'react-router-dom';
16
+
17
+ const linkClass = ({ isActive }) =>
18
+ `text-xs px-2 py-1 rounded-md transition ${
19
+ isActive ? 'bg-white/30 font-semibold' : 'opacity-80 hover:opacity-100'
20
+ }`;
21
+
22
+ export default function UserNav() {
23
+ return (
24
+ <nav className="flex gap-1 shrink-0" aria-label="Разделы заявок">
25
+ <NavLink to="/requests" className={linkClass} end title="п.3 Создание заявки">
26
+ Мои заявки
27
+ </NavLink>
28
+ <NavLink to="/requests/form" className={linkClass} title="п.4 Формирование заявки">
29
+ Новая заявка
30
+ </NavLink>
31
+ </nav>
32
+ );
33
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * =============================================================================
3
+ * HeroSlider.jsx — СЛАЙДЕР НА ЛЕНДИНГЕ (модуль 2 / М2)
4
+ * =============================================================================
5
+ * 1) SLIDE_KEYS — ключи из config/images.js (slide1, slide2…)
6
+ * 2) slideText — заголовок и подзаголовок на каждом слайде
7
+ * 3) public/images/slide-1.png … — файлы картинок (имена в images.js → file)
8
+ * 4) setInterval — автопрокрутка (БАНКЕТАМ.НЕТ: 3000 мс, 4 слайда)
9
+ *
10
+ * Кнопки ‹ › и точки внизу — переключение вручную (go, setIndex).
11
+ * БАНКЕТАМ.НЕТ: скопировать <HeroSlider /> в RequestsPage для п.3
12
+ *
13
+ * GUIDE_PAGES.md §3.2 | config/images.js (slide1…slide4)
14
+ * =============================================================================
15
+ */
16
+ import { useEffect, useState } from 'react';
17
+ import { imageAlt, imageUrl } from '../../config/images.js';
18
+
19
+ const SLIDE_KEYS = ['slide1', 'slide2', 'slide3']; // БАНКЕТАМ.НЕТ: добавить 'slide4'
20
+
21
+ const slideText = {
22
+ slide1: { title: 'Общий клининг', subtitle: 'Поддерживаем порядок регулярно' },
23
+ slide2: { title: 'Генеральная уборка', subtitle: 'Глубокая очистка всех поверхностей' },
24
+ slide3: { title: 'Химчистка', subtitle: 'Ковры, мебель, деликатные ткани' },
25
+ };
26
+
27
+ export default function HeroSlider() {
28
+ const [index, setIndex] = useState(0);
29
+
30
+ useEffect(() => {
31
+ const timer = setInterval(() => {
32
+ setIndex((i) => (i + 1) % SLIDE_KEYS.length);
33
+ }, 5000); // БАНКЕТАМ.НЕТ: 3000
34
+ return () => clearInterval(timer);
35
+ }, []);
36
+
37
+ const key = SLIDE_KEYS[index];
38
+ const text = slideText[key];
39
+
40
+ const go = (dir) => {
41
+ setIndex((i) => (i + dir + SLIDE_KEYS.length) % SLIDE_KEYS.length);
42
+ };
43
+
44
+ return (
45
+ <section className="relative w-full overflow-hidden bg-teal-900" aria-label="Слайдер услуг">
46
+ <div className="relative h-52 sm:h-64 lg:h-80">
47
+ {SLIDE_KEYS.map((k, i) => (
48
+ <div
49
+ key={k}
50
+ className={`absolute inset-0 transition-opacity duration-700 ${
51
+ i === index ? 'opacity-100 z-10' : 'opacity-0 z-0'
52
+ }`}
53
+ >
54
+ <img
55
+ src={imageUrl(k)}
56
+ alt={imageAlt(k)}
57
+ className="w-full h-full object-cover"
58
+ />
59
+ <div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent" />
60
+ </div>
61
+ ))}
62
+
63
+ <div className="absolute inset-0 z-20 flex flex-col justify-end p-6 lg:p-10 max-w-5xl mx-auto">
64
+ <h2 className="text-2xl lg:text-4xl font-bold text-white drop-shadow animate-fade-up">
65
+ {text.title}
66
+ </h2>
67
+ <p className="text-teal-100 text-sm lg:text-base mt-1">{text.subtitle}</p>
68
+ </div>
69
+
70
+ <button
71
+ type="button"
72
+ onClick={() => go(-1)}
73
+ className="absolute left-2 top-1/2 -translate-y-1/2 z-30 w-10 h-10 rounded-full bg-white/20 text-white hover:bg-white/40 backdrop-blur"
74
+ aria-label="Предыдущий слайд"
75
+ >
76
+
77
+ </button>
78
+ <button
79
+ type="button"
80
+ onClick={() => go(1)}
81
+ className="absolute right-2 top-1/2 -translate-y-1/2 z-30 w-10 h-10 rounded-full bg-white/20 text-white hover:bg-white/40 backdrop-blur"
82
+ aria-label="Следующий слайд"
83
+ >
84
+
85
+ </button>
86
+ </div>
87
+
88
+ <div className="absolute bottom-3 left-0 right-0 z-30 flex justify-center gap-2">
89
+ {SLIDE_KEYS.map((k, i) => (
90
+ <button
91
+ key={k}
92
+ type="button"
93
+ onClick={() => setIndex(i)}
94
+ className={`h-2 rounded-full transition-all ${
95
+ i === index ? 'w-8 bg-white' : 'w-2 bg-white/50'
96
+ }`}
97
+ aria-label={`Слайд ${i + 1}`}
98
+ />
99
+ ))}
100
+ </div>
101
+ </section>
102
+ );
103
+ }