add: shop capes and refactor cape card
This commit is contained in:
@ -30,6 +30,16 @@ export interface 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 {
|
||||
message: 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(
|
||||
username: string,
|
||||
cape_id: string,
|
||||
|
117
src/renderer/components/CapeCard.tsx
Normal file
117
src/renderer/components/CapeCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -20,13 +20,10 @@ import {
|
||||
MenuItem,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
Card,
|
||||
CardContent,
|
||||
CardMedia,
|
||||
Tooltip,
|
||||
CardActions,
|
||||
} from '@mui/material';
|
||||
|
||||
import CapeCard from '../components/CapeCard';
|
||||
|
||||
export default function Profile() {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [walkingSpeed, setWalkingSpeed] = useState<number>(0.5);
|
||||
@ -317,63 +314,24 @@ export default function Profile() {
|
||||
}}
|
||||
>
|
||||
<Typography>Ваши плащи</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 2,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
<CapeCard
|
||||
key={cape.cape_id}
|
||||
cape={cape}
|
||||
mode="profile"
|
||||
onAction={
|
||||
cape.is_active ? handleDeactivateCape : handleActivateCape
|
||||
}
|
||||
actionDisabled={loading}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -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() {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user