400 lines
12 KiB
TypeScript
400 lines
12 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import {
|
||
Box,
|
||
Typography,
|
||
Card,
|
||
CardContent,
|
||
CardMedia,
|
||
Button,
|
||
Grid,
|
||
Chip,
|
||
CircularProgress,
|
||
Alert,
|
||
Modal,
|
||
} from '@mui/material';
|
||
import { GridItem } from '../components/GridItem';
|
||
import { ShoppingBag, Inventory, LocalShipping, Code } from '@mui/icons-material';
|
||
import { useAuth } from '../context/AuthContext';
|
||
import { apiService } from '../services/api';
|
||
import type { Reward } from '../types';
|
||
|
||
export const ShopPage: React.FC = () => {
|
||
const { user, updateUser } = useAuth();
|
||
const [rewards, setRewards] = useState<Reward[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [selectedReward, setSelectedReward] = useState<Reward | null>(null);
|
||
const [purchaseModalOpen, setPurchaseModalOpen] = useState(false);
|
||
const [instructionModalOpen, setInstructionModalOpen] = useState(false);
|
||
const [purchasing, setPurchasing] = useState(false);
|
||
|
||
useEffect(() => {
|
||
const fetchRewards = async () => {
|
||
try {
|
||
const response = await apiService.getAllRewards();
|
||
if (response.success && response.data) {
|
||
setRewards(response.data);
|
||
} else {
|
||
setError('Не удалось загрузить призы');
|
||
}
|
||
} catch (err) {
|
||
console.error('Error fetching rewards:', err);
|
||
setError('Произошла ошибка при загрузке призов');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchRewards();
|
||
}, []);
|
||
|
||
const handlePurchase = async (reward: Reward) => {
|
||
setSelectedReward(reward);
|
||
setPurchaseModalOpen(true);
|
||
};
|
||
|
||
const confirmPurchase = async () => {
|
||
if (!selectedReward) return;
|
||
|
||
setPurchasing(true);
|
||
try {
|
||
const response = await apiService.purchaseReward(selectedReward.id);
|
||
if (response.success) {
|
||
setPurchaseModalOpen(false);
|
||
setInstructionModalOpen(true);
|
||
|
||
// Refresh user data to update balance
|
||
try {
|
||
const userResponse = await apiService.getCurrentUser();
|
||
if (userResponse.success && userResponse.data) {
|
||
updateUser({ stars_balance: userResponse.data.stars_balance });
|
||
}
|
||
} catch (err) {
|
||
console.error('Error refreshing user data:', err);
|
||
}
|
||
} else {
|
||
setError(response.message || 'Не удалось приобрести приз');
|
||
}
|
||
} catch (err) {
|
||
console.error('Error purchasing reward:', err);
|
||
setError('Произошла ошибка при покупке');
|
||
} finally {
|
||
setPurchasing(false);
|
||
}
|
||
};
|
||
|
||
const canAfford = (price: number) => {
|
||
return user && user.stars_balance >= price;
|
||
};
|
||
|
||
const isInStock = (stock: number) => {
|
||
return stock === 0 || stock > 0;
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
|
||
<CircularProgress sx={{ color: '#FFD700' }} />
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<Alert
|
||
severity="error"
|
||
sx={{
|
||
mt: 4,
|
||
backgroundColor: 'rgba(244, 67, 54, 0.1)',
|
||
color: '#ffffff',
|
||
border: '1px solid #f44336',
|
||
}}
|
||
>
|
||
{error}
|
||
</Alert>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Box>
|
||
{/* Header */}
|
||
<Box sx={{ mb: 4 }}>
|
||
<Typography
|
||
variant="h4"
|
||
sx={{
|
||
color: '#FFD700',
|
||
fontWeight: 'bold',
|
||
mb: 1,
|
||
}}
|
||
>
|
||
Магазин призов 🛍️
|
||
</Typography>
|
||
<Typography
|
||
variant="h6"
|
||
sx={{
|
||
color: '#ffffff',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 1,
|
||
}}
|
||
>
|
||
Ваш баланс: {user?.stars_balance} ⭐
|
||
</Typography>
|
||
</Box>
|
||
|
||
{/* Rewards Grid */}
|
||
<Grid container component="div" spacing={3}>
|
||
{rewards.map((reward) => (
|
||
<GridItem xs={12} sm={6} component="div" key={reward.id}>
|
||
<Card
|
||
sx={{
|
||
backgroundColor: '#1a1a1a',
|
||
border: '1px solid #333',
|
||
borderRadius: 2,
|
||
overflow: 'hidden',
|
||
transition: 'transform 0.2s',
|
||
'&:hover': {
|
||
transform: 'translateY(-4px)',
|
||
border: '1px solid #FFD700',
|
||
},
|
||
}}
|
||
>
|
||
<CardMedia
|
||
component="img"
|
||
height="200"
|
||
image={reward.image_url || '/placeholder-reward.jpg'}
|
||
alt={reward.title}
|
||
sx={{ objectFit: 'cover' }}
|
||
/>
|
||
<CardContent sx={{ p: 3 }}>
|
||
<Typography
|
||
variant="h6"
|
||
sx={{
|
||
color: '#ffffff',
|
||
fontWeight: 'bold',
|
||
mb: 1,
|
||
}}
|
||
>
|
||
{reward.title}
|
||
</Typography>
|
||
|
||
<Typography
|
||
variant="body2"
|
||
sx={{
|
||
color: '#888',
|
||
mb: 2,
|
||
display: '-webkit-box',
|
||
WebkitLineClamp: 3,
|
||
WebkitBoxOrient: 'vertical',
|
||
overflow: 'hidden',
|
||
}}
|
||
>
|
||
{reward.description}
|
||
</Typography>
|
||
|
||
{/* Price and Status */}
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||
<Chip
|
||
label={`${reward.price_stars} ⭐`}
|
||
size="small"
|
||
sx={{
|
||
backgroundColor: canAfford(reward.price_stars)
|
||
? 'rgba(255, 215, 0, 0.2)'
|
||
: 'rgba(244, 67, 54, 0.2)',
|
||
color: canAfford(reward.price_stars) ? '#FFD700' : '#f44336',
|
||
border: canAfford(reward.price_stars)
|
||
? '1px solid #FFD700'
|
||
: '1px solid #f44336',
|
||
}}
|
||
/>
|
||
<Chip
|
||
icon={reward.delivery_type === 'physical' ? <LocalShipping /> : <Code />}
|
||
label={reward.delivery_type === 'physical' ? 'Физический' : 'Цифровой'}
|
||
size="small"
|
||
sx={{
|
||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||
color: '#ffffff',
|
||
}}
|
||
/>
|
||
{!isInStock(reward.stock) && (
|
||
<Chip
|
||
icon={<Inventory />}
|
||
label="Нет в наличии"
|
||
size="small"
|
||
sx={{
|
||
backgroundColor: 'rgba(244, 67, 54, 0.2)',
|
||
color: '#f44336',
|
||
}}
|
||
/>
|
||
)}
|
||
</Box>
|
||
|
||
{/* Action Button */}
|
||
<Button
|
||
fullWidth
|
||
variant="contained"
|
||
onClick={() => handlePurchase(reward)}
|
||
disabled={!canAfford(reward.price_stars) || !isInStock(reward.stock)}
|
||
sx={{
|
||
backgroundColor: canAfford(reward.price_stars) && isInStock(reward.stock)
|
||
? '#FFD700'
|
||
: '#666',
|
||
color: canAfford(reward.price_stars) && isInStock(reward.stock)
|
||
? '#000000'
|
||
: '#ffffff',
|
||
fontWeight: 'bold',
|
||
'&:hover': {
|
||
backgroundColor: canAfford(reward.price_stars) && isInStock(reward.stock)
|
||
? '#FFC700'
|
||
: '#666',
|
||
},
|
||
}}
|
||
>
|
||
{!canAfford(reward.price_stars)
|
||
? 'Недостаточно ⭐'
|
||
: !isInStock(reward.stock)
|
||
? 'Нет в наличии'
|
||
: 'Купить'
|
||
}
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</GridItem>
|
||
))}
|
||
</Grid>
|
||
|
||
{rewards.length === 0 && (
|
||
<Box sx={{ textAlign: 'center', mt: 8 }}>
|
||
<ShoppingBag sx={{ fontSize: 64, color: '#666', mb: 2 }} />
|
||
<Typography
|
||
variant="h6"
|
||
sx={{ color: '#888', mb: 2 }}
|
||
>
|
||
Пока нет доступных призов
|
||
</Typography>
|
||
<Typography
|
||
variant="body2"
|
||
sx={{ color: '#666' }}
|
||
>
|
||
Загляните позже - призы появятся скоро!
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
|
||
{/* Purchase Confirmation Modal */}
|
||
<Modal
|
||
open={purchaseModalOpen}
|
||
onClose={() => setPurchaseModalOpen(false)}
|
||
sx={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
backgroundColor: '#1a1a1a',
|
||
border: '1px solid #333',
|
||
borderRadius: 2,
|
||
p: 4,
|
||
maxWidth: 400,
|
||
width: '90%',
|
||
}}
|
||
>
|
||
<Typography
|
||
variant="h6"
|
||
sx={{
|
||
color: '#ffffff',
|
||
fontWeight: 'bold',
|
||
mb: 2,
|
||
}}
|
||
>
|
||
Подтверждение покупки
|
||
</Typography>
|
||
<Typography
|
||
variant="body1"
|
||
sx={{ color: '#888', mb: 1 }}
|
||
>
|
||
Вы уверены, что хотите купить "{selectedReward?.title}" за {selectedReward?.price_stars} ⭐?
|
||
</Typography>
|
||
<Box sx={{ display: 'flex', gap: 2, mt: 3 }}>
|
||
<Button
|
||
fullWidth
|
||
variant="outlined"
|
||
onClick={() => setPurchaseModalOpen(false)}
|
||
sx={{
|
||
borderColor: '#333',
|
||
color: '#ffffff',
|
||
}}
|
||
>
|
||
Отмена
|
||
</Button>
|
||
<Button
|
||
fullWidth
|
||
variant="contained"
|
||
onClick={confirmPurchase}
|
||
disabled={purchasing}
|
||
sx={{
|
||
backgroundColor: '#FFD700',
|
||
color: '#000000',
|
||
}}
|
||
>
|
||
{purchasing ? <CircularProgress size={20} /> : 'Купить'}
|
||
</Button>
|
||
</Box>
|
||
</Box>
|
||
</Modal>
|
||
|
||
{/* Instructions Modal */}
|
||
<Modal
|
||
open={instructionModalOpen}
|
||
onClose={() => setInstructionModalOpen(false)}
|
||
sx={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
backgroundColor: '#1a1a1a',
|
||
border: '1px solid #333',
|
||
borderRadius: 2,
|
||
p: 4,
|
||
maxWidth: 400,
|
||
width: '90%',
|
||
}}
|
||
>
|
||
<Typography
|
||
variant="h6"
|
||
sx={{
|
||
color: '#4CAF50',
|
||
fontWeight: 'bold',
|
||
mb: 2,
|
||
}}
|
||
>
|
||
Покупка успешно оформлена! 🎉
|
||
</Typography>
|
||
<Typography
|
||
variant="body1"
|
||
sx={{ color: '#ffffff', mb: 2 }}
|
||
>
|
||
{selectedReward?.instructions}
|
||
</Typography>
|
||
<Button
|
||
fullWidth
|
||
variant="contained"
|
||
onClick={() => setInstructionModalOpen(false)}
|
||
sx={{
|
||
backgroundColor: '#4CAF50',
|
||
color: '#ffffff',
|
||
mt: 2,
|
||
}}
|
||
>
|
||
Понятно
|
||
</Button>
|
||
</Box>
|
||
</Modal>
|
||
</Box>
|
||
);
|
||
}; |