diff --git a/assets/icons/popa-popa.png b/assets/icons/popa-popa.png new file mode 100644 index 0000000..6dd5b8c Binary files /dev/null and b/assets/icons/popa-popa.png differ diff --git a/assets/icons/popa-popa.svg b/assets/icons/popa-popa.svg new file mode 100644 index 0000000..6000dfc --- /dev/null +++ b/assets/icons/popa-popa.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/images/heart.svg b/assets/images/heart.svg index 0262800..a450b1c 100644 --- a/assets/images/heart.svg +++ b/assets/images/heart.svg @@ -1,11 +1,23 @@ - - - + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 0ea2738..ad8cceb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "find-java-home": "^2.0.0", "https-browserify": "^1.0.0", "path-browserify": "^1.0.1", + "qr-code-styling": "^1.9.2", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.3.0", @@ -18136,6 +18137,24 @@ ], "license": "MIT" }, + "node_modules/qr-code-styling": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.9.2.tgz", + "integrity": "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==", + "license": "MIT", + "dependencies": { + "qrcode-generator": "^1.4.4" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/qrcode-generator": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.5.2.tgz", + "integrity": "sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==", + "license": "MIT" + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", diff --git a/package.json b/package.json index 8e687ee..42fd2e2 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "find-java-home": "^2.0.0", "https-browserify": "^1.0.0", "path-browserify": "^1.0.1", + "qr-code-styling": "^1.9.2", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.3.0", diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index d8d73ee..c9e45c5 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -17,6 +17,7 @@ import { VersionsExplorer } from './pages/VersionsExplorer'; import Profile from './pages/Profile'; import Shop from './pages/Shop'; import Marketplace from './pages/Marketplace'; +import { Registration } from './pages/Registration'; const AuthCheck = ({ children }: { children: ReactNode }) => { const [isAuthenticated, setIsAuthenticated] = useState(null); @@ -127,6 +128,7 @@ const App = () => { } /> + } /> { + const response = await fetch( + `${API_BASE_URL}/auth/verification_status/${username}`, + ); + if (!response.ok) { + throw new Error('Не удалось получить статус верификации'); + } + return await response.json(); +} + +export async function generateVerificationCode( + username: string, +): Promise { + const response = await fetch( + `${API_BASE_URL}/auth/generate_code?username=${username}`, + { + method: 'POST', + }, + ); + if (!response.ok) { + throw new Error('Не удалось сгенерировать код верификации'); + } + return await response.json(); +} +export async function registerUser( + username: string, + password: string, +): Promise { + const response = await fetch(`${API_BASE_URL}/auth/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username, + password, + }), + }); + if (!response.ok) { + throw new Error('Не удалось зарегистрировать пользователя'); + } + return await response.json(); +} + export async function getPlayerInventory( request_id: string, ): Promise { diff --git a/src/renderer/components/CapeCard.tsx b/src/renderer/components/CapeCard.tsx index 3b24527..d03dd3c 100644 --- a/src/renderer/components/CapeCard.tsx +++ b/src/renderer/components/CapeCard.tsx @@ -107,6 +107,16 @@ export default function CapeCard({ color={actionButton.color as 'primary' | 'success' | 'error'} onClick={() => onAction(capeId)} disabled={actionDisabled} + sx={{ + borderRadius: '20px', + p: '5px 25px', + color: 'white', + backgroundColor: 'rgb(0, 134, 0)', + '&:hover': { + backgroundColor: 'rgba(0, 134, 0, 0.5)', + }, + fontFamily: 'Benzin-Bold', + }} > {actionButton.text} diff --git a/src/renderer/components/PlayerInventory.tsx b/src/renderer/components/PlayerInventory.tsx index dfafb73..4d7d34c 100644 --- a/src/renderer/components/PlayerInventory.tsx +++ b/src/renderer/components/PlayerInventory.tsx @@ -183,13 +183,13 @@ export default function PlayerInventory({ }; return ( - + @@ -199,6 +199,18 @@ export default function PlayerInventory({ variant="outlined" onClick={fetchPlayerInventory} disabled={loading} + sx={{ + borderRadius: '20px', + p: '10px 25px', + color: 'white', + borderColor: 'rgba(255, 77, 77, 1)', + '&:hover': { + backgroundColor: 'rgba(255, 77, 77, 1)', + borderColor: 'rgba(255, 77, 77, 1)', + }, + fontFamily: 'Benzin-Bold', + fontSize: '1vw', + }} > Обновить @@ -235,28 +247,33 @@ export default function PlayerInventory({ cursor: 'pointer', transition: 'transform 0.2s', '&:hover': { transform: 'scale(1.03)' }, + borderRadius: '1vw', }} onClick={() => handleOpenSellDialog(item)} > - - {getItemDisplayName(item.material)} - - - x{item.amount} - + + + {getItemDisplayName(item.material)} + + + {item.amount > 1 ? `x${item.amount}` : ''} + + {Object.keys(item.enchants || {}).length > 0 && ( (0); const [value, setValue] = useState(0); + const [activePage, setActivePage] = useState('versions'); const handleChange = (event: React.SyntheticEvent, newValue: number) => { setValue(newValue); @@ -59,7 +61,18 @@ export default function TopBar({ onRegister, username }: TopBarProps) { return 'Запуск'; } if (isVersionsExplorerPage) { - return 'Версии'; + if (activePage === 'versions') { + return 'Версии'; + } + if (activePage === 'profile') { + return 'Профиль'; + } + if (activePage === 'shop') { + return 'Магазин'; + } + if (activePage === 'marketplace') { + return 'Рынок'; + } } return 'Неизвестная страница'; }; @@ -118,10 +131,9 @@ export default function TopBar({ onRegister, username }: TopBarProps) { marginLeft: '1vw', }} > - {isLaunchPage && ( + {(isLaunchPage || isRegistrationPage) && ( )} - {!isLaunchPage && ( - + {!isLaunchPage && !isRegistrationPage && !isLoginPage && ( + { + setActivePage('versions'); + }} sx={{ color: 'white', fontFamily: 'Benzin-Bold', fontSize: '0.7em', + '&.Mui-selected': { + color: 'rgba(255, 77, 77, 1)', + }, + '&:hover': { + color: 'rgb(177, 52, 52)', + }, + transition: 'all 0.3s ease', }} /> { + setActivePage('profile'); + }} sx={{ color: 'white', fontFamily: 'Benzin-Bold', fontSize: '0.7em', + '&.Mui-selected': { + color: 'rgba(255, 77, 77, 1)', + }, + '&:hover': { + color: 'rgb(177, 52, 52)', + }, + transition: 'all 0.3s ease', }} /> { + setActivePage('shop'); + }} sx={{ color: 'white', fontFamily: 'Benzin-Bold', fontSize: '0.7em', + '&.Mui-selected': { + color: 'rgba(255, 77, 77, 1)', + }, + '&:hover': { + color: 'rgb(177, 52, 52)', + }, + transition: 'all 0.3s ease', }} /> { + setActivePage('marketplace'); + }} sx={{ color: 'white', fontFamily: 'Benzin-Bold', fontSize: '0.7em', + '&.Mui-selected': { + color: 'rgba(255, 77, 77, 1)', + }, + '&:hover': { + color: 'rgb(177, 52, 52)', + }, + transition: 'all 0.3s ease', }} /> @@ -192,7 +257,10 @@ export default function TopBar({ onRegister, username }: TopBarProps) { WebkitAppRegion: 'drag', }} > - + {getPageTitle()} @@ -251,7 +319,7 @@ export default function TopBar({ onRegister, username }: TopBarProps) { @@ -232,32 +254,81 @@ export default function Marketplace() { } return ( - - - Рынок сервера {playerServer?.name || ''} - + + + + Рынок сервера{' '} + + + {translateServer(playerServer || { name: '' })} + + {/* Вкладки */} - + - - + + {/* Содержимое вкладки "Товары" */} {marketLoading ? ( - + ) : !marketItems || marketItems.items.length === 0 ? ( - + На данный момент на рынке нет предметов. @@ -276,14 +358,21 @@ export default function Marketplace() { {marketItems.items.map((item) => ( - + handleBuyItem(item.id)} > Купить diff --git a/src/renderer/pages/Profile.tsx b/src/renderer/pages/Profile.tsx index c176ed2..c505be7 100644 --- a/src/renderer/pages/Profile.tsx +++ b/src/renderer/pages/Profile.tsx @@ -167,7 +167,9 @@ export default function Profile() { display: 'flex', flexDirection: 'row', alignItems: 'center', - gap: 2, + gap: '100px', + width: '100%', + justifyContent: 'center', }} > {loading ? ( @@ -185,6 +187,20 @@ export default function Profile() { }} > {/* Используем переработанный компонент SkinViewer */} + + {username} + - + ({ + [`&.${stepConnectorClasses.alternativeLabel}`]: { + top: 22, + }, + [`&.${stepConnectorClasses.active}`]: { + [`& .${stepConnectorClasses.line}`]: { + backgroundImage: + 'linear-gradient( 95deg,rgb(242,113,33) 0%,rgb(233,64,87) 50%,rgb(138,35,135) 100%)', + }, + }, + [`&.${stepConnectorClasses.completed}`]: { + [`& .${stepConnectorClasses.line}`]: { + backgroundImage: + 'linear-gradient( 95deg,rgb(242,113,33) 0%,rgb(233,64,87) 50%,rgb(138,35,135) 100%)', + }, + }, + [`& .${stepConnectorClasses.line}`]: { + height: 3, + border: 0, + backgroundColor: '#eaeaf0', + borderRadius: 1, + ...theme.applyStyles('dark', { + backgroundColor: theme.palette.grey[800], + }), + }, +})); + +const ColorlibStepIconRoot = styled('div')<{ + ownerState: { completed?: boolean; active?: boolean }; +}>(({ theme }) => ({ + backgroundColor: '#ccc', + zIndex: 1, + color: '#fff', + width: 50, + height: 50, + display: 'flex', + borderRadius: '50%', + justifyContent: 'center', + alignItems: 'center', + ...theme.applyStyles('dark', { + backgroundColor: theme.palette.grey[700], + }), + variants: [ + { + props: ({ ownerState }) => ownerState.active, + style: { + backgroundImage: + 'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)', + boxShadow: '0 4px 10px 0 rgba(0,0,0,.25)', + }, + }, + { + props: ({ ownerState }) => ownerState.completed, + style: { + backgroundImage: + 'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)', + }, + }, + ], +})); + +function ColorlibStepIcon(props: StepIconProps) { + const { active, completed, className } = props; + + const icons: { [index: string]: React.ReactElement } = { + 1: , + 2: , + 3: , + }; + + return ( + + {icons[String(props.icon)]} + + ); +} + +const qrCode = new QRCodeStyling({ + width: 300, + height: 300, + image: popalogo, + data: 'https://t.me/popa_popa_popa_bot?start=test', + shape: 'square', + margin: 10, + dotsOptions: { + gradient: { + type: 'linear', + colorStops: [ + { + offset: 0, + color: 'rgb(242,113,33)', + }, + { + offset: 1, + color: 'rgb(233,64,87)', + }, + ], + }, + type: 'extra-rounded', + }, + imageOptions: { + crossOrigin: 'anonymous', + margin: 20, + imageSize: 0.5, + }, + backgroundOptions: { + color: 'transparent', + }, +}); + +export const Registration = () => { + const [activeStep, setActiveStep] = useState(0); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [open, setOpen] = useState(false); + const [message, setMessage] = useState(''); + const [verificationCode, setVerificationCode] = useState(null); + const ref = useRef(null); + const [url, setUrl] = useState(''); + const steps = ['Создание аккаунта', 'Верификация аккаунта в телеграмме']; + + useEffect(() => { + if (ref.current) { + qrCode.append(ref.current); + } + }, []); + + useEffect(() => { + qrCode.update({ + data: url, + }); + }, [url]); + + const handleCreateAccount = async () => { + const response = await registerUser(username, password); + if (response.status === 'success') { + setActiveStep(1); + } else { + setOpen(true); + setMessage(response.status); + } + }; + + const handleClose = () => { + setOpen(false); + }; + + useEffect(() => { + if (activeStep === 1) { + handleGenerateVerificationCode(username); + setUrl(`https://t.me/popa_popa_popa_bot?start=${username}`); + + while (ref.current.firstChild) { + ref.current.removeChild(ref.current.firstChild); + } + + const newQrCode = new QRCodeStyling({ + width: 300, + height: 300, + image: popalogo, + data: 'https://t.me/popa_popa_popa_bot?start=test', + shape: 'square', + margin: 10, + dotsOptions: { + gradient: { + type: 'linear', + colorStops: [ + { + offset: 0, + color: 'rgb(242,113,33)', + }, + { + offset: 1, + color: 'rgb(233,64,87)', + }, + ], + }, + type: 'extra-rounded', + }, + imageOptions: { + crossOrigin: 'anonymous', + margin: 20, + imageSize: 0.5, + }, + backgroundOptions: { + color: 'transparent', + }, + }); + + newQrCode.update({ + data: `https://t.me/popa_popa_popa_bot?start=${username}`, + }); + + setUrl(`https://t.me/popa_popa_popa_bot?start=${username}`); + + newQrCode.append(ref.current); + + const intervalId = setInterval(() => { + handleVerifyCode(); + }, 5000); + + return () => { + clearInterval(intervalId); + }; + } + }, [activeStep]); + + const handleGenerateVerificationCode = async (username: string) => { + console.log(username); + const response = await generateVerificationCode(username); + setVerificationCode(response.code); + }; + + const handleVerifyCode = async () => { + const response = await getVerificationStatus(username); + if (response.is_verified) { + window.location.href = '/login'; + } + }; + + const handleOpenBot = () => { + window.open(`https://t.me/popa_popa_popa_bot?start=${username}`, '_blank'); + }; + + return ( + + } + > + {steps.map((label) => ( + + + {label} + + + ))} + + {activeStep === 0 && ( + + Создание аккаунта + Введите ваш никнейм + setUsername(e.target.value)} + sx={{ + width: '100%', + // '& .MuiFormLabel-root': { + // color: 'white', + // }, + '& .MuiInputBase-input': { + color: 'white', + }, + '& .MuiInput-underline:after': { + borderBottomColor: '#B2BAC2', + }, + '& .MuiOutlinedInput-root': { + '& fieldset': { + borderColor: '#E0E3E7', + color: 'white', + }, + '&:hover fieldset': { + borderColor: '#B2BAC2', + }, + '&.Mui-focused fieldset': { + borderColor: '#6F7E8C', + }, + }, + }} + /> + Введите ваш пароль + setPassword(e.target.value)} + sx={{ + width: '100%', + // '& .MuiFormLabel-root': { + // color: 'white', + // }, + '& .MuiInputBase-input': { + color: 'white', + }, + '& .MuiInput-underline:after': { + borderBottomColor: '#B2BAC2', + }, + '& .MuiOutlinedInput-root': { + '& fieldset': { + borderColor: '#E0E3E7', + color: 'white', + }, + '&:hover fieldset': { + borderColor: '#B2BAC2', + }, + '&.Mui-focused fieldset': { + borderColor: '#6F7E8C', + }, + }, + }} + /> + + + )} + {activeStep === 1 && ( + + Откройте бота в телеграмме + +
+ + Введите код верификации в боте + + {verificationCode ? ( + <> + + {verificationCode} + + Ждем ответа от бота + + + ) : ( + + )} + + )} + + + ); +}; diff --git a/src/renderer/pages/Shop.tsx b/src/renderer/pages/Shop.tsx index 0e2e50b..0371157 100644 --- a/src/renderer/pages/Shop.tsx +++ b/src/renderer/pages/Shop.tsx @@ -83,9 +83,10 @@ export default function Shop() { gap: '2vw', justifyContent: 'center', alignItems: 'center', + width: '100%', + height: '100%', }} > - Shop {loading ? ( Загрузка... ) : ( @@ -93,9 +94,11 @@ export default function Shop() { sx={{ display: 'flex', flexDirection: 'column', + flexWrap: 'wrap', + alignContent: 'flex-start', + width: '90%', + height: '80%', gap: '2vw', - justifyContent: 'center', - alignItems: 'center', }} > Доступные плащи