add news page
This commit is contained in:
722
src/renderer/pages/News.tsx
Normal file
722
src/renderer/pages/News.tsx
Normal file
@ -0,0 +1,722 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Stack,
|
||||
Chip,
|
||||
IconButton,
|
||||
TextField,
|
||||
Button,
|
||||
} from '@mui/material';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import { fetchNews, NewsItem, createNews, fetchMe, deleteNews } from '../api';
|
||||
import { FullScreenLoader } from '../components/FullScreenLoader';
|
||||
import { MarkdownEditor } from '../components/MarkdownEditor';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
|
||||
export const News = () => {
|
||||
const [news, setNews] = useState<NewsItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
|
||||
// Markdown-рендерер (динамический импорт, чтобы не ругался CommonJS)
|
||||
const [ReactMarkdown, setReactMarkdown] = useState<any>(null);
|
||||
const [remarkGfm, setRemarkGfm] = useState<any>(null);
|
||||
|
||||
// --- Админский редактор ---
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [title, setTitle] = useState('');
|
||||
const [preview, setPreview] = useState('');
|
||||
const [markdown, setMarkdown] = useState('');
|
||||
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const me = await fetchMe();
|
||||
setIsAdmin(me.is_admin);
|
||||
} catch {
|
||||
setIsAdmin(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// Загружаем react-markdown + remark-gfm
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const md = await import('react-markdown');
|
||||
const gfm = await import('remark-gfm');
|
||||
setReactMarkdown(() => md.default);
|
||||
setRemarkGfm(() => gfm.default);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// Загрузка списка новостей
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const data = await fetchNews();
|
||||
setNews(data);
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Не удалось загрузить новости');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const handleToggleExpand = (id: string) => {
|
||||
setExpandedId((prev) => (prev === id ? null : id));
|
||||
};
|
||||
|
||||
const handleCreateNews = async () => {
|
||||
if (!title.trim() || !markdown.trim()) {
|
||||
setError('У новости должны быть заголовок и текст');
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setCreating(true);
|
||||
try {
|
||||
await createNews({
|
||||
title: title.trim(),
|
||||
preview: preview.trim() || undefined,
|
||||
markdown,
|
||||
is_published: true,
|
||||
});
|
||||
|
||||
const updated = await fetchNews();
|
||||
setNews(updated);
|
||||
|
||||
// Сброс формы
|
||||
setTitle('');
|
||||
setPreview('');
|
||||
setMarkdown('');
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Не удалось создать новость');
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteNews = async (id: string) => {
|
||||
const confirmed = window.confirm('Точно удалить эту новость?');
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await deleteNews(id);
|
||||
setNews((prev) => prev.filter((n) => n.id !== id));
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Не удалось удалить новость');
|
||||
}
|
||||
};
|
||||
|
||||
// ждём пока react-markdown / remark-gfm загрузятся
|
||||
if (!ReactMarkdown || !remarkGfm) {
|
||||
return <FullScreenLoader />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <FullScreenLoader />;
|
||||
}
|
||||
|
||||
if (error && news.length === 0) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
mt: '10vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
px: '3vw',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
color: '#ff8080',
|
||||
fontFamily: 'Benzin-Bold',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
mt: '7vh',
|
||||
px: '7vw',
|
||||
pb: '4vh',
|
||||
maxHeight: '90vh',
|
||||
overflowY: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2vh',
|
||||
}}
|
||||
>
|
||||
{/* Заголовок страницы */}
|
||||
<Box
|
||||
sx={{
|
||||
mb: '2vh',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '3vw',
|
||||
backgroundImage:
|
||||
'linear-gradient(71deg, #F27121 0%, #E940CD 60%, #8A2387 100%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
}}
|
||||
>
|
||||
Новости
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
mt: 0.5,
|
||||
color: 'rgba(255,255,255,0.6)',
|
||||
}}
|
||||
>
|
||||
Последние обновления лаунчера, сервера и ивентов
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Админский редактор */}
|
||||
{isAdmin && (
|
||||
<Paper
|
||||
sx={{
|
||||
mb: 3,
|
||||
p: 2.5,
|
||||
borderRadius: '1.5vw',
|
||||
background:
|
||||
'linear-gradient(145deg, rgba(255,255,255,0.06), rgba(0,0,0,0.85))',
|
||||
border: '1px solid rgba(255, 255, 255, 0.15)',
|
||||
boxShadow: '0 18px 45px rgba(0, 0, 0, 0.7)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '1.1vw',
|
||||
mb: 1.5,
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
}}
|
||||
>
|
||||
Создать новость
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label="Заголовок"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
sx={{
|
||||
mb: 2,
|
||||
'& .MuiInputBase-root': {
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
color: 'white',
|
||||
},
|
||||
'& .MuiInputLabel-root': {
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Краткий превью-текст (опционально)"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={preview}
|
||||
onChange={(e) => setPreview(e.target.value)}
|
||||
sx={{
|
||||
mb: 2,
|
||||
'& .MuiInputBase-root': {
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
color: 'white',
|
||||
},
|
||||
'& .MuiInputLabel-root': {
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mb: 2,
|
||||
'& .EasyMDEContainer': {
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
borderRadius: '0.8vw',
|
||||
overflow: 'hidden',
|
||||
border: '1px solid rgba(255,255,255,0.15)',
|
||||
},
|
||||
'& .editor-toolbar': {
|
||||
background: 'transparent',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 1)',
|
||||
color: 'white',
|
||||
},
|
||||
'& .editor-toolbar .fa': {
|
||||
color: 'white',
|
||||
},
|
||||
'& .CodeMirror': {
|
||||
backgroundColor: 'transparent',
|
||||
color: 'white',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MarkdownEditor value={markdown} onChange={setMarkdown} />
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Typography
|
||||
sx={{
|
||||
color: '#ff8080',
|
||||
fontSize: '0.8vw',
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={creating}
|
||||
onClick={handleCreateNews}
|
||||
sx={{
|
||||
px: 3,
|
||||
py: 0.8,
|
||||
borderRadius: '999px',
|
||||
textTransform: 'uppercase',
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '0.8vw',
|
||||
letterSpacing: '0.08em',
|
||||
backgroundImage:
|
||||
'linear-gradient(71deg, #F27121 0%, #E940CD 60%, #8A2387 100%)',
|
||||
boxShadow: '0 12px 30px rgba(0,0,0,0.9)',
|
||||
'&:hover': {
|
||||
boxShadow: '0 18px 40px rgba(0,0,0,1)',
|
||||
filter: 'brightness(1.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{creating ? 'Публикация...' : 'Опубликовать'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Если новостей нет */}
|
||||
{news.length === 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
mt: '5vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
px: '3vw',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '2vw',
|
||||
backgroundImage:
|
||||
'linear-gradient(71deg, #F27121 0%, #E940CD 60%, #8A2387 100%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Новостей пока нет
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Список новостей */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1.8vh',
|
||||
}}
|
||||
>
|
||||
{news.map((item) => {
|
||||
const isExpanded = expandedId === item.id;
|
||||
|
||||
const shortContent = item.preview || item.markdown;
|
||||
const fullContent = item.markdown;
|
||||
const contentToRender = isExpanded ? fullContent : shortContent;
|
||||
|
||||
const isImageUrl =
|
||||
!isExpanded &&
|
||||
typeof shortContent === 'string' &&
|
||||
/^https?:\/\/.*\.(png|jpe?g|gif|webp)$/i.test(shortContent.trim());
|
||||
|
||||
return (
|
||||
<Paper
|
||||
key={item.id}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
mb: 1,
|
||||
p: 2.5,
|
||||
borderRadius: '1.5vw',
|
||||
background:
|
||||
'linear-gradient(145deg, rgba(255,255,255,0.06), rgba(0,0,0,0.85))',
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
boxShadow: '0 18px 45px rgba(0, 0, 0, 0.7)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
width: '80vw',
|
||||
transition:
|
||||
'transform 0.25s ease, box-shadow 0.25s.ease, border-color 0.25s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-4px)',
|
||||
boxShadow: '0 24px 60px rgba(0, 0, 0, 0.9)',
|
||||
borderColor: 'rgba(242,113,33,0.5)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Шапка новости */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
mb: 1.5,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
color: 'white',
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '2.5vw',
|
||||
mb: 0.5,
|
||||
textShadow: '0 0 18px rgba(0,0,0,0.8)',
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Typography>
|
||||
|
||||
{item.created_at && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: 'rgba(255,255,255,0.5)',
|
||||
fontSize: '0.85vw',
|
||||
}}
|
||||
>
|
||||
{new Date(item.created_at).toLocaleString()}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
sx={{ mt: 1, flexWrap: 'wrap' }}
|
||||
>
|
||||
{item.tags.map((tag) => (
|
||||
<Chip
|
||||
key={tag}
|
||||
label={tag}
|
||||
size="small"
|
||||
sx={{
|
||||
fontSize: '0.7vw',
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
borderRadius: '999px',
|
||||
border: '1px solid rgba(242,113,33,0.6)',
|
||||
background:
|
||||
'linear-gradient(120deg, rgba(242,113,33,0.18), rgba(233,64,87,0.12), rgba(138,35,135,0.16))',
|
||||
backdropFilter: 'blur(12px)',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<IconButton
|
||||
onClick={() => handleToggleExpand(item.id)}
|
||||
sx={{
|
||||
ml: 1,
|
||||
alignSelf: 'center',
|
||||
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
|
||||
transition: 'transform 0.25s ease, background 0.25s ease',
|
||||
background:
|
||||
'linear-gradient(140deg, rgba(242,113,33,0.15), rgba(233,64,87,0.15))',
|
||||
borderRadius: '1.4vw',
|
||||
'&:hover': {
|
||||
background:
|
||||
'linear-gradient(140deg, rgba(242,113,33,0.4), rgba(233,64,87,0.4))',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ExpandMoreIcon
|
||||
sx={{ color: 'rgba(255,255,255,0.9)', fontSize: '1.4vw' }}
|
||||
/>
|
||||
</IconButton>
|
||||
{isAdmin && (
|
||||
<IconButton
|
||||
onClick={() => handleDeleteNews(item.id)}
|
||||
sx={{
|
||||
mt: 0.5,
|
||||
backgroundColor: 'rgba(255, 77, 77, 0.1)',
|
||||
borderRadius: '1.4vw',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 77, 77, 0.3)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DeleteOutlineIcon
|
||||
sx={{
|
||||
color: 'rgba(255, 120, 120, 0.9)',
|
||||
fontSize: '1.2vw',
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Контент */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
mt: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{isImageUrl ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={(shortContent as string).trim()}
|
||||
alt={item.title}
|
||||
sx={{
|
||||
maxHeight: '30vh',
|
||||
objectFit: 'cover',
|
||||
borderRadius: '1.2vw',
|
||||
mb: 2,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
maxHeight: isExpanded ? 'none' : '12em',
|
||||
overflow: 'hidden',
|
||||
pr: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
p: ({ node, ...props }) => (
|
||||
<Typography
|
||||
{...props}
|
||||
sx={{
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
fontSize: '1.5vw',
|
||||
lineHeight: 1.6,
|
||||
mb: 1,
|
||||
whiteSpace: 'pre-line', // ← вот это
|
||||
'&:last-of-type': { mb: 0 },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
/>
|
||||
),
|
||||
strong: ({ node, ...props }) => (
|
||||
<Box
|
||||
component="strong"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: 'rgba(255,255,255,1)',
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
em: ({ node, ...props }) => (
|
||||
<Box
|
||||
component="em"
|
||||
sx={{ fontStyle: 'italic' }}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
del: ({ node, ...props }) => (
|
||||
<Box
|
||||
component="del"
|
||||
sx={{ textDecoration: 'line-through' }}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
a: ({ node, ...props }) => (
|
||||
<Box
|
||||
component="a"
|
||||
{...props}
|
||||
sx={{
|
||||
color: '#F27121',
|
||||
textDecoration: 'none',
|
||||
borderBottom: '1px solid rgba(242,113,33,0.6)',
|
||||
'&:hover': {
|
||||
color: '#E940CD',
|
||||
borderBottomColor: 'rgba(233,64,205,0.8)',
|
||||
},
|
||||
}}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
),
|
||||
li: ({ node, ordered, ...props }) => (
|
||||
<li
|
||||
style={{
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
fontSize: '1.5vw',
|
||||
marginBottom: '0.3em',
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ul: ({ node, ...props }) => (
|
||||
<ul
|
||||
style={{
|
||||
paddingLeft: '1.3em',
|
||||
marginTop: '0.3em',
|
||||
marginBottom: '0.8em',
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ol: ({ node, ...props }) => (
|
||||
<ol
|
||||
style={{
|
||||
paddingLeft: '1.3em',
|
||||
marginTop: '0.3em',
|
||||
marginBottom: '0.8em',
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
img: ({ node, ...props }) => (
|
||||
<Box
|
||||
component="img"
|
||||
{...props}
|
||||
sx={{
|
||||
maxHeight: '30vh',
|
||||
objectFit: 'cover',
|
||||
borderRadius: '1.2vw',
|
||||
my: 2,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
h1: ({ node, ...props }) => (
|
||||
<Typography
|
||||
{...props}
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
color: 'white',
|
||||
mb: 1,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
h2: ({ node, ...props }) => (
|
||||
<Typography
|
||||
{...props}
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
color: 'white',
|
||||
mb: 1,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
h3: ({ node, ...props }) => (
|
||||
<Typography
|
||||
{...props}
|
||||
variant="h7"
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
color: 'white',
|
||||
mb: 1,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{contentToRender}
|
||||
</ReactMarkdown>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!isExpanded && !isImageUrl && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3.5em',
|
||||
// background:
|
||||
// 'linear-gradient(to top, rgba(0, 0, 0, 0.43), rgba(0,0,0,0))',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mt: 1.5,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
onClick={() => handleToggleExpand(item.id)}
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '0.8vw',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
cursor: 'pointer',
|
||||
backgroundImage:
|
||||
'linear-gradient(71deg, #F27121 0%, #E940CD 60%, #8A2387 100%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
textShadow: '0 0 15px rgba(0,0,0,0.9)',
|
||||
'&:hover': {
|
||||
opacity: 0.85,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isExpanded ? 'Свернуть' : 'Читать полностью'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user