add: capes switch in user profile

This commit is contained in:
2025-07-18 20:14:44 +05:00
parent ec54219192
commit 26f601635b
2 changed files with 319 additions and 113 deletions

View File

@ -19,6 +19,17 @@ export interface CoinsResponse {
}; };
} }
export interface Cape {
cape_id: string;
cape_name: string;
cape_description: string;
image_url: string;
purchased_at: string;
is_active: boolean;
}
export type CapesResponse = Cape[];
export interface ApiError { export interface ApiError {
message: string; message: string;
details?: string; details?: string;
@ -55,6 +66,63 @@ export async function fetchCoins(username: string): Promise<CoinsResponse> {
} }
} }
export async function fetchCapes(username: string): Promise<CapesResponse> {
try {
const response = await fetch(
`${API_BASE_URL}/store/user/${username}/capes`,
);
if (!response.ok) {
return []; // Если плащи не найдены, возвращаем пустой массив
}
return await response.json();
} catch (error) {
console.error('API ошибка:', error);
return []; // В случае ошибки возвращаем пустой массив
}
}
export async function activateCape(
username: string,
cape_id: string,
): Promise<void> {
const response = await fetch(
`${API_BASE_URL}/store/user/${username}/capes/activate/${cape_id}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
cape_id: cape_id,
}),
},
);
if (!response.ok) {
throw new Error('Не удалось активировать плащ');
}
}
export async function deactivateCape(
username: string,
cape_id: string,
): Promise<void> {
const response = await fetch(
`${API_BASE_URL}/store/user/${username}/capes/deactivate/${cape_id}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
cape_id: cape_id,
}),
},
);
if (!response.ok) {
throw new Error('Не удалось деактивировать плащ');
}
}
// Загрузка скина // Загрузка скина
export async function uploadSkin( export async function uploadSkin(
username: string, username: string,

View File

@ -1,6 +1,13 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import SkinViewer from '../components/SkinViewer'; import SkinViewer from '../components/SkinViewer';
import { fetchPlayer, uploadSkin } from '../api'; import {
fetchPlayer,
uploadSkin,
fetchCapes,
Cape,
activateCape,
deactivateCape,
} from '../api';
import { import {
Box, Box,
@ -13,6 +20,11 @@ import {
MenuItem, MenuItem,
Alert, Alert,
CircularProgress, CircularProgress,
Card,
CardContent,
CardMedia,
Tooltip,
CardActions,
} from '@mui/material'; } from '@mui/material';
export default function Profile() { export default function Profile() {
@ -28,6 +40,9 @@ export default function Profile() {
>('idle'); >('idle');
const [statusMessage, setStatusMessage] = useState<string>(''); const [statusMessage, setStatusMessage] = useState<string>('');
const [isDragOver, setIsDragOver] = useState<boolean>(false); const [isDragOver, setIsDragOver] = useState<boolean>(false);
const [capes, setCapes] = useState<Cape[]>([]);
const [uuid, setUuid] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
const savedConfig = localStorage.getItem('launcher_config'); const savedConfig = localStorage.getItem('launcher_config');
@ -36,15 +51,19 @@ export default function Profile() {
if (config.uuid) { if (config.uuid) {
loadPlayerData(config.uuid); loadPlayerData(config.uuid);
setUsername(config.username || ''); setUsername(config.username || '');
loadCapesData(config.username || '');
setUuid(config.uuid || '');
} }
} }
}, []); }, []);
const loadPlayerData = async (uuid: string) => { const loadPlayerData = async (uuid: string) => {
try { try {
setLoading(true);
const player = await fetchPlayer(uuid); const player = await fetchPlayer(uuid);
setSkin(player.skin_url); setSkin(player.skin_url);
setCape(player.cloak_url); setCape(player.cloak_url);
setLoading(false);
} catch (error) { } catch (error) {
console.error('Ошибка при получении данных игрока:', error); console.error('Ошибка при получении данных игрока:', error);
setSkin(''); setSkin('');
@ -52,6 +71,18 @@ export default function Profile() {
} }
}; };
const loadCapesData = async (username: string) => {
try {
setLoading(true);
const capesData = await fetchCapes(username);
setCapes(capesData);
setLoading(false);
} catch (error) {
console.error('Ошибка при получении плащей:', error);
setCapes([]);
}
};
// Обработка перетаскивания файла // Обработка перетаскивания файла
const handleFileDrop = (e: React.DragEvent<HTMLDivElement>) => { const handleFileDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault(); e.preventDefault();
@ -83,8 +114,24 @@ export default function Profile() {
} }
}; };
const handleActivateCape = async (cape_id: string) => {
setLoading(true);
await activateCape(username, cape_id);
await loadCapesData(username);
setLoading(false);
};
const handleDeactivateCape = async (cape_id: string) => {
setLoading(true);
await deactivateCape(username, cape_id);
await loadCapesData(username);
await loadPlayerData(uuid);
setLoading(false);
};
// Отправка запроса на установку скина // Отправка запроса на установку скина
const handleUploadSkin = async () => { const handleUploadSkin = async () => {
setLoading(true);
if (!skinFile || !username) { if (!skinFile || !username) {
setStatusMessage('Необходимо выбрать файл и указать имя пользователя'); setStatusMessage('Необходимо выбрать файл и указать имя пользователя');
setUploadStatus('error'); setUploadStatus('error');
@ -111,6 +158,8 @@ export default function Profile() {
`Ошибка: ${error instanceof Error ? error.message : 'Не удалось загрузить скин'}`, `Ошибка: ${error instanceof Error ? error.message : 'Не удалось загрузить скин'}`,
); );
setUploadStatus('error'); setUploadStatus('error');
} finally {
setLoading(false);
} }
}; };
@ -124,6 +173,10 @@ export default function Profile() {
gap: 2, gap: 2,
}} }}
> >
{loading ? (
<CircularProgress />
) : (
<>
<Paper <Paper
elevation={0} elevation={0}
sx={{ sx={{
@ -145,11 +198,16 @@ export default function Profile() {
/> />
</Paper> </Paper>
<Box sx={{ width: '100%', maxWidth: '500px', mt: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Typography variant="h6" gutterBottom> <Box
Установить скин sx={{
</Typography> width: '100%',
maxWidth: '500px',
bgcolor: 'rgba(255, 255, 255, 0.05)',
padding: '3vw',
borderRadius: '1vw',
}}
>
<Box <Box
sx={{ sx={{
border: '2px dashed', border: '2px dashed',
@ -159,7 +217,9 @@ export default function Profile() {
mb: 2, mb: 2,
textAlign: 'center', textAlign: 'center',
cursor: 'pointer', cursor: 'pointer',
bgcolor: isDragOver ? 'rgba(25, 118, 210, 0.08)' : 'transparent', bgcolor: isDragOver
? 'rgba(25, 118, 210, 0.08)'
: 'transparent',
transition: 'all 0.2s', transition: 'all 0.2s',
}} }}
onDragOver={(e) => { onDragOver={(e) => {
@ -184,7 +244,11 @@ export default function Profile() {
</Typography> </Typography>
</Box> </Box>
<FormControl color="primary" fullWidth sx={{ mb: 2, color: 'white' }}> <FormControl
color="primary"
fullWidth
sx={{ mb: 2, color: 'white' }}
>
<InputLabel sx={{ color: 'white' }}>Модель скина</InputLabel> <InputLabel sx={{ color: 'white' }}>Модель скина</InputLabel>
<Select <Select
value={skinModel} value={skinModel}
@ -238,10 +302,84 @@ export default function Profile() {
{uploadStatus === 'loading' ? ( {uploadStatus === 'loading' ? (
<Typography sx={{ color: 'white' }}>Загрузка...</Typography> <Typography sx={{ color: 'white' }}>Загрузка...</Typography>
) : ( ) : (
<Typography sx={{ color: 'white' }}>Установить скин</Typography> <Typography sx={{ color: 'white' }}>
Установить скин
</Typography>
)} )}
</Button> </Button>
</Box> </Box>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
gap: 2,
}}
>
<Typography>Ваши плащи</Typography>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
{capes.map((cape) => (
<Tooltip arrow title={cape.cape_description}>
<Card
key={cape.cape_id}
sx={{
bgcolor: 'rgba(255, 255, 255, 0.05)',
width: 200, // фиксированная ширина карточки
overflow: 'hidden',
}}
>
<CardMedia
component="img"
image={cape.image_url}
alt={cape.cape_name}
sx={{
display: 'block',
width: '100%',
transform:
'scale(2.9) translateX(66px) translateY(32px)',
imageRendering: 'pixelated',
}}
/>
<CardContent
sx={{ bgcolor: 'rgba(255, 255, 255, 0.05)', pt: '9vh' }}
>
<Typography sx={{ color: 'white' }}>
{cape.cape_name}
</Typography>
</CardContent>
{cape.is_active ? (
<CardActions
sx={{ display: 'flex', justifyContent: 'center' }}
>
<Button
variant="contained"
sx={{ bgcolor: 'red' }}
onClick={() => handleDeactivateCape(cape.cape_id)}
>
Снять
</Button>
</CardActions>
) : (
<CardActions
sx={{ display: 'flex', justifyContent: 'center' }}
>
<Button
variant="contained"
sx={{ bgcolor: 'green' }}
onClick={() => handleActivateCape(cape.cape_id)}
>
Надеть
</Button>
</CardActions>
)}
</Card>
</Tooltip>
))}
</Box>
</Box>
</Box>
</>
)}
</Box> </Box>
); );
} }