diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 96a867b..7005f13 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -16,7 +16,7 @@ import { } from '@mui/material'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import { Link } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import api from '../api'; import { useTitle } from '../hooks/useTitle'; @@ -31,6 +31,7 @@ interface DashboardData { const Dashboard: React.FC = () => { const { t } = useTranslation(); + const navigate = useNavigate(); useTitle(t('nav.dashboard')); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); @@ -43,6 +44,14 @@ const Dashboard: React.FC = () => { alert(t('dashboard.copySuccess')); }; + const handleGoBack = () => { + if (window.history.length > 1) { + navigate(-1); + } else { + navigate('/'); + } + }; + useEffect(() => { const fetchData = async () => { try { @@ -82,12 +91,23 @@ const Dashboard: React.FC = () => { diff --git a/frontend/src/pages/PublicProfile.tsx b/frontend/src/pages/PublicProfile.tsx index fcadccd..ff327cb 100644 --- a/frontend/src/pages/PublicProfile.tsx +++ b/frontend/src/pages/PublicProfile.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; import { Typography, Box, @@ -44,11 +44,20 @@ interface ProfileData { const PublicProfile: React.FC = () => { const { username } = useParams<{ username: string }>(); + const navigate = useNavigate(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState(0); const [currentUser, setCurrentUser] = useState(null); + const handleGoBack = () => { + if (window.history.length > 1) { + navigate(-1); + } else { + navigate('/'); + } + }; + useEffect(() => { const fetchProfile = async () => { try { @@ -119,12 +128,23 @@ const PublicProfile: React.FC = () => { {/* Profile Header */} diff --git a/frontend/src/pages/categories/CategoryFlavors.tsx b/frontend/src/pages/categories/CategoryFlavors.tsx index 085e24e..628dd53 100644 --- a/frontend/src/pages/categories/CategoryFlavors.tsx +++ b/frontend/src/pages/categories/CategoryFlavors.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { useParams, Link } from 'react-router-dom'; +import { useParams, Link, useNavigate } from 'react-router-dom'; import { Typography, Box, @@ -38,10 +38,19 @@ interface Flavor { const CategoryFlavors: React.FC = () => { const { slug } = useParams<{ slug: string }>(); + const navigate = useNavigate(); const [flavors, setFlavors] = useState([]); const [categoryName, setCategoryName] = useState(''); const [loading, setLoading] = useState(true); + const handleGoBack = () => { + if (window.history.length > 1) { + navigate(-1); + } else { + navigate('/'); + } + }; + useEffect(() => { const fetchFlavors = async () => { setLoading(true); @@ -71,12 +80,22 @@ const CategoryFlavors: React.FC = () => { {categoryName || 'Flavors'} diff --git a/frontend/src/pages/flavors/FlavorDetail.tsx b/frontend/src/pages/flavors/FlavorDetail.tsx index 2fee5b9..6377831 100644 --- a/frontend/src/pages/flavors/FlavorDetail.tsx +++ b/frontend/src/pages/flavors/FlavorDetail.tsx @@ -9,23 +9,24 @@ import { Rating as MuiRating, Button, Avatar, - Paper, CircularProgress, Container, Grid, Chip, Stack, alpha, - useTheme + useTheme, + Collapse } from '@mui/material'; import api from '../../api'; import { useTitle } from '../../hooks/useTitle'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import VerifiedIcon from '@mui/icons-material/Verified'; -import StarIcon from '@mui/icons-material/Star'; +import CommentIcon from '@mui/icons-material/Comment'; import { formatDate } from '../../utils/date'; import MentionTextField from '../../components/MentionTextField'; +import RichText from '../../components/RichText'; interface Reply { id: number; @@ -67,6 +68,7 @@ const FlavorDetail: React.FC = () => { const [flavor, setFlavor] = useState(null); const [loading, setLoading] = useState(true); const [replyInputs, setReplyInputs] = useState<{[key: number]: string}>({}); + const [expandedReplies, setExpandedReplies] = useState<{[key: number]: boolean}>({}); const [currentUser, setCurrentUser] = useState(null); // Rating form state @@ -128,6 +130,8 @@ const FlavorDetail: React.FC = () => { try { await api.post(`ratings/${ratingId}/reply/`, { text }); setReplyInputs({ ...replyInputs, [ratingId]: '' }); + // Auto-expand replies when submitting a new one + setExpandedReplies(prev => ({ ...prev, [ratingId]: true })); fetchFlavor(); } catch (err) { alert('Failed to submit reply'); @@ -185,338 +189,353 @@ const FlavorDetail: React.FC = () => { if (window.history.length > 1) { navigate(-1); } else { - navigate(`/category/${flavor?.category_slug}`); + navigate('/'); } }; - const renderTextWithMentions = (text: string) => { - const parts = text.split(/(@\w+)/g); - return parts.map((part, i) => { - if (part.startsWith('@')) { - const username = part.substring(1); - return ( - - {part} - - ); - } - return part; - }); - }; + const RatingBadge = ({ score, size = 'medium' }: { score: number, size?: 'small' | 'medium' | 'large' }) => { + const isLarge = size === 'large'; + const isSmall = size === 'small'; + + return ( + alpha(theme.palette.primary.main, 0.1), + color: 'primary.main', + px: isLarge ? 3 : (isSmall ? 1 : 2), + py: isLarge ? 1 : 0.5, + borderRadius: isLarge ? 3 : 2, + minWidth: isLarge ? 80 : 50, + border: '1px solid', + borderColor: (theme) => alpha(theme.palette.primary.main, 0.2) + }}> + + {score} + + + / 10 + + + ); + }; - if (loading) return ; + if (loading) return ; if (!flavor) return Flavor not found; return ( - + {/* Top Bar */} - {/* Hero Section */} - - {/* Left: Image */} - - alpha(theme.palette.background.paper, 0.4), - backdropFilter: 'blur(10px)', - borderRadius: 4, - overflow: 'hidden', - border: '1px solid', - borderColor: (theme) => alpha(theme.palette.text.primary, 0.05), - position: 'relative', - boxShadow: '0 20px 40px rgba(0,0,0,0.1)' - }} - > - {!flavor.is_available && ( - alpha(theme.palette[flavor.is_legacy ? 'warning' : 'error'].main, 0.8) }} - /> - )} - {flavor.image_url ? ( - - ) : ( - No Image - )} - - - - {/* Right: Info */} - - - - - {flavor.name} - - - - - {(flavor.average_rating || 0).toFixed(1)} - / 10 + {/* Main Content Grid */} + + {/* Left: Product Info Card */} + + + alpha(theme.palette.background.paper, 0.6), + backdropFilter: 'blur(12px)', + overflow: 'hidden' + }}> + + {!flavor.is_available && ( + + )} + {flavor.image_url ? ( + + ) : ( + No Image + )} - - - {flavor.ratings.length} {t('common.reviews')} - - + + + + {flavor.name} + + + + + + + + {flavor.ratings.length} {t('common.reviews')} + + + - - {flavor.description} - + + {flavor.description} + - {flavor.shop_url && ( - - )} + {flavor.shop_url && ( + + )} + + - - {/* Reviews Section */} - - - {t('home.communityVoice')} - + {/* Right: Ratings & Comments */} + + + Ratings & Comments + + {flavor.ratings.length === 0 ? "Be the first to rate this flavor!" : `Join the discussion with ${flavor.ratings.length} other fans.`} + + - {/* New Rating Form */} - {currentUser && flavor.user_rating === null && ( - alpha(theme.palette.background.paper, 0.3), - backdropFilter: 'blur(8px)', - border: '1px dashed', - borderColor: 'divider', - boxShadow: 'none' - }} - > - Rate this flavor -
- - Score - setNewScore(val)} size="large" /> - - setNewComment(val)} - /> - - -
- )} + {/* Add Rating Form */} + {currentUser && flavor.user_rating === null && ( + + + Rate this flavor +
+ + setNewScore(val)} size="large" /> + + setNewComment(val)} + /> + + +
+
+ )} - - {flavor.ratings.length === 0 ? ( - - {t('dashboard.noRatings')} - - ) : ( - flavor.ratings.map((rating: Rating) => ( - - - {editMode === rating.id ? ( - // Edit Mode - - setEditScore(val || 0)} size="large" /> - setEditComment(val)} - /> - - - + + {flavor.ratings.length === 0 ? ( + + {t('dashboard.noRatings')} + + ) : ( + flavor.ratings.map((rating: Rating) => ( + alpha(theme.palette.background.paper, 0.8) + }}> + + {editMode === rating.id ? ( + + setEditScore(val || 0)} size="large" /> + setEditComment(val)} + /> + + + + - - ) : ( - // View Mode - <> - - - - - {!rating.user_avatar && rating.user.charAt(0).toUpperCase()} - - - - - {rating.user} + ) : ( + <> + + + + + {!rating.user_avatar && rating.user.charAt(0).toUpperCase()} + - - {formatDate(rating.created_at)} - + + + {rating.user} + + + {formatDate(rating.created_at)} + + + - alpha(theme.palette.primary.main, 0.1), - backdropFilter: 'blur(4px)', - px: 1.5, py: 0.5, - borderRadius: 2, - border: '1px solid', - borderColor: (theme) => alpha(theme.palette.primary.main, 0.1) - }}> - - {rating.score}/10 - - - {rating.comment && ( - - {renderTextWithMentions(rating.comment)} - - )} - - - {/* Reply Actions or Metadata */} - - {currentUser === rating.user && ( - - - - + {rating.comment ? ( + + + + ) : ( + No comment provided. )} - - - )} - {/* Replies */} - {rating.replies.length > 0 && ( - alpha(theme.palette.text.primary, 0.03), - backdropFilter: 'blur(4px)', - p: 2, - borderRadius: 2, - border: '1px solid', - borderColor: (theme) => alpha(theme.palette.text.primary, 0.05) - }}> - {rating.replies.map((reply: any) => ( - - - - - {reply.user} - {reply.user === rating.user && } - - - - - {formatDate(reply.created_at)} - + + + + {currentUser === rating.user && ( + + + + + )} + + + )} + + {/* Replies Section */} + + + {rating.replies.map((reply: any) => ( + + + + + + {reply.user} + {reply.user === rating.user && } + + + + {formatDate(reply.created_at)} + + {currentUser === reply.user && editingReplyId !== reply.id && ( - - - + + + )} - - {editingReplyId === reply.id ? ( - - setEditReplyText(val)} - /> - - - + {editingReplyId === reply.id ? ( + + setEditReplyText(val)} + /> + + + + - - ) : ( - - {renderTextWithMentions(reply.text)} - - )} - - ))} - - )} - - {currentUser && ( - - setReplyInputs({ ...replyInputs, [rating.id]: val })} - onKeyDown={(e) => { - if (e.key === 'Enter' && !e.shiftKey) { - handleReplySubmit(rating.id); - } - }} - /> - - - )} - - - )) - )} - -
+ ) : ( + + + + )} + + ))} + + {currentUser && ( + + setReplyInputs({ ...replyInputs, [rating.id]: val })} + onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleReplySubmit(rating.id)} + /> + + + )} + + + + + )) + )} + + +
); };