Merge branch 'feat/VersionsExplorer' of https://git.popa-popa.ru/DIKER/popa-launcher into feat/VersionsExplorer

This commit is contained in:
aurinex
2025-07-21 17:42:25 +05:00
15 changed files with 819 additions and 50 deletions

BIN
assets/icons/popa-popa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -1,11 +1,23 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="4" width="8" height="4" fill="#FF2D0F"/> <rect x="0" y="12" width="28" height="4" fill="#BD2211"/>
<rect x="8" y="4" width="4" height="16" fill="#FF2D0F"/> <rect x="4" y="16" width="20" height="4" fill="#BD2211"/>
<rect x="4" y="8" width="4" height="8" fill="#FF2D0F"/> <rect x="8" y="20" width="12" height="4" fill="#BD2211"/>
<rect x="12" y="24" width="4" height="4" fill="#BD2211"/>
<rect x="4" width="8" height="4.5" fill="#FF2D0F"/>
<rect x="16" width="8" height="4.5" fill="#FF2D0F"/>
<rect x="0" y="4" width="28" height="8" fill="#FF2D0F"/>
<rect x="4" y="8" width="20" height="8" fill="#FF2D0F"/>
<rect x="8" y="12" width="12" height="8" fill="#FF2D0F"/>
<rect x="12" y="16" width="4" height="8" fill="#FF2D0F"/>
<rect x="4" y="4" width="4" height="4" fill="#FFCAC8"/>
<!-- <rect x="7" y="4" width="6" height="16" fill="#FF2D0F"/>
<rect x="6" y="3" width="6" height="16" fill="#FF2D0F"/>
<rect x="3" y="8" width="4" height="8" fill="#FF2D0F"/>
<rect y="4" width="4" height="8" fill="#FF2D0F"/> <rect y="4" width="4" height="8" fill="#FF2D0F"/>
<rect x="24" y="4" width="4" height="8" fill="#FF2D0F"/> <rect x="24" y="4" width="4" height="8" fill="#FF2D0F"/>
<rect x="16" width="8" height="16" fill="#FF2D0F"/> <rect x="16" width="8" height="16" fill="#FF2D0F"/>
<rect x="16" y="16" width="4" height="4" fill="#FF2D0F"/> <rect x="20" y="4" width="8" height="12" fill="#FF2D0F"/>
<rect x="15" y="4" width="5" height="16" fill="#FF2D0F"/>
<rect x="24" y="12" width="4" height="4" fill="#BD2211"/> <rect x="24" y="12" width="4" height="4" fill="#BD2211"/>
<rect x="20" y="16" width="4" height="4" fill="#BD2211"/> <rect x="20" y="16" width="4" height="4" fill="#BD2211"/>
<rect x="16" y="20" width="4" height="4" fill="#BD2211"/> <rect x="16" y="20" width="4" height="4" fill="#BD2211"/>
@ -14,5 +26,5 @@
<rect x="4" y="16" width="4" height="4" fill="#BD2211"/> <rect x="4" y="16" width="4" height="4" fill="#BD2211"/>
<rect x="4" y="4" width="4" height="4" fill="#FFCAC8"/> <rect x="4" y="4" width="4" height="4" fill="#FFCAC8"/>
<rect y="12" width="4" height="4" fill="#BD2211"/> <rect y="12" width="4" height="4" fill="#BD2211"/>
<rect x="12" y="4" width="4" height="20" fill="#FF2D0F"/> <rect x="12" y="4" width="4" height="20" fill="#FF2D0F"/> -->
</svg> </svg>

Before

Width:  |  Height:  |  Size: 994 B

After

Width:  |  Height:  |  Size: 1.7 KiB

19
package-lock.json generated
View File

