@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,178 @@
1
+
2
+ import { useEffect, 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 { api } from '../api.js';
8
+ import { formatPhoneInput, validateRequestForm } 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-cyan-400';
12
+
13
+ export default function RequestFormPage() {
14
+ const navigate = useNavigate();
15
+ const [services, setServices] = useState([]);
16
+ const [form, setForm] = useState({
17
+ address: '',
18
+ contactPhone: '',
19
+ scheduledAt: '',
20
+ paymentType: '',
21
+ serviceTypeId: '',
22
+ isCustomService: false,
23
+ customService: '',
24
+ });
25
+ const [errors, setErrors] = useState({});
26
+ const [serverError, setServerError] = useState('');
27
+
28
+ useEffect(() => {
29
+ api.getServices().then(setServices).catch(() => {});
30
+ }, []);
31
+
32
+ const set = (field) => (e) => {
33
+ const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
34
+ let v = value;
35
+ if (field === 'contactPhone') v = formatPhoneInput(value);
36
+ setForm((f) => ({ ...f, [field]: v }));
37
+ setErrors((err) => ({ ...err, [field]: undefined }));
38
+ };
39
+
40
+ const submit = async (e) => {
41
+ e.preventDefault();
42
+ setServerError('');
43
+ const localErrors = validateRequestForm(form);
44
+ if (Object.keys(localErrors).length) {
45
+ setErrors(localErrors);
46
+ return;
47
+ }
48
+ try {
49
+ await api.createRequest({
50
+ address: form.address,
51
+ contactPhone: form.contactPhone,
52
+ scheduledAt: new Date(form.scheduledAt).toISOString(),
53
+ paymentType: form.paymentType,
54
+ serviceTypeId: form.isCustomService ? null : Number(form.serviceTypeId),
55
+ isCustomService: form.isCustomService,
56
+ customService: form.isCustomService ? form.customService : null,
57
+ });
58
+ navigate('/requests');
59
+ } catch (err) {
60
+ if (err.data?.errors) setErrors(err.data.errors);
61
+ else setServerError(err.data?.message || err.message);
62
+ }
63
+ };
64
+
65
+ return (
66
+ <Layout variant="dashboard">
67
+ <p className="text-white/70 text-xs mb-2">Пункт 4 задания</p>
68
+ <PageImage imageKey="newRequestBanner" className="w-full h-20 object-cover rounded-xl mb-4" />
69
+
70
+ <Link
71
+ to="/requests"
72
+ className="inline-block text-sm text-white/90 underline mb-4 hover:text-white"
73
+ >
74
+ ← Назад к истории заявок (п.3)
75
+ </Link>
76
+
77
+ <form
78
+ onSubmit={submit}
79
+ className="bg-white rounded-2xl shadow-lg p-6 animate-fade-up border border-cyan-100"
80
+ noValidate
81
+ >
82
+
83
+ <h1 className="text-xl font-bold text-cyan-900 mb-1">Формирование заявки</h1>
84
+ <p className="text-sm text-slate-500 mb-6">Все поля обязательны</p>
85
+
86
+ <FormField label="Адрес" error={errors.address}>
87
+ <input
88
+ className={`${inputClass} ${errors.address ? 'input-error' : ''}`}
89
+ value={form.address}
90
+ onChange={set('address')}
91
+ placeholder="Адрес выполнения услуги"
92
+ />
93
+ </FormField>
94
+
95
+ <FormField label="Контактные данные (телефон)" error={errors.contactPhone}>
96
+ <input
97
+ placeholder="+7(999)-123-45-67"
98
+ className={`${inputClass} ${errors.contactPhone ? 'input-error' : ''}`}
99
+ value={form.contactPhone}
100
+ onChange={set('contactPhone')}
101
+ />
102
+ </FormField>
103
+
104
+ <FormField label="Желаемая дата и время получения услуги" error={errors.scheduledAt}>
105
+ <input
106
+ type="datetime-local"
107
+ className={`${inputClass} ${errors.scheduledAt ? 'input-error' : ''}`}
108
+ value={form.scheduledAt}
109
+ onChange={set('scheduledAt')}
110
+ />
111
+ </FormField>
112
+
113
+ {!form.isCustomService && (
114
+ <FormField label="Вид услуги" error={errors.serviceTypeId}>
115
+ <select
116
+ className={`${inputClass} ${errors.serviceTypeId ? 'input-error' : ''}`}
117
+ value={form.serviceTypeId}
118
+ onChange={set('serviceTypeId')}
119
+ >
120
+ <option value="">— выберите из списка —</option>
121
+ {services.map((s) => (
122
+ <option key={s.id} value={s.id}>
123
+ {s.name}
124
+ </option>
125
+ ))}
126
+ </select>
127
+ <p className="text-xs text-slate-400 mt-1">
128
+ Из задания: общий клининг, генеральная, послестроительная, химчистка
129
+ </p>
130
+ </FormField>
131
+ )}
132
+
133
+
134
+ <label className="flex items-center gap-2 mb-4 cursor-pointer">
135
+ <input
136
+ type="checkbox"
137
+ checked={form.isCustomService}
138
+ onChange={set('isCustomService')}
139
+ className="rounded border-slate-300 text-cyan-600"
140
+ />
141
+ <span className="text-sm">Иная услуга</span>
142
+ </label>
143
+
144
+ {form.isCustomService && (
145
+ <FormField label="Опишите, какая услуга вам необходима" error={errors.customService}>
146
+ <textarea
147
+ rows={3}
148
+ className={`${inputClass} ${errors.customService ? 'input-error' : ''}`}
149
+ value={form.customService}
150
+ onChange={set('customService')}
151
+ />
152
+ </FormField>
153
+ )}
154
+
155
+ <FormField label="Предпочтительный тип оплаты" error={errors.paymentType}>
156
+ <select
157
+ className={`${inputClass} ${errors.paymentType ? 'input-error' : ''}`}
158
+ value={form.paymentType}
159
+ onChange={set('paymentType')}
160
+ >
161
+ <option value="">— выберите —</option>
162
+ <option value="cash">Наличные</option>
163
+ <option value="card">Банковская карта</option>
164
+ </select>
165
+ </FormField>
166
+
167
+ {serverError && <p className="field-error mb-3">{serverError}</p>}
168
+
169
+ <button
170
+ type="submit"
171
+ className="w-full rounded-xl bg-cyan-600 text-white py-3 font-semibold hover:bg-cyan-700 transition"
172
+ >
173
+ Отправить заявку
174
+ </button>
175
+ </form>
176
+ </Layout>
177
+ );
178
+ }
@@ -0,0 +1,94 @@
1
+
2
+ import { useEffect, useState } from 'react';
3
+ import { Link } from 'react-router-dom';
4
+ import Layout from '../components/Layout.jsx';
5
+ import PageImage from '../components/PageImage.jsx';
6
+ import { api } from '../api.js';
7
+
8
+ const statusColors = {
9
+ new: 'bg-amber-100 text-amber-800',
10
+ in_progress: 'bg-blue-100 text-blue-800',
11
+ completed: 'bg-emerald-100 text-emerald-800',
12
+ cancelled: 'bg-red-100 text-red-800',
13
+ };
14
+
15
+ export default function RequestsPage() {
16
+ const [requests, setRequests] = useState([]);
17
+ const [loading, setLoading] = useState(true);
18
+ const [error, setError] = useState('');
19
+
20
+ useEffect(() => {
21
+ api
22
+ .getMyRequests()
23
+ .then(setRequests)
24
+ .catch((e) => setError(e.message))
25
+ .finally(() => setLoading(false));
26
+ }, []);
27
+
28
+ return (
29
+ <Layout variant="dashboard">
30
+ <div className="animate-fade-up">
31
+ <p className="text-white/70 text-xs mb-1">Пункт 3 задания</p>
32
+ <PageImage imageKey="requestsBanner" className="w-full h-20 object-cover rounded-xl mb-4" />
33
+
34
+
35
+ <h1 className="text-xl font-bold text-white drop-shadow mb-1">Создание заявки</h1>
36
+ <p className="text-white/80 text-sm mb-6">
37
+ История ваших заявок и переход к оформлению новой
38
+ </p>
39
+
40
+ <Link
41
+ to="/requests/form"
42
+ className="block mb-8 rounded-2xl bg-white p-5 shadow-lg border-2 border-teal-400 hover:border-teal-500 transition group"
43
+ >
44
+ <div className="flex items-center justify-between gap-3">
45
+ <div className="text-left">
46
+ <p className="font-bold text-teal-800 text-lg">Оставить новую заявку</p>
47
+ <p className="text-slate-600 text-sm mt-1">
48
+ Страница формирования заявки: адрес, контакты, услуга, дата, оплата
49
+ </p>
50
+ </div>
51
+ <span className="text-3xl text-teal-600 group-hover:translate-x-1 transition">→</span>
52
+ </div>
53
+ </Link>
54
+
55
+ <h2 className="text-lg font-semibold text-white mb-3">История заявок</h2>
56
+
57
+ {loading && <p className="text-white/80">Загрузка…</p>}
58
+ {error && <p className="text-red-200">{error}</p>}
59
+
60
+ {!loading && requests.length === 0 && (
61
+ <div className="bg-white rounded-xl p-6 text-center text-slate-600 shadow">
62
+ <PageImage imageKey="emptyRequests" className="w-32 h-auto mx-auto mb-3 opacity-80" />
63
+ Заявок пока нет. Нажмите «Оставить новую заявку» выше.
64
+ </div>
65
+ )}
66
+
67
+ <ul className="space-y-3">
68
+ {requests.map((r, i) => (
69
+ <li
70
+ key={r.id}
71
+ className="bg-white rounded-xl p-4 shadow animate-slide-in border-l-4 border-teal-500"
72
+ style={{ animationDelay: `${i * 0.05}s` }}
73
+ >
74
+ <div className="flex justify-between items-start gap-2 mb-2">
75
+ <span className="font-medium text-slate-800">{r.serviceName}</span>
76
+ <span className={`text-xs px-2 py-0.5 rounded-full ${statusColors[r.status]}`}>
77
+ {r.statusLabel}
78
+ </span>
79
+ </div>
80
+ <p className="text-sm text-slate-600">Адрес: {r.address}</p>
81
+ <p className="text-sm text-slate-600">Контакты: {r.contactPhone}</p>
82
+ <p className="text-xs text-slate-500 mt-1">
83
+ {new Date(r.scheduledAt).toLocaleString('ru-RU')} · {r.paymentLabel}
84
+ </p>
85
+ {r.cancelReason && (
86
+ <p className="text-xs text-red-600 mt-2">Причина отмены: {r.cancelReason}</p>
87
+ )}
88
+ </li>
89
+ ))}
90
+ </ul>
91
+ </div>
92
+ </Layout>
93
+ );
94
+ }
@@ -0,0 +1,54 @@
1
+
2
+
3
+ /** Телефон строго в формате задания — менять вместе с подсказкой на форме */
4
+ export const PHONE_REGEX = /^\+7\(\d{3}\)-\d{3}-\d{2}-\d{2}$/;
5
+ export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6
+ export const FIO_REGEX = /^[А-Яа-яЁё\s-]+$/;
7
+
8
+ /** Регистрация — поля users */
9
+ export function validateRegistration(form) {
10
+ const errors = {};
11
+ if (!form.login?.trim()) errors.login = 'Логин обязателен';
12
+ if (!form.password) errors.password = 'Пароль обязателен';
13
+ else if (form.password.length < 6) errors.password = 'Минимум 6 символов';
14
+ if (!form.fullName?.trim()) errors.fullName = 'ФИО обязательно';
15
+ else if (!FIO_REGEX.test(form.fullName.trim())) errors.fullName = 'Только кириллица и пробелы';
16
+ if (!form.phone?.trim()) errors.phone = 'Телефон обязателен';
17
+ else if (!PHONE_REGEX.test(form.phone.trim())) errors.phone = 'Формат: +7(XXX)-XXX-XX-XX';
18
+ if (!form.email?.trim()) errors.email = 'Email обязателен';
19
+ else if (!EMAIL_REGEX.test(form.email.trim())) errors.email = 'Некорректный email';
20
+ return errors;
21
+ }
22
+
23
+ /** Новая заявка — поля requests */
24
+ export function validateRequestForm(form) {
25
+ const errors = {};
26
+ if (!form.address?.trim()) errors.address = 'Адрес обязателен';
27
+ if (!form.contactPhone?.trim()) errors.contactPhone = 'Телефон обязателен';
28
+ else if (!PHONE_REGEX.test(form.contactPhone.trim()))
29
+ errors.contactPhone = 'Формат: +7(XXX)-XXX-XX-XX';
30
+ if (!form.scheduledAt) errors.scheduledAt = 'Укажите дату и время';
31
+ if (!form.paymentType) errors.paymentType = 'Выберите оплату';
32
+ if (form.isCustomService) {
33
+ if (!form.customService?.trim()) errors.customService = 'Опишите услугу';
34
+ } else if (!form.serviceTypeId) {
35
+ errors.serviceTypeId = 'Выберите услугу';
36
+ }
37
+ return errors;
38
+ }
39
+
40
+ /** Маска телефона при вводе — можно оставить для любой темы с тем же форматом */
41
+ export function formatPhoneInput(value) {
42
+ const digits = value.replace(/\D/g, '').slice(0, 11);
43
+ if (!digits.length) return '';
44
+ let d = digits;
45
+ if (d[0] === '8') d = '7' + d.slice(1);
46
+ if (d[0] !== '7') d = '7' + d;
47
+ const p = d.slice(1);
48
+ let out = '+7';
49
+ if (p.length > 0) out += `(${p.slice(0, 3)}`;
50
+ if (p.length >= 3) out += `)-${p.slice(3, 6)}`;
51
+ if (p.length >= 6) out += `-${p.slice(6, 8)}`;
52
+ if (p.length >= 8) out += `-${p.slice(8, 10)}`;
53
+ return out;
54
+ }
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import tailwindcss from '@tailwindcss/vite';
4
+
5
+
6
+ export default defineConfig({
7
+ plugins: [react(), tailwindcss()],
8
+ server: {
9
+ port: 5173,
10
+ proxy: {
11
+ '/api': {
12
+ target: 'http://localhost:3001',
13
+ changeOrigin: true,
14
+ },
15
+ },
16
+ },
17
+ });
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "exam-de-workspace",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Локальный проект ДЭ (exam-project). Публикация: npm run publish:npm из корня репозитория",
6
+ "type": "module",
7
+ "scripts": {
8
+ "install:all": "npm install && npm install --prefix server && npm install --prefix client",
9
+ "db:init": "npm run db:init --prefix server",
10
+ "dev": "concurrently \"npm run dev --prefix server\" \"npm run dev --prefix client\"",
11
+ "dev:server": "npm run dev --prefix server",
12
+ "dev:client": "npm run dev --prefix client",
13
+ "build": "npm run build --prefix client"
14
+ },
15
+ "devDependencies": {
16
+ "concurrently": "^9.1.2"
17
+ }
18
+ }
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Экзамен: создать exam-project + exam-guides в текущей папке.
4
+ * npx @mashka818/exam-de-template@1.0.0 init
5
+ */
6
+ import { execSync } from 'child_process';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
13
+
14
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.pack-tmp']);
15
+
16
+ function copyTree(src, dest) {
17
+ if (!fs.existsSync(src)) return;
18
+ fs.mkdirSync(dest, { recursive: true });
19
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
20
+ if (SKIP_DIRS.has(entry.name)) continue;
21
+ const s = path.join(src, entry.name);
22
+ const d = path.join(dest, entry.name);
23
+ if (entry.isDirectory()) copyTree(s, d);
24
+ else {
25
+ if (entry.name === '.env' && s.replace(/\\/g, '/').includes('/server/')) continue;
26
+ fs.mkdirSync(path.dirname(d), { recursive: true });
27
+ fs.copyFileSync(s, d);
28
+ }
29
+ }
30
+ }
31
+
32
+ const examRoot = process.argv.includes('--here')
33
+ ? process.cwd()
34
+ : process.argv[2]
35
+ ? path.resolve(process.argv[2])
36
+ : process.cwd();
37
+
38
+ const skipInstall = process.argv.includes('--no-install');
39
+
40
+ const srcProject = path.join(PACKAGE_ROOT, 'exam-project');
41
+ const srcGuides = path.join(PACKAGE_ROOT, 'exam-guides');
42
+ const destProject = path.join(examRoot, 'exam-project');
43
+ const destGuides = path.join(examRoot, 'exam-guides');
44
+
45
+ console.log('\n=== exam-de-init ===\n');
46
+ console.log(`Папка экзамена: ${examRoot}\n`);
47
+
48
+ if (!fs.existsSync(srcProject)) {
49
+ console.error('В пакете нет exam-project/. Обновите npm publish.');
50
+ process.exit(1);
51
+ }
52
+
53
+ if (fs.existsSync(destProject) && !process.argv.includes('--force')) {
54
+ console.log('Уже есть exam-project/. Добавьте --force для перезаписи.');
55
+ process.exit(1);
56
+ }
57
+
58
+ console.log('Копирую exam-project/ …');
59
+ if (fs.existsSync(destProject)) fs.rmSync(destProject, { recursive: true, force: true });
60
+ copyTree(srcProject, destProject);
61
+
62
+ if (fs.existsSync(srcGuides)) {
63
+ console.log('Копирую exam-guides/ …');
64
+ if (fs.existsSync(destGuides)) fs.rmSync(destGuides, { recursive: true, force: true });
65
+ copyTree(srcGuides, destGuides);
66
+ } else {
67
+ console.warn('В пакете нет exam-guides/ — только чистый проект.');
68
+ }
69
+
70
+ if (skipInstall) {
71
+ console.log('\nГотово (--no-install).\n');
72
+ process.exit(0);
73
+ }
74
+
75
+ console.log('\nУстановка зависимостей в exam-project/ …\n');
76
+ execSync('npm install', { cwd: destProject, stdio: 'inherit' });
77
+ execSync('npm run install:all', { cwd: destProject, stdio: 'inherit' });
78
+
79
+ console.log('\n=== Готово ===\n');
80
+ console.log(' exam-project/ — правите и запускаете здесь');
81
+ console.log(' exam-guides/ — шпоры; удалите перед сдачей\n');
82
+ console.log('Дальше:');
83
+ console.log(' cd exam-project');
84
+ console.log(' copy server\\.env.example server\\.env');
85
+ console.log(' npm run db:init');
86
+ console.log(' npm run dev\n');
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Проверка перед npm pack — в архив не должны попасть node_modules и .env
3
+ */
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
9
+
10
+ const bad = [];
11
+
12
+ for (const sub of ['node_modules', 'client/node_modules', 'server/node_modules', 'client/dist']) {
13
+ if (fs.existsSync(path.join(root, sub))) {
14
+ bad.push(sub);
15
+ }
16
+ }
17
+
18
+ if (fs.existsSync(path.join(root, 'server/.env'))) {
19
+ console.warn('prepack: server/.env есть локально — в .npmignore, в архив не попадёт.');
20
+ }
21
+
22
+ if (bad.length) {
23
+ console.warn('prepack: в проекте есть (в .npmignore, в tgz не войдут):');
24
+ bad.forEach((b) => console.warn(` - ${b}`));
25
+ }
26
+
27
+ console.log('prepack: ok — запускайте npm pack');
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Распаковка шаблона из npm-пакета в папку проекта (экзамен / новый ПК).
4
+ * Вызов: npx exam-de-unpack
5
+ * npx exam-de-unpack C:\DE\my-project
6
+ * npx exam-de-unpack --here
7
+ */
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
14
+
15
+ const SKIP_DIRS = new Set([
16
+ 'node_modules',
17
+ '.git',
18
+ 'dist',
19
+ 'exam-starter',
20
+ 'design',
21
+ ]);
22
+
23
+ const SKIP_ROOT_FILES = new Set([
24
+ 'exam-template-moy-ne-sam-1.0.0.tgz',
25
+ ]);
26
+
27
+ function shouldSkipDir(name) {
28
+ return SKIP_DIRS.has(name);
29
+ }
30
+
31
+ function shouldSkipFile(relPath) {
32
+ const base = path.basename(relPath);
33
+ if (SKIP_ROOT_FILES.has(base)) return true;
34
+ if (base.endsWith('.tgz')) return true;
35
+ if (relPath.replace(/\\/g, '/').includes('server/.env')) return true;
36
+ return false;
37
+ }
38
+
39
+ function parseTarget() {
40
+ const args = process.argv.slice(2).filter((a) => !a.startsWith('-'));
41
+ if (process.argv.includes('--here') || args.length === 0) {
42
+ return process.cwd();
43
+ }
44
+ return path.resolve(args[0]);
45
+ }
46
+
47
+ function copyRecursive(srcDir, destDir, relative = '') {
48
+ if (!fs.existsSync(srcDir)) return;
49
+
50
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
51
+
52
+ for (const entry of entries) {
53
+ const rel = relative ? `${relative}/${entry.name}` : entry.name;
54
+ const src = path.join(srcDir, entry.name);
55
+ const dest = path.join(destDir, entry.name);
56
+
57
+ if (entry.isDirectory()) {
58
+ if (shouldSkipDir(entry.name)) continue;
59
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
60
+ copyRecursive(src, dest, rel);
61
+ continue;
62
+ }
63
+
64
+ if (shouldSkipFile(rel)) continue;
65
+
66
+ const destEnv = path.join(destDir, 'server', '.env');
67
+ if (rel === 'server/.env' && fs.existsSync(path.join(destDir, 'server', '.env'))) {
68
+ console.log(' пропуск (уже есть): server/.env');
69
+ continue;
70
+ }
71
+
72
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
73
+ fs.copyFileSync(src, dest);
74
+ }
75
+ }
76
+
77
+ const target = parseTarget();
78
+ const force = process.argv.includes('--force');
79
+
80
+ console.log('');
81
+ console.log('=== exam-de-unpack: шаблон ДЭ ===');
82
+ console.log(`Из: ${PACKAGE_ROOT}`);
83
+ console.log(`В: ${target}`);
84
+ console.log('');
85
+
86
+ if (!fs.existsSync(target)) {
87
+ fs.mkdirSync(target, { recursive: true });
88
+ }
89
+
90
+ const marker = path.join(target, 'client', 'src', 'App.jsx');
91
+ if (fs.existsSync(marker) && !force) {
92
+ console.log('В папке уже есть шаблон (client/src/App.jsx).');
93
+ console.log('Чтобы перезаписать: npx exam-de-unpack --force');
94
+ process.exit(0);
95
+ }
96
+
97
+ copyRecursive(PACKAGE_ROOT, target);
98
+
99
+ console.log('Готово. Дальше в этой папке:');
100
+ console.log(' npm install');
101
+ console.log(' npm run install:all');
102
+ console.log(' настроить server/.env');
103
+ console.log(' npm run db:init');
104
+ console.log(' npm run dev');
105
+ console.log('');
@@ -0,0 +1,50 @@
1
+
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import bcrypt from 'bcryptjs';
6
+ import dotenv from 'dotenv';
7
+ import { pool } from './pool.js';
8
+
9
+ dotenv.config();
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ async function runSqlFile(filename) {
14
+ const sql = fs.readFileSync(path.join(__dirname, filename), 'utf8');
15
+ await pool.query(sql);
16
+ }
17
+
18
+ async function seedAdmin() {
19
+ // --- Учётка админа по заданию ДЭ (п.5) — менять здесь ---
20
+
21
+ const login = 'adminka';
22
+ const password = 'password';
23
+ const hash = await bcrypt.hash(password, 10);
24
+
25
+ await pool.query(
26
+ `INSERT INTO users (login, password_hash, full_name, phone, email, role)
27
+ VALUES ($1, $2, $3, $4, $5, 'admin')
28
+ ON CONFLICT (login) DO UPDATE SET
29
+ password_hash = EXCLUDED.password_hash,
30
+ role = 'admin'`,
31
+ [login, hash, 'Администратор системы', '+7(000)-000-00-00', 'admin@moynesam.local']
32
+ );
33
+ console.log('Админ: логин adminka, пароль password');
34
+ }
35
+
36
+ async function main() {
37
+ try {
38
+ await runSqlFile('schema.sql');
39
+ await runSqlFile('seed.sql');
40
+ await seedAdmin();
41
+ console.log('База данных готова.');
42
+ } catch (err) {
43
+ console.error('Ошибка инициализации:', err.message);
44
+ process.exit(1);
45
+ } finally {
46
+ await pool.end();
47
+ }
48
+ }
49
+
50
+ main();
@@ -0,0 +1,11 @@
1
+
2
+ import pg from 'pg';
3
+ import dotenv from 'dotenv';
4
+
5
+ dotenv.config();
6
+
7
+ const { Pool } = pg;
8
+
9
+ export const pool = new Pool({
10
+ connectionString: process.env.DATABASE_URL,
11
+ });