add: capes switch in user profile
This commit is contained in:
@ -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,
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user