@ -23,6 +23,7 @@
"find-java-home": "^2.0.0", "find-java-home": "^2.0.0",
"https-browserify": "^1.0.0", "https-browserify": "^1.0.0",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"qr-code-styling": "^1.9.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^7.3.0", "react-router-dom": "^7.3.0",
@ -18136,6 +18137,24 @@
], ],
"license": "MIT" "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": { "node_modules/qs": {
"version": "6.13.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",

View File

@ -117,6 +117,7 @@
"find-java-home": "^2.0.0", "find-java-home": "^2.0.0",
"https-browserify": "^1.0.0", "https-browserify": "^1.0.0",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"qr-code-styling": "^1.9.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^7.3.0", "react-router-dom": "^7.3.0",

View File

@ -17,6 +17,7 @@ import { VersionsExplorer } from './pages/VersionsExplorer';
import Profile from './pages/Profile'; import Profile from './pages/Profile';
import Shop from './pages/Shop'; import Shop from './pages/Shop';
import Marketplace from './pages/Marketplace'; import Marketplace from './pages/Marketplace';
import { Registration } from './pages/Registration';
const AuthCheck = ({ children }: { children: ReactNode }) => { const AuthCheck = ({ children }: { children: ReactNode }) => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null); const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
@ -127,6 +128,7 @@ const App = () => {
<Notifier /> <Notifier />
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/registration" element={<Registration />} />
<Route <Route
path="/" path="/"
element={ element={

View File

@ -1,4 +1,4 @@
export const API_BASE_URL = 'http://147.78.65.214:8000'; export const API_BASE_URL = 'https://minecraft.api.popa-popa.ru';
export interface Player { export interface Player {
uuid: string; uuid: string;
@ -169,6 +169,66 @@ export interface OperationsResponse {
operations: MarketplaceOperation[]; operations: MarketplaceOperation[];
} }
export interface RegisterUserResponse {
status: string;
uuid: string;
}
export interface GenerateVerificationCodeResponse {
status: string;
code: string;
}
export interface VerificationStatusResponse {
is_verified: boolean;
}
export async function getVerificationStatus(
username: string,
): Promise<VerificationStatusResponse> {
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<GenerateVerificationCodeResponse> {
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<RegisterUserResponse> {
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( export async function getPlayerInventory(
request_id: string, request_id: string,
): Promise<PlayerInventory> { ): Promise<PlayerInventory> {

View File

@ -107,6 +107,16 @@ export default function CapeCard({
color={actionButton.color as 'primary' | 'success' | 'error'} color={actionButton.color as 'primary' | 'success' | 'error'}
onClick={() => onAction(capeId)} onClick={() => onAction(capeId)}
disabled={actionDisabled} 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} {actionButton.text}
</Button> </Button>

View File

@ -183,13 +183,13 @@ export default function PlayerInventory({
}; };
return ( return (
<Box sx={{ mt: 4 }}> <Box sx={{ mt: '1vw' }}>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', gap: '1vw',
alignItems: 'center', alignItems: 'center',
mb: 2, mb: '2vw',
}} }}
> >
<Typography variant="h5" color="white"> <Typography variant="h5" color="white">
@ -199,6 +199,18 @@ export default function PlayerInventory({
variant="outlined" variant="outlined"
onClick={fetchPlayerInventory} onClick={fetchPlayerInventory}
disabled={loading} 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',
}}
> >
Обновить Обновить
</Button> </Button>
@ -235,28 +247,33 @@ export default function PlayerInventory({
cursor: 'pointer', cursor: 'pointer',
transition: 'transform 0.2s', transition: 'transform 0.2s',
'&:hover': { transform: 'scale(1.03)' }, '&:hover': { transform: 'scale(1.03)' },
borderRadius: '1vw',
}} }}
onClick={() => handleOpenSellDialog(item)} onClick={() => handleOpenSellDialog(item)}
> >
<CardMedia <CardMedia
component="img" component="img"
sx={{ sx={{
height: 100, minWidth: '10vw',
minHeight: '10vw',
maxHeight: '10vw',
objectFit: 'contain', objectFit: 'contain',
bgcolor: 'rgba(0, 0, 0, 0.2)', bgcolor: 'white',
p: 1, p: '1vw',
imageRendering: 'pixelated', imageRendering: 'pixelated',
}} }}
image={`/minecraft/${item.material.toLowerCase()}.png`} image={`/minecraft/${item.material.toLowerCase()}.png`}
alt={item.material} alt={item.material}
/> />
<CardContent sx={{ p: 1 }}> <CardContent sx={{ p: 1 }}>
<Typography variant="body2" color="white" noWrap> <Box sx={{ display: 'flex', gap: '1vw', justifyContent: 'space-between' }}>
{getItemDisplayName(item.material)} <Typography variant="body2" color="white" noWrap>
</Typography> {getItemDisplayName(item.material)}
<Typography variant="body2" color="white"> </Typography>
x{item.amount} <Typography variant="body2" color="white">
</Typography> {item.amount > 1 ? `x${item.amount}` : ''}
</Typography>
</Box>
{Object.keys(item.enchants || {}).length > 0 && ( {Object.keys(item.enchants || {}).length > 0 && (
<Typography <Typography
variant="caption" variant="caption"

View File

@ -33,7 +33,9 @@ export default function SkinViewer({
canvas: canvasRef.current, canvas: canvasRef.current,
width, width,
height, height,
skin: skinUrl || undefined, skin:
skinUrl ||
'https://static.planetminecraft.com/files/resource_media/skin/original-steve-15053860.png',
model: 'auto-detect', model: 'auto-detect',
cape: capeUrl || undefined, cape: capeUrl || undefined,
}); });

View File

@ -30,9 +30,11 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
const isLoginPage = location.pathname === '/login'; const isLoginPage = location.pathname === '/login';
const isLaunchPage = location.pathname.startsWith('/launch'); const isLaunchPage = location.pathname.startsWith('/launch');
const isVersionsExplorerPage = location.pathname.startsWith('/'); const isVersionsExplorerPage = location.pathname.startsWith('/');
const isRegistrationPage = location.pathname === '/registration';
const navigate = useNavigate(); const navigate = useNavigate();
const [coins, setCoins] = useState<number>(0); const [coins, setCoins] = useState<number>(0);
const [value, setValue] = useState(0); const [value, setValue] = useState(0);
const [activePage, setActivePage] = useState('versions');
const handleChange = (event: React.SyntheticEvent, newValue: number) => { const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setValue(newValue); setValue(newValue);
@ -59,7 +61,18 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
return 'Запуск'; return 'Запуск';
} }
if (isVersionsExplorerPage) { if (isVersionsExplorerPage) {
return 'Версии'; if (activePage === 'versions') {
return 'Версии';
}
if (activePage === 'profile') {
return 'Профиль';
}
if (activePage === 'shop') {
return 'Магазин';
}
if (activePage === 'marketplace') {
return 'Рынок';
}
} }
return 'Неизвестная страница'; return 'Неизвестная страница';
}; };
@ -118,10 +131,9 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
marginLeft: '1vw', marginLeft: '1vw',
}} }}
> >
{isLaunchPage && ( {(isLaunchPage || isRegistrationPage) && (
<Button <Button
variant="outlined" variant="outlined"
color="primary"
onClick={() => handleLaunchPage()} onClick={() => handleLaunchPage()}
sx={{ sx={{
width: '3em', width: '3em',
@ -136,43 +148,96 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
<ArrowBackRoundedIcon /> <ArrowBackRoundedIcon />
</Button> </Button>
)} )}
{!isLaunchPage && ( {!isLaunchPage && !isRegistrationPage && !isLoginPage && (
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}> <Box
sx={{
borderBottom: 1,
borderColor: 'transparent',
'& .MuiTabs-indicator': {
backgroundColor: 'rgba(255, 77, 77, 1)',
},
}}
>
<Tabs <Tabs
value={value} value={value}
onChange={handleChange} onChange={handleChange}
aria-label="basic tabs example" aria-label="basic tabs example"
disableRipple={true}
> >
<Tab <Tab
label="Версии" label="Версии"
disableRipple={true}
onClick={() => {
setActivePage('versions');
}}
sx={{ sx={{
color: 'white', color: 'white',
fontFamily: 'Benzin-Bold', fontFamily: 'Benzin-Bold',
fontSize: '0.7em', fontSize: '0.7em',
'&.Mui-selected': {
color: 'rgba(255, 77, 77, 1)',
},
'&:hover': {
color: 'rgb(177, 52, 52)',
},
transition: 'all 0.3s ease',
}} }}
/> />
<Tab <Tab
label="Профиль" label="Профиль"
disableRipple={true}
onClick={() => {
setActivePage('profile');
}}
sx={{ sx={{
color: 'white', color: 'white',
fontFamily: 'Benzin-Bold', fontFamily: 'Benzin-Bold',
fontSize: '0.7em', fontSize: '0.7em',
'&.Mui-selected': {
color: 'rgba(255, 77, 77, 1)',
},
'&:hover': {
color: 'rgb(177, 52, 52)',
},
transition: 'all 0.3s ease',
}} }}
/> />
<Tab <Tab
label="Магазин" label="Магазин"
disableRipple={true}
onClick={() => {
setActivePage('shop');
}}
sx={{ sx={{
color: 'white', color: 'white',
fontFamily: 'Benzin-Bold', fontFamily: 'Benzin-Bold',
fontSize: '0.7em', fontSize: '0.7em',
'&.Mui-selected': {
color: 'rgba(255, 77, 77, 1)',
},
'&:hover': {
color: 'rgb(177, 52, 52)',
},
transition: 'all 0.3s ease',
}} }}
/> />
<Tab <Tab
label="Рынок" label="Рынок"
disableRipple={true}
onClick={() => {
setActivePage('marketplace');
}}
sx={{ sx={{
color: 'white', color: 'white',
fontFamily: 'Benzin-Bold', fontFamily: 'Benzin-Bold',
fontSize: '0.7em', fontSize: '0.7em',
'&.Mui-selected': {
color: 'rgba(255, 77, 77, 1)',
},
'&:hover': {
color: 'rgb(177, 52, 52)',
},
transition: 'all 0.3s ease',
}} }}
/> />
</Tabs> </Tabs>
@ -192,7 +257,10 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
WebkitAppRegion: 'drag', WebkitAppRegion: 'drag',
}} }}
> >
<Typography variant="h6" sx={{ color: 'white' }}> <Typography
variant="h6"
sx={{ color: 'white', fontFamily: 'Benzin-Bold' }}
>
{getPageTitle()} {getPageTitle()}
</Typography> </Typography>
</Box> </Box>
@ -251,7 +319,7 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
<Button <Button
variant="outlined" variant="outlined"
color="primary" color="primary"
onClick={() => onRegister && onRegister()} onClick={() => navigate('/registration')}
sx={{ sx={{
width: '10em', width: '10em',
height: '3em', height: '3em',

View File

@ -63,6 +63,19 @@ export default function Marketplace() {
type: 'success', type: 'success',
}); });
const translateServer = (server: Server) => {
switch (server.name) {
case 'Server minecraft.hub.popa-popa.ru':
return 'Хаб';
case 'Server survival.hub.popa-popa.ru':
return 'Выживание';
case 'Server minecraft.minigames.popa-popa.ru':
return 'Миниигры';
default:
return server.name;
}
};
// Функция для проверки онлайн-статуса игрока и определения сервера // Функция для проверки онлайн-статуса игрока и определения сервера
const checkPlayerStatus = async () => { const checkPlayerStatus = async () => {
if (!username) return; if (!username) return;
@ -221,9 +234,18 @@ export default function Marketplace() {
</Typography> </Typography>
<Button <Button
variant="contained" variant="contained"
color="primary"
onClick={checkPlayerStatus} onClick={checkPlayerStatus}
sx={{ mt: 2 }} sx={{
mt: '1%',
borderRadius: '20px',
p: '10px 25px',
color: 'white',
backgroundColor: 'rgb(255, 77, 77)',
'&:hover': {
backgroundColor: 'rgba(255, 77, 77, 0.5)',
},
fontFamily: 'Benzin-Bold',
}}
> >
Проверить снова Проверить снова
</Button> </Button>
@ -232,32 +254,81 @@ export default function Marketplace() {
} }
return ( return (
<Box sx={{ padding: 3 }}> <Box sx={{ padding: 3, width: '95%', height: '80%' }}>
<Typography variant="h4" color="white" gutterBottom> <Box sx={{ display: 'flex', alignItems: 'center', gap: '1vw' }}>
Рынок сервера {playerServer?.name || ''} <Typography variant="h4" color="white" gutterBottom>
</Typography> Рынок сервера{' '}
</Typography>
<Typography
style={{
color: 'white',
backgroundColor: 'rgba(255, 77, 77, 1)',
padding: '0vw 2vw',
borderRadius: '5vw',
fontFamily: 'Benzin-Bold',
textAlign: 'center',
fontSize: '2vw',
}}
>
{translateServer(playerServer || { name: '' })}
</Typography>
</Box>
{/* Вкладки */} {/* Вкладки */}
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}> <Box sx={{ borderBottom: 1, borderColor: 'transparent' }}>
<Tabs <Tabs
value={tabValue} value={tabValue}
onChange={handleTabChange} onChange={handleTabChange}
aria-label="marketplace tabs" aria-label="marketplace tabs"
disableRipple={true}
sx={{
'& .MuiTabs-indicator': {
backgroundColor: 'rgba(255, 77, 77, 1)',
},
}}
> >
<Tab label="Товары" /> <Tab
<Tab label="Мой инвентарь" /> label="Товары"
disableRipple={true}
sx={{
fontFamily: 'Benzin-Bold',
color: 'white',
'&.Mui-selected': {
color: 'rgba(255, 77, 77, 1)',
},
'&:hover': {
color: 'rgba(255, 77, 77, 1)',
},
transition: 'all 0.3s ease',
}}
/>
<Tab
label="Мой инвентарь"
disableRipple={true}
sx={{
fontFamily: 'Benzin-Bold',
color: 'white',
'&.Mui-selected': {
color: 'rgba(255, 77, 77, 1)',
},
'&:hover': {
color: 'rgba(255, 77, 77, 1)',
},
transition: 'all 0.3s ease',
}}
/>
</Tabs> </Tabs>
</Box> </Box>
{/* Содержимое вкладки "Товары" */} {/* Содержимое вкладки "Товары" */}
<TabPanel value={tabValue} index={0}> <TabPanel value={tabValue} index={0}>
{marketLoading ? ( {marketLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}> <Box sx={{ display: 'flex', justifyContent: 'center', mt: '50vw' }}>
<CircularProgress /> <CircularProgress />
</Box> </Box>
) : !marketItems || marketItems.items.length === 0 ? ( ) : !marketItems || marketItems.items.length === 0 ? (
<Box sx={{ mt: 4, textAlign: 'center' }}> <Box sx={{ mt: 4, textAlign: 'center' }}>
<Typography variant="h6" color="white"> <Typography variant="h6" color="white" sx={{ mt: '10vw' }}>
На данный момент на рынке нет предметов. На данный момент на рынке нет предметов.
</Typography> </Typography>
<Button <Button
@ -266,7 +337,18 @@ export default function Marketplace() {
onClick={() => onClick={() =>
playerServer && loadMarketItems(playerServer.ip, 1) playerServer && loadMarketItems(playerServer.ip, 1)
} }
sx={{ mt: 2 }} sx={{
mt: 2,
borderRadius: '20px',
p: '10px 25px',
color: 'white',
backgroundColor: 'rgb(255, 77, 77)',
'&:hover': {
backgroundColor: 'rgba(255, 77, 77, 0.5)',
},
fontFamily: 'Benzin-Bold',
fontSize: '1vw',
}}
> >
Обновить Обновить
</Button> </Button>
@ -276,14 +358,21 @@ export default function Marketplace() {
<Grid container spacing={2} sx={{ mt: 2 }}> <Grid container spacing={2} sx={{ mt: 2 }}>
{marketItems.items.map((item) => ( {marketItems.items.map((item) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={item.id}> <Grid item xs={12} sm={6} md={4} lg={3} key={item.id}>
<Card sx={{ bgcolor: 'rgba(255, 255, 255, 0.05)' }}> <Card
sx={{
bgcolor: 'rgba(255, 255, 255, 0.05)',
borderRadius: '1vw',
}}
>
<CardMedia <CardMedia
component="img" component="img"
sx={{ sx={{
height: 140, minWidth: '10vw',
minHeight: '10vw',
maxHeight: '10vw',
objectFit: 'contain', objectFit: 'contain',
bgcolor: 'rgba(0, 0, 0, 0.2)', bgcolor: 'white',
p: 1, p: '1vw',
imageRendering: 'pixelated', imageRendering: 'pixelated',
}} }}
image={`/minecraft/${item.material.toLowerCase()}.png`} image={`/minecraft/${item.material.toLowerCase()}.png`}
@ -314,7 +403,18 @@ export default function Marketplace() {
variant="contained" variant="contained"
color="primary" color="primary"
fullWidth fullWidth
sx={{ mt: 2 }} sx={{
mt: '1vw',
borderRadius: '20px',
p: '0.3vw 0vw',
color: 'white',
backgroundColor: 'rgb(255, 77, 77)',
'&:hover': {
backgroundColor: 'rgba(255, 77, 77, 0.5)',
},
fontFamily: 'Benzin-Bold',
fontSize: '1vw',
}}
onClick={() => handleBuyItem(item.id)} onClick={() => handleBuyItem(item.id)}
> >
Купить Купить

View File

@ -167,7 +167,9 @@ export default function Profile() {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 2, gap: '100px',
width: '100%',
justifyContent: 'center',
}} }}
> >
{loading ? ( {loading ? (
@ -185,6 +187,20 @@ export default function Profile() {
}} }}
> >
{/* Используем переработанный компонент SkinViewer */} {/* Используем переработанный компонент SkinViewer */}
<Typography
sx={{
fontFamily: 'Benzin-Bold',
alignSelf: 'center',
justifySelf: 'center',
textAlign: 'center',
width: '100%',
mb: '5vw',
fontSize: '3vw',
color: 'white',
}}
>
{username}
</Typography>
<SkinViewer <SkinViewer
width={300} width={300}
height={400} height={400}
@ -195,7 +211,13 @@ export default function Profile() {
/> />
</Paper> </Paper>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
}}
>
<Box <Box
sx={{ sx={{
width: '100%', width: '100%',
@ -284,9 +306,17 @@ export default function Profile() {
)} )}
<Button <Button
sx={{ color: 'white' }} sx={{
color: 'white',
borderRadius: '20px',
p: '10px 25px',
backgroundColor: 'rgb(0, 134, 0)',
'&:hover': {
backgroundColor: 'rgba(0, 134, 0, 0.5)',
},
fontFamily: 'Benzin-Bold',
}}
variant="contained" variant="contained"
color="primary"
fullWidth fullWidth
onClick={handleUploadSkin} onClick={handleUploadSkin}
disabled={uploadStatus === 'loading' || !skinFile} disabled={uploadStatus === 'loading' || !skinFile}

View File

@ -0,0 +1,427 @@
import Stepper from '@mui/material/Stepper';
import Step from '@mui/material/Step';
import StepLabel from '@mui/material/StepLabel';
import { useEffect, useRef, useState } from 'react';
import {
StepConnector,
stepConnectorClasses,
StepIconProps,
styled,
Typography,
Box,
TextField,
Button,
Snackbar,
CircularProgress,
} from '@mui/material';
import LoginRoundedIcon from '@mui/icons-material/LoginRounded';
import VerifiedRoundedIcon from '@mui/icons-material/VerifiedRounded';
import AssignmentIndRoundedIcon from '@mui/icons-material/AssignmentIndRounded';
import {
generateVerificationCode,
registerUser,
getVerificationStatus,
} from '../api';
import QRCodeStyling from 'qr-code-styling';
import popalogo from '../../../assets/icons/popa-popa.svg';
const ColorlibConnector = styled(StepConnector)(({ theme }) => ({
[`&.${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<unknown> } = {
1: <AssignmentIndRoundedIcon />,
2: <VerifiedRoundedIcon />,
3: <LoginRoundedIcon />,
};
return (
<ColorlibStepIconRoot
ownerState={{ completed, active }}
className={className}
>
{icons[String(props.icon)]}
</ColorlibStepIconRoot>
);
}
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<string | null>(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 (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Stepper
activeStep={activeStep}
alternativeLabel
connector={<ColorlibConnector />}
>
{steps.map((label) => (
<Step key={label}>
<StepLabel
sx={{
'& .MuiStepLabel-label': {
color: 'white',
},
'& .Mui-completed': {
color: 'white !important',
},
'& .Mui-active': {
backgroundImage:
'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
},
}}
StepIconComponent={ColorlibStepIcon}
>
{label}
</StepLabel>
</Step>
))}
</Stepper>
{activeStep === 0 && (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
alignItems: 'center',
}}
>
<Typography variant="h6">Создание аккаунта</Typography>
<Typography variant="body1">Введите ваш никнейм</Typography>
<TextField
required
name="username"
variant="outlined"
value={username}
onChange={(e) => 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',
},
},
}}
/>
<Typography variant="body1">Введите ваш пароль</Typography>
<TextField
required
name="password"
type="password"
variant="outlined"
value={password}
onChange={(e) => 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',
},
},
}}
/>
<Button
variant="contained"
color="primary"
sx={{ width: '100%', mt: 2 }}
onClick={handleCreateAccount}
>
Создать
</Button>
</Box>
)}
{activeStep === 1 && (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
alignItems: 'center',
}}
>
<Typography variant="h6">Откройте бота в телеграмме</Typography>
<Button
variant="contained"
color="primary"
sx={{ width: '100%', mt: 2 }}
onClick={handleOpenBot}
>
Открыть бота
</Button>
<div
ref={ref}
style={{
minHeight: 300,
}}
/>
<Typography variant="body1">
Введите код верификации в боте
</Typography>
{verificationCode ? (
<>
<Typography
variant="h2"
sx={{
backgroundImage:
'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}
>
{verificationCode}
</Typography>
<Typography variant="body1">Ждем ответа от бота</Typography>
<CircularProgress />
</>
) : (
<CircularProgress />
)}
</Box>
)}
<Snackbar
open={open}
autoHideDuration={6000}
onClose={handleClose}
message={message}
/>
</Box>
);
};

View File

@ -83,9 +83,10 @@ export default function Shop() {
gap: '2vw', gap: '2vw',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
width: '100%',
height: '100%',
}} }}
> >
<Typography variant="h4">Shop</Typography>
{loading ? ( {loading ? (
<Typography>Загрузка...</Typography> <Typography>Загрузка...</Typography>
) : ( ) : (
@ -93,9 +94,11 @@ export default function Shop() {
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexWrap: 'wrap',
alignContent: 'flex-start',
width: '90%',
height: '80%',
gap: '2vw', gap: '2vw',
justifyContent: 'center',
alignItems: 'center',
}} }}
> >
<Typography variant="h6">Доступные плащи</Typography> <Typography variant="h6">Доступные плащи</Typography>