mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
added public profiles
This commit is contained in:
1 parent
f1f974bd2a
commit
3a9eec3f22
4 files changed
+171
-2
No files matched your search
@@ -27,6 +27,7 @@ import CategoryList from './pages/CategoryList';
|
||||
import CategoryFlavors from './pages/categories/CategoryFlavors';
|
||||
import FlavorDetail from './pages/flavors/FlavorDetail';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import PublicProfile from './pages/PublicProfile';
|
||||
import Login from './pages/Login';
|
||||
import Settings from './pages/Settings';
|
||||
import api from './api';
|
||||
@@ -185,6 +186,7 @@ const App: React.FC = () => {
|
||||
<Route path="/" element={<CategoryList />} />
|
||||
<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 />} />
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Typography, Card, CardContent, Box, Rating as MuiRating, Button } from '@mui/material';
|
||||
import { Typography, Card, CardContent, Box, Rating as MuiRating, Button, TextField } from '@mui/material';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import api from '../api';
|
||||
import { useTitle } from '../hooks/useTitle';
|
||||
|
||||
interface DashboardData {
|
||||
user: { username: string };
|
||||
rated_count: number;
|
||||
missing_count: number;
|
||||
missing_flavors: any[];
|
||||
@@ -15,6 +17,13 @@ const Dashboard: React.FC = () => {
|
||||
const [data, setData] = useState<DashboardData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const shareUrl = data ? `${window.location.origin}/profile/${data.user.username}` : '';
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
alert('Link copied to clipboard!');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
@@ -34,7 +43,21 @@ const Dashboard: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>Personal Dashboard</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4, flexWrap: 'wrap', gap: 2 }}>
|
||||
<Typography variant="h4">Personal Dashboard</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
value={shareUrl}
|
||||
slotProps={{ input: { readOnly: true } }}
|
||||
sx={{ width: { xs: '100%', sm: 300 } }}
|
||||
/>
|
||||
<Button variant="contained" startIcon={<ContentCopyIcon />} onClick={copyToClipboard}>
|
||||
Share Profile
|
||||
</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>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Rating as MuiRating,
|
||||
Avatar,
|
||||
Chip
|
||||
} from '@mui/material';
|
||||
import api from '../api';
|
||||
import { useTitle } from '../hooks/useTitle';
|
||||
|
||||
interface Rating {
|
||||
id: number;
|
||||
flavor_name: string;
|
||||
flavor_image: string | null;
|
||||
score: number;
|
||||
comment: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface ProfileData {
|
||||
username: string;
|
||||
theme: string;
|
||||
ratings: Rating[];
|
||||
}
|
||||
|
||||
const PublicProfile: React.FC = () => {
|
||||
const { username } = useParams<{ username: string }>();
|
||||
const [data, setData] = useState<ProfileData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProfile = async () => {
|
||||
try {
|
||||
const res = await api.get(`users/profile/${username}/`);
|
||||
setData(res.data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchProfile();
|
||||
}, [username]);
|
||||
|
||||
useTitle(data ? `${data.username}'s Ratings` : 'Profile');
|
||||
|
||||
if (loading) return <Typography>Loading Profile...</Typography>;
|
||||
if (!data) return <Typography>Profile not found.</Typography>;
|
||||
|
||||
const favorites = data.ratings.filter(r => r.score >= 8);
|
||||
const others = data.ratings.filter(r => r.score < 8);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 6, gap: 3 }}>
|
||||
<Avatar sx={{ width: 80, height: 80, fontSize: '2rem', bgcolor: 'primary.main' }}>
|
||||
{data.username.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h3" sx={{ fontWeight: 'bold' }}>{data.username}</Typography>
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Shared {data.ratings.length} flavor ratings
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{favorites.length > 0 && (
|
||||
<Box sx={{ mb: 6 }}>
|
||||
<Typography variant="h4" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
Top Favorites <Chip label="8-10 Score" color="secondary" />
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
|
||||
{favorites.map(rating => (
|
||||
<Card key={rating.id} sx={{ flex: { xs: '1 1 100%', sm: '1 1 45%', md: '1 1 30%' }, minWidth: 280 }}>
|
||||
<CardContent sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{rating.flavor_image && (
|
||||
<Box component="img" src={rating.flavor_image} sx={{ width: 80, height: 80, objectFit: 'contain' }} />
|
||||
)}
|
||||
<Box>
|
||||
<Typography variant="h6">{rating.flavor_name}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<MuiRating value={rating.score} readOnly max={10} size="small" />
|
||||
<Typography sx={{ ml: 1, fontWeight: 'bold' }}>{rating.score}/10</Typography>
|
||||
</Box>
|
||||
{rating.comment && (
|
||||
<Typography variant="body2" color="text.secondary">"{rating.comment}"</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{others.length > 0 && (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>Other Ratings</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
|
||||
{others.map(rating => (
|
||||
<Card key={rating.id} sx={{ flex: { xs: '1 1 100%', sm: '1 1 45%', md: '1 1 23%' }, minWidth: 200 }} variant="outlined">
|
||||
<CardContent>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>{rating.flavor_name}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', my: 1 }}>
|
||||
<MuiRating value={rating.score} readOnly max={10} size="small" />
|
||||
<Typography variant="body2" sx={{ ml: 1 }}>{rating.score}/10</Typography>
|
||||
</Box>
|
||||
{rating.comment && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontStyle: 'italic' }}>
|
||||
"{rating.comment}"
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PublicProfile;
|
||||
Reference in new issue
Block a user