added searchbar and restructured start page

This commit is contained in:
nvrl committed 2026-02-15 20:54:12 +01:00
1 parent 3437f69f3b
commit 2befe3eb7d
14 files changed
+542 -24

No files matched your search

@@ -0,0 +1,109 @@
import json
import urllib.request
import re
from django.core.management.base import BaseCommand
from django.utils.text import slugify
from api.models import Category, Flavor
class Command(BaseCommand):
help = 'Syncs flavors from Holy Energy Shopify API'
def clean_html(self, raw_html):
if not raw_html:
return ""
cleanr = re.compile('<.*?>')
cleantext = re.sub(cleanr, '', raw_html)
return cleantext.strip()
def handle(self, *args, **options):
url = "https://weareholy.com/products.json?limit=250"
self.stdout.write(f"Fetching from {url}...")
try:
with urllib.request.urlopen(url) as response:
data = json.loads(response.read().decode())
except Exception as e:
self.stdout.write(self.style.ERROR(f"Failed to fetch data: {e}"))
return
products = data.get('products', [])
self.stdout.write(f"Found {len(products)} products.")
# Ensure categories exist
cat_map = {
'energy': Category.objects.get_or_create(name='Energy', defaults={'slug': 'energy'})[0],
'hydration': Category.objects.get_or_create(name='Hydration', defaults={'slug': 'hydration'})[0],
'iced-tea': Category.objects.get_or_create(name='Iced Tea', defaults={'slug': 'iced-tea'})[0],
'milkshake': Category.objects.get_or_create(name='Milkshake', defaults={'slug': 'milkshake'})[0],
}
created_count = 0
updated_count = 0
for p in products:
title = p.get('title')
tags = p.get('tags', [])
if isinstance(tags, str):
tags = [t.strip() for t in tags.split(',')]
# Determine category
category = None
tags_lower = [t.lower() for t in tags]
if 'holy energy' in tags_lower or 'energy' in tags_lower:
category = cat_map['energy']
elif 'holy hydration' in tags_lower or 'hydration' in tags_lower:
category = cat_map['hydration']
elif 'holy iced tea' in tags_lower or 'iced tea' in tags_lower:
category = cat_map['iced-tea']
elif 'milkshake' in tags_lower:
category = cat_map['milkshake']
# Skip if not a flavor product (e.g. merch, shakers)
if not category or 'merch' in tags_lower or 'shaker' in tags_lower:
continue
# Check availability
variants = p.get('variants', [])
is_available = any(v.get('available') for v in variants)
# Image
images = p.get('images', [])
image_url = images[0].get('src') if images else None
# Description
description = self.clean_html(p.get('body_html', ''))
# Shop URL
handle = p.get('handle')
shop_url = f"https://weareholy.com/products/{handle}" if handle else None
# Create or update
# Try finding by external_id first, then by name
flavor = Flavor.objects.filter(external_id=p['id']).first()
if not flavor:
flavor = Flavor.objects.filter(name=title).first()
if flavor:
flavor.external_id = p['id']
flavor.name = title
flavor.category = category
flavor.description = description
flavor.image_url = image_url
flavor.shop_url = shop_url
flavor.is_available = is_available
flavor.save()
updated_count += 1
else:
Flavor.objects.create(
external_id=p['id'],
name=title,
category=category,
description=description,
image_url=image_url,
shop_url=shop_url,
is_available=is_available
)
created_count += 1
self.stdout.write(self.style.SUCCESS(f"Finished! Created: {created_count}, Updated: {updated_count}"))
@@ -0,0 +1,28 @@
# Generated by Django 6.0.2 on 2026-02-15 19:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='flavor',
name='external_id',
field=models.BigIntegerField(blank=True, null=True, unique=True),
),
migrations.AddField(
model_name='flavor',
name='image_url',
field=models.URLField(blank=True, max_length=500, null=True),
),
migrations.AddField(
model_name='flavor',
name='is_available',
field=models.BooleanField(default=True),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 6.0.2 on 2026-02-15 19:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0002_flavor_external_id_flavor_image_url_and_more'),
]
operations = [
migrations.AddField(
model_name='flavor',
name='shop_url',
field=models.URLField(blank=True, max_length=500, null=True),
),
]
+4
View File
@@ -28,6 +28,10 @@ class Flavor(models.Model):
category = models.ForeignKey(Category, related_name='flavors', on_delete=models.CASCADE) category = models.ForeignKey(Category, related_name='flavors', on_delete=models.CASCADE)
name = models.CharField(max_length=100, unique=True) name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True) description = models.TextField(blank=True)
image_url = models.URLField(max_length=500, blank=True, null=True)
shop_url = models.URLField(max_length=500, blank=True, null=True)
is_available = models.BooleanField(default=True)
external_id = models.BigIntegerField(unique=True, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
+3 -2
View File
@@ -14,10 +14,11 @@ class CategorySerializer(serializers.ModelSerializer):
class RatingSerializer(serializers.ModelSerializer): class RatingSerializer(serializers.ModelSerializer):
user = serializers.StringRelatedField(read_only=True) user = serializers.StringRelatedField(read_only=True)
flavor_name = serializers.CharField(source='flavor.name', read_only=True) flavor_name = serializers.CharField(source='flavor.name', read_only=True)
flavor_image = serializers.CharField(source='flavor.image_url', read_only=True)
class Meta: class Meta:
model = Rating model = Rating
fields = ['id', 'user', 'flavor', 'flavor_name', 'score', 'comment', 'created_at'] fields = ['id', 'user', 'flavor', 'flavor_name', 'flavor_image', 'score', 'comment', 'created_at']
class FlavorSerializer(serializers.ModelSerializer): class FlavorSerializer(serializers.ModelSerializer):
category_name = serializers.CharField(source='category.name', read_only=True) category_name = serializers.CharField(source='category.name', read_only=True)
@@ -27,7 +28,7 @@ class FlavorSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Flavor model = Flavor
fields = ['id', 'name', 'category', 'category_name', 'description', 'average_rating', 'user_rating', 'ratings'] fields = ['id', 'name', 'category', 'category_name', 'description', 'average_rating', 'user_rating', 'ratings', 'image_url', 'is_available', 'shop_url']
def get_user_rating(self, obj): def get_user_rating(self, obj):
request = self.context.get('request') request = self.context.get('request')
+2 -2
View File
@@ -2,9 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title> <title>Holy Flavors</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" rx="20" fill="#9d7cd8"/>
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="sans-serif" font-size="60" fill="white" font-weight="bold">H</text>
</svg>

After

Width:  |  Height:  |  Size: 281 B

+5 -3
View File
@@ -15,7 +15,8 @@ import {
} from '@mui/material'; } from '@mui/material';
import PaletteIcon from '@mui/icons-material/Palette'; import PaletteIcon from '@mui/icons-material/Palette';
import { getTheme, type CatppuccinTheme } from './theme'; import { getTheme, type CatppuccinTheme } from './theme';
import FlavorList from './pages/FlavorList'; import CategoryList from './pages/CategoryList';
import CategoryFlavors from './pages/categories/CategoryFlavors';
import Dashboard from './pages/Dashboard'; import Dashboard from './pages/Dashboard';
import Login from './pages/Login'; import Login from './pages/Login';
import api from './api'; import api from './api';
@@ -74,7 +75,7 @@ const App: React.FC = () => {
</Typography> </Typography>
<Box sx={{ display: 'flex', alignItems: 'center' }}> <Box sx={{ display: 'flex', alignItems: 'center' }}>
<Button color="inherit" component={Link} to="/">Flavors</Button> <Button color="inherit" component={Link} to="/">Categories</Button>
{user ? ( {user ? (
<> <>
<Button color="inherit" component={Link} to="/dashboard">Dashboard</Button> <Button color="inherit" component={Link} to="/dashboard">Dashboard</Button>
@@ -103,7 +104,8 @@ const App: React.FC = () => {
<Container sx={{ mt: 4 }}> <Container sx={{ mt: 4 }}>
<Routes> <Routes>
<Route path="/" element={<FlavorList />} /> <Route path="/" element={<CategoryList />} />
<Route path="/category/:slug" element={<CategoryFlavors />} />
<Route path="/dashboard" element={<Dashboard />} /> <Route path="/dashboard" element={<Dashboard />} />
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
</Routes> </Routes>
+7
View File
@@ -0,0 +1,7 @@
import { useEffect } from 'react';
export const useTitle = (title: string) => {
useEffect(() => {
document.title = `${title} | Holy Flavors`;
}, [title]);
};
+95
View File
@@ -0,0 +1,95 @@
import React, { useEffect, useState } from 'react';
import {
Typography,
Box,
Card,
CardContent,
TextField,
InputAdornment
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import { Link } from 'react-router-dom';
import api from '../api';
import { useTitle } from '../hooks/useTitle';
interface Category {
id: number;
name: string;
slug: string;
}
const CategoryList: React.FC = () => {
useTitle('Categories');
const [categories, setCategories] = useState<Category[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
useEffect(() => {
const fetchCategories = async () => {
try {
const res = await api.get('categories/');
setCategories(res.data);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
fetchCategories();
}, []);
const filteredCategories = categories.filter(c =>
c.name.toLowerCase().includes(search.toLowerCase())
);
if (loading) return <Typography>Loading Categories...</Typography>;
return (
<Box>
<Typography variant="h4" gutterBottom>Flavor Categories</Typography>
<TextField
fullWidth
variant="outlined"
placeholder="Search categories..."
value={search}
onChange={(e) => setSearch(e.target.value)}
sx={{ mb: 4 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
{filteredCategories.map(category => (
<Box key={category.id} sx={{ flex: '1 1 45%', minWidth: 280 }}>
<Link to={`/category/${category.slug}`} style={{ textDecoration: 'none' }}>
<Card sx={{
height: 150,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'transform 0.2s',
'&:hover': {
transform: 'scale(1.02)',
cursor: 'pointer',
bgcolor: 'action.hover'
}
}}>
<CardContent>
<Typography variant="h4" color="primary">{category.name}</Typography>
</CardContent>
</Card>
</Link>
</Box>
))}
</Box>
</Box>
);
};
export default CategoryList;
+47 -15
View File
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Typography, Card, CardContent, Box, Rating as MuiRating } from '@mui/material'; import { Typography, Card, CardContent, Box, Rating as MuiRating, Button } from '@mui/material';
import api from '../api'; import api from '../api';
import { useTitle } from '../hooks/useTitle';
interface DashboardData { interface DashboardData {
rated_count: number; rated_count: number;
@@ -10,6 +11,7 @@ interface DashboardData {
} }
const Dashboard: React.FC = () => { const Dashboard: React.FC = () => {
useTitle('Dashboard');
const [data, setData] = useState<DashboardData | null>(null); const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -39,17 +41,26 @@ const Dashboard: React.FC = () => {
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{data.my_ratings.map((rating: any) => ( {data.my_ratings.map((rating: any) => (
<Card key={rating.id}> <Card key={rating.id}>
<CardContent> <CardContent sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<Typography variant="h6">{rating.flavor_name}</Typography> {rating.flavor_image && (
<Box sx={{ display: 'flex', alignItems: 'center', my: 1 }}> <Box
<MuiRating value={rating.score} readOnly max={10} size="small" /> component="img"
<Typography variant="body2" sx={{ ml: 1 }}>{rating.score}/10</Typography> src={rating.flavor_image}
</Box> sx={{ width: 60, height: 60, objectFit: 'contain', bgcolor: 'rgba(255,255,255,0.05)', p: 0.5, borderRadius: 1 }}
{rating.comment && ( />
<Typography variant="body2" color="text.secondary">
"{rating.comment}"
</Typography>
)} )}
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ fontSize: '1rem' }}>{rating.flavor_name}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', my: 0.5 }}>
<MuiRating value={rating.score} readOnly max={10} size="small" />
<Typography variant="body2" sx={{ ml: 1, fontSize: '0.8rem' }}>{rating.score}/10</Typography>
</Box>
{rating.comment && (
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.85rem' }}>
"{rating.comment}"
</Typography>
)}
</Box>
</CardContent> </CardContent>
</Card> </Card>
))} ))}
@@ -60,10 +71,31 @@ const Dashboard: React.FC = () => {
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{data.missing_flavors.map((flavor: any) => ( {data.missing_flavors.map((flavor: any) => (
<Card key={flavor.id}> <Card key={flavor.id}>
<CardContent> <CardContent sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
<Typography variant="h6">{flavor.name}</Typography> {flavor.image_url && (
<Typography variant="caption" color="text.secondary">{flavor.category_name}</Typography> <Box
<Typography variant="body2">{flavor.description}</Typography> component="img"
src={flavor.image_url}
sx={{ width: 60, height: 60, objectFit: 'contain', bgcolor: 'rgba(255,255,255,0.05)', p: 0.5, borderRadius: 1 }}
/>
)}
<Box sx={{ flex: 1 }}>
<Typography variant="h6" sx={{ fontSize: '1rem' }}>{flavor.name}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>{flavor.category_name}</Typography>
<Typography variant="body2" sx={{ fontSize: '0.85rem', mb: 1 }}>{flavor.description.substring(0, 100)}...</Typography>
{flavor.shop_url && (
<Button
variant="outlined"
size="small"
component="a"
href={flavor.shop_url}
target="_blank"
sx={{ fontSize: '0.65rem', py: 0 }}
>
Buy Now
</Button>
)}
</Box>
</CardContent> </CardContent>
</Card> </Card>
))} ))}
+45 -2
View File
@@ -30,6 +30,9 @@ interface Flavor {
average_rating: number; average_rating: number;
user_rating: number | null; user_rating: number | null;
ratings: Rating[]; ratings: Rating[];
image_url: string | null;
is_available: boolean;
shop_url: string | null;
} }
interface Category { interface Category {
@@ -92,12 +95,52 @@ const FlavorList: React.FC = () => {
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}> <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
{flavors.filter(f => f.category_name === category.name).map(flavor => ( {flavors.filter(f => f.category_name === category.name).map(flavor => (
<Box sx={{ flex: '1 1 30%', minWidth: 280 }} key={flavor.id}> <Box sx={{ flex: '1 1 30%', minWidth: 280 }} key={flavor.id}>
<Card> <Card sx={{ height: '100%', display: 'flex', flexDirection: 'column', position: 'relative' }}>
<CardContent> {!flavor.is_available && (
<Box sx={{
position: 'absolute',
top: 10,
right: 10,
bgcolor: 'error.main',
color: 'white',
px: 1,
borderRadius: 1,
fontSize: '0.75rem',
zIndex: 1
}}>
Out of Stock
</Box>
)}
{flavor.image_url && (
<Box
component="img"
src={flavor.image_url}
sx={{
width: '100%',
height: 200,
objectFit: 'contain',
p: 2,
bgcolor: 'rgba(255,255,255,0.05)'
}}
/>
)}
<CardContent sx={{ flexGrow: 1 }}>
<Typography variant="h6">{flavor.name}</Typography> <Typography variant="h6">{flavor.name}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{flavor.description} {flavor.description}
</Typography> </Typography>
{flavor.shop_url && (
<Button
variant="outlined"
size="small"
component="a"
href={flavor.shop_url}
target="_blank"
sx={{ mb: 2, fontSize: '0.7rem' }}
>
View in Shop
</Button>
)}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<Typography variant="body2" sx={{ mr: 1 }}>Avg:</Typography> <Typography variant="body2" sx={{ mr: 1 }}>Avg:</Typography>
<MuiRating value={flavor.average_rating || 0} readOnly precision={0.5} max={10} size="small" /> <MuiRating value={flavor.average_rating || 0} readOnly precision={0.5} max={10} size="small" />
+2
View File
@@ -1,11 +1,13 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Typography, TextField, Button, Box, Paper, Tab, Tabs } from '@mui/material'; import { Typography, TextField, Button, Box, Paper, Tab, Tabs } from '@mui/material';
import api from '../api'; import api from '../api';
import { useTitle } from '../hooks/useTitle';
interface LoginProps { interface LoginProps {
} }
const Login: React.FC<LoginProps> = () => { const Login: React.FC<LoginProps> = () => {
useTitle('Login');
const [tab, setTab] = useState(0); const [tab, setTab] = useState(0);
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
@@ -0,0 +1,173 @@
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import {
Typography,
Box,
Card,
CardContent,
Rating as MuiRating,
TextField,
Button,
Accordion,
AccordionSummary,
AccordionDetails,
InputAdornment
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import SearchIcon from '@mui/icons-material/Search';
import api from '../../api';
import { useTitle } from '../../hooks/useTitle';
interface Rating {
id: number;
user: string;
score: number;
comment: string;
created_at: string;
}
interface Flavor {
id: number;
name: string;
category_name: string;
description: string;
average_rating: number;
user_rating: number | null;
ratings: Rating[];
image_url: string | null;
is_available: boolean;
shop_url: string | null;
}
const CategoryFlavors: React.FC = () => {
const { slug } = useParams<{ slug: string }>();
const [flavors, setFlavors] = useState<Flavor[]>([]);
const [categoryName, setCategoryName] = useState('');
useTitle(categoryName || 'Flavors');
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [ratingInput, setRatingInput] = useState<{[key: number]: {score: number, comment: string}}>({});
useEffect(() => {
const fetchFlavors = async () => {
try {
const res = await api.get(`flavors/?category__slug=${slug}`);
setFlavors(res.data);
if (res.data.length > 0) {
setCategoryName(res.data[0].category_name);
}
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
fetchFlavors();
}, [slug]);
const handleRate = async (flavorId: number) => {
const input = ratingInput[flavorId];
if (!input) return;
try {
await api.post('ratings/', { flavor: flavorId, score: input.score, comment: input.comment });
const res = await api.get(`flavors/?category__slug=${slug}`);
setFlavors(res.data);
alert('Rating submitted!');
} catch (err) {
alert("Failed to submit rating. Make sure you are logged in.");
}
};
const filteredFlavors = flavors.filter(f =>
f.name.toLowerCase().includes(search.toLowerCase()) ||
f.description.toLowerCase().includes(search.toLowerCase())
);
if (loading) return <Typography>Loading Flavors...</Typography>;
return (
<Box>
<Typography variant="h4" gutterBottom>{categoryName || 'Flavors'}</Typography>
<TextField
fullWidth
variant="outlined"
placeholder={`Search ${categoryName} flavors...`}
value={search}
onChange={(e) => setSearch(e.target.value)}
sx={{ mb: 4 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
{filteredFlavors.map(flavor => (
<Box sx={{ flex: '1 1 30%', minWidth: 280 }} key={flavor.id}>
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column', position: 'relative' }}>
{!flavor.is_available && (
<Box sx={{ position: 'absolute', top: 10, right: 10, bgcolor: 'error.main', color: 'white', px: 1, borderRadius: 1, fontSize: '0.75rem', zIndex: 1 }}>
Out of Stock
</Box>
)}
{flavor.image_url && (
<Box component="img" src={flavor.image_url} sx={{ width: '100%', height: 200, objectFit: 'contain', p: 2, bgcolor: 'rgba(255,255,255,0.05)' }} />
)}
<CardContent sx={{ flexGrow: 1 }}>
<Typography variant="h6">{flavor.name}</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>{flavor.description}</Typography>
{flavor.shop_url && (
<Button variant="outlined" size="small" component="a" href={flavor.shop_url} target="_blank" sx={{ mb: 2, fontSize: '0.7rem' }}>
View in Shop
</Button>
)}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<Typography variant="body2" sx={{ mr: 1 }}>Avg:</Typography>
<MuiRating value={flavor.average_rating || 0} readOnly precision={0.5} max={10} size="small" />
<Typography variant="body2" sx={{ ml: 1 }}>({(flavor.average_rating || 0).toFixed(1)})</Typography>
</Box>
{flavor.ratings && flavor.ratings.length > 0 && (
<Accordion variant="outlined" sx={{ mt: 1, mb: 1 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="caption">Comments ({flavor.ratings.filter(r => r.comment).length})</Typography>
</AccordionSummary>
<AccordionDetails sx={{ maxHeight: 200, overflow: 'auto', p: 1 }}>
{flavor.ratings.filter(r => r.comment).map(r => (
<Box key={r.id} sx={{ mb: 1, borderBottom: '1px solid rgba(0,0,0,0.1)', pb: 0.5 }}>
<Typography variant="caption" sx={{ fontWeight: 'bold', display: 'block' }}>{r.user} ({r.score}/10):</Typography>
<Typography variant="body2" sx={{ fontSize: '0.8rem' }}>{r.comment}</Typography>
</Box>
))}
</AccordionDetails>
</Accordion>
)}
{flavor.user_rating ? (
<Typography variant="body2" color="secondary">Your Rating: {flavor.user_rating}/10</Typography>
) : (
<Accordion variant="outlined" sx={{ mt: 2 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="body2">Rate this flavor</Typography>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<MuiRating max={10} value={ratingInput[flavor.id]?.score || 0} onChange={(_, val) => setRatingInput({...ratingInput, [flavor.id]: {...(ratingInput[flavor.id] || {comment: ''}), score: val || 0}})} />
<TextField label="Comment (Optional)" variant="outlined" size="small" multiline value={ratingInput[flavor.id]?.comment || ''} onChange={(e) => setRatingInput({...ratingInput, [flavor.id]: {...(ratingInput[flavor.id] || {score: 0}), comment: e.target.value}})} />
<Button variant="contained" size="small" onClick={() => handleRate(flavor.id)}>Submit</Button>
</Box>
</AccordionDetails>
</Accordion>
)}
</CardContent>
</Card>
</Box>
))}
</Box>
</Box>
);
};
export default CategoryFlavors;