diff --git a/package-lock.json b/package-lock.json index 4ec11e0..b3be890 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "qr-code-styling": "^1.9.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-icons": "^5.5.0", "react-markdown": "^10.1.0", "react-router-dom": "^7.3.0", "remark-gfm": "^4.0.1", @@ -20357,6 +20358,15 @@ "react": "^19.2.1" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "19.2.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.1.tgz", diff --git a/package.json b/package.json index 23e2542..bd339ec 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "qr-code-styling": "^1.9.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-icons": "^5.5.0", "react-markdown": "^10.1.0", "react-router-dom": "^7.3.0", "remark-gfm": "^4.0.1", diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 43472ac..b5255df 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -29,6 +29,7 @@ import Inventory from './pages/Inventory'; import FakePaymentPage from './pages/FakePaymentPage'; import { TrayBridge } from './utils/TrayBridge'; import { API_BASE_URL } from './api'; +import { PromoRedeem } from './pages/PromoRedeem'; const AuthCheck = ({ children }: { children: ReactNode }) => { const [isAuthenticated, setIsAuthenticated] = useState(null); @@ -407,6 +408,14 @@ const AppLayout = () => { } /> + + + + } + /> diff --git a/src/renderer/api/promocodes.ts b/src/renderer/api/promocodes.ts new file mode 100644 index 0000000..14cdbd9 --- /dev/null +++ b/src/renderer/api/promocodes.ts @@ -0,0 +1,32 @@ +import { API_BASE_URL } from '../api'; + +export interface RedeemPromoResponse { + code: string; + reward_coins: number; + new_balance: number; +} + +export async function redeemPromoCode( + username: string, + code: string, +): Promise { + const formData = new FormData(); + formData.append('username', username); + formData.append('code', code); + + const response = await fetch(`${API_BASE_URL}/promo/redeem`, { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + let msg = 'Не удалось активировать промокод'; + try { + const errorData = await response.json(); + msg = errorData.message || errorData.detail || msg; + } catch {} + throw new Error(msg); + } + + return await response.json(); +} diff --git a/src/renderer/components/PageHeader.tsx b/src/renderer/components/PageHeader.tsx index db83526..6256085 100644 --- a/src/renderer/components/PageHeader.tsx +++ b/src/renderer/components/PageHeader.tsx @@ -35,6 +35,7 @@ export default function PageHeader() { path === '/profile' || path === '/inventory' || path === '/fakepaymentpage' || + path === '/promocode' || path.startsWith('/launch') ) { return { title: '', subtitle: '', hidden: true }; diff --git a/src/renderer/components/TopBar.tsx b/src/renderer/components/TopBar.tsx index 312b737..aa20bc1 100644 --- a/src/renderer/components/TopBar.tsx +++ b/src/renderer/components/TopBar.tsx @@ -21,7 +21,8 @@ import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; import PersonIcon from '@mui/icons-material/Person'; import SettingsIcon from '@mui/icons-material/Settings'; import { useTheme } from '@mui/material/styles'; -import CategoryIcon from '@mui/icons-material/Category'; +import InventoryIcon from '@mui/icons-material/Inventory'; +import { RiCoupon3Fill } from 'react-icons/ri'; declare global { interface Window { @@ -589,7 +590,7 @@ export default function TopBar({ onRegister, username }: TopBarProps) { theme.launcher.topbar.menuItem, ]} > - Инвентарь + Инвентарь {/* ===== 2 строка: ежедневные задания ===== */} @@ -633,27 +634,42 @@ export default function TopBar({ onRegister, username }: TopBarProps) { Настройки + { + handleAvatarMenuClose(); + navigate('/promocode'); + }} + sx={[ + { fontSize: '1.5vw', gap: '0.5vw', py: '0.7vw' }, + theme.launcher.topbar.menuItem, + ]} + > + Промокоды + + {!isLoginPage && !isRegistrationPage && username && ( - + + + )} {/* ↓↓↓ дальше ты сам добавишь пункты ↓↓↓ */} diff --git a/src/renderer/pages/PromoRedeem.tsx b/src/renderer/pages/PromoRedeem.tsx new file mode 100644 index 0000000..1fd087c --- /dev/null +++ b/src/renderer/pages/PromoRedeem.tsx @@ -0,0 +1,170 @@ +import { Box, Button, Typography } from '@mui/material'; +import { useEffect, useState } from 'react'; +import GradientTextField from '../components/GradientTextField'; +import CustomNotification from '../components/Notifications/CustomNotification'; +import type { NotificationPosition } from '../components/Notifications/CustomNotification'; +import { + isNotificationsEnabled, + getNotifPositionFromSettings, +} from '../utils/notifications'; +import { redeemPromoCode } from '../api/promocodes'; +import { useNavigate } from 'react-router-dom'; + +export const PromoRedeem = () => { + const navigate = useNavigate(); + + const [username, setUsername] = useState(''); // будет автозаполнение + const [code, setCode] = useState(''); + + const [loading, setLoading] = useState(false); + + const [notifOpen, setNotifOpen] = useState(false); + const [notifMsg, setNotifMsg] = useState(''); + const [notifSeverity, setNotifSeverity] = useState< + 'success' | 'info' | 'warning' | 'error' + >('info'); + + const [notifPos, setNotifPos] = useState({ + vertical: 'bottom', + horizontal: 'center', + }); + + 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); + }; + + // как в Profile.tsx: читаем launcher_config + useEffect(() => { + try { + const savedConfig = localStorage.getItem('launcher_config'); + if (savedConfig) { + const config = JSON.parse(savedConfig); + setUsername(config.username || ''); + } + } catch { + setUsername(''); + } + }, []); + + const handleRedeem = async () => { + if (!username) { + showNotification( + 'Не удалось определить никнейм. Войдите в аккаунт заново.', + 'warning', + ); + // по желанию можно отправить в login/profile: + // navigate('/login', { replace: true }); + return; + } + + if (!code) { + showNotification('Введите промокод', 'warning'); + return; + } + + setLoading(true); + try { + const res = await redeemPromoCode(username, code); + + showNotification( + <> + Промокод {res.code} успешно активирован! + , + 'success', + ); + + setCode(''); + } catch (e: any) { + showNotification(e?.message || 'Ошибка активации промокода', 'error'); + } finally { + setLoading(false); + } + }; + + return ( + <> + + + Активация промокода + + + + setCode(e.target.value.toUpperCase())} + /> + + + + + setNotifOpen(false)} + autoHideDuration={2500} + /> + + + ); +};