add: shop capes and refactor cape card

This commit is contained in:
2025-07-19 01:36:33 +05:00
parent 26f601635b
commit 56da7c7543
4 changed files with 315 additions and 62 deletions

View File

@ -30,6 +30,16 @@ export interface Cape {
export type CapesResponse = Cape[]; export type CapesResponse = Cape[];
export interface StoreCape {
id: string;
name: string;
description: string;
price: number;
image_url: string;
}
export type StoreCapesResponse = StoreCape[];
export interface ApiError { export interface ApiError {
message: string; message: string;
details?: string; details?: string;
@ -81,6 +91,50 @@ export async function fetchCapes(username: string): Promise<CapesResponse> {
} }
} }
export async function purchaseCape(
username: string,
cape_id: string,
): Promise<void> {
// Создаем URL с query-параметрами
const url = new URL(`${API_BASE_URL}/store/purchase/cape`);
url.searchParams.append('username', username);
url.searchParams.append('cape_id', cape_id);
const response = await fetch(url.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
// Не нужно отправлять тело запроса
// body: JSON.stringify({
// username: username,
// cape_id: cape_id,
// }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(
errorData.message ||
errorData.detail?.toString() ||
'Не удалось купить плащ',
);
}
}
export async function fetchCapesStore(): Promise<StoreCape[]> {
try {
const response = await fetch(`${API_BASE_URL}/store/capes`);
if (!response.ok) {
return [];
}
return await response.json();
} catch (error) {
console.error('API ошибка:', error);
return [];
}
}
export async function activateCape( export async function activateCape(
username: string, username: string,
cape_id: string, cape_id: string,

View File

@ -0,0 +1,117 @@
// src/renderer/components/CapeCard.tsx
import React from 'react';
import {
Card,
CardMedia,
CardContent,
Typography,
CardActions,
Button,
Tooltip,
Box,
Chip,
} from '@mui/material';
// Тип для плаща с необязательными полями для обоих вариантов использования
export interface CapeCardProps {
cape: {
cape_id?: string;
id?: string;
cape_name?: string;
name?: string;
cape_description?: string;
description?: string;
image_url: string;
is_active?: boolean;
price?: number;
purchased_at?: string;
};
mode: 'profile' | 'shop';
onAction: (capeId: string) => void;
actionDisabled?: boolean;
}
export default function CapeCard({
cape,
mode,
onAction,
actionDisabled = false,
}: CapeCardProps) {
// Определяем текст и цвет кнопки в зависимости от режима
const getActionButton = () => {
if (mode === 'shop') {
return {
text: 'Купить',
color: 'primary',
};
} else {
// Профиль
return cape.is_active
? { text: 'Снять', color: 'error' }
: { text: 'Надеть', color: 'success' };
}
};
const actionButton = getActionButton();
// В функции компонента добавьте нормализацию данных
const capeId = cape.cape_id || cape.id || '';
const capeName = cape.cape_name || cape.name || '';
const capeDescription = cape.cape_description || cape.description || '';
return (
<Tooltip arrow title={capeDescription}>
<Card
sx={{
bgcolor: 'rgba(255, 255, 255, 0.05)',
width: 200,
overflow: 'hidden',
position: 'relative', // для позиционирования ценника
}}
>
{/* Ценник для магазина */}
{mode === 'shop' && cape.price !== undefined && (
<Chip
label={`${cape.price} коинов`}
sx={{
position: 'absolute',
top: 8,
right: 8,
zIndex: 2,
bgcolor: 'rgba(0, 0, 0, 0.7)',
color: 'white',
fontWeight: 'bold',
}}
/>
)}
<CardMedia
component="img"
image={cape.image_url}
alt={capeName}
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' }}>{capeName}</Typography>
</CardContent>
<CardActions sx={{ display: 'flex', justifyContent: 'center' }}>
<Button
variant="contained"
color={actionButton.color as 'primary' | 'success' | 'error'}
onClick={() => onAction(capeId)}
disabled={actionDisabled}
>
{actionButton.text}
</Button>
</CardActions>
</Card>
</Tooltip>
);
}

View File

@ -20,13 +20,10 @@ import {
MenuItem, MenuItem,
Alert, Alert,
CircularProgress, CircularProgress,
Card,
CardContent,
CardMedia,
Tooltip,
CardActions,
} from '@mui/material'; } from '@mui/material';
import CapeCard from '../components/CapeCard';
export default function Profile() { export default function Profile() {
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [walkingSpeed, setWalkingSpeed] = useState<number>(0.5); const [walkingSpeed, setWalkingSpeed] = useState<number>(0.5);
@ -317,63 +314,24 @@ export default function Profile() {
}} }}
> >
<Typography>Ваши плащи</Typography> <Typography>Ваши плащи</Typography>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}> <Box
sx={{
display: 'flex',
flexDirection: 'row',
gap: 2,
flexWrap: 'wrap',
}}
>
{capes.map((cape) => ( {capes.map((cape) => (
<Tooltip arrow title={cape.cape_description}> <CapeCard
<Card
key={cape.cape_id} key={cape.cape_id}
sx={{ cape={cape}
bgcolor: 'rgba(255, 255, 255, 0.05)', mode="profile"
width: 200, // фиксированная ширина карточки onAction={
overflow: 'hidden', cape.is_active ? handleDeactivateCape : handleActivateCape
}} }
> actionDisabled={loading}
<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>

View File

@ -1,3 +1,127 @@
import { Box } from '@mui/material';
import { Typography } from '@mui/material';
import CapeCard from '../components/CapeCard';
import {
Cape,
fetchCapes,
fetchCapesStore,
purchaseCape,
StoreCape,
} from '../api';
import { useEffect, useState } from 'react';
export default function Shop() { export default function Shop() {
return <div>Shop</div>; const [storeCapes, setStoreCapes] = useState<StoreCape[]>([]);
const [userCapes, setUserCapes] = useState<Cape[]>([]);
const [username, setUsername] = useState<string>('');
const [uuid, setUuid] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
// Функция для загрузки плащей из магазина
const loadStoreCapes = async () => {
try {
const capes = await fetchCapesStore();
setStoreCapes(capes);
} catch (error) {
console.error('Ошибка при получении плащей магазина:', error);
setStoreCapes([]);
}
};
// Функция для загрузки плащей пользователя
const loadUserCapes = async (username: string) => {
try {
const userCapes = await fetchCapes(username);
setUserCapes(userCapes);
} catch (error) {
console.error('Ошибка при получении плащей пользователя:', error);
setUserCapes([]);
}
};
const handlePurchaseCape = async (cape_id: string) => {
try {
await purchaseCape(username, cape_id);
await loadUserCapes(username);
} catch (error) {
console.error('Ошибка при покупке плаща:', error);
}
};
// Загружаем данные при монтировании
useEffect(() => {
const savedConfig = localStorage.getItem('launcher_config');
if (savedConfig) {
const config = JSON.parse(savedConfig);
if (config.uuid && config.username) {
setUsername(config.username);
setUuid(config.uuid);
setLoading(true);
// Загружаем оба списка плащей
Promise.all([loadStoreCapes(), loadUserCapes(config.username)]).finally(
() => {
setLoading(false);
},
);
}
}
}, []);
// Фильтруем плащи, которые уже куплены пользователем
const availableCapes = storeCapes.filter(
(storeCape) =>
!userCapes.some((userCape) => userCape.cape_id === storeCape.id),
);
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: '2vw',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Typography variant="h4">Shop</Typography>
{loading ? (
<Typography>Загрузка...</Typography>
) : (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: '2vw',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Typography variant="h6">Доступные плащи</Typography>
{availableCapes.length > 0 ? (
<Box
sx={{
display: 'flex',
flexDirection: 'row',
gap: '2vw',
flexWrap: 'wrap',
}}
>
{availableCapes.map((cape) => (
<CapeCard
key={cape.id}
cape={cape}
mode="shop"
onAction={handlePurchaseCape}
/>
))}
</Box>
) : (
<Typography>У вас уже есть все доступные плащи!</Typography>
)}
</Box>
)}
</Box>
);
} }