From 4952ed637179a4f381304868a9294ff05f92e104 Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 19 May 2026 14:43:26 +0500 Subject: [PATCH 01/13] feat: add HowToOrderSection with purchase step stepper --- .../info/ui/sections/HowToOrderSection.tsx | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 client/src/pages/info/ui/sections/HowToOrderSection.tsx diff --git a/client/src/pages/info/ui/sections/HowToOrderSection.tsx b/client/src/pages/info/ui/sections/HowToOrderSection.tsx new file mode 100644 index 0000000..af539f7 --- /dev/null +++ b/client/src/pages/info/ui/sections/HowToOrderSection.tsx @@ -0,0 +1,55 @@ +import Paper from '@mui/material/Paper' +import Step from '@mui/material/Step' +import StepContent from '@mui/material/StepContent' +import StepLabel from '@mui/material/StepLabel' +import Stepper from '@mui/material/Stepper' +import Typography from '@mui/material/Typography' +import { CheckCircle, ClipboardList, Mail, ShoppingCart, Truck } from 'lucide-react' + +const steps = [ + { + label: 'Выберите товары', + icon: , + text: 'Найдите нужные изделия в каталоге и добавьте их в корзину. Вы можете выбрать несколько товаров от разных мастеров — все они соберутся в одном заказе.', + }, + { + label: 'Проверьте корзину', + icon: , + text: 'Перейдите в корзину и проверьте состав заказа: названия товаров, количество и итоговую сумму. Здесь же можно изменить количество или удалить позиции.', + }, + { + label: 'Укажите контакты и адрес', + icon: , + text: 'Заполните имя, телефон и email для связи. Укажите адрес доставки — город, улицу, дом и квартиру. Эти данные нужны для расчёта стоимости и сроков.', + }, + { + label: 'Выберите доставку и оплату', + icon: , + text: 'Выберите способ доставки: самовывоз, курьер или почта/СДЭК. Затем укажите способ оплаты: картой онлайн или при получении.', + }, + { + label: 'Подтвердите заказ', + icon: , + text: 'Проверьте все данные ещё раз и нажмите «Оформить заказ». После этого мастер получит уведомление и начнёт подготовку вашего изделия.', + }, +] + +export function HowToOrderSection() { + return ( + + + Как оформить заказ + + + {steps.map((step, idx) => ( + + step.icon}>{step.label} + + {step.text} + + + ))} + + + ) +} From 22ac9e381d1dc465728855e87c4f683b32e70075 Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 19 May 2026 14:43:58 +0500 Subject: [PATCH 02/13] feat: add PaymentSection with card and cash methods --- .../pages/info/ui/sections/PaymentSection.tsx | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 client/src/pages/info/ui/sections/PaymentSection.tsx diff --git a/client/src/pages/info/ui/sections/PaymentSection.tsx b/client/src/pages/info/ui/sections/PaymentSection.tsx new file mode 100644 index 0000000..53eeb41 --- /dev/null +++ b/client/src/pages/info/ui/sections/PaymentSection.tsx @@ -0,0 +1,33 @@ +import List from '@mui/material/List' +import ListItem from '@mui/material/ListItem' +import ListItemIcon from '@mui/material/ListItemIcon' +import ListItemText from '@mui/material/ListItemText' +import Paper from '@mui/material/Paper' +import Typography from '@mui/material/Typography' +import { Banknote, CreditCard } from 'lucide-react' + +const methods = [ + { icon: , primary: 'Банковская карта онлайн', secondary: 'Оплата картой Visa, Mastercard или МИР сразу при оформлении заказа.' }, + { icon: , primary: 'Оплата при получении', secondary: 'Оплата наличными или картой при получении заказа у курьера или в пункте самовывоза.' }, +] + +export function PaymentSection() { + return ( + + + Оплата + + + Оплата происходит после подтверждения заказа мастером. Вы получите уведомление, когда заказ будет подтверждён и готов к оплате. + + + {methods.map((m) => ( + + {m.icon} + + + ))} + + + ) +} From 2ffa11be5046009b0162979cec628b49249e9f87 Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 19 May 2026 14:44:08 +0500 Subject: [PATCH 03/13] feat: add DeliverySection with pickup, courier, and postal cards --- .../info/ui/sections/DeliverySection.tsx | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 client/src/pages/info/ui/sections/DeliverySection.tsx diff --git a/client/src/pages/info/ui/sections/DeliverySection.tsx b/client/src/pages/info/ui/sections/DeliverySection.tsx new file mode 100644 index 0000000..23a6d2a --- /dev/null +++ b/client/src/pages/info/ui/sections/DeliverySection.tsx @@ -0,0 +1,61 @@ +import Grid from '@mui/material/Grid' +import Paper from '@mui/material/Paper' +import Stack from '@mui/material/Stack' +import Typography from '@mui/material/Typography' +import { Package, Store, Truck } from 'lucide-react' +import { PICKUP_ADDRESS_FULL } from '@/shared/constants/pickup-point' + +const deliveries = [ + { + title: 'Самовывоз', + icon: , + lines: ['Бесплатно.', PICKUP_ADDRESS_FULL, 'Перед визитом согласуем время — чтобы заказ точно был готов к выдаче.'], + }, + { + title: 'Курьер по городу', + icon: , + lines: [ + 'Доставка в пределах города.', + 'Сроки и стоимость зависят от адреса и веса заказа.', + 'Мастер свяжется с вами для уточнения деталей после оформления.', + ], + }, + { + title: 'Почта / СДЭК', + icon: , + lines: [ + 'Отправка в другие города.', + 'Каждому заказу присваивается трек-номер для отслеживания.', + 'Стоимость рассчитывается по тарифу перевозчика при оформлении.', + ], + }, +] + +export function DeliverySection() { + return ( + + + Доставка + + + {deliveries.map((d) => ( + + + + + {d.icon} + {d.title} + + {d.lines.map((line, i) => ( + + {line} + + ))} + + + + ))} + + + ) +} From f01ede6ee91cb6c00c4040d73b9204e5ef4b59dc Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 19 May 2026 14:44:23 +0500 Subject: [PATCH 04/13] feat: add ReturnsSection with return and warranty blocks --- .../pages/info/ui/sections/ReturnsSection.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 client/src/pages/info/ui/sections/ReturnsSection.tsx diff --git a/client/src/pages/info/ui/sections/ReturnsSection.tsx b/client/src/pages/info/ui/sections/ReturnsSection.tsx new file mode 100644 index 0000000..fba384a --- /dev/null +++ b/client/src/pages/info/ui/sections/ReturnsSection.tsx @@ -0,0 +1,35 @@ +import Paper from '@mui/material/Paper' +import Stack from '@mui/material/Stack' +import Typography from '@mui/material/Typography' + +export function ReturnsSection() { + return ( + + + Возврат и гарантии + + + + + Возврат + + + Если товар не соответствует описанию или имеет производственный дефект, свяжитесь с нами в течение 7 дней + после получения. Мы заменим изделие на аналогичное или вернём деньги. Возврат товара надлежащего качества + возможен в течение 14 дней, если изделие не было в употреблении и сохранён его товарный вид. + + + + + Гарантия качества + + + Мы отвечаем за качество каждого изделия ручной работы. Все дефекты, возникшие не по вине покупателя, + устраняются или компенсируются заменой изделия. Если у вас возникли вопросы по качеству — напишите нам, + и мы решим проблему в кратчайшие сроки. + + + + + ) +} From e7cc518d7fd18f9cd5b5111ab8f5ef219e813681 Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 19 May 2026 14:52:11 +0500 Subject: [PATCH 05/13] fix: prettier formatting and step key in info sections --- .../pages/info/ui/sections/HowToOrderSection.tsx | 4 ++-- .../src/pages/info/ui/sections/PaymentSection.tsx | 15 ++++++++++++--- .../src/pages/info/ui/sections/ReturnsSection.tsx | 4 ++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/client/src/pages/info/ui/sections/HowToOrderSection.tsx b/client/src/pages/info/ui/sections/HowToOrderSection.tsx index af539f7..07249f7 100644 --- a/client/src/pages/info/ui/sections/HowToOrderSection.tsx +++ b/client/src/pages/info/ui/sections/HowToOrderSection.tsx @@ -41,8 +41,8 @@ export function HowToOrderSection() { Как оформить заказ - {steps.map((step, idx) => ( - + {steps.map((step) => ( + step.icon}>{step.label} {step.text} diff --git a/client/src/pages/info/ui/sections/PaymentSection.tsx b/client/src/pages/info/ui/sections/PaymentSection.tsx index 53eeb41..a32639f 100644 --- a/client/src/pages/info/ui/sections/PaymentSection.tsx +++ b/client/src/pages/info/ui/sections/PaymentSection.tsx @@ -7,8 +7,16 @@ import Typography from '@mui/material/Typography' import { Banknote, CreditCard } from 'lucide-react' const methods = [ - { icon: , primary: 'Банковская карта онлайн', secondary: 'Оплата картой Visa, Mastercard или МИР сразу при оформлении заказа.' }, - { icon: , primary: 'Оплата при получении', secondary: 'Оплата наличными или картой при получении заказа у курьера или в пункте самовывоза.' }, + { + icon: , + primary: 'Банковская карта онлайн', + secondary: 'Оплата картой Visa, Mastercard или МИР сразу при оформлении заказа.', + }, + { + icon: , + primary: 'Оплата при получении', + secondary: 'Оплата наличными или картой при получении заказа у курьера или в пункте самовывоза.', + }, ] export function PaymentSection() { @@ -18,7 +26,8 @@ export function PaymentSection() { Оплата - Оплата происходит после подтверждения заказа мастером. Вы получите уведомление, когда заказ будет подтверждён и готов к оплате. + Оплата происходит после подтверждения заказа мастером. Вы получите уведомление, когда заказ будет подтверждён и + готов к оплате. {methods.map((m) => ( diff --git a/client/src/pages/info/ui/sections/ReturnsSection.tsx b/client/src/pages/info/ui/sections/ReturnsSection.tsx index fba384a..9e691f8 100644 --- a/client/src/pages/info/ui/sections/ReturnsSection.tsx +++ b/client/src/pages/info/ui/sections/ReturnsSection.tsx @@ -25,8 +25,8 @@ export function ReturnsSection() { Мы отвечаем за качество каждого изделия ручной работы. Все дефекты, возникшие не по вине покупателя, - устраняются или компенсируются заменой изделия. Если у вас возникли вопросы по качеству — напишите нам, - и мы решим проблему в кратчайшие сроки. + устраняются или компенсируются заменой изделия. Если у вас возникли вопросы по качеству — напишите нам, и мы + решим проблему в кратчайшие сроки. From 777ba6ec15f97874ad1e48d6cdfb6d64fb6e50b6 Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 19 May 2026 14:53:42 +0500 Subject: [PATCH 06/13] feat: rewrite InfoPage as static container with section components --- client/src/pages/info/ui/InfoPage.tsx | 37 ++++++++------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/client/src/pages/info/ui/InfoPage.tsx b/client/src/pages/info/ui/InfoPage.tsx index 318b328..bbfad43 100644 --- a/client/src/pages/info/ui/InfoPage.tsx +++ b/client/src/pages/info/ui/InfoPage.tsx @@ -1,42 +1,27 @@ -import Alert from '@mui/material/Alert' import Box from '@mui/material/Box' -import Paper from '@mui/material/Paper' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' -import { useQuery } from '@tanstack/react-query' -import { fetchPublicInfoBlocks } from '@/entities/info' +import { DeliverySection } from './sections/DeliverySection' +import { HowToOrderSection } from './sections/HowToOrderSection' +import { PaymentSection } from './sections/PaymentSection' +import { ReturnsSection } from './sections/ReturnsSection' export function InfoPage() { - const q = useQuery({ - queryKey: ['info-page', 'public', 'blocks'], - queryFn: fetchPublicInfoBlocks, - }) - return ( Информация для покупателей - + Как оформить заказ, как проходит доставка, оплата и другие важные детали. - {q.isLoading && Загрузка…} - {q.isError && Не удалось загрузить информацию.} - {q.isSuccess && q.data.items.length === 0 && Раздел пока не заполнен.} - - {q.isSuccess && q.data.items.length > 0 && ( - - {q.data.items.map((block) => ( - - - {block.title} - - {block.body} - - ))} - - )} + + + + + + ) } From dbe36ce6fdb3c4d81006751a958253e31210065a Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 19 May 2026 14:55:13 +0500 Subject: [PATCH 07/13] feat: remove admin info page CRUD --- client/src/pages/admin-info/index.ts | 1 - .../src/pages/admin-info/ui/AdminInfoPage.tsx | 216 ------------------ 2 files changed, 217 deletions(-) delete mode 100644 client/src/pages/admin-info/index.ts delete mode 100644 client/src/pages/admin-info/ui/AdminInfoPage.tsx diff --git a/client/src/pages/admin-info/index.ts b/client/src/pages/admin-info/index.ts deleted file mode 100644 index aeb45fa..0000000 --- a/client/src/pages/admin-info/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AdminInfoPage } from './ui/AdminInfoPage' diff --git a/client/src/pages/admin-info/ui/AdminInfoPage.tsx b/client/src/pages/admin-info/ui/AdminInfoPage.tsx deleted file mode 100644 index 2c537e3..0000000 --- a/client/src/pages/admin-info/ui/AdminInfoPage.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import Alert from '@mui/material/Alert' -import Box from '@mui/material/Box' -import Button from '@mui/material/Button' -import Dialog from '@mui/material/Dialog' -import DialogActions from '@mui/material/DialogActions' -import DialogContent from '@mui/material/DialogContent' -import DialogTitle from '@mui/material/DialogTitle' -import FormControlLabel from '@mui/material/FormControlLabel' -import Stack from '@mui/material/Stack' -import Switch from '@mui/material/Switch' -import Table from '@mui/material/Table' -import TableBody from '@mui/material/TableBody' -import TableCell from '@mui/material/TableCell' -import TableHead from '@mui/material/TableHead' -import TableRow from '@mui/material/TableRow' -import TextField from '@mui/material/TextField' -import Typography from '@mui/material/Typography' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { Controller, useForm } from 'react-hook-form' -import { - createInfoBlock, - deleteInfoBlock, - fetchAdminInfoBlocks, - type InfoPageBlock, - updateInfoBlock, -} from '@/entities/info' -import { getErrorMessage } from '@/shared/lib/get-error-message' -import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys' -import { useEditDialogState } from '@/shared/lib/use-edit-dialog-state' -import { EntityRowActions } from '@/shared/ui/EntityRowActions' - -type FormState = { - key: string - title: string - body: string - sort: string - published: boolean -} - -const emptyForm = (): FormState => ({ key: '', title: '', body: '', sort: '0', published: true }) - -export function AdminInfoPage() { - const qc = useQueryClient() - const { dialogOpen, editing, openCreateDialog, openEditDialog, closeDialog } = useEditDialogState() - const form = useForm({ defaultValues: emptyForm(), mode: 'onChange' }) - - const blocksQuery = useQuery({ - queryKey: ['admin', 'info-page', 'blocks'], - queryFn: fetchAdminInfoBlocks, - }) - - const saveMut = useMutation({ - mutationFn: async () => { - const values = form.getValues() - const payload = { - key: values.key.trim(), - title: values.title.trim(), - body: values.body.trim(), - sort: Number(values.sort || 0), - published: values.published, - } - if (editing) return updateInfoBlock(editing.id, payload) - return createInfoBlock(payload) - }, - onSuccess: async () => { - closeDialog() - form.reset(emptyForm()) - await invalidateQueryKeys(qc, [ - ['admin', 'info-page', 'blocks'], - ['info-page', 'public', 'blocks'], - ]) - }, - }) - - const deleteMut = useMutation({ - mutationFn: (id: string) => deleteInfoBlock(id), - onSuccess: async () => { - await invalidateQueryKeys(qc, [ - ['admin', 'info-page', 'blocks'], - ['info-page', 'public', 'blocks'], - ]) - }, - }) - - const openCreate = () => { - form.reset(emptyForm()) - openCreateDialog() - } - - const openEdit = (item: InfoPageBlock) => { - openEditDialog(item) - form.reset({ - key: item.key, - title: item.title, - body: item.body, - sort: String(item.sort), - published: item.published, - }) - } - - const items = blocksQuery.data?.items ?? [] - const err = saveMut.error ?? deleteMut.error - - return ( - - - Информационная страница - - - - - Управление блоками страницы с процессом покупки, оплаты и доставки. - - - {blocksQuery.isError && Не удалось загрузить блоки.} - {err && ( - - {getErrorMessage(err)} - - )} - - - - - Key - Заголовок - Порядок - Опубликован - Действия - - - - {items.map((item) => ( - - {item.key} - {item.title} - {item.sort} - {item.published ? 'Да' : 'Нет'} - - openEdit(item)} - onDelete={() => deleteMut.mutate(item.id)} - deleteDisabled={deleteMut.isPending} - /> - - - ))} - {items.length === 0 && !blocksQuery.isLoading && ( - - - Блоков пока нет. - - - )} - -
- - - {editing ? 'Редактировать блок' : 'Новый блок'} - - - } - /> - } - /> - ( - - )} - /> - } - /> - ( - field.onChange(v)} />} - label="Показывать на публичной странице" - /> - )} - /> - - - - - - - -
- ) -} From 5eadbd0d0e92d07a9e9739b8101e66af20a12b1e Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 19 May 2026 14:55:28 +0500 Subject: [PATCH 08/13] feat: remove info entity (admin CRUD layer) --- client/src/entities/info/api/info-page-api.ts | 31 ------------------- client/src/entities/info/index.ts | 8 ----- client/src/entities/info/model/types.ts | 10 ------ 3 files changed, 49 deletions(-) delete mode 100644 client/src/entities/info/api/info-page-api.ts delete mode 100644 client/src/entities/info/index.ts delete mode 100644 client/src/entities/info/model/types.ts diff --git a/client/src/entities/info/api/info-page-api.ts b/client/src/entities/info/api/info-page-api.ts deleted file mode 100644 index 70873ac..0000000 --- a/client/src/entities/info/api/info-page-api.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { apiClient } from '@/shared/api/client' -import type { InfoPageBlock } from '../model/types' - -export async function fetchPublicInfoBlocks(): Promise<{ items: InfoPageBlock[] }> { - const { data } = await apiClient.get<{ items: InfoPageBlock[] }>('info-page/blocks') - return data -} - -export async function fetchAdminInfoBlocks(): Promise<{ items: InfoPageBlock[] }> { - const { data } = await apiClient.get<{ items: InfoPageBlock[] }>('admin/info-page/blocks') - return data -} - -export async function createInfoBlock( - body: Pick, -): Promise<{ item: InfoPageBlock }> { - const { data } = await apiClient.post<{ item: InfoPageBlock }>('admin/info-page/blocks', body) - return data -} - -export async function updateInfoBlock( - id: string, - body: Partial>, -): Promise<{ item: InfoPageBlock }> { - const { data } = await apiClient.patch<{ item: InfoPageBlock }>(`admin/info-page/blocks/${id}`, body) - return data -} - -export async function deleteInfoBlock(id: string): Promise { - await apiClient.delete(`admin/info-page/blocks/${id}`) -} diff --git a/client/src/entities/info/index.ts b/client/src/entities/info/index.ts deleted file mode 100644 index 5714569..0000000 --- a/client/src/entities/info/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { - fetchPublicInfoBlocks, - fetchAdminInfoBlocks, - createInfoBlock, - updateInfoBlock, - deleteInfoBlock, -} from './api/info-page-api' -export type { InfoPageBlock } from './model/types' diff --git a/client/src/entities/info/model/types.ts b/client/src/entities/info/model/types.ts deleted file mode 100644 index 76fb54a..0000000 --- a/client/src/entities/info/model/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type InfoPageBlock = { - id: string - key: string - title: string - body: string - sort: number - published: boolean - createdAt: string - updatedAt: string -} From 348ffd940cf63bb3606a34c2db2e75919fcc9959 Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 19 May 2026 14:56:08 +0500 Subject: [PATCH 09/13] feat: remove info page from admin navigation and routes --- client/src/pages/admin-layout/ui/AdminLayoutPage.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/src/pages/admin-layout/ui/AdminLayoutPage.tsx b/client/src/pages/admin-layout/ui/AdminLayoutPage.tsx index 8dfb726..69333d8 100644 --- a/client/src/pages/admin-layout/ui/AdminLayoutPage.tsx +++ b/client/src/pages/admin-layout/ui/AdminLayoutPage.tsx @@ -15,12 +15,11 @@ import Typography from '@mui/material/Typography' import useMediaQuery from '@mui/material/useMediaQuery' import { useQuery } from '@tanstack/react-query' import { useUnit } from 'effector-react' -import { Bell, FileText, Image, LayoutGrid, ListOrdered, MessageSquare, Store, Users } from 'lucide-react' +import { Bell, Image, LayoutGrid, ListOrdered, MessageSquare, Store, Users } from 'lucide-react' import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom' import { fetchAdminOrdersSummary } from '@/entities/order/api/admin-order-api' import { AdminCategoriesPage } from '@/pages/admin-categories' import { AdminGalleryPage } from '@/pages/admin-gallery' -import { AdminInfoPage } from '@/pages/admin-info' import { AdminOrdersPage } from '@/pages/admin-orders' import { AdminProductsPage } from '@/pages/admin-products' import { AdminReviewsPage } from '@/pages/admin-reviews' @@ -61,7 +60,6 @@ export function AdminLayoutPage() { { to: '/admin/orders', label: 'Заказы', icon: }, { to: '/admin/reviews', label: 'Отзывы', icon: }, { to: '/admin/users', label: 'Пользователи', icon: }, - { to: '/admin/info', label: 'Инфо-страница', icon: }, { to: '/admin/notifications', label: 'Оповещения', icon: }, ], [], @@ -189,7 +187,6 @@ export function AdminLayoutPage() { } /> } /> } /> - } /> } /> } /> From 57275514bf279032507506c0aae4a61d572ef49a Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 19 May 2026 14:56:37 +0500 Subject: [PATCH 10/13] feat: remove server info-page routes --- server/src/routes/api.js | 2 - server/src/routes/api/info-page.js | 102 ----------------------------- 2 files changed, 104 deletions(-) delete mode 100644 server/src/routes/api/info-page.js diff --git a/server/src/routes/api.js b/server/src/routes/api.js index 07fd2f0..32612d6 100644 --- a/server/src/routes/api.js +++ b/server/src/routes/api.js @@ -7,7 +7,6 @@ import { registerAdminProductRoutes } from './api/admin-products.js' import { registerAdminReviewRoutes } from './api/admin-reviews.js' import { registerAdminUserRoutes } from './api/admin-users.js' import { registerCatalogSliderRoutes } from './api/catalog-slider.js' -import { registerInfoPageRoutes } from './api/info-page.js' import { registerPublicCatalogRoutes } from './api/public-catalog.js' import { registerPublicReviewRoutes } from './api/public-reviews.js' @@ -18,7 +17,6 @@ export async function registerApiRoutes(fastify) { await registerPublicCatalogRoutes(fastify) await registerPublicReviewRoutes(fastify) - await registerInfoPageRoutes(fastify) await registerCatalogSliderRoutes(fastify) await registerAdminProductRoutes(fastify) diff --git a/server/src/routes/api/info-page.js b/server/src/routes/api/info-page.js deleted file mode 100644 index 58863f1..0000000 --- a/server/src/routes/api/info-page.js +++ /dev/null @@ -1,102 +0,0 @@ -import { prisma } from '../../lib/prisma.js' - -function validateBlockPayload(body, reply) { - const key = String(body?.key || '').trim() - const title = String(body?.title || '').trim() - const content = String(body?.body || '').trim() - const sort = Number(body?.sort ?? 0) - const published = body?.published === undefined ? true : Boolean(body.published) - - if (!key) return reply.code(400).send({ error: 'key обязателен' }) - if (!/^[a-z0-9_-]{2,60}$/i.test(key)) { - return reply.code(400).send({ error: 'key должен быть 2-60 символов: буквы, цифры, "_" или "-"' }) - } - if (!title) return reply.code(400).send({ error: 'title обязателен' }) - if (title.length > 120) return reply.code(400).send({ error: 'title максимум 120 символов' }) - if (!content) return reply.code(400).send({ error: 'body обязателен' }) - if (content.length > 5000) return reply.code(400).send({ error: 'body максимум 5000 символов' }) - if (!Number.isFinite(sort) || Math.abs(sort) > 10_000) return reply.code(400).send({ error: 'Некорректный sort' }) - - return { key, title, body: content, sort: Math.trunc(sort), published } -} - -export async function registerInfoPageRoutes(fastify) { - fastify.get('/api/info-page/blocks', async () => { - const items = await prisma.infoPageBlock.findMany({ - where: { published: true }, - orderBy: [{ sort: 'asc' }, { createdAt: 'asc' }], - }) - return { items } - }) - - fastify.get('/api/admin/info-page/blocks', { preHandler: [fastify.verifyAdmin] }, async () => { - const items = await prisma.infoPageBlock.findMany({ orderBy: [{ sort: 'asc' }, { createdAt: 'asc' }] }) - return { items } - }) - - fastify.post('/api/admin/info-page/blocks', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => { - const validated = validateBlockPayload(request.body, reply) - if (!validated) return - - try { - const item = await prisma.infoPageBlock.create({ data: validated }) - return reply.code(201).send({ item }) - } catch { - return reply.code(409).send({ error: 'Блок с таким key уже существует' }) - } - }) - - fastify.patch('/api/admin/info-page/blocks/:id', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => { - const { id } = request.params - const existing = await prisma.infoPageBlock.findUnique({ where: { id } }) - if (!existing) return reply.code(404).send({ error: 'Блок не найден' }) - - const body = request.body ?? {} - const data = {} - if (body.key !== undefined) { - const key = String(body.key || '').trim() - if (!key) return reply.code(400).send({ error: 'key обязателен' }) - if (!/^[a-z0-9_-]{2,60}$/i.test(key)) { - return reply.code(400).send({ error: 'key должен быть 2-60 символов: буквы, цифры, "_" или "-"' }) - } - data.key = key - } - if (body.title !== undefined) { - const title = String(body.title || '').trim() - if (!title) return reply.code(400).send({ error: 'title обязателен' }) - if (title.length > 120) return reply.code(400).send({ error: 'title максимум 120 символов' }) - data.title = title - } - if (body.body !== undefined) { - const content = String(body.body || '').trim() - if (!content) return reply.code(400).send({ error: 'body обязателен' }) - if (content.length > 5000) return reply.code(400).send({ error: 'body максимум 5000 символов' }) - data.body = content - } - if (body.sort !== undefined) { - const sort = Number(body.sort) - if (!Number.isFinite(sort) || Math.abs(sort) > 10_000) return reply.code(400).send({ error: 'Некорректный sort' }) - data.sort = Math.trunc(sort) - } - if (body.published !== undefined) { - data.published = Boolean(body.published) - } - - try { - const item = await prisma.infoPageBlock.update({ where: { id }, data }) - return { item } - } catch { - return reply.code(409).send({ error: 'Блок с таким key уже существует' }) - } - }) - - fastify.delete('/api/admin/info-page/blocks/:id', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => { - const { id } = request.params - try { - await prisma.infoPageBlock.delete({ where: { id } }) - return reply.code(204).send() - } catch { - return reply.code(404).send({ error: 'Блок не найден' }) - } - }) -} From 17b683f131ead177a8fcc84d5896c2339832a0df Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 19 May 2026 15:14:38 +0500 Subject: [PATCH 11/13] feat: remove InfoPageBlock model from Prisma schema --- .../migration.sql | 10 ++++++++++ server/prisma/schema.prisma | 13 ------------- 2 files changed, 10 insertions(+), 13 deletions(-) create mode 100644 server/prisma/migrations/20260519095803_remove_infopageblock/migration.sql diff --git a/server/prisma/migrations/20260519095803_remove_infopageblock/migration.sql b/server/prisma/migrations/20260519095803_remove_infopageblock/migration.sql new file mode 100644 index 0000000..2ecacbb --- /dev/null +++ b/server/prisma/migrations/20260519095803_remove_infopageblock/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to drop the `InfoPageBlock` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "InfoPageBlock"; +PRAGMA foreign_keys=on; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 2e34bc3..c2c6a83 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -259,19 +259,6 @@ model AuthCode { @@index([expiresAt]) } -model InfoPageBlock { - id String @id @default(cuid()) - key String @unique - title String - body String - sort Int @default(0) - published Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([published, sort]) -} - /// Настройки оповещений пользователя model NotificationPreference { id String @id @default(cuid()) From 0b01b61e48f63e11cbbd811ca8d3ee5bdfcbac1a Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 19 May 2026 15:18:42 +0500 Subject: [PATCH 12/13] fix: use MUI v9 slots API for StepLabel stepIcon --- client/src/pages/info/ui/sections/HowToOrderSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/info/ui/sections/HowToOrderSection.tsx b/client/src/pages/info/ui/sections/HowToOrderSection.tsx index 07249f7..c51a52d 100644 --- a/client/src/pages/info/ui/sections/HowToOrderSection.tsx +++ b/client/src/pages/info/ui/sections/HowToOrderSection.tsx @@ -43,7 +43,7 @@ export function HowToOrderSection() { {steps.map((step) => ( - step.icon}>{step.label} + step.icon }}>{step.label} {step.text} From cb4661dc136143d6a88c3e948c6f8351d3e37e00 Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 19 May 2026 15:32:45 +0500 Subject: [PATCH 13/13] test commit --- .../info/ui/sections/DeliverySection.tsx | 13 +- .../pages/info/ui/sections/PaymentSection.tsx | 2 +- .../plans/2026-05-19-info-page-static.md | 636 ++++++++++++++++++ .../2026-05-19-info-page-static-design.md | 102 +++ server/prisma/prisma/dev.db | Bin 0 -> 311296 bytes 5 files changed, 741 insertions(+), 12 deletions(-) create mode 100644 docs/superpowers/plans/2026-05-19-info-page-static.md create mode 100644 docs/superpowers/specs/2026-05-19-info-page-static-design.md create mode 100644 server/prisma/prisma/dev.db diff --git a/client/src/pages/info/ui/sections/DeliverySection.tsx b/client/src/pages/info/ui/sections/DeliverySection.tsx index 23a6d2a..2a6cc01 100644 --- a/client/src/pages/info/ui/sections/DeliverySection.tsx +++ b/client/src/pages/info/ui/sections/DeliverySection.tsx @@ -2,7 +2,7 @@ import Grid from '@mui/material/Grid' import Paper from '@mui/material/Paper' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' -import { Package, Store, Truck } from 'lucide-react' +import { Package, Store } from 'lucide-react' import { PICKUP_ADDRESS_FULL } from '@/shared/constants/pickup-point' const deliveries = [ @@ -12,16 +12,7 @@ const deliveries = [ lines: ['Бесплатно.', PICKUP_ADDRESS_FULL, 'Перед визитом согласуем время — чтобы заказ точно был готов к выдаче.'], }, { - title: 'Курьер по городу', - icon: , - lines: [ - 'Доставка в пределах города.', - 'Сроки и стоимость зависят от адреса и веса заказа.', - 'Мастер свяжется с вами для уточнения деталей после оформления.', - ], - }, - { - title: 'Почта / СДЭК', + title: 'Почта / Службы доставки', icon: , lines: [ 'Отправка в другие города.', diff --git a/client/src/pages/info/ui/sections/PaymentSection.tsx b/client/src/pages/info/ui/sections/PaymentSection.tsx index a32639f..d558824 100644 --- a/client/src/pages/info/ui/sections/PaymentSection.tsx +++ b/client/src/pages/info/ui/sections/PaymentSection.tsx @@ -15,7 +15,7 @@ const methods = [ { icon: , primary: 'Оплата при получении', - secondary: 'Оплата наличными или картой при получении заказа у курьера или в пункте самовывоза.', + secondary: 'Оплата наличными или картой при получении заказа.', }, ] diff --git a/docs/superpowers/plans/2026-05-19-info-page-static.md b/docs/superpowers/plans/2026-05-19-info-page-static.md new file mode 100644 index 0000000..6efdf63 --- /dev/null +++ b/docs/superpowers/plans/2026-05-19-info-page-static.md @@ -0,0 +1,636 @@ +# Static Info Page Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace admin-managed dynamic InfoPageBlock CRUD with a hardcoded static React page featuring process schemas, delivery cards, and payment info. + +**Architecture:** New `InfoPage.tsx` container composes four hardcoded section components (no API calls, no DB reads). All admin CRUD files, server routes, Prisma model, and entity layer are removed. + +**Tech Stack:** React + TypeScript + MUI, lucide-react icons. + +--- + +## File Structure + +### Created + +- `client/src/pages/info/ui/sections/HowToOrderSection.tsx` — Stepper with 5 purchase steps +- `client/src/pages/info/ui/sections/DeliverySection.tsx` — 3 delivery option cards in Grid +- `client/src/pages/info/ui/sections/PaymentSection.tsx` — List of payment methods +- `client/src/pages/info/ui/sections/ReturnsSection.tsx` — Returns & warranty Paper blocks + +### Modified + +- `client/src/pages/info/ui/InfoPage.tsx` — Rewrite as static container without useQuery +- `client/src/pages/admin-layout/ui/AdminLayoutPage.tsx` — Remove info page nav item and import +- `server/src/routes/api.js` — Remove import and registration call +- `server/prisma/schema.prisma` — Remove InfoPageBlock model + +### Deleted + +- `client/src/pages/admin-info/ui/AdminInfoPage.tsx` +- `client/src/pages/admin-info/index.ts` +- `client/src/entities/info/api/info-page-api.ts` +- `client/src/entities/info/model/types.ts` +- `client/src/entities/info/index.ts` +- `server/src/routes/api/info-page.js` + +--- + +### Task 1: Create HowToOrderSection + +**Files:** + +- Create: `client/src/pages/info/ui/sections/HowToOrderSection.tsx` + +- [ ] **Step 1: Write the component** + +```tsx +import Paper from "@mui/material/Paper"; +import Step from "@mui/material/Step"; +import StepContent from "@mui/material/StepContent"; +import StepLabel from "@mui/material/StepLabel"; +import Stepper from "@mui/material/Stepper"; +import Typography from "@mui/material/Typography"; +import { + CheckCircle, + ClipboardList, + Mail, + ShoppingCart, + Truck, +} from "lucide-react"; + +const steps = [ + { + label: "Выберите товары", + icon: , + text: "Найдите нужные изделия в каталоге и добавьте их в корзину. Вы можете выбрать несколько товаров от разных мастеров — все они соберутся в одном заказе.", + }, + { + label: "Проверьте корзину", + icon: , + text: "Перейдите в корзину и проверьте состав заказа: названия товаров, количество и итоговую сумму. Здесь же можно изменить количество или удалить позиции.", + }, + { + label: "Укажите контакты и адрес", + icon: , + text: "Заполните имя, телефон и email для связи. Укажите адрес доставки — город, улицу, дом и квартиру. Эти данные нужны для расчёта стоимости и сроков.", + }, + { + label: "Выберите доставку и оплату", + icon: , + text: "Выберите способ доставки: самовывоз, курьер или почта/СДЭК. Затем укажите способ оплаты: картой онлайн или при получении.", + }, + { + label: "Подтвердите заказ", + icon: , + text: "Проверьте все данные ещё раз и нажмите «Оформить заказ». После этого мастер получит уведомление и начнёт подготовку вашего изделия.", + }, +]; + +export function HowToOrderSection() { + return ( + + + Как оформить заказ + + + {steps.map((step, idx) => ( + + step.icon}> + {step.label} + + + {step.text} + + + ))} + + + ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add client/src/pages/info/ui/sections/HowToOrderSection.tsx +git commit -m "feat: add HowToOrderSection with purchase step stepper" +``` + +--- + +### Task 2: Create DeliverySection + +**Files:** + +- Create: `client/src/pages/info/ui/sections/DeliverySection.tsx` + +- [ ] **Step 1: Write the component** + +```tsx +import Grid from "@mui/material/Grid"; +import Paper from "@mui/material/Paper"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Package, Store, Truck } from "lucide-react"; +import { PICKUP_ADDRESS_FULL } from "@/shared/constants/pickup-point"; + +const deliveries = [ + { + title: "Самовывоз", + icon: , + lines: [ + "Бесплатно.", + PICKUP_ADDRESS_FULL, + "Перед визитом согласуем время — чтобы заказ точно был готов к выдаче.", + ], + }, + { + title: "Курьер по городу", + icon: , + lines: [ + "Доставка в пределах города.", + "Сроки и стоимость зависят от адреса и веса заказа.", + "Мастер свяжется с вами для уточнения деталей после оформления.", + ], + }, + { + title: "Почта / СДЭК", + icon: , + lines: [ + "Отправка в другие города.", + "Каждому заказу присваивается трек-номер для отслеживания.", + "Стоимость рассчитывается по тарифу перевозчика при оформлении.", + ], + }, +]; + +export function DeliverySection() { + return ( + + + Доставка + + + {deliveries.map((d) => ( + + + + + {d.icon} + {d.title} + + {d.lines.map((line, i) => ( + + {line} + + ))} + + + + ))} + + + ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add client/src/pages/info/ui/sections/DeliverySection.tsx +git commit -m "feat: add DeliverySection with pickup, courier, and postal cards" +``` + +--- + +### Task 3: Create PaymentSection + +**Files:** + +- Create: `client/src/pages/info/ui/sections/PaymentSection.tsx` + +- [ ] **Step 1: Write the component** + +```tsx +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Paper from "@mui/material/Paper"; +import Typography from "@mui/material/Typography"; +import { Banknote, CreditCard } from "lucide-react"; + +const methods = [ + { + icon: , + primary: "Банковская карта онлайн", + secondary: + "Оплата картой Visa, Mastercard или МИР сразу при оформлении заказа.", + }, + { + icon: , + primary: "Оплата при получении", + secondary: "Оплата наличными или картой при получении заказа.", + }, +]; + +export function PaymentSection() { + return ( + + + Оплата + + + Оплата происходит после подтверждения заказа мастером. Вы получите + уведомление, когда заказ будет подтверждён и готов к оплате. + + + {methods.map((m) => ( + + {m.icon} + + + ))} + + + ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add client/src/pages/info/ui/sections/PaymentSection.tsx +git commit -m "feat: add PaymentSection with card and cash methods" +``` + +--- + +### Task 4: Create ReturnsSection + +**Files:** + +- Create: `client/src/pages/info/ui/sections/ReturnsSection.tsx` + +- [ ] **Step 1: Write the component** + +```tsx +import Paper from "@mui/material/Paper"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; + +export function ReturnsSection() { + return ( + + + Возврат и гарантии + + + + + Возврат + + + Если товар не соответствует описанию или имеет производственный + дефект, свяжитесь с нами в течение 7 дней после получения. Мы + заменим изделие на аналогичное или вернём деньги. Возврат товара + надлежащего качества возможен в течение 14 дней, если изделие не + было в употреблении и сохранён его товарный вид. + + + + + Гарантия качества + + + Мы отвечаем за качество каждого изделия ручной работы. Все дефекты, + возникшие не по вине покупателя, устраняются или компенсируются + заменой изделия. Если у вас возникли вопросы по качеству — напишите + нам, и мы решим проблему в кратчайшие сроки. + + + + + ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add client/src/pages/info/ui/sections/ReturnsSection.tsx +git commit -m "feat: add ReturnsSection with return and warranty blocks" +``` + +--- + +### Task 5: Rewrite InfoPage as static container + +**Files:** + +- Modify: `client/src/pages/info/ui/InfoPage.tsx` + +- [ ] **Step 1: Rewrite InfoPage.tsx** + +```tsx +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { DeliverySection } from "./sections/DeliverySection"; +import { HowToOrderSection } from "./sections/HowToOrderSection"; +import { PaymentSection } from "./sections/PaymentSection"; +import { ReturnsSection } from "./sections/ReturnsSection"; + +export function InfoPage() { + return ( + + + Информация для покупателей + + + Как оформить заказ, как проходит доставка, оплата и другие важные + детали. + + + + + + + + + + ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add client/src/pages/info/ui/InfoPage.tsx +git commit -m "feat: rewrite InfoPage as static container with section components" +``` + +--- + +### Task 6: Delete admin info page + +**Files:** + +- Delete: `client/src/pages/admin-info/ui/AdminInfoPage.tsx` +- Delete: `client/src/pages/admin-info/index.ts` + +- [ ] **Step 1: Delete files** + +```bash +rm client/src/pages/admin-info/ui/AdminInfoPage.tsx +rm client/src/pages/admin-info/index.ts +``` + +- [ ] **Step 2: Commit** + +```bash +git add client/src/pages/admin-info/ +git commit -m "feat: remove admin info page CRUD" +``` + +--- + +### Task 7: Delete entities/info + +**Files:** + +- Delete: `client/src/entities/info/api/info-page-api.ts` +- Delete: `client/src/entities/info/model/types.ts` +- Delete: `client/src/entities/info/index.ts` + +- [ ] **Step 1: Delete files** + +```bash +rm client/src/entities/info/api/info-page-api.ts +rm client/src/entities/info/model/types.ts +rm client/src/entities/info/index.ts +``` + +- [ ] **Step 2: Commit** + +```bash +git add client/src/entities/info/ +git commit -m "feat: remove info entity (admin CRUD layer)" +``` + +--- + +### Task 8: Clean up AdminLayoutPage + +**Files:** + +- Modify: `client/src/pages/admin-layout/ui/AdminLayoutPage.tsx` + +- [ ] **Step 1: Remove import** + +```tsx +// REMOVE line 23: +import { AdminInfoPage } from "@/pages/admin-info"; +``` + +Execute this edit: In `client/src/pages/admin-layout/ui/AdminLayoutPage.tsx`, remove the import line: + +```tsx +import { AdminInfoPage } from "@/pages/admin-info"; +``` + +- [ ] **Step 2: Remove FileText from lucide-react import** + +In line 18, change: + +```tsx +import { + Bell, + FileText, + Image, + LayoutGrid, + ListOrdered, + MessageSquare, + Store, + Users, +} from "lucide-react"; +``` + +to: + +```tsx +import { + Bell, + Image, + LayoutGrid, + ListOrdered, + MessageSquare, + Store, + Users, +} from "lucide-react"; +``` + +- [ ] **Step 3: Remove nav item** + +Remove the nav item entry (line 64): + +```tsx +{ to: '/admin/info', label: 'Инфо-страница', icon: }, +``` + +- [ ] **Step 4: Remove route** + +Remove the route line 192: + +```tsx +} /> +``` + +- [ ] **Step 5: Commit** + +```bash +git add client/src/pages/admin-layout/ui/AdminLayoutPage.tsx +git commit -m "feat: remove info page from admin navigation and routes" +``` + +--- + +### Task 9: Delete server info-page routes + +**Files:** + +- Delete: `server/src/routes/api/info-page.js` +- Modify: `server/src/routes/api.js` + +- [ ] **Step 1: Delete the routes file** + +```bash +rm server/src/routes/api/info-page.js +``` + +- [ ] **Step 2: Clean up api.js** + +In `server/src/routes/api.js`: + +Remove the import line 10: + +```js +import { registerInfoPageRoutes } from "./api/info-page.js"; +``` + +Remove the call line 21: + +```js +await registerInfoPageRoutes(fastify); +``` + +- [ ] **Step 3: Commit** + +```bash +git add server/src/routes/api/info-page.js server/src/routes/api.js +git commit -m "feat: remove server info-page routes" +``` + +--- + +### Task 10: Remove InfoPageBlock model from Prisma schema + +**Files:** + +- Modify: `server/prisma/schema.prisma` + +- [ ] **Step 1: Remove model from schema** + +Remove lines 262-273 from `server/prisma/schema.prisma`: + +```prisma +model InfoPageBlock { + id String @id @default(cuid()) + key String @unique + title String + body String + sort Int @default(0) + published Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([published, sort]) +} +``` + +Also remove the blank line before it (line 261). + +- [ ] **Step 2: Run migration** + +```bash +cd server && npm run db:migrate +``` + +Expected: Prisma creates a new migration dropping the `InfoPageBlock` table. + +- [ ] **Step 3: Commit** + +```bash +git add server/prisma/schema.prisma server/prisma/migrations/ +git commit -m "feat: remove InfoPageBlock model from Prisma schema" +``` + +--- + +### Task 11: Verify build and lint + +- [ ] **Step 1: Run server tests** + +```bash +cd server && npm test +``` + +Expected: all tests pass (no info-page tests exist, but other tests should still pass after removing routes). + +- [ ] **Step 2: Run client lint** + +```bash +cd client && npm run lint +``` + +Expected: no errors. + +- [ ] **Step 3: Run client format check** + +```bash +cd client && npm run format:check +``` + +Expected: all files formatted. + +- [ ] **Step 4: Run client tests** + +```bash +cd client && npm test +``` + +Expected: all tests pass. + +- [ ] **Step 5: Build client** + +```bash +cd client && npm run build +``` + +Expected: tsc + Vite build succeed with no errors. + +- [ ] **Step 6: Commit if any fixes** + +```bash +git add -A +git commit -m "chore: lint and build fixes after info page migration" +``` diff --git a/docs/superpowers/specs/2026-05-19-info-page-static-design.md b/docs/superpowers/specs/2026-05-19-info-page-static-design.md new file mode 100644 index 0000000..97bcd3f --- /dev/null +++ b/docs/superpowers/specs/2026-05-19-info-page-static-design.md @@ -0,0 +1,102 @@ +# 2026-05-19 — Статическая страница «О покупке» (удаление админ-CRUD) + +## Цель + +Убрать ручное наполнение информационной страницы админом через `InfoPageBlock` CRUD. +Заменить на статическую страницу с хардкод-контентом в React-компонентах: схемы процессов, +пошаговые инструкции, карточки доставки, список оплат, условия возврата. + +## Что удаляется + +| Файл/директория | Действие | +|---|---| +| `client/src/pages/admin-info/` | Удалить целиком (AdminInfoPage + index.ts) | +| `client/src/entities/info/` | Удалить целиком (api, model, index.ts) | +| `server/src/routes/api/info-page.js` | Удалить целиком | +| `server/src/routes/api.js` | Убрать `import` + вызов `registerInfoPageRoutes` | +| `server/prisma/schema.prisma` | Удалить модель `InfoPageBlock` и связанный индекс | +| `client/src/pages/admin-layout/ui/AdminLayoutPage.tsx` | Убрать пункт меню «Инфо-страница», импорт и роут | +| Миграция `20260503144425_...` | Не трогать, Prisma обработает через `db:migrate` | + +После удаления модели Prisma нужно выполнить `npm run db:migrate` в `server/`. + +## Что остаётся + +- Публичный роут `GET /api/info-page/blocks` удаляется — страница больше не ходит на сервер +- Роут `/info` в `client/src/app/routes/index.tsx` остаётся как есть +- Ссылка «О покупке» в футере `MainLayout.tsx` остаётся как есть + +## Новая структура страницы + +``` +client/src/pages/info/ + ui/ + InfoPage.tsx -- контейнер: заголовок + секции + sections/ + HowToOrderSection.tsx -- Stepper: 5 шагов покупки + DeliverySection.tsx -- Grid-карточки: самовывоз, курьер, почта + PaymentSection.tsx -- List со способами оплаты + ReturnsSection.tsx -- Paper-блоки: возврат, гарантия + index.ts -- export { InfoPage } +``` + +## Дизайн секций + +### InfoPage (контейнер) + +- Typography `variant="h4"`: «Информация для покупателей» +- Typography `color="text.secondary"` с текущим подзаголовком +- Секции рендерятся последовательно с `Stack spacing={4}` + +### HowToOrderSection + +- MUI `Stepper` вертикальный, `activeStep=-1` (все шаги видны, не активные) +- 5 шагов с `StepLabel`, каждый содержит: + - Заголовок шага + - Пояснительный текст + - Иконку через `StepIconComponent` или проп icon в `Step` + +Шаги: +1. «Выберите товары» (ShoppingCart) — Найдите нужное в каталоге, добавьте в корзину +2. «Проверьте корзину» (ClipboardList) — Проверьте состав заказа, количество и итоговую сумму +3. «Укажите контакты и адрес» (Mail) — Заполните имя, телефон, email и адрес доставки +4. «Выберите доставку и оплату» (Truck) — Выберите удобный способ получения и оплаты +5. «Подтвердите заказ» (CheckCircle) — Проверьте всё ещё раз и нажмите «Оформить заказ» + +### DeliverySection + +- Три карточки в `Grid container spacing={2}` с `Paper variant="outlined"`: + - **Самовывоз** (Store) — Бесплатно. Адрес. Перед визитом согласуем время. + - **Курьер по городу** (Truck) — Доставка в пределах города. Сроки и стоимость уточняются. + - **Почта / СДЭК** (Package) — Отправка с трек-номером. Стоимость по тарифу перевозчика. + +### PaymentSection + +- MUI `List` с `ListItem` элементами, каждый с `ListItemIcon`: + - Банковская карта онлайн (CreditCard) + - Оплата при получении (Banknote) +- Текст: «Оплата происходит после подтверждения заказа мастером.» + +### ReturnsSection + +- Два `Paper variant="outlined"` блока: + - «Возврат» — Если товар не соответствует описанию или есть дефект, свяжитесь с нами. Мы заменим изделие или вернём деньги. + - «Гарантия» — Мы отвечаем за качество каждого изделия. Все дефекты, возникшие не по вине покупателя, устраняем или меняем изделие. + +## Константы + +Использовать существующие: +- `PICKUP_ADDRESS_FULL`, `PICKUP_COORDINATES` из `@/shared/constants/pickup-point` +- `STORE_EMAIL` из `@/shared/constants/store` +- Для способов оплаты — текст захардкожен, т.к. payment-method из shared/constants содержит только ключи + +## Иконки + +Все иконки из `lucide-react`: ShoppingCart, ClipboardList, Mail, Truck, CheckCircle, Store, Package, CreditCard, Banknote. + +## Что НЕ входит в scope + +- Сохранение обратной совместимости с текущей динамической страницей +- Миграция существующих данных InfoPageBlock (они просто удаляются) +- Автообновление контента админом (страница статическая) +- Телефоны поддержки (если понадобятся — отдельной задачей) diff --git a/server/prisma/prisma/dev.db b/server/prisma/prisma/dev.db new file mode 100644 index 0000000000000000000000000000000000000000..e72aaa953026346fe3580096297f2556a80b59ec GIT binary patch literal 311296 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 literal 0 HcmV?d00001