add promo to News
This commit is contained in:
@ -14,6 +14,14 @@ import { fetchNews, NewsItem, createNews, fetchMe, deleteNews } from '../api';
|
|||||||
import { FullScreenLoader } from '../components/FullScreenLoader';
|
import { FullScreenLoader } from '../components/FullScreenLoader';
|
||||||
import { MarkdownEditor } from '../components/MarkdownEditor';
|
import { MarkdownEditor } from '../components/MarkdownEditor';
|
||||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||||
|
import React from 'react';
|
||||||
|
import CustomNotification, {
|
||||||
|
NotificationPosition,
|
||||||
|
} from '../components/Notifications/CustomNotification';
|
||||||
|
import {
|
||||||
|
getNotifPositionFromSettings,
|
||||||
|
isNotificationsEnabled,
|
||||||
|
} from '../utils/notifications';
|
||||||
|
|
||||||
export const News = () => {
|
export const News = () => {
|
||||||
const [news, setNews] = useState<NewsItem[]>([]);
|
const [news, setNews] = useState<NewsItem[]>([]);
|
||||||
@ -31,6 +39,17 @@ export const News = () => {
|
|||||||
const [preview, setPreview] = useState('');
|
const [preview, setPreview] = useState('');
|
||||||
const [markdown, setMarkdown] = useState('');
|
const [markdown, setMarkdown] = useState('');
|
||||||
|
|
||||||
|
const [notifOpen, setNotifOpen] = useState(false);
|
||||||
|
const [notifMsg, setNotifMsg] = useState<React.ReactNode>('');
|
||||||
|
const [notifSeverity, setNotifSeverity] = useState<
|
||||||
|
'success' | 'info' | 'warning' | 'error'
|
||||||
|
>('info');
|
||||||
|
|
||||||
|
const [notifPos, setNotifPos] = useState<NotificationPosition>({
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -103,6 +122,18 @@ export const News = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showNotification = (
|
||||||
|
message: React.ReactNode,
|
||||||
|
severity: 'success' | 'info' | 'warning' | 'error' = 'info',
|
||||||
|
position: NotificationPosition = getNotifPositionFromSettings(),
|
||||||
|
) => {
|
||||||
|
if (!isNotificationsEnabled()) return;
|
||||||
|
setNotifMsg(message);
|
||||||
|
setNotifSeverity(severity);
|
||||||
|
setNotifPos(position);
|
||||||
|
setNotifOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteNews = async (id: string) => {
|
const handleDeleteNews = async (id: string) => {
|
||||||
const confirmed = window.confirm('Точно удалить эту новость?');
|
const confirmed = window.confirm('Точно удалить эту новость?');
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
@ -148,6 +179,69 @@ export const News = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PromoInline = ({ code }: { code: string }) => {
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(code);
|
||||||
|
showNotification(`Промокод ${code} скопирован`, 'success');
|
||||||
|
} catch {
|
||||||
|
// fallback для старых браузеров
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = code;
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.opacity = '0';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.focus();
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
showNotification(`Промокод ${code} скопирован`, 'success');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
onClick={handleCopy}
|
||||||
|
title="Нажмите, чтобы скопировать промокод"
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(71deg, #F27121 0%, #E940CD 60%, #8A2387 100%)',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
px: '0.2em',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
|
||||||
|
cursor: 'pointer', // 👈 показывает интерактивность
|
||||||
|
userSelect: 'none',
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
filter: 'brightness(1.15)',
|
||||||
|
},
|
||||||
|
'&:active': {
|
||||||
|
transform: 'scale(0.97)',
|
||||||
|
},
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderWithPromoCodes = (text: string) => {
|
||||||
|
const parts = text.split(/(\/\/[A-Z0-9-_]+)/g);
|
||||||
|
|
||||||
|
return parts.map((part, i) => {
|
||||||
|
if (part.startsWith('//')) {
|
||||||
|
const code = part.slice(2);
|
||||||
|
return <PromoInline key={i} code={code} />;
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -196,7 +290,7 @@ export const News = () => {
|
|||||||
'& .MuiInputBase-root': {
|
'& .MuiInputBase-root': {
|
||||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
borderRadius: '1.2vw'
|
borderRadius: '1.2vw',
|
||||||
},
|
},
|
||||||
'& .MuiInputLabel-root': {
|
'& .MuiInputLabel-root': {
|
||||||
color: 'rgba(255,255,255,0.7)',
|
color: 'rgba(255,255,255,0.7)',
|
||||||
@ -230,21 +324,24 @@ export const News = () => {
|
|||||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
borderRadius: '1.2vw',
|
borderRadius: '1.2vw',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
border: 'none'
|
border: 'none',
|
||||||
},
|
},
|
||||||
'& .editor-toolbar': { // полоски(разделители) иконок
|
'& .editor-toolbar': {
|
||||||
|
// полоски(разделители) иконок
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderBottom: '1px solid #FFFFFF'
|
borderBottom: '1px solid #FFFFFF',
|
||||||
},
|
},
|
||||||
'& .editor-toolbar .fa': { // все иконки
|
'& .editor-toolbar .fa': {
|
||||||
|
// все иконки
|
||||||
color: 'white',
|
color: 'white',
|
||||||
},
|
},
|
||||||
'& .CodeMirror': { // поле ввода
|
'& .CodeMirror': {
|
||||||
|
// поле ввода
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
border: 'none'
|
border: 'none',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -283,7 +380,7 @@ export const News = () => {
|
|||||||
filter: 'brightness(1.05)',
|
filter: 'brightness(1.05)',
|
||||||
transform: 'scale(1.02)',
|
transform: 'scale(1.02)',
|
||||||
},
|
},
|
||||||
transition: 'all 0.5s ease'
|
transition: 'all 0.5s ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{creating ? 'Публикация...' : 'Опубликовать'}
|
{creating ? 'Публикация...' : 'Опубликовать'}
|
||||||
@ -432,12 +529,10 @@ export const News = () => {
|
|||||||
onClick={() => handleToggleExpand(item.id)}
|
onClick={() => handleToggleExpand(item.id)}
|
||||||
sx={{
|
sx={{
|
||||||
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
|
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
|
||||||
background:
|
background: 'rgba(242,113,33,0.15)',
|
||||||
'rgba(242,113,33,0.15)',
|
|
||||||
borderRadius: '1.4vw',
|
borderRadius: '1.4vw',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
background:
|
background: 'rgba(242,113,33,0.4)',
|
||||||
'rgba(242,113,33,0.4)',
|
|
||||||
},
|
},
|
||||||
transition: 'all 0.25s ease',
|
transition: 'all 0.25s ease',
|
||||||
}}
|
}}
|
||||||
@ -505,7 +600,7 @@ export const News = () => {
|
|||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
components={{
|
components={{
|
||||||
p: ({ node, ...props }) => (
|
p: ({ node, children, ...props }) => (
|
||||||
<Typography
|
<Typography
|
||||||
{...props}
|
{...props}
|
||||||
sx={{
|
sx={{
|
||||||
@ -513,13 +608,19 @@ export const News = () => {
|
|||||||
fontSize: '1.5vw',
|
fontSize: '1.5vw',
|
||||||
lineHeight: 1.6,
|
lineHeight: 1.6,
|
||||||
mb: 1,
|
mb: 1,
|
||||||
whiteSpace: 'pre-line', // ← вот это
|
whiteSpace: 'pre-line',
|
||||||
|
textAlign: 'center', // вместо alignItems center
|
||||||
'&:last-of-type': { mb: 0 },
|
'&:last-of-type': { mb: 0 },
|
||||||
display: 'flex',
|
wordBreak: 'break-word',
|
||||||
flexDirection: 'column',
|
overflowWrap: 'anywhere',
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
{React.Children.map(children, (child) =>
|
||||||
|
typeof child === 'string'
|
||||||
|
? renderWithPromoCodes(child)
|
||||||
|
: child,
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
),
|
),
|
||||||
strong: ({ node, ...props }) => (
|
strong: ({ node, ...props }) => (
|
||||||
<Box
|
<Box
|
||||||
@ -691,6 +792,14 @@ export const News = () => {
|
|||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<CustomNotification
|
||||||
|
open={notifOpen}
|
||||||
|
message={notifMsg}
|
||||||
|
severity={notifSeverity}
|
||||||
|
position={notifPos}
|
||||||
|
onClose={() => setNotifOpen(false)}
|
||||||
|
autoHideDuration={2500}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user