diff --git a/backend/api/views.py b/backend/api/views.py
index 23e1809..fe7b865 100644
--- a/backend/api/views.py
+++ b/backend/api/views.py
@@ -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'])
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 2e9dada..dfb55ae 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -248,60 +248,69 @@ const App: React.FC = () => {
-
- {!isMobile && !loadingUser && (
-
- {navItems.map((item) => (
-
- ))}
- {user && (
-
- )}
-
- )}
-
- setAnchorEl(e.currentTarget)} sx={{ ml: 1 }}>
-
- {user?.avatar ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
-
-
-
-
+
+
+ {!isMobile && !loadingUser && (
+
+
+
+
+
+
+
+ )}
+
+
+
+ {!loadingUser && (
+
+ user ? (
+
+ <>
+
+ setAnchorEl(e.currentTarget)} sx={{ ml: 1 }}>
+
+
+
+ {!user.avatar && user.username.charAt(0).toUpperCase()}
+
+
+
+
+
+
+
+ >
+
+ ) : (
+
+
+
+ )
+
+ )}
+
+
+
+
@@ -312,9 +321,10 @@ const App: React.FC = () => {
} />
} />
} />
- } />
- } />
- } />
+ } />
+ } />
+ } />
+
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
index 1c96d2e..6fa2160 100644
--- a/frontend/src/pages/Dashboard.tsx
+++ b/frontend/src/pages/Dashboard.tsx
@@ -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(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 ;
if (!data) return Please login to view dashboard.;
+ // 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 (
@@ -48,7 +83,7 @@ const Dashboard: React.FC = () => {
{!data.user.avatar && data.user.username.charAt(0).toUpperCase()}
- Personal Dashboard
+ Dashboard
{
sx={{ width: { xs: '100%', sm: 300 } }}
/>
} onClick={copyToClipboard}>
- Share Profile
+ Share
-
-
- My Ratings ({data.rated_count})
-
- {data.my_ratings.map((rating: any) => (
-
-
- {rating.flavor_image && (
-
- )}
-
- {rating.flavor_name}
-
-
- {rating.score}/10
-
- {rating.comment && (
-
- "{rating.comment}"
-
- )}
-
-
-
- ))}
-
-
-
- Missing Flavors ({data.missing_count})
-
- {data.missing_flavors.map((flavor: any) => (
-
-
- {flavor.image_url && (
-
- )}
-
- {flavor.name}
- {flavor.category_name}
- {flavor.description.substring(0, 100)}...
- {flavor.shop_url && (
-
- )}
-
-
-
- ))}
-
-
+
+ setActiveTab(v)}>
+
+
+
+
+ {activeTab === 0 ? (
+
+ {Object.keys(ratedGrouped).length === 0 ? (
+ You haven't rated any flavors yet.
+ ) : (
+ Object.entries(ratedGrouped).map(([category, ratings]: [string, any]) => (
+
+ {category}
+
+ {ratings.map((rating: any) => (
+
+
+ {rating.flavor_image && (
+
+ )}
+
+
+
+ {rating.flavor_name}
+
+
+
+
+ {rating.score}/10
+
+ {rating.comment && (
+
+ "{rating.comment}"
+
+ )}
+
+
+
+ ))}
+
+
+ ))
+ )}
+
+ ) : (
+
+ {Object.keys(missingGrouped).length === 0 ? (
+ Amazing! You've rated everything!
+ ) : (
+ Object.entries(missingGrouped).map(([category, flavors]: [string, any]) => (
+
+ {category}
+
+ {flavors.map((flavor: any) => (
+
+
+ {flavor.image_url && (
+
+ )}
+
+
+
+ {flavor.name}
+
+
+
+ {flavor.description.substring(0, 60)}...
+
+ {flavor.shop_url && (
+
+ )}
+
+
+
+ ))}
+
+
+ ))
+ )}
+
+ )}
);
};
diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx
index fae5553..fa980b6 100644
--- a/frontend/src/pages/Settings.tsx
+++ b/frontend/src/pages/Settings.tsx
@@ -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 = ({ themeName, onThemeChange }) => {
useTitle('Account Settings');
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
@@ -113,6 +125,38 @@ const Settings: React.FC = () => {
)}
+
+ Appearance
+
+ Theme
+
+
+
+
Profile Picture