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 -} 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="Показывать на публичной странице" - /> - )} - /> - - - - - - - -
- ) -} 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() { } /> } /> } /> - } /> } /> } /> 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} - - ))} - - )} + + + + + + ) } 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..2a6cc01 --- /dev/null +++ b/client/src/pages/info/ui/sections/DeliverySection.tsx @@ -0,0 +1,52 @@ +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 } from 'lucide-react' +import { PICKUP_ADDRESS_FULL } from '@/shared/constants/pickup-point' + +const deliveries = [ + { + title: 'Самовывоз', + icon: , + lines: ['Бесплатно.', PICKUP_ADDRESS_FULL, 'Перед визитом согласуем время — чтобы заказ точно был готов к выдаче.'], + }, + { + title: 'Почта / Службы доставки', + icon: , + lines: [ + 'Отправка в другие города.', + 'Каждому заказу присваивается трек-номер для отслеживания.', + 'Стоимость рассчитывается по тарифу перевозчика при оформлении.', + ], + }, +] + +export function DeliverySection() { + return ( + + + Доставка + + + {deliveries.map((d) => ( + + + + + {d.icon} + {d.title} + + {d.lines.map((line, i) => ( + + {line} + + ))} + + + + ))} + + + ) +} 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..c51a52d --- /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) => ( + + 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 new file mode 100644 index 0000000..d558824 --- /dev/null +++ b/client/src/pages/info/ui/sections/PaymentSection.tsx @@ -0,0 +1,42 @@ +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} + + + ))} + + + ) +} 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..9e691f8 --- /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 дней, если изделие не было в употреблении и сохранён его товарный вид. + + + + + Гарантия качества + + + Мы отвечаем за качество каждого изделия ручной работы. Все дефекты, возникшие не по вине покупателя, + устраняются или компенсируются заменой изделия. Если у вас возникли вопросы по качеству — напишите нам, и мы + решим проблему в кратчайшие сроки. + + + + + ) +} 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/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/prisma/dev.db b/server/prisma/prisma/dev.db new file mode 100644 index 0000000..e72aaa9 Binary files /dev/null and b/server/prisma/prisma/dev.db differ 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()) 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: 'Блок не найден' }) - } - }) -}