mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-04 17:07:01 +01:00
605 lines
31 KiB
TypeScript
605 lines
31 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
|
import {
|
|
Typography,
|
|
Box,
|
|
Card,
|
|
CardContent,
|
|
Avatar,
|
|
Container,
|
|
CircularProgress,
|
|
Tabs,
|
|
Tab,
|
|
Paper,
|
|
Divider,
|
|
Button,
|
|
Stack,
|
|
alpha,
|
|
useTheme,
|
|
Grid,
|
|
TextField,
|
|
IconButton,
|
|
Chip,
|
|
Tooltip
|
|
} from '@mui/material';
|
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
|
import SendIcon from '@mui/icons-material/Send';
|
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
import VerifiedIcon from '@mui/icons-material/Verified';
|
|
import PersonAddIcon from '@mui/icons-material/PersonAdd';
|
|
import PersonRemoveIcon from '@mui/icons-material/PersonRemove';
|
|
import ColorThief from 'colorthief';
|
|
import api from '../api';
|
|
import { useTitle } from '../hooks/useTitle';
|
|
import { useTranslation } from 'react-i18next';
|
|
import RatingBadge from '../components/RatingBadge';
|
|
import RichText from '../components/RichText';
|
|
import { formatDate } from '../utils/date';
|
|
|
|
interface Rating {
|
|
id: number;
|
|
flavor: number;
|
|
flavor_name: string;
|
|
flavor_image: string | null;
|
|
category_name: string;
|
|
score: number;
|
|
comment: string;
|
|
created_at: string;
|
|
}
|
|
|
|
interface ProfileComment {
|
|
id: number;
|
|
author_username: string;
|
|
author_avatar: string | null;
|
|
text: string;
|
|
created_at: string;
|
|
}
|
|
|
|
interface MiniUser {
|
|
id: number;
|
|
username: string;
|
|
avatar: string | null;
|
|
is_following?: boolean;
|
|
}
|
|
|
|
interface ProfileData {
|
|
id: number;
|
|
username: string;
|
|
theme: string;
|
|
avatar: string | null;
|
|
following_count: number;
|
|
followers_count: number;
|
|
is_following: boolean;
|
|
ratings: Rating[];
|
|
comments: ProfileComment[];
|
|
followers: MiniUser[];
|
|
following: MiniUser[];
|
|
}
|
|
|
|
interface PublicProfileProps {
|
|
adminMode?: boolean;
|
|
}
|
|
|
|
const PublicProfile: React.FC<PublicProfileProps> = ({ adminMode }) => {
|
|
const { t } = useTranslation();
|
|
const theme = useTheme();
|
|
const { username } = useParams<{ username: string }>();
|
|
const navigate = useNavigate();
|
|
const [data, setData] = useState<ProfileData | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [activeTab, setActiveTab] = useState(0);
|
|
const [categoryTab, setCategoryTab] = useState(0);
|
|
const [currentUser, setCurrentUser] = useState<{username: string, is_superuser: boolean} | null>(null);
|
|
const [palette, setPalette] = useState<string[]>([]);
|
|
|
|
const [newComment, setNewComment] = useState('');
|
|
|
|
const fetchProfile = async () => {
|
|
try {
|
|
const [profileRes, meRes] = await Promise.all([
|
|
api.get(`users/profile/${username}/`),
|
|
localStorage.getItem('token') ? api.get('users/me/') : Promise.resolve({ data: null })
|
|
]);
|
|
setData(profileRes.data);
|
|
if (meRes.data) setCurrentUser(meRes.data);
|
|
} catch (err) {
|
|
console.error(err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchProfile();
|
|
}, [username]);
|
|
|
|
useEffect(() => {
|
|
if (data?.avatar) {
|
|
const img = new Image();
|
|
img.crossOrigin = 'Anonymous';
|
|
img.src = data.avatar;
|
|
img.onload = () => {
|
|
const colorThief = new ColorThief();
|
|
try {
|
|
const colors = colorThief.getPalette(img, 3);
|
|
setPalette(colors.map(c => `rgb(${c[0]}, ${c[1]}, ${c[2]})`));
|
|
} catch (e) {
|
|
setPalette([]);
|
|
}
|
|
};
|
|
} else {
|
|
setPalette([]);
|
|
}
|
|
}, [data?.avatar]);
|
|
|
|
const handleFollowToggle = async () => {
|
|
if (!data) return;
|
|
try {
|
|
if (data.is_following) {
|
|
await api.post(`users/${data.id}/unfollow/`);
|
|
setData({ ...data, is_following: false, followers_count: data.followers_count - 1 });
|
|
} else {
|
|
await api.post(`users/${data.id}/follow/`);
|
|
setData({ ...data, is_following: true, followers_count: data.followers_count + 1 });
|
|
}
|
|
} catch (err) {
|
|
alert('Failed to update follow status. Please login.');
|
|
}
|
|
};
|
|
|
|
const handleMiniFollowToggle = async (user: MiniUser) => {
|
|
try {
|
|
if (user.is_following) {
|
|
await api.post(`users/${user.id}/unfollow/`);
|
|
} else {
|
|
await api.post(`users/${user.id}/follow/`);
|
|
}
|
|
fetchProfile();
|
|
} catch (err) { alert('Failed'); }
|
|
};
|
|
|
|
const handleAddComment = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!newComment.trim() || !data) return;
|
|
try {
|
|
await api.post(`users/${data.id}/add_comment/`, { text: newComment });
|
|
setNewComment('');
|
|
fetchProfile();
|
|
} catch (err) {
|
|
alert('Failed to add comment');
|
|
}
|
|
};
|
|
|
|
const handleDeleteComment = async (commentId: number) => {
|
|
if (!data || !confirm('Delete this comment?')) return;
|
|
try {
|
|
await api.delete(`users/${data.id}/delete_comment/${commentId}/`);
|
|
fetchProfile();
|
|
} catch (err) {
|
|
alert('Failed to delete comment');
|
|
}
|
|
};
|
|
|
|
useTitle(data ? `${data.username}'s Ratings` : 'Profile');
|
|
|
|
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}><CircularProgress /></Box>;
|
|
if (!data) return <Typography>Profile not found.</Typography>;
|
|
|
|
const categoryGroups = data.ratings.reduce((acc: any, rating) => {
|
|
const cat = rating.category_name || 'Other';
|
|
if (!acc[cat]) acc[cat] = [];
|
|
acc[cat].push(rating);
|
|
return acc;
|
|
}, {});
|
|
|
|
const ratingCategories = ['All', ...Object.keys(categoryGroups)];
|
|
const currentRatings = categoryTab === 0 ? data.ratings : categoryGroups[ratingCategories[categoryTab]];
|
|
|
|
const tiers = [
|
|
{ key: 'S', title: 'S-Tier', min: 9, color: '#ff7f7f' },
|
|
{ key: 'A', title: 'A-Tier', min: 7, color: '#ffbf7f' },
|
|
{ key: 'B', title: 'B-Tier', min: 5, color: '#ffff7f' },
|
|
{ key: 'C', title: 'C-Tier', min: 3, color: '#bfff7f' },
|
|
{ key: 'D', title: 'D-Tier', min: 0, color: '#7fff7f' },
|
|
];
|
|
|
|
const bannerGradient = palette.length >= 2
|
|
? `linear-gradient(135deg, ${palette[0]} 0%, ${palette[1]} 100%)`
|
|
: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.secondary.main} 100%)`;
|
|
|
|
const handleGoBack = () => {
|
|
if (window.history.length > 1) navigate(-1);
|
|
else navigate('/');
|
|
};
|
|
|
|
return (
|
|
<Container maxWidth="lg" sx={{ py: 4 }}>
|
|
<Button
|
|
variant="outlined"
|
|
onClick={handleGoBack}
|
|
startIcon={<ArrowBackIcon />}
|
|
sx={{ mb: 4, borderRadius: 2, textTransform: 'none', fontWeight: 'bold', color: 'text.secondary', borderColor: 'divider' }}
|
|
>
|
|
{t('common.back')}
|
|
</Button>
|
|
|
|
{/* Uplifted Profile Header */}
|
|
<Card elevation={0} sx={{
|
|
borderRadius: 5,
|
|
border: '1px solid',
|
|
borderColor: 'divider',
|
|
bgcolor: (theme) => alpha(theme.palette.background.paper, 0.6),
|
|
backdropFilter: 'blur(20px)',
|
|
mb: 4,
|
|
overflow: 'hidden',
|
|
position: 'relative'
|
|
}}>
|
|
<Box sx={{
|
|
height: { xs: 120, sm: 180 },
|
|
background: bannerGradient,
|
|
opacity: 0.8,
|
|
position: 'relative',
|
|
'&::after': {
|
|
content: '""',
|
|
position: 'absolute',
|
|
top: 0, left: 0, right: 0, bottom: 0,
|
|
backgroundImage: 'radial-gradient(circle at 2px 2px, rgba(255,255,255,0.1) 1px, transparent 0)',
|
|
backgroundSize: '32px 32px'
|
|
}
|
|
}} />
|
|
|
|
<CardContent sx={{ pt: 0, px: { xs: 2, md: 5 }, pb: 4 }}>
|
|
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, alignItems: { xs: 'center', sm: 'flex-end' }, gap: { xs: 2, sm: 4 }, mt: { xs: -7, sm: -10 } }}>
|
|
<Box sx={{ position: 'relative' }}>
|
|
<Avatar
|
|
src={data.avatar || undefined}
|
|
sx={{
|
|
width: { xs: 140, sm: 180 },
|
|
height: { xs: 140, sm: 180 },
|
|
border: '6px solid',
|
|
borderColor: (theme) => theme.palette.background.paper,
|
|
boxShadow: '0 12px 40px rgba(0,0,0,0.2)',
|
|
fontSize: '4.5rem',
|
|
bgcolor: palette[0] || 'primary.main',
|
|
}}
|
|
>
|
|
{!data.avatar && data.username.charAt(0).toUpperCase()}
|
|
</Avatar>
|
|
{data.ratings.length > 50 && (
|
|
<Tooltip title="Master Collector">
|
|
<VerifiedIcon color="primary" sx={{ position: 'absolute', bottom: 12, right: 12, bgcolor: 'background.paper', borderRadius: '50%', fontSize: '2.2rem', p: 0.2 }} />
|
|
</Tooltip>
|
|
)}
|
|
</Box>
|
|
|
|
<Box sx={{ flex: 1, textAlign: { xs: 'center', sm: 'left' }, mt: { xs: 2, sm: 6 } }}>
|
|
<Typography
|
|
variant="h2"
|
|
sx={{
|
|
fontWeight: '900',
|
|
letterSpacing: -2,
|
|
fontSize: { xs: '2.8rem', sm: '4.2rem' },
|
|
lineHeight: 0.9,
|
|
color: 'text.primary',
|
|
position: 'relative',
|
|
display: 'inline-block',
|
|
mb: 2.5
|
|
}}
|
|
>
|
|
{data.username}
|
|
</Typography>
|
|
|
|
<Box>
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
display: 'inline-flex',
|
|
bgcolor: (theme) => alpha(theme.palette.text.primary, 0.04),
|
|
border: '1px solid',
|
|
borderColor: 'divider',
|
|
borderRadius: 1,
|
|
overflow: 'hidden',
|
|
backdropFilter: 'blur(8px)'
|
|
}}
|
|
>
|
|
{[
|
|
{ label: 'Rated', val: data.ratings.length },
|
|
{ label: 'Followers', val: data.followers_count },
|
|
{ label: 'Following', val: data.following_count }
|
|
].map((stat, i) => (
|
|
<React.Fragment key={stat.label}>
|
|
<Box sx={{ py: 1.2, width: { xs: 90, sm: 120 }, textAlign: 'center' }}>
|
|
<Typography variant="h6" sx={{ fontWeight: '900', lineHeight: 1, color: 'text.primary' }}>{stat.val}</Typography>
|
|
<Typography variant="caption" sx={{ fontWeight: '900', textTransform: 'uppercase', opacity: 0.5, fontSize: '0.6rem', letterSpacing: 1 }}>{stat.label}</Typography>
|
|
</Box>
|
|
{i < 2 && <Divider orientation="vertical" flexItem sx={{ borderStyle: 'solid', opacity: 0.1 }} />}
|
|
</React.Fragment>
|
|
))}
|
|
</Paper>
|
|
</Box>
|
|
</Box>
|
|
|
|
<Stack direction="row" spacing={1.5} sx={{ width: { xs: '100%', sm: 'auto' }, justifyContent: 'center', mb: 1 }}>
|
|
{currentUser?.username !== data.username && (
|
|
<Button
|
|
variant={data.is_following ? "outlined" : "contained"}
|
|
onClick={handleFollowToggle}
|
|
startIcon={data.is_following ? <PersonRemoveIcon /> : <PersonAddIcon />}
|
|
sx={{ borderRadius: 3, px: 4, fontWeight: '900', py: 1.2, textTransform: 'none', fontSize: '1rem' }}
|
|
>
|
|
{data.is_following ? "Unfollow" : "Follow"}
|
|
</Button>
|
|
)}
|
|
{currentUser?.is_superuser && (
|
|
<Button
|
|
variant="outlined"
|
|
color="secondary"
|
|
onClick={() => navigate(`/admin-panel/user/${data.id}`)}
|
|
sx={{ borderRadius: 3, px: 2.5, fontWeight: 'bold', textTransform: 'none' }}
|
|
>
|
|
{t('admin.manageUser')}
|
|
</Button>
|
|
)}
|
|
</Stack>
|
|
</Box>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Box sx={{ mb: 4, position: 'sticky', top: 80, zIndex: 10, bgcolor: alpha(theme.palette.background.default, 0.9), backdropFilter: 'blur(12px)', py: 1, borderRadius: 1, border: '1px solid', borderColor: alpha(theme.palette.divider, 0.1) }}>
|
|
<Tabs
|
|
value={activeTab}
|
|
onChange={(_, v) => setActiveTab(v)}
|
|
variant="fullWidth"
|
|
sx={{
|
|
'& .MuiTab-root': { fontWeight: '900', textTransform: 'none', fontSize: '1rem', borderRadius: 1 },
|
|
'& .Mui-selected': { color: 'primary.main' }
|
|
}}
|
|
>
|
|
<Tab label="Ratings" />
|
|
<Tab label="Guestbook" />
|
|
<Tab label="Followers" />
|
|
<Tab label="Following" />
|
|
</Tabs>
|
|
</Box>
|
|
|
|
<Box sx={{ minHeight: 400 }}>
|
|
{activeTab === 0 && (
|
|
<Box>
|
|
<Box sx={{ mb: 5, display: 'flex', overflowX: 'auto', pb: 1, gap: 1.5, '&::-webkit-scrollbar': { display: 'none' } }}>
|
|
{ratingCategories.map((cat, idx) => (
|
|
<Chip
|
|
key={cat} label={cat}
|
|
onClick={() => setCategoryTab(idx)}
|
|
color={categoryTab === idx ? 'primary' : 'default'}
|
|
variant={categoryTab === idx ? 'filled' : 'outlined'}
|
|
sx={{ fontWeight: '900', px: 1.5, borderRadius: 2 }}
|
|
/>
|
|
))}
|
|
</Box>
|
|
|
|
<Stack spacing={10}>
|
|
{tiers.map(tier => {
|
|
const items = currentRatings.filter((r: Rating) => {
|
|
const s = r.score;
|
|
if (tier.key === 'S') return s >= 9;
|
|
if (tier.key === 'A') return s >= 7 && s < 9;
|
|
if (tier.key === 'B') return s >= 5 && s < 7;
|
|
if (tier.key === 'C') return s >= 3 && s < 5;
|
|
return s < 3;
|
|
});
|
|
|
|
if (items.length === 0) return null;
|
|
|
|
return (
|
|
<Box key={tier.key}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 4, gap: 3 }}>
|
|
<Paper sx={{
|
|
width: 64, height: 64, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
bgcolor: tier.color, color: 'black', fontWeight: '900', fontSize: '2rem',
|
|
borderRadius: 0.5, boxShadow: `0 0 20px ${alpha(tier.color, 0.6)}`
|
|
}}>
|
|
{tier.key}
|
|
</Paper>
|
|
<Box>
|
|
<Typography variant="h4" sx={{ fontWeight: '900', lineHeight: 1.1, letterSpacing: -0.5 }}>{tier.title}</Typography>
|
|
<Typography variant="subtitle2" color="text.secondary" sx={{ fontWeight: 'bold', opacity: 0.7 }}>{items.length} items</Typography>
|
|
</Box>
|
|
<Divider sx={{ flexGrow: 1, borderStyle: 'dashed', opacity: 0.3 }} />
|
|
</Box>
|
|
|
|
<Grid container spacing={3}>
|
|
{items.map((rating: Rating) => (
|
|
<Grid key={rating.id} size={{ xs: 6, sm: 4, md: 3, lg: 2.4 }}>
|
|
<Card sx={{
|
|
height: '100%', borderRadius: 1, border: '1px solid', borderColor: 'divider',
|
|
bgcolor: (theme) => alpha(theme.palette.background.paper, 0.4),
|
|
transition: 'all 0.3s ease',
|
|
overflow: 'hidden',
|
|
'&:hover': { transform: 'translateY(-8px)', borderColor: 'primary.main', boxShadow: '0 12px 32px rgba(0,0,0,0.1)' }
|
|
}}>
|
|
<Link to={`/flavor/${rating.flavor}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
|
<Box sx={{
|
|
position: 'relative',
|
|
aspectRatio: '1/1',
|
|
bgcolor: 'background.default',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderBottom: '1px solid',
|
|
borderColor: 'divider',
|
|
overflow: 'hidden'
|
|
}}>
|
|
<Box
|
|
component="img"
|
|
src={rating.flavor_image || undefined}
|
|
sx={{
|
|
height: '100%',
|
|
width: '100%',
|
|
objectFit: 'cover',
|
|
transition: 'transform 0.5s ease',
|
|
'&:hover': { transform: 'scale(1.1)' }
|
|
}}
|
|
/>
|
|
<Box sx={{ position: 'absolute', top: 10, right: 10, zIndex: 2 }}>
|
|
<RatingBadge score={rating.score} size="small" />
|
|
</Box>
|
|
</Box>
|
|
<CardContent sx={{ p: 1.5, textAlign: 'center' }}>
|
|
<Typography variant="subtitle2" sx={{ fontWeight: '900', fontSize: '0.85rem', lineHeight: 1.2, display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
|
|
{rating.flavor_name}
|
|
</Typography>
|
|
</CardContent>
|
|
</Link>
|
|
{adminMode && (
|
|
<Box sx={{ px: 1, pb: 1 }}>
|
|
<Button
|
|
size="small" fullWidth variant="outlined" color="secondary"
|
|
onClick={(e) => { e.preventDefault(); e.stopPropagation(); navigate(`/admin-panel/rating/${rating.id}`); }}
|
|
sx={{ borderRadius: 1, textTransform: 'none', fontSize: '0.6rem', py: 0, fontWeight: 'bold' }}
|
|
>
|
|
Manage
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
</Card>
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
</Box>
|
|
);
|
|
})}
|
|
</Stack>
|
|
</Box>
|
|
)}
|
|
|
|
{activeTab === 1 && (
|
|
<Box maxWidth="md" sx={{ mx: 'auto' }}>
|
|
{currentUser && (
|
|
<Paper sx={{ p: 4, mb: 5, borderRadius: 4, border: '1px solid', borderColor: 'primary.main', bgcolor: alpha(theme.palette.primary.main, 0.02) }}>
|
|
<Typography variant="h5" gutterBottom sx={{ fontWeight: '900', mb: 2 }}>Leave a message</Typography>
|
|
<form onSubmit={handleAddComment}>
|
|
<TextField
|
|
fullWidth multiline rows={3} placeholder="Say something about their taste..."
|
|
value={newComment} onChange={(e) => setNewComment(e.target.value)}
|
|
sx={{ mb: 3, '& .MuiOutlinedInput-root': { borderRadius: 3, bgcolor: 'background.paper' } }}
|
|
/>
|
|
<Button variant="contained" type="submit" endIcon={<SendIcon />} sx={{ borderRadius: 2, px: 5, py: 1.2, fontWeight: '900', fontSize: '1rem', textTransform: 'none' }}>
|
|
Send Message
|
|
</Button>
|
|
</form>
|
|
</Paper>
|
|
)}
|
|
|
|
<Stack spacing={2.5}>
|
|
{data.comments.length === 0 ? (
|
|
<Box sx={{ py: 12, textAlign: 'center', bgcolor: alpha(theme.palette.action.hover, 0.5), borderRadius: 4, border: '2px dashed', borderColor: 'divider' }}>
|
|
<Typography color="text.secondary" variant="h6" sx={{ fontWeight: 'bold' }}>No entries in the guestbook yet.</Typography>
|
|
<Typography variant="body2" color="text.secondary">Be the first to leave a message!</Typography>
|
|
</Box>
|
|
) : (
|
|
data.comments.map(comment => (
|
|
<Card key={comment.id} elevation={0} sx={{ borderRadius: 3, border: '1px solid', borderColor: 'divider', transition: 'all 0.2s', '&:hover': { borderColor: 'primary.main', bgcolor: alpha(theme.palette.background.paper, 0.8) } }}>
|
|
<CardContent sx={{ p: 3 }}>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
<Stack direction="row" spacing={2.5} alignItems="center">
|
|
<Link to={`/profile/${comment.author_username}`} style={{ textDecoration: 'none' }}>
|
|
<Avatar src={comment.author_avatar || undefined} sx={{ width: 48, height: 48, boxShadow: '0 4px 12px rgba(0,0,0,0.1)', border: '2px solid white' }}>
|
|
{comment.author_username.charAt(0).toUpperCase()}
|
|
</Avatar>
|
|
</Link>
|
|
<Box>
|
|
<Link to={`/profile/${comment.author_username}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
|
<Typography variant="subtitle1" sx={{ fontWeight: '900', lineHeight: 1.2 }}>{comment.author_username}</Typography>
|
|
</Link>
|
|
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 'bold' }}>{formatDate(comment.created_at)}</Typography>
|
|
</Box>
|
|
</Stack>
|
|
{(currentUser?.username === comment.author_username || currentUser?.username === data.username || currentUser?.is_superuser) && (
|
|
<IconButton size="small" color="error" onClick={() => handleDeleteComment(comment.id)}>
|
|
<DeleteIcon fontSize="small" />
|
|
</IconButton>
|
|
)}
|
|
</Box>
|
|
<Typography variant="body1" sx={{ mt: 3, pl: { xs: 0, sm: 9.5 }, lineHeight: 1.7, fontSize: '1rem' }}>
|
|
<RichText text={comment.text} />
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
))
|
|
)}
|
|
</Stack>
|
|
</Box>
|
|
)}
|
|
|
|
{activeTab === 2 && (
|
|
<Grid container spacing={3}>
|
|
{data.followers.length === 0 ? (
|
|
<Grid size={12}><Typography variant="h6" color="text.secondary" sx={{ py: 12, textAlign: 'center', fontWeight: 'bold' }}>No followers yet.</Typography></Grid>
|
|
) : (
|
|
data.followers.map(user => (
|
|
<Grid key={user.id} size={{ xs: 12, sm: 6, md: 4 }}>
|
|
<Card sx={{ borderRadius: 3, border: '1px solid', borderColor: 'divider', overflow: 'hidden', transition: 'all 0.3s ease', '&:hover': { borderColor: 'primary.main', transform: 'translateY(-4px)' } }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', p: 2, gap: 2 }}>
|
|
<Avatar component={Link} to={`/profile/${user.username}`} src={user.avatar || undefined} sx={{ width: 56, height: 56, cursor: 'pointer', border: '2px solid', borderColor: 'divider' }}>{user.username.charAt(0)}</Avatar>
|
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
|
<Typography component={Link} to={`/profile/${user.username}`} variant="subtitle1" sx={{ fontWeight: '900', color: 'text.primary', textDecoration: 'none', '&:hover': { color: 'primary.main' }, display: 'block', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
{user.username}
|
|
</Typography>
|
|
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 'bold' }}>Community Member</Typography>
|
|
</Box>
|
|
{currentUser && currentUser.username !== user.username && (
|
|
<IconButton
|
|
color={user.is_following ? "secondary" : "primary"}
|
|
onClick={() => handleMiniFollowToggle(user)}
|
|
sx={{ bgcolor: alpha(user.is_following ? theme.palette.secondary.main : theme.palette.primary.main, 0.1), '&:hover': { bgcolor: alpha(user.is_following ? theme.palette.secondary.main : theme.palette.primary.main, 0.2) } }}
|
|
>
|
|
{user.is_following ? <PersonRemoveIcon /> : <PersonAddIcon />}
|
|
</IconButton>
|
|
)}
|
|
</Box>
|
|
</Card>
|
|
</Grid>
|
|
))
|
|
)}
|
|
</Grid>
|
|
)}
|
|
|
|
{activeTab === 3 && (
|
|
<Grid container spacing={3}>
|
|
{data.following.length === 0 ? (
|
|
<Grid size={12}><Typography variant="h6" color="text.secondary" sx={{ py: 12, textAlign: 'center', fontWeight: 'bold' }}>Not following anyone yet.</Typography></Grid>
|
|
) : (
|
|
data.following.map(user => (
|
|
<Grid key={user.id} size={{ xs: 12, sm: 6, md: 4 }}>
|
|
<Card sx={{ borderRadius: 3, border: '1px solid', borderColor: 'divider', overflow: 'hidden', transition: 'all 0.3s ease', '&:hover': { borderColor: 'primary.main', transform: 'translateY(-4px)' } }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', p: 2, gap: 2 }}>
|
|
<Avatar component={Link} to={`/profile/${user.username}`} src={user.avatar || undefined} sx={{ width: 56, height: 56, cursor: 'pointer', border: '2px solid', borderColor: 'divider' }}>{user.username.charAt(0)}</Avatar>
|
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
|
<Typography component={Link} to={`/profile/${user.username}`} variant="subtitle1" sx={{ fontWeight: '900', color: 'text.primary', textDecoration: 'none', '&:hover': { color: 'primary.main' }, display: 'block', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
{user.username}
|
|
</Typography>
|
|
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 'bold' }}>Community Member</Typography>
|
|
</Box>
|
|
{currentUser && currentUser.username !== user.username && (
|
|
<IconButton
|
|
color={user.is_following ? "secondary" : "primary"}
|
|
onClick={() => handleMiniFollowToggle(user)}
|
|
sx={{ bgcolor: alpha(user.is_following ? theme.palette.secondary.main : theme.palette.primary.main, 0.1), '&:hover': { bgcolor: alpha(user.is_following ? theme.palette.secondary.main : theme.palette.primary.main, 0.2) } }}
|
|
>
|
|
{user.is_following ? <PersonRemoveIcon /> : <PersonAddIcon />}
|
|
</IconButton>
|
|
)}
|
|
</Box>
|
|
</Card>
|
|
</Grid>
|
|
))
|
|
)}
|
|
</Grid>
|
|
)}
|
|
</Box>
|
|
</Container>
|
|
);
|
|
};
|
|
|
|
export default PublicProfile;
|