diff --git a/backend/api/migrations/0020_notification_profile_comment_and_more.py b/backend/api/migrations/0020_notification_profile_comment_and_more.py new file mode 100644 index 0000000..2223a30 --- /dev/null +++ b/backend/api/migrations/0020_notification_profile_comment_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 6.0.2 on 2026-02-18 04:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0019_alter_flavor_options'), + ] + + operations = [ + migrations.AddField( + model_name='notification', + name='profile_comment', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.profilecomment'), + ), + migrations.AlterField( + model_name='notification', + name='notification_type', + field=models.CharField(choices=[('reply', 'Reply'), ('mention', 'Mention'), ('ticket_new', 'New Ticket'), ('ticket_reply', 'Ticket Reply'), ('profile_comment', 'Profile Comment')], max_length=20), + ), + ] diff --git a/backend/api/models.py b/backend/api/models.py index 9584039..d1583a9 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -128,6 +128,7 @@ class Notification(models.Model): ('mention', 'Mention'), ('ticket_new', 'New Ticket'), ('ticket_reply', 'Ticket Reply'), + ('profile_comment', 'Profile Comment'), ] recipient = models.ForeignKey(User, related_name='notifications', on_delete=models.CASCADE) actor = models.ForeignKey(User, related_name='actions', on_delete=models.CASCADE) @@ -135,6 +136,7 @@ class Notification(models.Model): rating = models.ForeignKey(Rating, on_delete=models.CASCADE, null=True, blank=True) reply = models.ForeignKey(Reply, on_delete=models.CASCADE, null=True, blank=True) ticket = models.ForeignKey('Ticket', on_delete=models.CASCADE, null=True, blank=True) + profile_comment = models.ForeignKey('ProfileComment', on_delete=models.CASCADE, null=True, blank=True) is_read = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 9b4947e..ed2adb3 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -179,10 +179,11 @@ class NotificationSerializer(serializers.ModelSerializer): actor_avatar = serializers.SerializerMethodField() flavor_name = serializers.SerializerMethodField() flavor_id = serializers.SerializerMethodField() + profile_owner_username = serializers.SerializerMethodField() class Meta: model = Notification - fields = ['id', 'actor_username', 'actor_avatar', 'notification_type', 'rating', 'reply', 'ticket', 'is_read', 'created_at', 'flavor_name', 'flavor_id'] + fields = ['id', 'actor_username', 'actor_avatar', 'notification_type', 'rating', 'reply', 'ticket', 'profile_comment', 'profile_owner_username', 'is_read', 'created_at', 'flavor_name', 'flavor_id'] def get_actor_avatar(self, obj): if obj.actor.avatar: @@ -205,3 +206,8 @@ class NotificationSerializer(serializers.ModelSerializer): if obj.reply: return obj.reply.rating.flavor.id return None + + def get_profile_owner_username(self, obj): + if obj.profile_comment: + return obj.profile_comment.profile_owner.username + return None diff --git a/backend/api/views.py b/backend/api/views.py index 3938633..6770e13 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -381,6 +381,16 @@ class UserViewSet(viewsets.ModelViewSet): author=request.user, text=text ) + + # Notify profile owner + if profile_owner != request.user: + Notification.objects.create( + recipient=profile_owner, + actor=request.user, + notification_type='profile_comment', + profile_comment=comment + ) + return Response(ProfileCommentSerializer(comment, context={'request': request}).data, status=status.HTTP_201_CREATED) @action(detail=True, methods=['delete'], url_path='delete_comment/(?P[^/.]+)', permission_classes=[permissions.IsAuthenticated]) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a5198cf..e25e18e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -73,13 +73,14 @@ interface Notification { id: number; actor_username: string; actor_avatar: string | null; - notification_type: 'reply' | 'mention'; + notification_type: 'reply' | 'mention' | 'ticket_new' | 'ticket_reply' | 'profile_comment'; rating: number | null; reply: number | null; is_read: boolean; created_at: string; flavor_name: string | null; flavor_id: number | null; + profile_owner_username: string | null; } const GlobalSearch = () => { @@ -494,16 +495,13 @@ const App: React.FC = () => { ) : ( notifications.map(n => { - let message = ""; - if (n.notification_type === 'reply') message = `replied to your review on ${n.flavor_name}`; - else if (n.notification_type === 'mention') message = `mentioned you on ${n.flavor_name}`; - else if (n.notification_type === 'ticket_new') message = t('community.notifTicketNew'); - else if (n.notification_type === 'ticket_reply') message = user?.is_superuser ? t('community.notifTicketReplyAdmin') : t('community.notifTicketReply'); - const handleClick = () => { if (n.notification_type.startsWith('ticket')) { handleNotificationClick(n); navigate('/support'); + } else if (n.notification_type === 'profile_comment') { + handleNotificationClick(n); + navigate(`/profile/${user?.username}`); } else { handleNotificationClick(n); } @@ -530,7 +528,13 @@ const App: React.FC = () => { - {n.actor_username} {message} + {n.actor_username} { + n.notification_type === 'reply' ? `replied to your review on ${n.flavor_name}` : + n.notification_type === 'mention' ? `mentioned you on ${n.flavor_name}` : + n.notification_type === 'profile_comment' ? `left a message on your guestbook` : + n.notification_type === 'ticket_new' ? t('community.notifTicketNew') : + user?.is_superuser ? t('community.notifTicketReplyAdmin') : t('community.notifTicketReply') + } {formatDate(n.created_at)} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 4545d50..405ab6a 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -255,38 +255,38 @@ const Dashboard: React.FC = () => { {/* TAB 0: MY REVIEWS */} {activeTab === 0 && ( - - + + setRatedSearch(e.target.value)} - slotProps={{ input: { startAdornment: } }} - sx={{ bgcolor: 'background.paper', borderRadius: 1, '& .MuiOutlinedInput-root': { borderRadius: 1 } }} + slotProps={{ input: { startAdornment: } }} + sx={{ bgcolor: 'background.paper', borderRadius: 1, '& .MuiOutlinedInput-root': { borderRadius: 1, height: 40 } }} /> - + {ratedCategories.map(cat => ( r.category_name === cat)?.category_slug}`, { defaultValue: cat })} onClick={() => setRatedCategory(cat)} color={ratedCategory === cat ? 'primary' : 'default'} variant={ratedCategory === cat ? 'filled' : 'outlined'} - sx={{ fontWeight: 'bold', height: 40, px: 1, borderRadius: 1 }} + sx={{ fontWeight: 'bold', height: 32, px: 0.5, borderRadius: 1, fontSize: '0.8rem' }} /> ))} - + @@ -417,38 +417,38 @@ const Dashboard: React.FC = () => { {activeTab === 1 && ( {/* Filter Controls */} - - + + setExploreSearch(e.target.value)} - slotProps={{ input: { startAdornment: } }} - sx={{ bgcolor: 'background.paper', borderRadius: 1, '& .MuiOutlinedInput-root': { borderRadius: 1 } }} + slotProps={{ input: { startAdornment: } }} + sx={{ bgcolor: 'background.paper', borderRadius: 1, '& .MuiOutlinedInput-root': { borderRadius: 1, height: 40 } }} /> - + {exploreCategories.map(cat => ( f.category_name === cat)?.category_slug}`, { defaultValue: cat })} onClick={() => setExploreCategory(cat)} color={exploreCategory === cat ? 'primary' : 'default'} variant={exploreCategory === cat ? 'filled' : 'outlined'} - sx={{ fontWeight: 'bold', height: 40, px: 1, borderRadius: 1 }} + sx={{ fontWeight: 'bold', height: 32, px: 0.5, borderRadius: 1, fontSize: '0.8rem' }} /> ))} - +