Merge branch 'instruktion'
This commit is contained in:
@@ -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<InfoPageBlock, 'key' | 'title' | 'body' | 'sort' | 'published'>,
|
|
||||||
): 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<Pick<InfoPageBlock, 'key' | 'title' | 'body' | 'sort' | 'published'>>,
|
|
||||||
): 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<void> {
|
|
||||||
await apiClient.delete(`admin/info-page/blocks/${id}`)
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export {
|
|
||||||
fetchPublicInfoBlocks,
|
|
||||||
fetchAdminInfoBlocks,
|
|
||||||
createInfoBlock,
|
|
||||||
updateInfoBlock,
|
|
||||||
deleteInfoBlock,
|
|
||||||
} from './api/info-page-api'
|
|
||||||
export type { InfoPageBlock } from './model/types'
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export type InfoPageBlock = {
|
|
||||||
id: string
|
|
||||||
key: string
|
|
||||||
title: string
|
|
||||||
body: string
|
|
||||||
sort: number
|
|
||||||
published: boolean
|
|
||||||
createdAt: string
|
|
||||||
updatedAt: string
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { AdminInfoPage } from './ui/AdminInfoPage'
|
|
||||||
@@ -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<InfoPageBlock>()
|
|
||||||
const form = useForm<FormState>({ 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 (
|
|
||||||
<Box>
|
|
||||||
<Stack direction="row" sx={{ mb: 2, alignItems: 'center', justifyContent: 'space-between' }}>
|
|
||||||
<Typography variant="h4">Информационная страница</Typography>
|
|
||||||
<Button variant="contained" onClick={openCreate}>
|
|
||||||
Новый блок
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Typography color="text.secondary" sx={{ mb: 2 }}>
|
|
||||||
Управление блоками страницы с процессом покупки, оплаты и доставки.
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{blocksQuery.isError && <Alert severity="error">Не удалось загрузить блоки.</Alert>}
|
|
||||||
{err && (
|
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
|
||||||
{getErrorMessage(err)}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Table size="small">
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>Key</TableCell>
|
|
||||||
<TableCell>Заголовок</TableCell>
|
|
||||||
<TableCell>Порядок</TableCell>
|
|
||||||
<TableCell>Опубликован</TableCell>
|
|
||||||
<TableCell align="right">Действия</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{items.map((item) => (
|
|
||||||
<TableRow key={item.id} hover>
|
|
||||||
<TableCell>{item.key}</TableCell>
|
|
||||||
<TableCell>{item.title}</TableCell>
|
|
||||||
<TableCell>{item.sort}</TableCell>
|
|
||||||
<TableCell>{item.published ? 'Да' : 'Нет'}</TableCell>
|
|
||||||
<TableCell align="right">
|
|
||||||
<EntityRowActions
|
|
||||||
onEdit={() => openEdit(item)}
|
|
||||||
onDelete={() => deleteMut.mutate(item.id)}
|
|
||||||
deleteDisabled={deleteMut.isPending}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
{items.length === 0 && !blocksQuery.isLoading && (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={5} sx={{ color: 'text.secondary' }}>
|
|
||||||
Блоков пока нет.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onClose={closeDialog} fullWidth maxWidth="md">
|
|
||||||
<DialogTitle>{editing ? 'Редактировать блок' : 'Новый блок'}</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<Stack spacing={2} sx={{ mt: 1 }}>
|
|
||||||
<Controller
|
|
||||||
control={form.control}
|
|
||||||
name="key"
|
|
||||||
render={({ field }) => <TextField label="Key (латиница, цифры, _-)" fullWidth required {...field} />}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={form.control}
|
|
||||||
name="title"
|
|
||||||
render={({ field }) => <TextField label="Заголовок" fullWidth required {...field} />}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={form.control}
|
|
||||||
name="body"
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField label="Содержимое" fullWidth multiline minRows={5} required {...field} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={form.control}
|
|
||||||
name="sort"
|
|
||||||
render={({ field }) => <TextField label="Порядок сортировки" fullWidth {...field} />}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={form.control}
|
|
||||||
name="published"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormControlLabel
|
|
||||||
control={<Switch checked={field.value} onChange={(_, v) => field.onChange(v)} />}
|
|
||||||
label="Показывать на публичной странице"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={closeDialog}>Отмена</Button>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={() => saveMut.mutate()}
|
|
||||||
disabled={
|
|
||||||
saveMut.isPending ||
|
|
||||||
!form.watch('key').trim() ||
|
|
||||||
!form.watch('title').trim() ||
|
|
||||||
!form.watch('body').trim()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{editing ? 'Сохранить' : 'Создать'}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -15,12 +15,11 @@ import Typography from '@mui/material/Typography'
|
|||||||
import useMediaQuery from '@mui/material/useMediaQuery'
|
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { useUnit } from 'effector-react'
|
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 { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { fetchAdminOrdersSummary } from '@/entities/order/api/admin-order-api'
|
import { fetchAdminOrdersSummary } from '@/entities/order/api/admin-order-api'
|
||||||
import { AdminCategoriesPage } from '@/pages/admin-categories'
|
import { AdminCategoriesPage } from '@/pages/admin-categories'
|
||||||
import { AdminGalleryPage } from '@/pages/admin-gallery'
|
import { AdminGalleryPage } from '@/pages/admin-gallery'
|
||||||
import { AdminInfoPage } from '@/pages/admin-info'
|
|
||||||
import { AdminOrdersPage } from '@/pages/admin-orders'
|
import { AdminOrdersPage } from '@/pages/admin-orders'
|
||||||
import { AdminProductsPage } from '@/pages/admin-products'
|
import { AdminProductsPage } from '@/pages/admin-products'
|
||||||
import { AdminReviewsPage } from '@/pages/admin-reviews'
|
import { AdminReviewsPage } from '@/pages/admin-reviews'
|
||||||
@@ -61,7 +60,6 @@ export function AdminLayoutPage() {
|
|||||||
{ to: '/admin/orders', label: 'Заказы', icon: <ListOrdered /> },
|
{ to: '/admin/orders', label: 'Заказы', icon: <ListOrdered /> },
|
||||||
{ to: '/admin/reviews', label: 'Отзывы', icon: <MessageSquare /> },
|
{ to: '/admin/reviews', label: 'Отзывы', icon: <MessageSquare /> },
|
||||||
{ to: '/admin/users', label: 'Пользователи', icon: <Users /> },
|
{ to: '/admin/users', label: 'Пользователи', icon: <Users /> },
|
||||||
{ to: '/admin/info', label: 'Инфо-страница', icon: <FileText /> },
|
|
||||||
{ to: '/admin/notifications', label: 'Оповещения', icon: <Bell /> },
|
{ to: '/admin/notifications', label: 'Оповещения', icon: <Bell /> },
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
@@ -189,7 +187,6 @@ export function AdminLayoutPage() {
|
|||||||
<Route path="orders" element={<AdminOrdersPage />} />
|
<Route path="orders" element={<AdminOrdersPage />} />
|
||||||
<Route path="reviews" element={<AdminReviewsPage />} />
|
<Route path="reviews" element={<AdminReviewsPage />} />
|
||||||
<Route path="users" element={<AdminUsersPage />} />
|
<Route path="users" element={<AdminUsersPage />} />
|
||||||
<Route path="info" element={<AdminInfoPage />} />
|
|
||||||
<Route path="notifications" element={<AdminNotificationsPage />} />
|
<Route path="notifications" element={<AdminNotificationsPage />} />
|
||||||
<Route path="*" element={<Navigate to="/admin" replace />} />
|
<Route path="*" element={<Navigate to="/admin" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -1,42 +1,27 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Paper from '@mui/material/Paper'
|
|
||||||
import Stack from '@mui/material/Stack'
|
import Stack from '@mui/material/Stack'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { DeliverySection } from './sections/DeliverySection'
|
||||||
import { fetchPublicInfoBlocks } from '@/entities/info'
|
import { HowToOrderSection } from './sections/HowToOrderSection'
|
||||||
|
import { PaymentSection } from './sections/PaymentSection'
|
||||||
|
import { ReturnsSection } from './sections/ReturnsSection'
|
||||||
|
|
||||||
export function InfoPage() {
|
export function InfoPage() {
|
||||||
const q = useQuery({
|
|
||||||
queryKey: ['info-page', 'public', 'blocks'],
|
|
||||||
queryFn: fetchPublicInfoBlocks,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
Информация для покупателей
|
Информация для покупателей
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color="text.secondary" sx={{ mb: 2 }}>
|
<Typography color="text.secondary" sx={{ mb: 3 }}>
|
||||||
Как оформить заказ, как проходит доставка, оплата и другие важные детали.
|
Как оформить заказ, как проходит доставка, оплата и другие важные детали.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{q.isLoading && <Typography color="text.secondary">Загрузка…</Typography>}
|
<Stack spacing={3}>
|
||||||
{q.isError && <Alert severity="error">Не удалось загрузить информацию.</Alert>}
|
<HowToOrderSection />
|
||||||
{q.isSuccess && q.data.items.length === 0 && <Alert severity="info">Раздел пока не заполнен.</Alert>}
|
<DeliverySection />
|
||||||
|
<PaymentSection />
|
||||||
{q.isSuccess && q.data.items.length > 0 && (
|
<ReturnsSection />
|
||||||
<Stack spacing={2}>
|
</Stack>
|
||||||
{q.data.items.map((block) => (
|
|
||||||
<Paper key={block.id} variant="outlined" sx={{ p: 2, borderRadius: 2 }}>
|
|
||||||
<Typography variant="h6" sx={{ mb: 0.75 }}>
|
|
||||||
{block.title}
|
|
||||||
</Typography>
|
|
||||||
<Typography sx={{ whiteSpace: 'pre-wrap' }}>{block.body}</Typography>
|
|
||||||
</Paper>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: <Store size={28} />,
|
||||||
|
lines: ['Бесплатно.', PICKUP_ADDRESS_FULL, 'Перед визитом согласуем время — чтобы заказ точно был готов к выдаче.'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Почта / Службы доставки',
|
||||||
|
icon: <Package size={28} />,
|
||||||
|
lines: [
|
||||||
|
'Отправка в другие города.',
|
||||||
|
'Каждому заказу присваивается трек-номер для отслеживания.',
|
||||||
|
'Стоимость рассчитывается по тарифу перевозчика при оформлении.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function DeliverySection() {
|
||||||
|
return (
|
||||||
|
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>
|
||||||
|
Доставка
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{deliveries.map((d) => (
|
||||||
|
<Grid key={d.title} size={{ xs: 12, sm: 6, md: 4 }}>
|
||||||
|
<Paper variant="outlined" sx={{ p: 2, borderRadius: 2, height: '100%' }}>
|
||||||
|
<Stack spacing={1.5}>
|
||||||
|
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||||
|
{d.icon}
|
||||||
|
<Typography variant="h6">{d.title}</Typography>
|
||||||
|
</Stack>
|
||||||
|
{d.lines.map((line, i) => (
|
||||||
|
<Typography key={i} variant="body2" color="text.secondary">
|
||||||
|
{line}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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: <ShoppingCart size={20} />,
|
||||||
|
text: 'Найдите нужные изделия в каталоге и добавьте их в корзину. Вы можете выбрать несколько товаров от разных мастеров — все они соберутся в одном заказе.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Проверьте корзину',
|
||||||
|
icon: <ClipboardList size={20} />,
|
||||||
|
text: 'Перейдите в корзину и проверьте состав заказа: названия товаров, количество и итоговую сумму. Здесь же можно изменить количество или удалить позиции.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Укажите контакты и адрес',
|
||||||
|
icon: <Mail size={20} />,
|
||||||
|
text: 'Заполните имя, телефон и email для связи. Укажите адрес доставки — город, улицу, дом и квартиру. Эти данные нужны для расчёта стоимости и сроков.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Выберите доставку и оплату',
|
||||||
|
icon: <Truck size={20} />,
|
||||||
|
text: 'Выберите способ доставки: самовывоз, курьер или почта/СДЭК. Затем укажите способ оплаты: картой онлайн или при получении.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Подтвердите заказ',
|
||||||
|
icon: <CheckCircle size={20} />,
|
||||||
|
text: 'Проверьте все данные ещё раз и нажмите «Оформить заказ». После этого мастер получит уведомление и начнёт подготовку вашего изделия.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function HowToOrderSection() {
|
||||||
|
return (
|
||||||
|
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>
|
||||||
|
Как оформить заказ
|
||||||
|
</Typography>
|
||||||
|
<Stepper orientation="vertical" activeStep={-1}>
|
||||||
|
{steps.map((step) => (
|
||||||
|
<Step key={step.label} completed={false}>
|
||||||
|
<StepLabel slots={{ stepIcon: () => step.icon }}>{step.label}</StepLabel>
|
||||||
|
<StepContent>
|
||||||
|
<Typography color="text.secondary">{step.text}</Typography>
|
||||||
|
</StepContent>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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: <CreditCard size={22} />,
|
||||||
|
primary: 'Банковская карта онлайн',
|
||||||
|
secondary: 'Оплата картой Visa, Mastercard или МИР сразу при оформлении заказа.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Banknote size={22} />,
|
||||||
|
primary: 'Оплата при получении',
|
||||||
|
secondary: 'Оплата наличными или картой при получении заказа.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function PaymentSection() {
|
||||||
|
return (
|
||||||
|
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>
|
||||||
|
Оплата
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||||
|
Оплата происходит после подтверждения заказа мастером. Вы получите уведомление, когда заказ будет подтверждён и
|
||||||
|
готов к оплате.
|
||||||
|
</Typography>
|
||||||
|
<List disablePadding>
|
||||||
|
{methods.map((m) => (
|
||||||
|
<ListItem key={m.primary} disableGutters>
|
||||||
|
<ListItemIcon sx={{ minWidth: 40 }}>{m.icon}</ListItemIcon>
|
||||||
|
<ListItemText primary={m.primary} secondary={m.secondary} />
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>
|
||||||
|
Возврат и гарантии
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Paper variant="outlined" sx={{ p: 2, borderRadius: 2 }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }} gutterBottom>
|
||||||
|
Возврат
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Если товар не соответствует описанию или имеет производственный дефект, свяжитесь с нами в течение 7 дней
|
||||||
|
после получения. Мы заменим изделие на аналогичное или вернём деньги. Возврат товара надлежащего качества
|
||||||
|
возможен в течение 14 дней, если изделие не было в употреблении и сохранён его товарный вид.
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
<Paper variant="outlined" sx={{ p: 2, borderRadius: 2 }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }} gutterBottom>
|
||||||
|
Гарантия качества
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Мы отвечаем за качество каждого изделия ручной работы. Все дефекты, возникшие не по вине покупателя,
|
||||||
|
устраняются или компенсируются заменой изделия. Если у вас возникли вопросы по качеству — напишите нам, и мы
|
||||||
|
решим проблему в кратчайшие сроки.
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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: <ShoppingCart size={20} />,
|
||||||
|
text: "Найдите нужные изделия в каталоге и добавьте их в корзину. Вы можете выбрать несколько товаров от разных мастеров — все они соберутся в одном заказе.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Проверьте корзину",
|
||||||
|
icon: <ClipboardList size={20} />,
|
||||||
|
text: "Перейдите в корзину и проверьте состав заказа: названия товаров, количество и итоговую сумму. Здесь же можно изменить количество или удалить позиции.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Укажите контакты и адрес",
|
||||||
|
icon: <Mail size={20} />,
|
||||||
|
text: "Заполните имя, телефон и email для связи. Укажите адрес доставки — город, улицу, дом и квартиру. Эти данные нужны для расчёта стоимости и сроков.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Выберите доставку и оплату",
|
||||||
|
icon: <Truck size={20} />,
|
||||||
|
text: "Выберите способ доставки: самовывоз, курьер или почта/СДЭК. Затем укажите способ оплаты: картой онлайн или при получении.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Подтвердите заказ",
|
||||||
|
icon: <CheckCircle size={20} />,
|
||||||
|
text: "Проверьте все данные ещё раз и нажмите «Оформить заказ». После этого мастер получит уведомление и начнёт подготовку вашего изделия.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function HowToOrderSection() {
|
||||||
|
return (
|
||||||
|
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>
|
||||||
|
Как оформить заказ
|
||||||
|
</Typography>
|
||||||
|
<Stepper orientation="vertical" activeStep={-1}>
|
||||||
|
{steps.map((step, idx) => (
|
||||||
|
<Step key={idx} completed={false}>
|
||||||
|
<StepLabel StepIconComponent={() => step.icon}>
|
||||||
|
{step.label}
|
||||||
|
</StepLabel>
|
||||||
|
<StepContent>
|
||||||
|
<Typography color="text.secondary">{step.text}</Typography>
|
||||||
|
</StepContent>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **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: <Store size={28} />,
|
||||||
|
lines: [
|
||||||
|
"Бесплатно.",
|
||||||
|
PICKUP_ADDRESS_FULL,
|
||||||
|
"Перед визитом согласуем время — чтобы заказ точно был готов к выдаче.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Курьер по городу",
|
||||||
|
icon: <Truck size={28} />,
|
||||||
|
lines: [
|
||||||
|
"Доставка в пределах города.",
|
||||||
|
"Сроки и стоимость зависят от адреса и веса заказа.",
|
||||||
|
"Мастер свяжется с вами для уточнения деталей после оформления.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Почта / СДЭК",
|
||||||
|
icon: <Package size={28} />,
|
||||||
|
lines: [
|
||||||
|
"Отправка в другие города.",
|
||||||
|
"Каждому заказу присваивается трек-номер для отслеживания.",
|
||||||
|
"Стоимость рассчитывается по тарифу перевозчика при оформлении.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function DeliverySection() {
|
||||||
|
return (
|
||||||
|
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>
|
||||||
|
Доставка
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{deliveries.map((d) => (
|
||||||
|
<Grid key={d.title} size={{ xs: 12, sm: 6, md: 4 }}>
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ p: 2, borderRadius: 2, height: "100%" }}
|
||||||
|
>
|
||||||
|
<Stack spacing={1.5}>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={1}
|
||||||
|
sx={{ alignItems: "center" }}
|
||||||
|
>
|
||||||
|
{d.icon}
|
||||||
|
<Typography variant="h6">{d.title}</Typography>
|
||||||
|
</Stack>
|
||||||
|
{d.lines.map((line, i) => (
|
||||||
|
<Typography key={i} variant="body2" color="text.secondary">
|
||||||
|
{line}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **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: <CreditCard size={22} />,
|
||||||
|
primary: "Банковская карта онлайн",
|
||||||
|
secondary:
|
||||||
|
"Оплата картой Visa, Mastercard или МИР сразу при оформлении заказа.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Banknote size={22} />,
|
||||||
|
primary: "Оплата при получении",
|
||||||
|
secondary: "Оплата наличными или картой при получении заказа.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function PaymentSection() {
|
||||||
|
return (
|
||||||
|
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>
|
||||||
|
Оплата
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||||
|
Оплата происходит после подтверждения заказа мастером. Вы получите
|
||||||
|
уведомление, когда заказ будет подтверждён и готов к оплате.
|
||||||
|
</Typography>
|
||||||
|
<List disablePadding>
|
||||||
|
{methods.map((m) => (
|
||||||
|
<ListItem key={m.primary} disableGutters>
|
||||||
|
<ListItemIcon sx={{ minWidth: 40 }}>{m.icon}</ListItemIcon>
|
||||||
|
<ListItemText primary={m.primary} secondary={m.secondary} />
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **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 (
|
||||||
|
<Paper variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
|
||||||
|
<Typography variant="h5" gutterBottom>
|
||||||
|
Возврат и гарантии
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Paper variant="outlined" sx={{ p: 2, borderRadius: 2 }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }} gutterBottom>
|
||||||
|
Возврат
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Если товар не соответствует описанию или имеет производственный
|
||||||
|
дефект, свяжитесь с нами в течение 7 дней после получения. Мы
|
||||||
|
заменим изделие на аналогичное или вернём деньги. Возврат товара
|
||||||
|
надлежащего качества возможен в течение 14 дней, если изделие не
|
||||||
|
было в употреблении и сохранён его товарный вид.
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
<Paper variant="outlined" sx={{ p: 2, borderRadius: 2 }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }} gutterBottom>
|
||||||
|
Гарантия качества
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Мы отвечаем за качество каждого изделия ручной работы. Все дефекты,
|
||||||
|
возникшие не по вине покупателя, устраняются или компенсируются
|
||||||
|
заменой изделия. Если у вас возникли вопросы по качеству — напишите
|
||||||
|
нам, и мы решим проблему в кратчайшие сроки.
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **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 (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Информация для покупателей
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
Как оформить заказ, как проходит доставка, оплата и другие важные
|
||||||
|
детали.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<HowToOrderSection />
|
||||||
|
<DeliverySection />
|
||||||
|
<PaymentSection />
|
||||||
|
<ReturnsSection />
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **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: <FileText /> },
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Remove route**
|
||||||
|
|
||||||
|
Remove the route line 192:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Route path="info" element={<AdminInfoPage />} />
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **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"
|
||||||
|
```
|
||||||
@@ -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 (они просто удаляются)
|
||||||
|
- Автообновление контента админом (страница статическая)
|
||||||
|
- Телефоны поддержки (если понадобятся — отдельной задачей)
|
||||||
@@ -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;
|
||||||
Binary file not shown.
@@ -259,19 +259,6 @@ model AuthCode {
|
|||||||
@@index([expiresAt])
|
@@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 {
|
model NotificationPreference {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { registerAdminProductRoutes } from './api/admin-products.js'
|
|||||||
import { registerAdminReviewRoutes } from './api/admin-reviews.js'
|
import { registerAdminReviewRoutes } from './api/admin-reviews.js'
|
||||||
import { registerAdminUserRoutes } from './api/admin-users.js'
|
import { registerAdminUserRoutes } from './api/admin-users.js'
|
||||||
import { registerCatalogSliderRoutes } from './api/catalog-slider.js'
|
import { registerCatalogSliderRoutes } from './api/catalog-slider.js'
|
||||||
import { registerInfoPageRoutes } from './api/info-page.js'
|
|
||||||
import { registerPublicCatalogRoutes } from './api/public-catalog.js'
|
import { registerPublicCatalogRoutes } from './api/public-catalog.js'
|
||||||
import { registerPublicReviewRoutes } from './api/public-reviews.js'
|
import { registerPublicReviewRoutes } from './api/public-reviews.js'
|
||||||
|
|
||||||
@@ -18,7 +17,6 @@ export async function registerApiRoutes(fastify) {
|
|||||||
|
|
||||||
await registerPublicCatalogRoutes(fastify)
|
await registerPublicCatalogRoutes(fastify)
|
||||||
await registerPublicReviewRoutes(fastify)
|
await registerPublicReviewRoutes(fastify)
|
||||||
await registerInfoPageRoutes(fastify)
|
|
||||||
await registerCatalogSliderRoutes(fastify)
|
await registerCatalogSliderRoutes(fastify)
|
||||||
|
|
||||||
await registerAdminProductRoutes(fastify)
|
await registerAdminProductRoutes(fastify)
|
||||||
|
|||||||
@@ -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: 'Блок не найден' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user