@mashka818/exam-de-template 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/exam-guides/COMMENTS_GUIDE.md +53 -0
- package/exam-guides/EXAM_COMMANDS.txt +47 -0
- package/exam-guides/GUIDE_PAGES.md +529 -0
- package/exam-guides/NPM_PACKAGE.md +206 -0
- package/exam-guides/README.md +40 -0
- package/exam-guides/RESPONSIVE.md +224 -0
- package/exam-guides/TECH_STACK.txt +142 -0
- package/exam-guides/THEME_BANQUETAM_NET.md +106 -0
- package/exam-guides/commented-code/README.txt +5 -0
- package/exam-guides/commented-code/client/index.html +14 -0
- package/exam-guides/commented-code/client/package-lock.json +2298 -0
- package/exam-guides/commented-code/client/package.json +21 -0
- package/exam-guides/commented-code/client/public/images/README.txt +26 -0
- package/exam-guides/commented-code/client/public/images/about-cleaning.svg +4 -0
- package/exam-guides/commented-code/client/public/images/admin-banner.svg +1 -0
- package/exam-guides/commented-code/client/public/images/empty-requests.svg +1 -0
- package/exam-guides/commented-code/client/public/images/footer-photo-1.svg +4 -0
- package/exam-guides/commented-code/client/public/images/footer-photo-2.svg +4 -0
- package/exam-guides/commented-code/client/public/images/footer-photo-3.svg +4 -0
- package/exam-guides/commented-code/client/public/images/home-hero.svg +4 -0
- package/exam-guides/commented-code/client/public/images/login-banner.svg +1 -0
- package/exam-guides/commented-code/client/public/images/logo.svg +4 -0
- package/exam-guides/commented-code/client/public/images/new-request-banner.svg +1 -0
- package/exam-guides/commented-code/client/public/images/register-banner.svg +1 -0
- package/exam-guides/commented-code/client/public/images/requests-banner.svg +1 -0
- package/exam-guides/commented-code/client/public/images/slide-1.svg +6 -0
- package/exam-guides/commented-code/client/public/images/slide-2.svg +5 -0
- package/exam-guides/commented-code/client/public/images/slide-3.svg +5 -0
- package/exam-guides/commented-code/client/src/App.jsx +72 -0
- package/exam-guides/commented-code/client/src/api.js +71 -0
- package/exam-guides/commented-code/client/src/components/FormField.jsx +25 -0
- package/exam-guides/commented-code/client/src/components/Layout.jsx +83 -0
- package/exam-guides/commented-code/client/src/components/PageImage.jsx +38 -0
- package/exam-guides/commented-code/client/src/components/ProtectedRoute.jsx +35 -0
- package/exam-guides/commented-code/client/src/components/UserNav.jsx +33 -0
- package/exam-guides/commented-code/client/src/components/landing/HeroSlider.jsx +103 -0
- package/exam-guides/commented-code/client/src/components/landing/LandingLayout.jsx +76 -0
- package/exam-guides/commented-code/client/src/components/landing/SiteFooter.jsx +74 -0
- package/exam-guides/commented-code/client/src/config/images.js +104 -0
- package/exam-guides/commented-code/client/src/constants/services.js +19 -0
- package/exam-guides/commented-code/client/src/context/AuthContext.jsx +72 -0
- package/exam-guides/commented-code/client/src/index.css +73 -0
- package/exam-guides/commented-code/client/src/main.jsx +28 -0
- package/exam-guides/commented-code/client/src/pages/AdminPage.jsx +151 -0
- package/exam-guides/commented-code/client/src/pages/LandingPage.jsx +131 -0
- package/exam-guides/commented-code/client/src/pages/LoginPage.jsx +81 -0
- package/exam-guides/commented-code/client/src/pages/RegisterPage.jsx +117 -0
- package/exam-guides/commented-code/client/src/pages/RequestFormPage.jsx +196 -0
- package/exam-guides/commented-code/client/src/pages/RequestsPage.jsx +112 -0
- package/exam-guides/commented-code/client/src/utils/validation.js +71 -0
- package/exam-guides/commented-code/client/vite.config.js +31 -0
- package/exam-guides/commented-code/server/db/init.js +67 -0
- package/exam-guides/commented-code/server/db/pool.js +23 -0
- package/exam-guides/commented-code/server/db/schema.sql +53 -0
- package/exam-guides/commented-code/server/db/seed.sql +15 -0
- package/exam-guides/commented-code/server/index.js +45 -0
- package/exam-guides/commented-code/server/middleware/auth.js +38 -0
- package/exam-guides/commented-code/server/package-lock.json +1084 -0
- package/exam-guides/commented-code/server/package.json +17 -0
- package/exam-guides/commented-code/server/routes/admin.js +96 -0
- package/exam-guides/commented-code/server/routes/auth.js +128 -0
- package/exam-guides/commented-code/server/routes/requests.js +115 -0
- package/exam-guides/commented-code/server/routes/services.js +31 -0
- package/exam-guides/commented-code/server/utils/validation.js +81 -0
- package/exam-guides/exam-starter/README.txt +22 -0
- package/exam-guides/exam-starter/package.json +13 -0
- package/exam-guides//320/243/320/224/320/220/320/233/320/230/320/242/320/254-/320/237/320/225/320/240/320/225/320/224-/320/241/320/224/320/220/320/247/320/225/320/231.txt +9 -0
- package/exam-project/README.md +16 -0
- package/exam-project/client/index.html +14 -0
- package/exam-project/client/package-lock.json +2298 -0
- package/exam-project/client/package.json +21 -0
- package/exam-project/client/public/images/README.txt +26 -0
- package/exam-project/client/public/images/about-cleaning.svg +4 -0
- package/exam-project/client/public/images/admin-banner.svg +1 -0
- package/exam-project/client/public/images/empty-requests.svg +1 -0
- package/exam-project/client/public/images/footer-photo-1.svg +4 -0
- package/exam-project/client/public/images/footer-photo-2.svg +4 -0
- package/exam-project/client/public/images/footer-photo-3.svg +4 -0
- package/exam-project/client/public/images/home-hero.svg +4 -0
- package/exam-project/client/public/images/login-banner.svg +1 -0
- package/exam-project/client/public/images/logo.svg +4 -0
- package/exam-project/client/public/images/new-request-banner.svg +1 -0
- package/exam-project/client/public/images/register-banner.svg +1 -0
- package/exam-project/client/public/images/requests-banner.svg +1 -0
- package/exam-project/client/public/images/slide-1.svg +6 -0
- package/exam-project/client/public/images/slide-2.svg +5 -0
- package/exam-project/client/public/images/slide-3.svg +5 -0
- package/exam-project/client/src/App.jsx +52 -0
- package/exam-project/client/src/api.js +50 -0
- package/exam-project/client/src/components/FormField.jsx +11 -0
- package/exam-project/client/src/components/Layout.jsx +61 -0
- package/exam-project/client/src/components/PageImage.jsx +22 -0
- package/exam-project/client/src/components/ProtectedRoute.jsx +20 -0
- package/exam-project/client/src/components/UserNav.jsx +20 -0
- package/exam-project/client/src/components/landing/HeroSlider.jsx +89 -0
- package/exam-project/client/src/components/landing/LandingLayout.jsx +61 -0
- package/exam-project/client/src/components/landing/SiteFooter.jsx +61 -0
- package/exam-project/client/src/config/images.js +83 -0
- package/exam-project/client/src/constants/services.js +7 -0
- package/exam-project/client/src/context/AuthContext.jsx +55 -0
- package/exam-project/client/src/index.css +54 -0
- package/exam-project/client/src/main.jsx +17 -0
- package/exam-project/client/src/pages/AdminPage.jsx +128 -0
- package/exam-project/client/src/pages/LandingPage.jsx +115 -0
- package/exam-project/client/src/pages/LoginPage.jsx +63 -0
- package/exam-project/client/src/pages/RegisterPage.jsx +97 -0
- package/exam-project/client/src/pages/RequestFormPage.jsx +178 -0
- package/exam-project/client/src/pages/RequestsPage.jsx +94 -0
- package/exam-project/client/src/utils/validation.js +54 -0
- package/exam-project/client/vite.config.js +17 -0
- package/exam-project/package.json +18 -0
- package/exam-project/scripts/init-project.js +86 -0
- package/exam-project/scripts/prepack.js +27 -0
- package/exam-project/scripts/unpack-template.js +105 -0
- package/exam-project/server/db/init.js +50 -0
- package/exam-project/server/db/pool.js +11 -0
- package/exam-project/server/db/schema.sql +41 -0
- package/exam-project/server/db/seed.sql +12 -0
- package/exam-project/server/index.js +29 -0
- package/exam-project/server/middleware/auth.js +24 -0
- package/exam-project/server/package-lock.json +1084 -0
- package/exam-project/server/package.json +17 -0
- package/exam-project/server/routes/admin.js +76 -0
- package/exam-project/server/routes/auth.js +109 -0
- package/exam-project/server/routes/requests.js +99 -0
- package/exam-project/server/routes/services.js +18 -0
- package/exam-project/server/utils/validation.js +63 -0
- package/package.json +25 -0
- package/scripts/init-project.js +86 -0
- package/scripts/prepack.js +27 -0
- package/scripts/unpack-template.js +105 -0
|
@@ -0,0 +1,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="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,52 @@
|
|
|
1
|
+
|
|
2
|
+
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
3
|
+
import ProtectedRoute from './components/ProtectedRoute.jsx';
|
|
4
|
+
import LandingPage from './pages/LandingPage.jsx';
|
|
5
|
+
import RegisterPage from './pages/RegisterPage.jsx';
|
|
6
|
+
import LoginPage from './pages/LoginPage.jsx';
|
|
7
|
+
import RequestsPage from './pages/RequestsPage.jsx';
|
|
8
|
+
import RequestFormPage from './pages/RequestFormPage.jsx';
|
|
9
|
+
import AdminPage from './pages/AdminPage.jsx';
|
|
10
|
+
export default function App() {
|
|
11
|
+
return (
|
|
12
|
+
<Routes>
|
|
13
|
+
{/* Лендинг для всех; по клику «Мой Не Сам» — сюда же */}
|
|
14
|
+
<Route path="/" element={<LandingPage />} />
|
|
15
|
+
<Route path="/register" element={<RegisterPage />} />
|
|
16
|
+
<Route path="/login" element={<LoginPage />} />
|
|
17
|
+
|
|
18
|
+
{/* п.3 — страница создания заявки */}
|
|
19
|
+
<Route
|
|
20
|
+
path="/requests"
|
|
21
|
+
element={
|
|
22
|
+
<ProtectedRoute role="user">
|
|
23
|
+
<RequestsPage />
|
|
24
|
+
</ProtectedRoute>
|
|
25
|
+
}
|
|
26
|
+
/>
|
|
27
|
+
|
|
28
|
+
{/* п.4 — страница формирования заявки */}
|
|
29
|
+
<Route
|
|
30
|
+
path="/requests/form"
|
|
31
|
+
element={
|
|
32
|
+
<ProtectedRoute role="user">
|
|
33
|
+
<RequestFormPage />
|
|
34
|
+
</ProtectedRoute>
|
|
35
|
+
}
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
{/* старый путь — редирект на п.4 */}
|
|
39
|
+
<Route path="/requests/new" element={<Navigate to="/requests/form" replace />} />
|
|
40
|
+
|
|
41
|
+
<Route
|
|
42
|
+
path="/admin"
|
|
43
|
+
element={
|
|
44
|
+
<ProtectedRoute role="admin">
|
|
45
|
+
<AdminPage />
|
|
46
|
+
</ProtectedRoute>
|
|
47
|
+
}
|
|
48
|
+
/>
|
|
49
|
+
<Route path="*" element={<Navigate to="/" replace />} />
|
|
50
|
+
</Routes>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
const TOKEN_KEY = 'exam_token';
|
|
4
|
+
|
|
5
|
+
export function getToken() {
|
|
6
|
+
return localStorage.getItem(TOKEN_KEY);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function setToken(token) {
|
|
10
|
+
if (token) localStorage.setItem(TOKEN_KEY, token);
|
|
11
|
+
else localStorage.removeItem(TOKEN_KEY);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function request(path, options = {}) {
|
|
15
|
+
const headers = { 'Content-Type': 'application/json', ...options.headers };
|
|
16
|
+
const token = getToken();
|
|
17
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
18
|
+
|
|
19
|
+
const res = await fetch(`/api${path}`, { ...options, headers });
|
|
20
|
+
const data = await res.json().catch(() => ({}));
|
|
21
|
+
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
const err = new Error(data.message || 'Ошибка запроса');
|
|
24
|
+
err.status = res.status;
|
|
25
|
+
err.data = data;
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
return data;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const api = {
|
|
32
|
+
// --- Авторизация → server/routes/auth.js (п.1 register, п.2 login) ---
|
|
33
|
+
// body register: { login, password, fullName, phone, email }
|
|
34
|
+
register: (body) => request('/auth/register', { method: 'POST', body: JSON.stringify(body) }),
|
|
35
|
+
login: (body) => request('/auth/login', { method: 'POST', body: JSON.stringify(body) }),
|
|
36
|
+
me: () => request('/auth/me'), // проверка токена при F5
|
|
37
|
+
|
|
38
|
+
// --- Справочник → server/routes/services.js + db/seed.sql (помещения/услуги) ---
|
|
39
|
+
getServices: () => request('/services'),
|
|
40
|
+
|
|
41
|
+
// --- Заявки пользователя → server/routes/requests.js (п.3 mine, п.4 POST) ---
|
|
42
|
+
getMyRequests: () => request('/requests/mine'),
|
|
43
|
+
createRequest: (body) => request('/requests', { method: 'POST', body: JSON.stringify(body) }),
|
|
44
|
+
|
|
45
|
+
// --- Админ → server/routes/admin.js (п.5) ---
|
|
46
|
+
|
|
47
|
+
adminGetRequests: () => request('/admin/requests'),
|
|
48
|
+
adminUpdateStatus: (id, body) =>
|
|
49
|
+
request(`/admin/requests/${id}/status`, { method: 'PATCH', body: JSON.stringify(body) }),
|
|
50
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
export default function FormField({ label, error, children, className = '' }) {
|
|
3
|
+
return (
|
|
4
|
+
<label className={`block mb-4 ${className}`}>
|
|
5
|
+
{/* Подпись поля — задаётся на странице (RegisterPage, NewRequestPage…) */}
|
|
6
|
+
<span className="block text-sm font-medium text-slate-700 mb-1">{label}</span>
|
|
7
|
+
{children}
|
|
8
|
+
{error && <p className="field-error">{error}</p>}
|
|
9
|
+
</label>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
|
|
2
|
+
import { Link, useNavigate } from 'react-router-dom';
|
|
3
|
+
import { useAuth } from '../context/AuthContext.jsx';
|
|
4
|
+
import PageImage from './PageImage.jsx';
|
|
5
|
+
import UserNav from './UserNav.jsx';
|
|
6
|
+
|
|
7
|
+
// --- Тексты бренда (менять на экзамене в первую очередь) ---
|
|
8
|
+
const brandName = 'Мой Не Сам';
|
|
9
|
+
const brandTagline = 'Клининг без хлопот';
|
|
10
|
+
|
|
11
|
+
// --- Стили шапки: variant передаётся со страницы (register, login, dashboard, admin) ---
|
|
12
|
+
const headerStyles = {
|
|
13
|
+
default: 'bg-white border-b border-slate-200 text-slate-800',
|
|
14
|
+
register: 'bg-teal-700 text-white',
|
|
15
|
+
login: 'bg-slate-800 text-white',
|
|
16
|
+
dashboard: 'bg-gradient-to-r from-teal-600 to-cyan-600 text-white',
|
|
17
|
+
admin: 'bg-violet-900 text-white',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default function Layout({ children, variant = 'default' }) {
|
|
21
|
+
const { user, logout } = useAuth();
|
|
22
|
+
const navigate = useNavigate();
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="min-h-screen flex flex-col">
|
|
26
|
+
|
|
27
|
+
<header className={`${headerStyles[variant]} shadow-sm animate-fade-up`}>
|
|
28
|
+
<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">
|
|
29
|
+
<Link to="/" className="flex items-center gap-2 min-w-0">
|
|
30
|
+
{/* Картинка: client/public/images/ + ключ logo в config/images.js */}
|
|
31
|
+
<PageImage imageKey="logo" className="h-9 lg:h-11 w-auto shrink-0" />
|
|
32
|
+
<span className="font-semibold text-lg tracking-tight truncate">{brandName}</span>
|
|
33
|
+
</Link>
|
|
34
|
+
|
|
35
|
+
{user ? (
|
|
36
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
37
|
+
{user.role === 'user' && variant === 'dashboard' && <UserNav />}
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
onClick={() => {
|
|
41
|
+
logout();
|
|
42
|
+
navigate('/login');
|
|
43
|
+
}}
|
|
44
|
+
className="text-sm opacity-90 hover:opacity-100"
|
|
45
|
+
>
|
|
46
|
+
Выйти
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
) : (
|
|
50
|
+
<span className="text-xs opacity-80 shrink-0 hidden sm:inline">{brandTagline}</span>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
</header>
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
<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">
|
|
57
|
+
{children}
|
|
58
|
+
</main>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
import { imageAlt, imageUrl } from '../config/images.js';
|
|
3
|
+
|
|
4
|
+
export default function PageImage({ imageKey, className = 'w-full h-auto rounded-xl object-cover' }) {
|
|
5
|
+
const src = imageUrl(imageKey);
|
|
6
|
+
const alt = imageAlt(imageKey);
|
|
7
|
+
|
|
8
|
+
if (!src) return null;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<img
|
|
12
|
+
src={src}
|
|
13
|
+
alt={alt}
|
|
14
|
+
className={className}
|
|
15
|
+
loading="lazy"
|
|
16
|
+
/* при ошибке загрузки (забыли положить PNG) — скрываем, чтобы не ломать вёрстку */
|
|
17
|
+
onError={(e) => {
|
|
18
|
+
e.currentTarget.style.display = 'none';
|
|
19
|
+
}}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
import { Navigate } from 'react-router-dom';
|
|
3
|
+
import { useAuth } from '../context/AuthContext.jsx';
|
|
4
|
+
|
|
5
|
+
export default function ProtectedRoute({ children, role }) {
|
|
6
|
+
const { user, loading } = useAuth();
|
|
7
|
+
|
|
8
|
+
// Пока AuthContext проверяет токен — не редиректим преждевременно
|
|
9
|
+
if (loading) {
|
|
10
|
+
return <p className="text-center text-slate-500 animate-pulse py-12">Загрузка…</p>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!user) return <Navigate to="/login" replace />;
|
|
14
|
+
|
|
15
|
+
if (role && user.role !== role) {
|
|
16
|
+
return <Navigate to={user.role === 'admin' ? '/admin' : '/requests'} replace />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return children;
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
import { NavLink } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
const linkClass = ({ isActive }) =>
|
|
5
|
+
`text-xs px-2 py-1 rounded-md transition ${
|
|
6
|
+
isActive ? 'bg-white/30 font-semibold' : 'opacity-80 hover:opacity-100'
|
|
7
|
+
}`;
|
|
8
|
+
|
|
9
|
+
export default function UserNav() {
|
|
10
|
+
return (
|
|
11
|
+
<nav className="flex gap-1 shrink-0" aria-label="Разделы заявок">
|
|
12
|
+
<NavLink to="/requests" className={linkClass} end title="п.3 Создание заявки">
|
|
13
|
+
Мои заявки
|
|
14
|
+
</NavLink>
|
|
15
|
+
<NavLink to="/requests/form" className={linkClass} title="п.4 Формирование заявки">
|
|
16
|
+
Новая заявка
|
|
17
|
+
</NavLink>
|
|
18
|
+
</nav>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { imageAlt, imageUrl } from '../../config/images.js';
|
|
4
|
+
|
|
5
|
+
const SLIDE_KEYS = ['slide1', 'slide2', 'slide3'];
|
|
6
|
+
|
|
7
|
+
const slideText = {
|
|
8
|
+
slide1: { title: 'Общий клининг', subtitle: 'Поддерживаем порядок регулярно' },
|
|
9
|
+
slide2: { title: 'Генеральная уборка', subtitle: 'Глубокая очистка всех поверхностей' },
|
|
10
|
+
slide3: { title: 'Химчистка', subtitle: 'Ковры, мебель, деликатные ткани' },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function HeroSlider() {
|
|
14
|
+
const [index, setIndex] = useState(0);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const timer = setInterval(() => {
|
|
18
|
+
setIndex((i) => (i + 1) % SLIDE_KEYS.length);
|
|
19
|
+
}, 5000);
|
|
20
|
+
return () => clearInterval(timer);
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
const key = SLIDE_KEYS[index];
|
|
24
|
+
const text = slideText[key];
|
|
25
|
+
|
|
26
|
+
const go = (dir) => {
|
|
27
|
+
setIndex((i) => (i + dir + SLIDE_KEYS.length) % SLIDE_KEYS.length);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<section className="relative w-full overflow-hidden bg-teal-900" aria-label="Слайдер услуг">
|
|
32
|
+
<div className="relative h-52 sm:h-64 lg:h-80">
|
|
33
|
+
{SLIDE_KEYS.map((k, i) => (
|
|
34
|
+
<div
|
|
35
|
+
key={k}
|
|
36
|
+
className={`absolute inset-0 transition-opacity duration-700 ${
|
|
37
|
+
i === index ? 'opacity-100 z-10' : 'opacity-0 z-0'
|
|
38
|
+
}`}
|
|
39
|
+
>
|
|
40
|
+
<img
|
|
41
|
+
src={imageUrl(k)}
|
|
42
|
+
alt={imageAlt(k)}
|
|
43
|
+
className="w-full h-full object-cover"
|
|
44
|
+
/>
|
|
45
|
+
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent" />
|
|
46
|
+
</div>
|
|
47
|
+
))}
|
|
48
|
+
|
|
49
|
+
<div className="absolute inset-0 z-20 flex flex-col justify-end p-6 lg:p-10 max-w-5xl mx-auto">
|
|
50
|
+
<h2 className="text-2xl lg:text-4xl font-bold text-white drop-shadow animate-fade-up">
|
|
51
|
+
{text.title}
|
|
52
|
+
</h2>
|
|
53
|
+
<p className="text-teal-100 text-sm lg:text-base mt-1">{text.subtitle}</p>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
onClick={() => go(-1)}
|
|
59
|
+
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"
|
|
60
|
+
aria-label="Предыдущий слайд"
|
|
61
|
+
>
|
|
62
|
+
‹
|
|
63
|
+
</button>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
onClick={() => go(1)}
|
|
67
|
+
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"
|
|
68
|
+
aria-label="Следующий слайд"
|
|
69
|
+
>
|
|
70
|
+
›
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div className="absolute bottom-3 left-0 right-0 z-30 flex justify-center gap-2">
|
|
75
|
+
{SLIDE_KEYS.map((k, i) => (
|
|
76
|
+
<button
|
|
77
|
+
key={k}
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={() => setIndex(i)}
|
|
80
|
+
className={`h-2 rounded-full transition-all ${
|
|
81
|
+
i === index ? 'w-8 bg-white' : 'w-2 bg-white/50'
|
|
82
|
+
}`}
|
|
83
|
+
aria-label={`Слайд ${i + 1}`}
|
|
84
|
+
/>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
</section>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
3
|
+
import PageImage from '../PageImage.jsx';
|
|
4
|
+
import { useAuth } from '../../context/AuthContext.jsx';
|
|
5
|
+
|
|
6
|
+
const brandName = 'Мой Не Сам';
|
|
7
|
+
|
|
8
|
+
export default function LandingLayout({ children }) {
|
|
9
|
+
const { user, logout } = useAuth();
|
|
10
|
+
|
|
11
|
+
const cabinetPath = user?.role === 'admin' ? '/admin' : '/requests';
|
|
12
|
+
const cabinetLabel = user?.role === 'admin' ? 'Панель админа' : 'Мои заявки';
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="min-h-screen flex flex-col bg-slate-50">
|
|
16
|
+
<header className="bg-white border-b border-slate-200 shadow-sm sticky top-0 z-40">
|
|
17
|
+
<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">
|
|
18
|
+
<Link to="/" className="flex items-center gap-2 min-w-0">
|
|
19
|
+
<PageImage imageKey="logo" className="h-9 lg:h-11 w-auto shrink-0" />
|
|
20
|
+
<span className="font-semibold text-lg text-teal-900 truncate">{brandName}</span>
|
|
21
|
+
</Link>
|
|
22
|
+
|
|
23
|
+
{user ? (
|
|
24
|
+
<div className="flex items-center gap-2 shrink-0 text-sm">
|
|
25
|
+
<Link
|
|
26
|
+
to={cabinetPath}
|
|
27
|
+
className="px-3 py-1.5 rounded-lg bg-teal-600 text-white hover:bg-teal-700 transition"
|
|
28
|
+
>
|
|
29
|
+
{cabinetLabel}
|
|
30
|
+
</Link>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
onClick={logout}
|
|
34
|
+
className="px-3 py-1.5 rounded-lg text-slate-600 hover:bg-slate-100 transition"
|
|
35
|
+
>
|
|
36
|
+
Выйти
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
) : (
|
|
40
|
+
<div className="flex gap-2 shrink-0 text-sm">
|
|
41
|
+
<Link
|
|
42
|
+
to="/login"
|
|
43
|
+
className="px-3 py-1.5 rounded-lg text-slate-700 hover:bg-slate-100 transition"
|
|
44
|
+
>
|
|
45
|
+
Вход
|
|
46
|
+
</Link>
|
|
47
|
+
<Link
|
|
48
|
+
to="/register"
|
|
49
|
+
className="px-3 py-1.5 rounded-lg bg-teal-600 text-white hover:bg-teal-700 transition"
|
|
50
|
+
>
|
|
51
|
+
Регистрация
|
|
52
|
+
</Link>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
</header>
|
|
57
|
+
|
|
58
|
+
<div className="flex-1 w-full">{children}</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
3
|
+
import PageImage from '../PageImage.jsx';
|
|
4
|
+
|
|
5
|
+
const footerLinks = [
|
|
6
|
+
{ label: 'О компании', to: '/' },
|
|
7
|
+
{ label: 'Услуги', to: '/' },
|
|
8
|
+
{ label: 'Контакты', to: '/' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export default function SiteFooter() {
|
|
12
|
+
return (
|
|
13
|
+
<footer className="bg-slate-900 text-slate-300 mt-auto">
|
|
14
|
+
<div className="mx-auto max-w-lg lg:max-w-5xl px-4 lg:px-8 py-10">
|
|
15
|
+
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-8">
|
|
16
|
+
<div>
|
|
17
|
+
<p className="text-white font-semibold text-lg mb-2">Мой Не Сам</p>
|
|
18
|
+
<p className="text-sm leading-relaxed max-w-sm">
|
|
19
|
+
Портал заявок на клининговые услуги. Профессиональная уборка жилых и
|
|
20
|
+
производственных помещений — вы отдыхаете, мы наводим порядок.
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
{/* Фото в футере — замените PNG в public/images/footer-photo-*.png */}
|
|
25
|
+
<div className="flex gap-4 justify-center lg:justify-end">
|
|
26
|
+
<PageImage
|
|
27
|
+
imageKey="footerPhoto1"
|
|
28
|
+
className="w-20 h-20 lg:w-24 lg:h-24 rounded-full object-cover border-2 border-teal-600"
|
|
29
|
+
/>
|
|
30
|
+
<PageImage
|
|
31
|
+
imageKey="footerPhoto2"
|
|
32
|
+
className="w-20 h-20 lg:w-24 lg:h-24 rounded-full object-cover border-2 border-teal-600"
|
|
33
|
+
/>
|
|
34
|
+
<PageImage
|
|
35
|
+
imageKey="footerPhoto3"
|
|
36
|
+
className="w-20 h-20 lg:w-24 lg:h-24 rounded-full object-cover border-2 border-teal-600"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<nav className="flex flex-wrap gap-4 justify-center lg:justify-start mt-8 text-sm">
|
|
42
|
+
{footerLinks.map((l) => (
|
|
43
|
+
<Link key={l.label} to={l.to} className="hover:text-white transition">
|
|
44
|
+
{l.label}
|
|
45
|
+
</Link>
|
|
46
|
+
))}
|
|
47
|
+
<Link to="/register" className="hover:text-teal-400 transition">
|
|
48
|
+
Регистрация
|
|
49
|
+
</Link>
|
|
50
|
+
<Link to="/login" className="hover:text-teal-400 transition">
|
|
51
|
+
Вход
|
|
52
|
+
</Link>
|
|
53
|
+
</nav>
|
|
54
|
+
|
|
55
|
+
<p className="text-center lg:text-left text-xs text-slate-500 mt-8 border-t border-slate-700 pt-6">
|
|
56
|
+
© {new Date().getFullYear()} Мой Не Сам · Шаблон для ДЭ
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
</footer>
|
|
60
|
+
);
|
|
61
|
+
}
|