updated ui for user specific content

This commit is contained in:
nvrl committed 2026-02-16 15:26:22 +01:00
1 parent 860858e384
commit 3e8e71fc02
4 files changed
+236 -128

No files matched your search

+2 -1
View File
@@ -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
View File
@@ -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>
+121 -68
View File
@@ -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>
);
};
+46 -2
View File
@@ -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 }}>