mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
updated ui for user specific content
This commit is contained in:
1 parent
860858e384
commit
3e8e71fc02
4 files changed
+236
-128
No files matched your search
@@ -153,7 +153,8 @@ class UserViewSet(viewsets.ModelViewSet):
|
||||
return Response({
|
||||
'username': user.username,
|
||||
'theme': user.theme,
|
||||
'ratings': RatingSerializer(ratings, many=True).data
|
||||
'avatar': request.build_absolute_uri(user.avatar.url) if user.avatar else None,
|
||||
'ratings': RatingSerializer(ratings, many=True, context={'request': request}).data
|
||||
})
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
|
||||
+67
-57
@@ -248,60 +248,69 @@ const App: React.FC = () => {
|
||||
|
||||
<GlobalSearch />
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{!isMobile && !loadingUser && (
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
{navItems.map((item) => (
|
||||
<Button key={item.label} color="inherit" component={Link} to={item.path}>{item.label}</Button>
|
||||
))}
|
||||
{user && (
|
||||
<Button color="inherit" onClick={handleLogout} sx={{ ml: 1 }}>Logout</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<IconButton color="inherit" onClick={(e) => setAnchorEl(e.currentTarget)} sx={{ ml: 1 }}>
|
||||
|
||||
{user?.avatar ? (
|
||||
|
||||
<Avatar src={user.avatar} sx={{ width: 32, height: 32 }} />
|
||||
|
||||
) : (
|
||||
|
||||
<PaletteIcon />
|
||||
|
||||
)}
|
||||
|
||||
</IconButton>
|
||||
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
sx={{ maxHeight: 500 }}
|
||||
>
|
||||
<ListSubheader>Light Themes</ListSubheader>
|
||||
<MenuItem onClick={() => handleThemeChange('latte')}>Latte</MenuItem>
|
||||
<MenuItem onClick={() => handleThemeChange('pink')}>Pastel Pink</MenuItem>
|
||||
<MenuItem onClick={() => handleThemeChange('solarized_light')}>Solarized Light</MenuItem>
|
||||
<MenuItem onClick={() => handleThemeChange('one_light')}>One Light</MenuItem>
|
||||
<MenuItem onClick={() => handleThemeChange('paper')}>Paper White</MenuItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListSubheader>Dark Themes</ListSubheader>
|
||||
<MenuItem onClick={() => handleThemeChange('mocha')}>Mocha (Default)</MenuItem>
|
||||
<MenuItem onClick={() => handleThemeChange('frappe')}>Frappé</MenuItem>
|
||||
<MenuItem onClick={() => handleThemeChange('macchiato')}>Macchiato</MenuItem>
|
||||
<MenuItem onClick={() => handleThemeChange('atom')}>Atom One Dark</MenuItem>
|
||||
<MenuItem onClick={() => handleThemeChange('dracula')}>Dracula</MenuItem>
|
||||
<MenuItem onClick={() => handleThemeChange('gruvbox')}>Gruvbox Dark</MenuItem>
|
||||
<MenuItem onClick={() => handleThemeChange('nord')}>Nord</MenuItem>
|
||||
<MenuItem onClick={() => handleThemeChange('cyberpunk')}>Cyberpunk</MenuItem>
|
||||
<MenuItem onClick={() => handleThemeChange('forest')}>Forest Dark</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
|
||||
{!isMobile && !loadingUser && (
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
|
||||
<Button color="inherit" component={Link} to="/">Catalog</Button>
|
||||
|
||||
</Box>
|
||||
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{!loadingUser && (
|
||||
|
||||
user ? (
|
||||
|
||||
<>
|
||||
|
||||
<IconButton color="inherit" onClick={(e) => setAnchorEl(e.currentTarget)} sx={{ ml: 1 }}>
|
||||
|
||||
<Avatar src={user.avatar || undefined} sx={{ width: 32, height: 32 }}>
|
||||
|
||||
{!user.avatar && user.username.charAt(0).toUpperCase()}
|
||||
|
||||
</Avatar>
|
||||
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
|
||||
anchorEl={anchorEl}
|
||||
|
||||
open={Boolean(anchorEl)}
|
||||
|
||||
onClose={() => setAnchorEl(null)}
|
||||
|
||||
>
|
||||
|
||||
<MenuItem component={Link} to="/dashboard" onClick={() => setAnchorEl(null)}>Dashboard</MenuItem>
|
||||
|
||||
<MenuItem component={Link} to="/settings" onClick={() => setAnchorEl(null)}>Settings</MenuItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<MenuItem onClick={handleLogout}>Logout</MenuItem>
|
||||
|
||||
</Menu>
|
||||
|
||||
</>
|
||||
|
||||
) : (
|
||||
|
||||
<Button color="inherit" component={Link} to="/login">Login</Button>
|
||||
|
||||
)
|
||||
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Toolbar sx={{ minHeight: { xs: 56, sm: 64 } }} />
|
||||
@@ -312,9 +321,10 @@ const App: React.FC = () => {
|
||||
<Route path="/category/:slug" element={<CategoryFlavors />} />
|
||||
<Route path="/flavor/:id" element={<FlavorDetail />} />
|
||||
<Route path="/profile/:username" element={<PublicProfile />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/settings" element={<Settings themeName={themeName} onThemeChange={handleThemeChange} />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
</Routes>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -12,10 +12,33 @@ interface DashboardData {
|
||||
my_ratings: any[];
|
||||
}
|
||||
|
||||
import {
|
||||
Typography,
|
||||
Card,
|
||||
CardContent,
|
||||
Box,
|
||||
Rating as MuiRating,
|
||||
Button,
|
||||
TextField,
|
||||
Container,
|
||||
CircularProgress,
|
||||
Avatar,
|
||||
Tabs,
|
||||
Tab,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import { Link } from 'react-router-dom';
|
||||
// ...
|
||||
const Dashboard: React.FC = () => {
|
||||
useTitle('Dashboard');
|
||||
const [data, setData] = useState<DashboardData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
const shareUrl = data ? `${window.location.origin}/profile/${data.user.username}` : '';
|
||||
|
||||
@@ -41,6 +64,18 @@ const Dashboard: React.FC = () => {
|
||||
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}><CircularProgress /></Box>;
|
||||
if (!data) return <Typography>Please login to view dashboard.</Typography>;
|
||||
|
||||
// Helper to group by category
|
||||
const groupBy = (array: any[], key: string) => {
|
||||
return array.reduce((result, currentValue) => {
|
||||
const groupKey = currentValue.flavor?.category_name || currentValue.category_name;
|
||||
(result[groupKey] = result[groupKey] || []).push(currentValue);
|
||||
return result;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const ratedGrouped = groupBy(data.my_ratings, 'category_name');
|
||||
const missingGrouped = groupBy(data.missing_flavors, 'category_name');
|
||||
|
||||
return (
|
||||
<Container maxWidth={false} sx={{ px: { xs: 2, sm: 4, md: 6 }, py: 4 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4, flexWrap: 'wrap', gap: 2 }}>
|
||||
@@ -48,7 +83,7 @@ const Dashboard: React.FC = () => {
|
||||
<Avatar src={data.user.avatar || undefined} sx={{ width: 48, height: 48 }}>
|
||||
{!data.user.avatar && data.user.username.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Typography variant="h4">Personal Dashboard</Typography>
|
||||
<Typography variant="h4">Dashboard</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TextField
|
||||
@@ -58,78 +93,96 @@ const Dashboard: React.FC = () => {
|
||||
sx={{ width: { xs: '100%', sm: 300 } }}
|
||||
/>
|
||||
<Button variant="contained" startIcon={<ContentCopyIcon />} onClick={copyToClipboard}>
|
||||
Share Profile
|
||||
Share
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||
<Box sx={{ flex: { xs: '1 1 100%', md: '1 1 45%' }, minWidth: 300 }}>
|
||||
<Typography variant="h5" color="secondary" gutterBottom>My Ratings ({data.rated_count})</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{data.my_ratings.map((rating: any) => (
|
||||
<Card key={rating.id}>
|
||||
<CardContent sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||
{rating.flavor_image && (
|
||||
<Box
|
||||
component="img"
|
||||
src={rating.flavor_image}
|
||||
sx={{ width: 60, height: 60, objectFit: 'contain', bgcolor: 'rgba(255,255,255,0.05)', p: 0.5, borderRadius: 1 }}
|
||||
/>
|
||||
)}
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="h6" sx={{ fontSize: '1rem' }}>{rating.flavor_name}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', my: 0.5 }}>
|
||||
<MuiRating value={rating.score} readOnly max={10} size="small" />
|
||||
<Typography variant="body2" sx={{ ml: 1, fontSize: '0.8rem' }}>{rating.score}/10</Typography>
|
||||
</Box>
|
||||
{rating.comment && (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.85rem' }}>
|
||||
"{rating.comment}"
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ flex: { xs: '1 1 100%', md: '1 1 45%' }, minWidth: 300 }}>
|
||||
<Typography variant="h5" color="primary" gutterBottom>Missing Flavors ({data.missing_count})</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{data.missing_flavors.map((flavor: any) => (
|
||||
<Card key={flavor.id}>
|
||||
<CardContent sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||
{flavor.image_url && (
|
||||
<Box
|
||||
component="img"
|
||||
src={flavor.image_url}
|
||||
sx={{ width: 60, height: 60, objectFit: 'contain', bgcolor: 'rgba(255,255,255,0.05)', p: 0.5, borderRadius: 1 }}
|
||||
/>
|
||||
)}
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="h6" sx={{ fontSize: '1rem' }}>{flavor.name}</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>{flavor.category_name}</Typography>
|
||||
<Typography variant="body2" sx={{ fontSize: '0.85rem', mb: 1 }}>{flavor.description.substring(0, 100)}...</Typography>
|
||||
{flavor.shop_url && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
component="a"
|
||||
href={flavor.shop_url}
|
||||
target="_blank"
|
||||
sx={{ fontSize: '0.65rem', py: 0 }}
|
||||
>
|
||||
Buy Now
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||
<Tabs value={activeTab} onChange={(_, v) => setActiveTab(v)}>
|
||||
<Tab label={`My Ratings (${data.rated_count})`} />
|
||||
<Tab label={`Missing (${data.missing_count})`} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{activeTab === 0 ? (
|
||||
<Box>
|
||||
{Object.keys(ratedGrouped).length === 0 ? (
|
||||
<Typography color="text.secondary">You haven't rated any flavors yet.</Typography>
|
||||
) : (
|
||||
Object.entries(ratedGrouped).map(([category, ratings]: [string, any]) => (
|
||||
<Box key={category} sx={{ mb: 4 }}>
|
||||
<Typography variant="h6" color="secondary" gutterBottom sx={{ fontWeight: 'bold' }}>{category}</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
|
||||
{ratings.map((rating: any) => (
|
||||
<Card key={rating.id} sx={{ flex: { xs: '1 1 100%', sm: '1 1 45%', md: '1 1 30%', lg: '1 1 23%' }, minWidth: 280 }}>
|
||||
<CardContent sx={{ display: 'flex', gap: 2 }}>
|
||||
{rating.flavor_image && (
|
||||
<Box component="img" src={rating.flavor_image} sx={{ width: 50, height: 50, objectFit: 'contain' }} />
|
||||
)}
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Link to={`/flavor/${rating.flavor}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', '&:hover': { color: 'primary.main' } }}>
|
||||
{rating.flavor_name}
|
||||
</Typography>
|
||||
</Link>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', my: 0.5 }}>
|
||||
<MuiRating value={rating.score} readOnly max={10} size="small" />
|
||||
<Typography variant="caption" sx={{ ml: 1 }}>{rating.score}/10</Typography>
|
||||
</Box>
|
||||
{rating.comment && (
|
||||
<Typography variant="caption" color="text.secondary" noWrap sx={{ display: 'block', maxWidth: 200 }}>
|
||||
"{rating.comment}"
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<Box>
|
||||
{Object.keys(missingGrouped).length === 0 ? (
|
||||
<Typography color="text.secondary">Amazing! You've rated everything!</Typography>
|
||||
) : (
|
||||
Object.entries(missingGrouped).map(([category, flavors]: [string, any]) => (
|
||||
<Box key={category} sx={{ mb: 4 }}>
|
||||
<Typography variant="h6" color="primary" gutterBottom sx={{ fontWeight: 'bold' }}>{category}</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
|
||||
{flavors.map((flavor: any) => (
|
||||
<Card key={flavor.id} sx={{ flex: { xs: '1 1 100%', sm: '1 1 45%', md: '1 1 30%', lg: '1 1 23%' }, minWidth: 280 }}>
|
||||
<CardContent sx={{ display: 'flex', gap: 2 }}>
|
||||
{flavor.image_url && (
|
||||
<Box component="img" src={flavor.image_url} sx={{ width: 50, height: 50, objectFit: 'contain' }} />
|
||||
)}
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Link to={`/flavor/${flavor.id}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', '&:hover': { color: 'primary.main' } }}>
|
||||
{flavor.name}
|
||||
</Typography>
|
||||
</Link>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1 }}>
|
||||
{flavor.description.substring(0, 60)}...
|
||||
</Typography>
|
||||
{flavor.shop_url && (
|
||||
<Button size="small" variant="text" component="a" href={flavor.shop_url} target="_blank" sx={{ fontSize: '0.7rem', p: 0 }}>
|
||||
Buy Now
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,12 +7,24 @@ import {
|
||||
Paper,
|
||||
Alert,
|
||||
Container,
|
||||
Avatar
|
||||
Avatar,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
ListSubheader,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import api from '../api';
|
||||
import { useTitle } from '../hooks/useTitle';
|
||||
import { CatppuccinTheme } from '../theme';
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
interface SettingsProps {
|
||||
themeName: CatppuccinTheme;
|
||||
onThemeChange: (newTheme: CatppuccinTheme) => void;
|
||||
}
|
||||
|
||||
const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
|
||||
useTitle('Account Settings');
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
@@ -113,6 +125,38 @@ const Settings: React.FC = () => {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Paper sx={{ p: 3, mb: 4 }}>
|
||||
<Typography variant="h6" gutterBottom>Appearance</Typography>
|
||||
<FormControl fullWidth margin="normal">
|
||||
<InputLabel>Theme</InputLabel>
|
||||
<Select
|
||||
value={themeName}
|
||||
label="Theme"
|
||||
onChange={(e) => onThemeChange(e.target.value as CatppuccinTheme)}
|
||||
>
|
||||
<ListSubheader>Light Themes</ListSubheader>
|
||||
<MenuItem value="latte">Latte</MenuItem>
|
||||
<MenuItem value="pink">Pastel Pink</MenuItem>
|
||||
<MenuItem value="solarized_light">Solarized Light</MenuItem>
|
||||
<MenuItem value="one_light">One Light</MenuItem>
|
||||
<MenuItem value="paper">Paper White</MenuItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListSubheader>Dark Themes</ListSubheader>
|
||||
<MenuItem value="mocha">Mocha (Default)</MenuItem>
|
||||
<MenuItem value="frappe">Frappé</MenuItem>
|
||||
<MenuItem value="macchiato">Macchiato</MenuItem>
|
||||
<MenuItem value="atom">Atom One Dark</MenuItem>
|
||||
<MenuItem value="dracula">Dracula</MenuItem>
|
||||
<MenuItem value="gruvbox">Gruvbox Dark</MenuItem>
|
||||
<MenuItem value="nord">Nord</MenuItem>
|
||||
<MenuItem value="cyberpunk">Cyberpunk</MenuItem>
|
||||
<MenuItem value="forest">Forest Dark</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{ p: 3, mb: 4 }}>
|
||||
<Typography variant="h6" gutterBottom>Profile Picture</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 2 }}>
|
||||
|
||||
Reference in new issue
Block a user