sno-quiz/frontend/src/pages/ProfilePage.tsx
2025-09-17 22:22:14 +03:00

407 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
Card,
CardContent,
Tab,
Tabs,
List,
ListItem,
ListItemText,
ListItemIcon,
Chip,
CircularProgress,
Alert,
Button,
} from '@mui/material';
import {
Person,
Star,
History,
ShoppingCart,
AddCircle,
RemoveCircle,
} from '@mui/icons-material';
import { useAuth } from '../context/AuthContext';
import { apiService } from '../services/api';
import type { Transaction, Purchase } from '../types';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
const TabPanel: React.FC<TabPanelProps> = ({ children, value, index }) => {
return (
<div
role="tabpanel"
hidden={value !== index}
id={`profile-tabpanel-${index}`}
aria-labelledby={`profile-tab-${index}`}
>
{value === index && <Box sx={{ mt: 2 }}>{children}</Box>}
</div>
);
};
export const ProfilePage: React.FC = () => {
const { user, logout } = useAuth();
const [value, setValue] = useState(0);
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [purchases, setPurchases] = useState<Purchase[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const [transactionsResponse, purchasesResponse] = await Promise.all([
apiService.getUserTransactions(),
apiService.getUserPurchases(),
]);
if (transactionsResponse.success && transactionsResponse.data) {
setTransactions(transactionsResponse.data);
}
if (purchasesResponse.success && purchasesResponse.data) {
setPurchases(purchasesResponse.data);
}
} catch (err) {
console.error('Error fetching profile data:', err);
setError('Произошла ошибка при загрузке данных');
} finally {
setLoading(false);
}
};
fetchData();
}, []);
const handleChange = (_event: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
const handleLogout = () => {
logout();
};
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>
{/* Profile Header */}
<Card
sx={{
backgroundColor: '#1a1a1a',
border: '1px solid #333',
borderRadius: 2,
mb: 3,
}}
>
<CardContent sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
{user?.photo_url ? (
<img
src={user.photo_url}
alt="User Avatar"
style={{
width: 80,
height: 80,
borderRadius: '50%',
objectFit: 'cover',
marginRight: 16,
}}
/>
) : (
<Box
sx={{
width: 80,
height: 80,
borderRadius: '50%',
backgroundColor: '#FFD700',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
mr: 3,
}}
>
<Person sx={{ fontSize: 40, color: '#000000' }} />
</Box>
)}
<Box>
<Typography
variant="h4"
sx={{
color: '#ffffff',
fontWeight: 'bold',
mb: 1,
}}
>
{user?.first_name} {user?.last_name}
</Typography>
<Typography
variant="body1"
sx={{ color: '#888', mb: 1 }}
>
@{user?.username}
</Typography>
<Typography
variant="body2"
sx={{ color: '#666' }}
>
ID: {user?.telegram_id}
</Typography>
</Box>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: 'rgba(255, 215, 0, 0.1)',
p: 2,
borderRadius: 1,
border: '1px solid #FFD700',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Star sx={{ color: '#FFD700' }} />
<Typography
variant="h6"
sx={{ color: '#FFD700', fontWeight: 'bold' }}
>
{user?.stars_balance}
</Typography>
</Box>
<Button
variant="outlined"
onClick={handleLogout}
sx={{
borderColor: '#f44336',
color: '#f44336',
'&:hover': {
borderColor: '#f44336',
backgroundColor: 'rgba(244, 67, 54, 0.1)',
},
}}
>
Выйти
</Button>
</Box>
</CardContent>
</Card>
{/* Tabs */}
<Box sx={{ borderBottom: 1, borderColor: '#333' }}>
<Tabs
value={value}
onChange={handleChange}
sx={{
'& .MuiTab-root': {
color: '#888',
'&.Mui-selected': {
color: '#FFD700',
},
},
'& .MuiTabs-indicator': {
backgroundColor: '#FFD700',
},
}}
>
<Tab
icon={<History />}
label="История"
id="profile-tab-0"
aria-controls="profile-tabpanel-0"
/>
<Tab
icon={<ShoppingCart />}
label="Покупки"
id="profile-tab-1"
aria-controls="profile-tabpanel-1"
/>
</Tabs>
</Box>
{/* Tab Panels */}
<TabPanel value={value} index={0}>
<Card
sx={{
backgroundColor: '#1a1a1a',
border: '1px solid #333',
borderRadius: 2,
}}
>
<List>
{transactions.map((transaction, index) => (
<ListItem
key={index}
sx={{
borderBottom: index < transactions.length - 1 ? '1px solid #333' : 'none',
}}
>
<ListItemIcon>
{transaction.type === 'earned' ? (
<AddCircle sx={{ color: '#4CAF50' }} />
) : (
<RemoveCircle sx={{ color: '#f44336' }} />
)}
</ListItemIcon>
<ListItemText
primary={
<Typography sx={{ color: '#ffffff' }}>
{transaction.description}
</Typography>
}
secondary={
<Typography sx={{ color: '#666', fontSize: '0.875rem' }}>
{formatDate(transaction.created_at)}
</Typography>
}
/>
<Chip
label={`${transaction.type === 'earned' ? '+' : ''}${transaction.amount}`}
size="small"
sx={{
backgroundColor:
transaction.type === 'earned'
? 'rgba(76, 175, 80, 0.2)'
: 'rgba(244, 67, 54, 0.2)',
color: transaction.type === 'earned' ? '#4CAF50' : '#f44336',
border: transaction.type === 'earned'
? '1px solid #4CAF50'
: '1px solid #f44336',
}}
/>
</ListItem>
))}
</List>
{transactions.length === 0 && (
<Box sx={{ textAlign: 'center', p: 4 }}>
<History sx={{ fontSize: 48, color: '#666', mb: 2 }} />
<Typography sx={{ color: '#888' }}>
Пока нет транзакций
</Typography>
</Box>
)}
</Card>
</TabPanel>
<TabPanel value={value} index={1}>
<Card
sx={{
backgroundColor: '#1a1a1a',
border: '1px solid #333',
borderRadius: 2,
}}
>
<List>
{purchases.map((purchase, index) => (
<ListItem
key={index}
sx={{
borderBottom: index < purchases.length - 1 ? '1px solid #333' : 'none',
}}
>
<ListItemIcon>
<ShoppingCart sx={{ color: '#FFD700' }} />
</ListItemIcon>
<ListItemText
primary={
<Typography sx={{ color: '#ffffff' }}>
Покупка #{purchase.id}
</Typography>
}
secondary={
<Typography sx={{ color: '#666', fontSize: '0.875rem' }}>
{formatDate(purchase.purchased_at)}
</Typography>
}
/>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Chip
label={`${purchase.stars_spent}`}
size="small"
sx={{
backgroundColor: 'rgba(255, 215, 0, 0.2)',
color: '#FFD700',
border: '1px solid #FFD700',
}}
/>
<Chip
label={purchase.status}
size="small"
sx={{
backgroundColor:
purchase.status === 'delivered'
? 'rgba(76, 175, 80, 0.2)'
: purchase.status === 'pending'
? 'rgba(255, 193, 7, 0.2)'
: 'rgba(244, 67, 54, 0.2)',
color:
purchase.status === 'delivered'
? '#4CAF50'
: purchase.status === 'pending'
? '#FFC700'
: '#f44336',
border:
purchase.status === 'delivered'
? '1px solid #4CAF50'
: purchase.status === 'pending'
? '1px solid #FFC700'
: '1px solid #f44336',
}}
/>
</Box>
</ListItem>
))}
</List>
{purchases.length === 0 && (
<Box sx={{ textAlign: 'center', p: 4 }}>
<ShoppingCart sx={{ fontSize: 48, color: '#666', mb: 2 }} />
<Typography sx={{ color: '#888' }}>
Пока нет покупок
</Typography>
</Box>
)}
</Card>
</TabPanel>
</Box>
);
};