WhiteNightsAdminPanel/src/components/LinkedItems.tsx

233 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {useState, useEffect} from 'react'
import {Stack, Typography, Button, MenuItem, Select, FormControl, InputLabel, Grid, Box, Accordion, AccordionSummary, AccordionDetails, useTheme, TextField} from '@mui/material'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import axios from 'axios'
import {BACKEND_URL} from '../lib/constants'
type Field<T> = {
label: string
data: keyof T
}
type ExtraFieldConfig = {
type: 'number'
label: string
minValue: number
maxValue: (linkedItems: any[]) => number
}
type LinkedItemsProps<T> = {
parentId: string | number
parentResource: string
childResource: string
fields: Field<T>[]
title: string
type: 'show' | 'edit'
extraField?: ExtraFieldConfig
}
export const LinkedItems = <T extends {id: number; [key: string]: any}>({parentId, parentResource, childResource, fields, title, type}: LinkedItemsProps<T>) => {
const [items, setItems] = useState<T[]>([])
const [linkedItems, setLinkedItems] = useState<T[]>([])
const [selectedItemId, setSelectedItemId] = useState<number | null>(null)
const [pageNum, setPageNum] = useState<number>(1)
const [isLoading, setIsLoading] = useState<boolean>(true)
const [mediaOrder, setMediaOrder] = useState<number>(1)
const theme = useTheme()
useEffect(() => {
if (parentId) {
axios
.get(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`)
.then((response) => {
setLinkedItems(response?.data || [])
})
.catch(() => {
setLinkedItems([])
})
}
}, [parentId, parentResource, childResource])
useEffect(() => {
if (type === 'edit') {
axios
.get(`${BACKEND_URL}/${childResource}/`)
.then((response) => {
setItems(response?.data || [])
setIsLoading(false)
})
.catch(() => {
setItems([])
setIsLoading(false)
})
} else {
setIsLoading(false)
}
}, [childResource, type])
useEffect(() => {
if (childResource === 'article' && parentResource === 'sight') {
setPageNum(linkedItems.length + 1)
}
}, [linkedItems, childResource, parentResource])
const availableItems = items.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
const linkItem = () => {
if (selectedItemId !== null) {
const requestData =
childResource === 'article'
? {
[`${childResource}_id`]: selectedItemId,
page_num: pageNum,
}
: childResource === 'media'
? {
[`${childResource}_id`]: selectedItemId,
media_order: mediaOrder,
}
: {
[`${childResource}_id`]: selectedItemId,
}
axios
.post(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`, requestData)
.then(() => {
axios.get(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`).then((response) => {
setLinkedItems(response?.data || [])
setSelectedItemId(null)
if (childResource === 'article') {
setPageNum(pageNum + 1)
}
})
})
.catch((error) => {
console.error('Error linking item:', error)
})
}
}
const deleteItem = (itemId: number) => {
axios
.delete(`${BACKEND_URL}/${parentResource}/${parentId}/${childResource}`, {
data: {[`${childResource}_id`]: itemId},
})
.then(() => {
setLinkedItems((prev) => prev.filter((item) => item.id !== itemId))
})
.catch((error) => {
console.error('Error unlinking item:', error)
})
}
return (
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
sx={{
background: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.divider}`,
}}
>
<Typography variant="subtitle1" fontWeight="bold">
Привязанные {title}
</Typography>
</AccordionSummary>
<AccordionDetails sx={{background: theme.palette.background.paper}}>
<Stack gap={2}>
<Grid container gap={1.25}>
{isLoading ? (
<Typography>Загрузка...</Typography>
) : linkedItems.length > 0 ? (
linkedItems.map((item, index) => (
<Box
key={index}
sx={{
marginTop: '8px',
padding: '14px',
borderRadius: 2,
border: `2px solid ${theme.palette.divider}`,
}}
>
<Stack gap={0.25}>
{fields.map(({label, data}) => (
<Typography variant="body2" color="textSecondary" key={String(data)}>
<strong>{label}:</strong> {item[data]}
</Typography>
))}
{type === 'edit' && (
<Button variant="outlined" color="error" size="small" onClick={() => deleteItem(item.id)} sx={{mt: 1.5}}>
Отвязать
</Button>
)}
</Stack>
</Box>
))
) : (
<Typography>{title} не найдены</Typography>
)}
</Grid>
{type === 'edit' && (
<Stack gap={2}>
<Typography variant="subtitle1">Добавить {title}</Typography>
<FormControl fullWidth>
<InputLabel>Выберите {title}</InputLabel>
<Select value={selectedItemId || ''} onChange={(e) => setSelectedItemId(e.target.value as number)} label={`Выберите ${title}`}>
{availableItems.map((item) => (
<MenuItem key={item.id} value={item.id}>
{/* {fields.map((field) => item[field.data]).join(' - ')} */}
{item[fields[0].data]}
</MenuItem>
))}
</Select>
</FormControl>
{childResource === 'article' && (
<FormControl fullWidth>
<TextField
type="number"
label="Номер страницы"
name="page_num"
value={pageNum}
onChange={(e) => {
const newValue = Number(e.target.value)
const minValue = linkedItems.length + 1 // page number on articles lenght
setPageNum(newValue < minValue ? minValue : newValue)
}}
fullWidth
InputLabelProps={{shrink: true}}
/>
</FormControl>
)}
{childResource === 'media' && type === 'edit' && (
<FormControl fullWidth>
<TextField
type="number"
label="Порядок отображения медиа"
value={mediaOrder}
onChange={(e) => {
const newValue = Number(e.target.value)
const maxValue = linkedItems.length + 1
const value = Math.max(1, Math.min(newValue, maxValue))
setMediaOrder(value)
}}
fullWidth
InputLabelProps={{shrink: true}}
/>
</FormControl>
)}
<Button variant="contained" onClick={linkItem} disabled={!selectedItemId}>
Добавить
</Button>
</Stack>
)}
</Stack>
</AccordionDetails>
</Accordion>
)
}