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