add: shop capes and refactor cape card
This commit is contained in:
@ -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,
|
||||||
|
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,
|
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>
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user