@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,83 @@
1
+
2
+
3
+ /** Базовый путь. Менять не нужно, если папка называется images */
4
+ export const IMAGES_BASE = '/images';
5
+
6
+ /**
7
+ * Карта картинок по страницам.
8
+ * Ключ (logo, homeHero…) — не трогать, менять только file и alt.
9
+ */
10
+ export const IMAGES = {
11
+ // Шапка на всех страницах → логотип компании / портала
12
+ logo: {
13
+ file: 'logo.svg',
14
+ alt: 'Логотип «Мой Не Сам»', // → название вашей организации
15
+ },
16
+
17
+ // Главная (/) — иллюстрация услуги
18
+ homeHero: {
19
+ file: 'home-hero.svg',
20
+ alt: 'Клининговые услуги на дому',
21
+ },
22
+
23
+ // Регистрация (/register)
24
+ registerBanner: {
25
+ file: 'register-banner.svg',
26
+ alt: 'Регистрация нового заказчика',
27
+ },
28
+
29
+ // Вход (/login)
30
+ loginBanner: {
31
+ file: 'login-banner.svg',
32
+ alt: 'Вход в личный кабинет',
33
+ },
34
+
35
+ // Список заявок (/requests)
36
+ requestsBanner: {
37
+ file: 'requests-banner.svg',
38
+ alt: 'История заявок на уборку',
39
+ },
40
+
41
+ // Новая заявка (/requests/new)
42
+ newRequestBanner: {
43
+ file: 'new-request-banner.svg',
44
+ alt: 'Оформление заявки на услугу',
45
+ },
46
+
47
+ // Админ-панель (/admin)
48
+ adminBanner: {
49
+ file: 'admin-banner.svg',
50
+ alt: 'Панель администратора',
51
+ },
52
+
53
+ // Пустой список заявок
54
+ emptyRequests: {
55
+ file: 'empty-requests.svg',
56
+ alt: 'Заявок пока нет',
57
+ },
58
+
59
+ // --- Лендинг: слайдер (HeroSlider.jsx) ---
60
+ slide1: { file: 'slide-1.svg', alt: 'Общий клининг' },
61
+ slide2: { file: 'slide-2.svg', alt: 'Генеральная уборка' },
62
+ slide3: { file: 'slide-3.svg', alt: 'Химчистка ковров и мебели' },
63
+
64
+ // Лендинг: блок «О клининге»
65
+ aboutCleaning: { file: 'about-cleaning.svg', alt: 'Профессиональный клининг' },
66
+
67
+ // Лендинг: футер — 3 фото (команда / офис / работы)
68
+ footerPhoto1: { file: 'footer-photo-1.svg', alt: 'Сотрудник 1' },
69
+ footerPhoto2: { file: 'footer-photo-2.svg', alt: 'Сотрудник 2' },
70
+ footerPhoto3: { file: 'footer-photo-3.svg', alt: 'Сотрудник 3' },
71
+ };
72
+
73
+ /** Собрать полный URL для тега <img src="..."> */
74
+ export function imageUrl(imageKey) {
75
+ const item = IMAGES[imageKey];
76
+ if (!item) return '';
77
+ return `${IMAGES_BASE}/${item.file}`;
78
+ }
79
+
80
+ /** Текст alt по ключу */
81
+ export function imageAlt(imageKey) {
82
+ return IMAGES[imageKey]?.alt ?? '';
83
+ }
@@ -0,0 +1,7 @@
1
+
2
+ export const DEFAULT_SERVICE_NAMES = [
3
+ 'Общий клининг',
4
+ 'Генеральная уборка',
5
+ 'Послестроительная уборка',
6
+ 'Химчистка ковров и мебели',
7
+ ];
@@ -0,0 +1,55 @@
1
+
2
+ import { createContext, useContext, useEffect, useState } from 'react';
3
+ import { api, getToken, setToken } from '../api.js';
4
+
5
+ const AuthContext = createContext(null);
6
+
7
+ export function AuthProvider({ children }) {
8
+ const [user, setUser] = useState(null);
9
+ const [loading, setLoading] = useState(true);
10
+
11
+ // При загрузке приложения — проверить сохранённый токен
12
+ useEffect(() => {
13
+ const token = getToken();
14
+ if (!token) {
15
+ setLoading(false);
16
+ return;
17
+ }
18
+ api
19
+ .me()
20
+ .then(({ user: u }) => setUser(u))
21
+ .catch(() => setToken(null))
22
+ .finally(() => setLoading(false));
23
+ }, []);
24
+
25
+ const login = async (credentials) => {
26
+ const { user: u, token } = await api.login(credentials);
27
+ setToken(token);
28
+ setUser(u);
29
+ return u;
30
+ };
31
+
32
+ const register = async (data) => {
33
+ const { user: u, token } = await api.register(data);
34
+ setToken(token);
35
+ setUser(u);
36
+ return u;
37
+ };
38
+
39
+ const logout = () => {
40
+ setToken(null);
41
+ setUser(null);
42
+ };
43
+
44
+ return (
45
+ <AuthContext.Provider value={{ user, loading, login, register, logout }}>
46
+ {children}
47
+ </AuthContext.Provider>
48
+ );
49
+ }
50
+
51
+ export function useAuth() {
52
+ const ctx = useContext(AuthContext);
53
+ if (!ctx) throw new Error('useAuth вне AuthProvider');
54
+ return ctx;
55
+ }
@@ -0,0 +1,54 @@
1
+ @import 'tailwindcss';
2
+
3
+
4
+
5
+ @theme {
6
+ --color-brand: #0d9488;
7
+ --color-brand-dark: #0f766e;
8
+ --font-sans: 'Segoe UI', system-ui, sans-serif;
9
+ }
10
+
11
+ @layer base {
12
+ body {
13
+ @apply min-h-screen bg-slate-50 text-slate-800 antialiased;
14
+ font-family: var(--font-sans);
15
+ }
16
+ }
17
+
18
+ @keyframes fade-up {
19
+ from {
20
+ opacity: 0;
21
+ transform: translateY(12px);
22
+ }
23
+ to {
24
+ opacity: 1;
25
+ transform: translateY(0);
26
+ }
27
+ }
28
+
29
+ @keyframes slide-in {
30
+ from {
31
+ opacity: 0;
32
+ transform: translateX(-8px);
33
+ }
34
+ to {
35
+ opacity: 1;
36
+ transform: translateX(0);
37
+ }
38
+ }
39
+
40
+ .animate-fade-up {
41
+ animation: fade-up 0.45s ease-out both;
42
+ }
43
+
44
+ .animate-slide-in {
45
+ animation: slide-in 0.35s ease-out both;
46
+ }
47
+
48
+ .field-error {
49
+ @apply mt-1 text-sm text-red-600;
50
+ }
51
+
52
+ .input-error {
53
+ @apply border-red-400 ring-red-100;
54
+ }
@@ -0,0 +1,17 @@
1
+
2
+ import { StrictMode } from 'react';
3
+ import { createRoot } from 'react-dom/client';
4
+ import { BrowserRouter } from 'react-router-dom';
5
+ import App from './App.jsx';
6
+ import { AuthProvider } from './context/AuthContext.jsx';
7
+ import './index.css';
8
+
9
+ createRoot(document.getElementById('root')).render(
10
+ <StrictMode>
11
+ <BrowserRouter>
12
+ <AuthProvider>
13
+ <App />
14
+ </AuthProvider>
15
+ </BrowserRouter>
16
+ </StrictMode>
17
+ );
@@ -0,0 +1,128 @@
1
+
2
+ import { useEffect, useState } from 'react';
3
+ import Layout from '../components/Layout.jsx';
4
+ import PageImage from '../components/PageImage.jsx';
5
+ import { api } from '../api.js';
6
+
7
+ export default function AdminPage() {
8
+ const [requests, setRequests] = useState([]);
9
+ const [loading, setLoading] = useState(true);
10
+ const [cancelReasons, setCancelReasons] = useState({});
11
+ const [message, setMessage] = useState('');
12
+
13
+ const load = () => {
14
+ setLoading(true);
15
+ api
16
+ .adminGetRequests()
17
+ .then(setRequests)
18
+ .catch((e) => setMessage(e.message))
19
+ .finally(() => setLoading(false));
20
+ };
21
+
22
+ useEffect(() => {
23
+ load();
24
+ }, []);
25
+
26
+ const updateStatus = async (id, status) => {
27
+ setMessage('');
28
+ const cancelReason = cancelReasons[id];
29
+ if (status === 'cancelled' && !cancelReason?.trim()) {
30
+ setMessage('Для отмены укажите причину в поле под заявкой');
31
+ return;
32
+ }
33
+ try {
34
+ await api.adminUpdateStatus(id, { status, cancelReason });
35
+ load();
36
+ } catch (e) {
37
+ setMessage(e.data?.message || e.message);
38
+ }
39
+ };
40
+
41
+ return (
42
+ <Layout variant="admin">
43
+ <div className="animate-fade-up">
44
+ <PageImage imageKey="adminBanner" className="w-full h-20 object-cover rounded-xl mb-4" />
45
+
46
+
47
+ <h2 className="text-xl font-bold text-white mb-4">Панель администратора</h2>
48
+ {message && <p className="text-amber-200 text-sm mb-3">{message}</p>}
49
+ {loading && <p className="text-white/70">Загрузка…</p>}
50
+
51
+ <div className="space-y-4">
52
+ {requests.map((r) => (
53
+ <article
54
+ key={r.id}
55
+ className="bg-white rounded-xl p-4 shadow text-sm animate-slide-in"
56
+ >
57
+ <div className="flex justify-between gap-2 mb-2">
58
+ <strong className="text-violet-900">#{r.id}</strong>
59
+ <span className="text-xs bg-slate-100 px-2 py-0.5 rounded">{r.statusLabel}</span>
60
+ </div>
61
+ <p>
62
+ <span className="text-slate-500">Заказчик:</span> {r.userFullName}
63
+ </p>
64
+ <p>
65
+ <span className="text-slate-500">Контакты:</span> {r.contactPhone}, {r.userEmail}
66
+ </p>
67
+ <p>
68
+ <span className="text-slate-500">Адрес:</span> {r.address}
69
+ </p>
70
+ <p>
71
+ <span className="text-slate-500">Услуга:</span> {r.serviceName}
72
+ </p>
73
+ <p>
74
+ <span className="text-slate-500">Когда:</span>{' '}
75
+ {new Date(r.scheduledAt).toLocaleString('ru-RU')} · {r.paymentLabel}
76
+ </p>
77
+
78
+
79
+ <div className="mt-3 flex flex-wrap gap-2">
80
+ {r.status === 'new' && (
81
+ <button
82
+ type="button"
83
+ onClick={() => updateStatus(r.id, 'in_progress')}
84
+ className="px-3 py-1.5 rounded-lg bg-blue-600 text-white text-xs hover:bg-blue-700"
85
+ >
86
+ В работе
87
+ </button>
88
+ )}
89
+ {r.status !== 'completed' && r.status !== 'cancelled' && (
90
+ <button
91
+ type="button"
92
+ onClick={() => updateStatus(r.id, 'completed')}
93
+ className="px-3 py-1.5 rounded-lg bg-emerald-600 text-white text-xs hover:bg-emerald-700"
94
+ >
95
+ Выполнено
96
+ </button>
97
+ )}
98
+ {r.status !== 'cancelled' && (
99
+ <button
100
+ type="button"
101
+ onClick={() => updateStatus(r.id, 'cancelled')}
102
+ className="px-3 py-1.5 rounded-lg bg-red-600 text-white text-xs hover:bg-red-700"
103
+ >
104
+ Отменить
105
+ </button>
106
+ )}
107
+ </div>
108
+
109
+ {r.status !== 'cancelled' && (
110
+ <input
111
+ placeholder="Причина отмены (обязательно при отмене)"
112
+ className="mt-2 w-full text-xs border rounded px-2 py-1"
113
+ value={cancelReasons[r.id] || ''}
114
+ onChange={(e) =>
115
+ setCancelReasons((prev) => ({ ...prev, [r.id]: e.target.value }))
116
+ }
117
+ />
118
+ )}
119
+ {r.cancelReason && (
120
+ <p className="text-red-600 text-xs mt-1">Отмена: {r.cancelReason}</p>
121
+ )}
122
+ </article>
123
+ ))}
124
+ </div>
125
+ </div>
126
+ </Layout>
127
+ );
128
+ }
@@ -0,0 +1,115 @@
1
+
2
+ import { Link } from 'react-router-dom';
3
+ import { useAuth } from '../context/AuthContext.jsx';
4
+ import LandingLayout from '../components/landing/LandingLayout.jsx';
5
+ import HeroSlider from '../components/landing/HeroSlider.jsx';
6
+ import SiteFooter from '../components/landing/SiteFooter.jsx';
7
+ import PageImage from '../components/PageImage.jsx';
8
+ import { DEFAULT_SERVICE_NAMES } from '../constants/services.js';
9
+
10
+ const perks = [
11
+ 'Опытные клинеры и проверенные средства',
12
+ 'Удобная подача заявки онлайн',
13
+ 'Наличные или карта — как вам удобнее',
14
+ 'Жилые и производственные помещения',
15
+ ];
16
+
17
+ export default function LandingPage() {
18
+ const { user } = useAuth();
19
+ const cabinetPath = user?.role === 'admin' ? '/admin' : '/requests';
20
+
21
+ return (
22
+ <LandingLayout>
23
+ <HeroSlider />
24
+
25
+ <section className="mx-auto max-w-lg lg:max-w-5xl px-4 lg:px-8 py-10 lg:py-14">
26
+ <div className="lg:grid lg:grid-cols-2 lg:gap-10 lg:items-center animate-fade-up">
27
+ <PageImage
28
+ imageKey="aboutCleaning"
29
+ className="w-full h-48 lg:h-64 object-cover rounded-2xl shadow mb-6 lg:mb-0"
30
+ />
31
+ <div>
32
+ <h2 className="text-2xl font-bold text-teal-900 mb-3">О нашем клининге</h2>
33
+ <p className="text-slate-600 text-sm leading-relaxed mb-4">
34
+ «Мой Не Сам» — портал для заказа уборки жилых и производственных помещений.
35
+ Вы регистрируетесь, оформляете заявку с адресом и видом услуги, выбираете дату и
36
+ способ оплаты — мы берём задачу в работу.
37
+ </p>
38
+ <p className="text-slate-600 text-sm leading-relaxed">
39
+ Работаем аккуратно, с соблюдением сроков и прозрачными статусами заявки в личном
40
+ кабинете.
41
+ </p>
42
+ </div>
43
+ </div>
44
+ </section>
45
+
46
+ <section className="bg-teal-50 border-y border-teal-100 py-10 lg:py-14">
47
+ <div className="mx-auto max-w-lg lg:max-w-5xl px-4 lg:px-8">
48
+ <h2 className="text-xl font-bold text-teal-900 mb-6 text-center">Наши услуги</h2>
49
+ <ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
50
+ {DEFAULT_SERVICE_NAMES.map((name) => (
51
+ <li
52
+ key={name}
53
+ 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"
54
+ >
55
+ {name}
56
+ </li>
57
+ ))}
58
+ </ul>
59
+ </div>
60
+ </section>
61
+
62
+ <section className="mx-auto max-w-lg lg:max-w-5xl px-4 lg:px-8 py-10">
63
+ <h2 className="text-xl font-bold text-slate-800 mb-4">Почему выбирают нас</h2>
64
+ <ul className="space-y-3">
65
+ {perks.map((p) => (
66
+ <li key={p} className="flex gap-3 text-sm text-slate-600">
67
+ <span className="text-teal-600 font-bold">✓</span>
68
+ {p}
69
+ </li>
70
+ ))}
71
+ </ul>
72
+ </section>
73
+
74
+ <section className="mx-auto max-w-lg lg:max-w-5xl px-4 lg:px-8 pb-12">
75
+ <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">
76
+ <h2 className="text-xl font-bold mb-2">
77
+ {user ? 'Добро пожаловать!' : 'Готовы заказать уборку?'}
78
+ </h2>
79
+ <p className="text-teal-100 text-sm mb-6">
80
+ {user
81
+ ? 'Перейдите в кабинет, чтобы посмотреть заявки или оформить новую.'
82
+ : 'Зарегистрируйтесь за минуту и оформите первую заявку.'}
83
+ </p>
84
+ <div className="flex flex-col sm:flex-row gap-3 justify-center">
85
+ {user ? (
86
+ <Link
87
+ to={cabinetPath}
88
+ className="rounded-xl bg-white text-teal-800 py-3 px-6 font-semibold hover:bg-teal-50 transition"
89
+ >
90
+ {user.role === 'admin' ? 'Панель администратора' : 'Мои заявки'}
91
+ </Link>
92
+ ) : (
93
+ <>
94
+ <Link
95
+ to="/register"
96
+ className="rounded-xl bg-white text-teal-800 py-3 px-6 font-semibold hover:bg-teal-50 transition"
97
+ >
98
+ Зарегистрироваться
99
+ </Link>
100
+ <Link
101
+ to="/login"
102
+ className="rounded-xl border border-white/50 py-3 px-6 font-medium hover:bg-white/10 transition"
103
+ >
104
+ Уже есть аккаунт
105
+ </Link>
106
+ </>
107
+ )}
108
+ </div>
109
+ </div>
110
+ </section>
111
+
112
+ <SiteFooter />
113
+ </LandingLayout>
114
+ );
115
+ }
@@ -0,0 +1,63 @@
1
+
2
+ import { useState } from 'react';
3
+ import { Link, useNavigate } from 'react-router-dom';
4
+ import Layout from '../components/Layout.jsx';
5
+ import FormField from '../components/FormField.jsx';
6
+ import PageImage from '../components/PageImage.jsx';
7
+ import { useAuth } from '../context/AuthContext.jsx';
8
+
9
+ const inputClass =
10
+ 'w-full rounded-lg border border-slate-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-slate-400';
11
+
12
+ export default function LoginPage() {
13
+ const { login } = useAuth();
14
+ const navigate = useNavigate();
15
+ const [loginVal, setLoginVal] = useState('');
16
+ const [password, setPassword] = useState('');
17
+ const [error, setError] = useState('');
18
+
19
+ const submit = async (e) => {
20
+ e.preventDefault();
21
+ setError('');
22
+ if (!loginVal.trim() || !password) {
23
+ setError('Введите логин и пароль');
24
+ return;
25
+ }
26
+ try {
27
+ const user = await login({ login: loginVal, password });
28
+ navigate(user.role === 'admin' ? '/admin' : '/requests');
29
+ } catch (err) {
30
+ setError(err.data?.message || 'Неверный логин или пароль');
31
+ }
32
+ };
33
+
34
+ return (
35
+ <Layout variant="login">
36
+ <PageImage imageKey="loginBanner" className="w-full h-24 object-cover rounded-xl mb-4" />
37
+
38
+ <div className="bg-slate-100 rounded-2xl p-6 shadow animate-slide-in">
39
+ <h2 className="text-xl font-bold text-slate-800 mb-6">Вход в систему</h2>
40
+ <form onSubmit={submit}>
41
+ <FormField label="Логин">
42
+ <input className={inputClass} value={loginVal} onChange={(e) => setLoginVal(e.target.value)} autoComplete="username" />
43
+ </FormField>
44
+ <FormField label="Пароль">
45
+ <input type="password" className={inputClass} value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="current-password" />
46
+ </FormField>
47
+ {error && <p className="field-error mb-4 bg-red-50 border border-red-200 rounded-lg px-3 py-2">{error}</p>}
48
+ <button type="submit" className="w-full rounded-xl bg-slate-800 text-white py-3 font-semibold hover:bg-slate-900 transition">
49
+ Войти
50
+ </button>
51
+ </form>
52
+
53
+ <p className="text-center text-sm mt-4">
54
+ <Link to="/register" className="text-teal-700 underline">Создать аккаунт</Link>
55
+ </p>
56
+
57
+ <p className="text-xs text-slate-500 mt-6 text-center">
58
+ Админ: логин <code>adminka</code>, пароль <code>password</code>
59
+ </p>
60
+ </div>
61
+ </Layout>
62
+ );
63
+ }
@@ -0,0 +1,97 @@
1
+
2
+ import { useState } from 'react';
3
+ import { Link, useNavigate } from 'react-router-dom';
4
+ import Layout from '../components/Layout.jsx';
5
+ import FormField from '../components/FormField.jsx';
6
+ import PageImage from '../components/PageImage.jsx';
7
+ import { useAuth } from '../context/AuthContext.jsx';
8
+ import { formatPhoneInput, validateRegistration } from '../utils/validation.js';
9
+
10
+ const inputClass =
11
+ 'w-full rounded-lg border border-slate-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-teal-400';
12
+
13
+ export default function RegisterPage() {
14
+ const { register } = useAuth();
15
+ const navigate = useNavigate();
16
+
17
+ // --- Состояние формы: имена полей = ключи для API (login, fullName…) ---
18
+ const [form, setForm] = useState({
19
+ login: '',
20
+ password: '',
21
+ fullName: '',
22
+ phone: '',
23
+ email: '',
24
+ });
25
+ const [errors, setErrors] = useState({});
26
+ const [serverError, setServerError] = useState('');
27
+
28
+ const set = (field) => (e) => {
29
+ let value = e.target.value;
30
+ if (field === 'phone') value = formatPhoneInput(value);
31
+ setForm((f) => ({ ...f, [field]: value }));
32
+ setErrors((err) => ({ ...err, [field]: undefined }));
33
+ };
34
+
35
+ const submit = async (e) => {
36
+ e.preventDefault();
37
+ setServerError('');
38
+ const localErrors = validateRegistration(form);
39
+ if (Object.keys(localErrors).length) {
40
+ setErrors(localErrors);
41
+ return;
42
+ }
43
+ try {
44
+ const user = await register({
45
+ login: form.login,
46
+ password: form.password,
47
+ fullName: form.fullName,
48
+ phone: form.phone,
49
+ email: form.email,
50
+ });
51
+ navigate(user.role === 'admin' ? '/admin' : '/requests');
52
+ } catch (err) {
53
+ if (err.data?.errors) setErrors(err.data.errors);
54
+ else setServerError(err.data?.message || err.message);
55
+ }
56
+ };
57
+
58
+ return (
59
+ <Layout variant="register">
60
+ <PageImage imageKey="registerBanner" className="w-full h-24 object-cover rounded-xl mb-4" />
61
+
62
+ <div className="bg-white rounded-2xl shadow-lg p-6 animate-fade-up border border-teal-100">
63
+ <h2 className="text-xl font-bold text-teal-900 mb-1">Регистрация</h2>
64
+ <p className="text-sm text-slate-500 mb-6">Все поля обязательны</p>
65
+
66
+ <form onSubmit={submit} noValidate>
67
+ <FormField label="Логин" error={errors.login}>
68
+ <input className={`${inputClass} ${errors.login ? 'input-error' : ''}`} value={form.login} onChange={set('login')} />
69
+ </FormField>
70
+ <FormField label="Пароль (от 6 символов)" error={errors.password}>
71
+ <input type="password" className={`${inputClass} ${errors.password ? 'input-error' : ''}`} value={form.password} onChange={set('password')} />
72
+ </FormField>
73
+ <FormField label="ФИО" error={errors.fullName}>
74
+ <input className={`${inputClass} ${errors.fullName ? 'input-error' : ''}`} value={form.fullName} onChange={set('fullName')} />
75
+ </FormField>
76
+ <FormField label="Телефон" error={errors.phone}>
77
+ <input placeholder="+7(999)-123-45-67" className={`${inputClass} ${errors.phone ? 'input-error' : ''}`} value={form.phone} onChange={set('phone')} />
78
+ </FormField>
79
+ <FormField label="Email" error={errors.email}>
80
+ <input type="email" className={`${inputClass} ${errors.email ? 'input-error' : ''}`} value={form.email} onChange={set('email')} />
81
+ </FormField>
82
+
83
+ {serverError && <p className="field-error mb-3">{serverError}</p>}
84
+
85
+ <button type="submit" className="w-full rounded-xl bg-teal-600 text-white py-3 font-semibold hover:bg-teal-700 transition">
86
+ Зарегистрироваться
87
+ </button>
88
+ </form>
89
+
90
+
91
+ <p className="text-center text-sm mt-4 text-slate-600">
92
+ Уже есть аккаунт? <Link to="/login" className="text-teal-700 underline">Войти</Link>
93
+ </p>
94
+ </div>
95
+ </Layout>
96
+ );
97
+ }