diff --git a/backend/api/migrations/0018_alter_category_options.py b/backend/api/migrations/0018_alter_category_options.py new file mode 100644 index 0000000..d7451d1 --- /dev/null +++ b/backend/api/migrations/0018_alter_category_options.py @@ -0,0 +1,17 @@ +# Generated by Django 6.0.2 on 2026-02-18 03:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0017_profilecomment'), + ] + + operations = [ + migrations.AlterModelOptions( + name='category', + options={'ordering': ['name'], 'verbose_name_plural': 'Categories'}, + ), + ] diff --git a/backend/api/migrations/0019_alter_flavor_options.py b/backend/api/migrations/0019_alter_flavor_options.py new file mode 100644 index 0000000..db2c257 --- /dev/null +++ b/backend/api/migrations/0019_alter_flavor_options.py @@ -0,0 +1,17 @@ +# Generated by Django 6.0.2 on 2026-02-18 03:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0018_alter_category_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='flavor', + options={'ordering': ['name']}, + ), + ] diff --git a/backend/api/models.py b/backend/api/models.py index 1e4ad90..9584039 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -56,6 +56,7 @@ class Category(models.Model): class Meta: verbose_name_plural = "Categories" + ordering = ['name'] def __str__(self): return self.name @@ -77,6 +78,7 @@ class Flavor(models.Model): constraints = [ models.UniqueConstraint(fields=['name', 'category'], name='unique_flavor_name_per_category') ] + ordering = ['name'] def __str__(self): return f"{self.name} ({self.category.name})" diff --git a/backend/holy_backend/settings.py b/backend/holy_backend/settings.py index cab74d7..f5168d5 100644 --- a/backend/holy_backend/settings.py +++ b/backend/holy_backend/settings.py @@ -97,14 +97,13 @@ DATABASES = { # Cache configuration for Ratelimiting CACHES = { 'default': { - 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', - 'LOCATION': os.path.join(BASE_DIR, 'django_cache'), + 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', + 'LOCATION': 'django_ratelimit_cache', } } -# Silence the ratelimit error for locmem if it appears, -# though we just switched to file-based which is supported. -SILENCED_SYSTEM_CHECKS = ['django_ratelimit.E003'] +# Silence ratelimit warnings for non-Redis/Memcached backends +SILENCED_SYSTEM_CHECKS = ['django_ratelimit.W001', 'django_ratelimit.E003'] # Password validation diff --git a/frontend/src/pages/PublicProfile.tsx b/frontend/src/pages/PublicProfile.tsx index b65a2ed..79fc923 100644 --- a/frontend/src/pages/PublicProfile.tsx +++ b/frontend/src/pages/PublicProfile.tsx @@ -19,11 +19,15 @@ import { Grid, TextField, IconButton, - Chip + 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 api from '../api'; import { useTitle } from '../hooks/useTitle'; import { useTranslation } from 'react-i18next'; @@ -54,6 +58,7 @@ interface MiniUser { id: number; username: string; avatar: string | null; + is_following?: boolean; } interface ProfileData { @@ -81,11 +86,10 @@ const PublicProfile: React.FC = ({ adminMode }) => { const navigate = useNavigate(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); - const [activeTab, setActiveTab] = useState(0); // 0: Ratings, 1: Guestbook, 2: Followers, 3: Following + const [activeTab, setActiveTab] = useState(0); const [categoryTab, setCategoryTab] = useState(0); const [currentUser, setCurrentUser] = useState<{username: string, is_superuser: boolean} | null>(null); - // Comment state const [newComment, setNewComment] = useState(''); const fetchProfile = async () => { @@ -122,6 +126,17 @@ const PublicProfile: React.FC = ({ adminMode }) => { } }; + 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; @@ -149,7 +164,6 @@ const PublicProfile: React.FC = ({ adminMode }) => { if (loading) return ; if (!data) return Profile not found.; - // Rating filtering const categoryGroups = data.ratings.reduce((acc: any, rating) => { const cat = rating.category_name || 'Other'; if (!acc[cat]) acc[cat] = []; @@ -184,67 +198,109 @@ const PublicProfile: React.FC = ({ adminMode }) => { {t('common.back')} - {/* Modern Profile Header */} + {/* Uplifted Profile Header */} alpha(theme.palette.background.paper, 0.6), - backdropFilter: 'blur(12px)', + backdropFilter: 'blur(20px)', mb: 4, - overflow: 'hidden' + overflow: 'hidden', + position: 'relative' }}> - - - - theme.palette.background.paper, - boxShadow: '0 8px 24px rgba(0,0,0,0.12)', - fontSize: '3rem', - bgcolor: 'primary.main' - }} - > - {!data.avatar && data.username.charAt(0).toUpperCase()} - + {/* Decorative Gradient Banner */} + + + + + + theme.palette.background.paper, + boxShadow: '0 12px 32px rgba(0,0,0,0.15)', + fontSize: '4rem', + bgcolor: 'primary.main', + transition: 'transform 0.3s ease', + '&:hover': { transform: 'scale(1.02)' } + }} + > + {!data.avatar && data.username.charAt(0).toUpperCase()} + + {data.ratings.length > 50 && ( + + + + )} + - - {data.username} - - - {data.ratings.length} - Rated + + + {data.username} + + + + + {data.ratings.length} + Rated - - {data.followers_count} - Followers + + + {data.followers_count} + Followers - - {data.following_count} - Following + + + {data.following_count} + Following - + {currentUser?.username !== data.username && ( )} - {(currentUser?.is_superuser || (adminMode && currentUser?.is_superuser)) && ( + {currentUser?.is_superuser && ( @@ -255,14 +311,14 @@ const PublicProfile: React.FC = ({ adminMode }) => { {/* Profile Navigation Tabs */} - + setActiveTab(v)} variant="fullWidth" sx={{ - bgcolor: alpha(theme.palette.background.paper, 0.4), - '& .MuiTab-root': { fontWeight: 'bold', py: 2 } + '& .MuiTab-root': { fontWeight: '900', textTransform: 'none', fontSize: '1rem' }, + '& .Mui-selected': { color: 'primary.main' } }} > @@ -270,11 +326,10 @@ const PublicProfile: React.FC = ({ adminMode }) => { - + {/* Content Area */} - {/* TAB 0: RATINGS */} {activeTab === 0 && ( @@ -283,13 +338,12 @@ const PublicProfile: React.FC = ({ adminMode }) => { key={cat} label={cat} onClick={() => setCategoryTab(idx)} color={categoryTab === idx ? 'primary' : 'default'} - variant={categoryTab === idx ? 'filled' : 'outlined'} sx={{ fontWeight: 'bold', px: 1 }} /> ))} - + {tiers.map(tier => { const items = currentRatings.filter((r: Rating) => { const s = r.score; @@ -304,51 +358,56 @@ const PublicProfile: React.FC = ({ adminMode }) => { return ( - + {tier.key} - {tier.title} - + + {tier.title} + {items.length} items + + - + {items.map((rating: Rating) => ( - + alpha(theme.palette.background.paper, 0.4), + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', + '&:hover': { transform: 'translateY(-8px)', borderColor: 'primary.main', boxShadow: '0 12px 32px rgba(0,0,0,0.12)' } }}> - + - + {rating.flavor_name} {rating.comment && ( - + "" )} {adminMode && ( - + )} @@ -363,20 +422,19 @@ const PublicProfile: React.FC = ({ adminMode }) => { )} - {/* TAB 1: GUESTBOOK */} {activeTab === 1 && ( {currentUser && ( - - Leave a message + + Leave a message
setNewComment(e.target.value)} - sx={{ mb: 2 }} + sx={{ mb: 2, '& .MuiOutlinedInput-root': { borderRadius: 3 } }} /> -
@@ -384,25 +442,25 @@ const PublicProfile: React.FC = ({ adminMode }) => { {data.comments.length === 0 ? ( - - No messages yet. Be the first! + + No messages yet. Be the first to break the ice! ) : ( data.comments.map(comment => ( - - + + - + {comment.author_username.charAt(0).toUpperCase()} - {comment.author_username} + {comment.author_username} - {formatDate(comment.created_at)} + {formatDate(comment.created_at)} {(currentUser?.username === comment.author_username || currentUser?.username === data.username || currentUser?.is_superuser) && ( @@ -411,7 +469,7 @@ const PublicProfile: React.FC = ({ adminMode }) => { )} - + @@ -422,19 +480,28 @@ const PublicProfile: React.FC = ({ adminMode }) => { )} - {/* TAB 2: FOLLOWERS */} {activeTab === 2 && ( - + {data.followers.length === 0 ? ( - No followers yet. + No followers yet. ) : ( - data.followers.map(follower => ( - - - - {follower.username.charAt(0)} - {follower.username} - + data.followers.map(user => ( + + + + {user.username.charAt(0)} + + + {user.username} + + Community Member + + {currentUser && currentUser.username !== user.username && ( + handleMiniFollowToggle(user)}> + {user.is_following ? : } + + )} + )) @@ -442,19 +509,28 @@ const PublicProfile: React.FC = ({ adminMode }) => { )} - {/* TAB 3: FOLLOWING */} {activeTab === 3 && ( - + {data.following.length === 0 ? ( - Not following anyone yet. + Not following anyone yet. ) : ( data.following.map(user => ( - - - {user.username.charAt(0)} - {user.username} - + + + {user.username.charAt(0)} + + + {user.username} + + Community Member + + {currentUser && currentUser.username !== user.username && ( + handleMiniFollowToggle(user)}> + {user.is_following ? : } + + )} + ))