From 6b26b77e0e46b2bada696c5695b957d0dc2a0a86 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 16 Feb 2026 00:14:28 +0100 Subject: [PATCH] updated user features and added palette --- .env.example | 1 + .../api/management/commands/sync_flavors.py | 27 +- .../api/migrations/0005_alter_user_theme.py | 18 ++ backend/api/migrations/0006_reply.py | 28 ++ backend/api/models.py | 13 + backend/api/serializers.py | 13 +- backend/api/views.py | 112 ++++--- frontend/src/App.tsx | 12 +- .../src/pages/categories/CategoryFlavors.tsx | 94 ++---- frontend/src/pages/flavors/FlavorDetail.tsx | 297 ++++++++++++++++++ frontend/src/theme.ts | 40 ++- 11 files changed, 515 insertions(+), 140 deletions(-) create mode 100644 backend/api/migrations/0005_alter_user_theme.py create mode 100644 backend/api/migrations/0006_reply.py create mode 100644 frontend/src/pages/flavors/FlavorDetail.tsx diff --git a/.env.example b/.env.example index 8a18e92..a0dd66c 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,7 @@ CSRF_TRUSTED_ORIGINS=http://localhost:5173 EMAIL_HOST=smtp.example.com EMAIL_PORT=587 EMAIL_HOST_USER=your-email@example.com +# Do not wrap the password in quotes or backticks EMAIL_HOST_PASSWORD=your-app-password # --- Optional (defaults are usually fine) --- diff --git a/backend/api/management/commands/sync_flavors.py b/backend/api/management/commands/sync_flavors.py index 57a1132..bc6afe3 100644 --- a/backend/api/management/commands/sync_flavors.py +++ b/backend/api/management/commands/sync_flavors.py @@ -35,6 +35,7 @@ class Command(BaseCommand): '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], + 'packs': Category.objects.get_or_create(name='Packs and other', defaults={'slug': 'packs-and-other'})[0], } created_count = 0 @@ -43,14 +44,30 @@ class Command(BaseCommand): for p in products: title = p.get('title') tags = p.get('tags', []) + product_type = p.get('product_type', '').lower() if isinstance(tags, str): tags = [t.strip() for t in tags.split(',')] # Determine category category = None tags_lower = [t.lower() for t in tags] + title_lower = title.lower() + + # Check for Packs/Bundles/Samples first + is_pack = False + pack_keywords = ['bundle', 'pack', 'set', 'box', 'probe', 'sample', 'taster', 'shaker', 'starter'] - if 'holy energy' in tags_lower or 'energy' in tags_lower: + # Check title keywords + if any(k in title_lower for k in pack_keywords): + is_pack = True + + # Check product type keywords + if any(k in product_type for k in pack_keywords): + is_pack = True + + if is_pack: + category = cat_map['packs'] + elif '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'] @@ -59,8 +76,12 @@ class Command(BaseCommand): 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: + # Skip only pure merch (clothing etc) if it's not a pack + if not category: + if 'merch' in tags_lower: + continue + # If we still don't have a category but it's a product, maybe put it in Packs/Other as fallback? + # For now, let's skip to avoid clutter unless we are sure. continue # Check availability diff --git a/backend/api/migrations/0005_alter_user_theme.py b/backend/api/migrations/0005_alter_user_theme.py new file mode 100644 index 0000000..277f4cc --- /dev/null +++ b/backend/api/migrations/0005_alter_user_theme.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.2 on 2026-02-15 22:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0004_user_email_confirmation_code_user_pending_email'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='theme', + field=models.CharField(choices=[('latte', 'Latte'), ('frappe', 'Frappé'), ('macchiato', 'Macchiato'), ('mocha', 'Mocha'), ('pink', 'Pastel Pink')], default='mocha', max_length=20), + ), + ] diff --git a/backend/api/migrations/0006_reply.py b/backend/api/migrations/0006_reply.py new file mode 100644 index 0000000..5334234 --- /dev/null +++ b/backend/api/migrations/0006_reply.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.2 on 2026-02-15 22:57 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0005_alter_user_theme'), + ] + + operations = [ + migrations.CreateModel( + name='Reply', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('rating', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='replies', to='api.rating')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='replies', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['created_at'], + }, + ), + ] diff --git a/backend/api/models.py b/backend/api/models.py index e83354e..ebe67dd 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -8,6 +8,7 @@ class User(AbstractUser): ('frappe', 'Frappé'), ('macchiato', 'Macchiato'), ('mocha', 'Mocha'), + ('pink', 'Pastel Pink'), ] theme = models.CharField(max_length=20, choices=THEME_CHOICES, default='mocha') pending_email = models.EmailField(max_length=254, blank=True, null=True) @@ -59,3 +60,15 @@ class Rating(models.Model): def __str__(self): return f"{self.user.username} - {self.flavor.name}: {self.score}" + +class Reply(models.Model): + user = models.ForeignKey(User, related_name='replies', on_delete=models.CASCADE) + rating = models.ForeignKey(Rating, related_name='replies', on_delete=models.CASCADE) + text = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['created_at'] + + def __str__(self): + return f"Reply by {self.user.username} on {self.rating.id}" diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 81ab79b..b75a4e6 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import User, Flavor, Category, Rating +from .models import User, Flavor, Category, Rating, Reply class UserSerializer(serializers.ModelSerializer): class Meta: @@ -11,14 +11,23 @@ class CategorySerializer(serializers.ModelSerializer): model = Category fields = ['id', 'name', 'slug'] +class ReplySerializer(serializers.ModelSerializer): + user = serializers.StringRelatedField(read_only=True) + + class Meta: + model = Reply + fields = ['id', 'user', 'rating', 'text', 'created_at'] + read_only_fields = ['rating'] + class RatingSerializer(serializers.ModelSerializer): user = serializers.StringRelatedField(read_only=True) flavor_name = serializers.CharField(source='flavor.name', read_only=True) flavor_image = serializers.CharField(source='flavor.image_url', read_only=True) + replies = ReplySerializer(many=True, read_only=True) class Meta: model = Rating - fields = ['id', 'user', 'flavor', 'flavor_name', 'flavor_image', 'score', 'comment', 'created_at'] + fields = ['id', 'user', 'flavor', 'flavor_name', 'flavor_image', 'score', 'comment', 'created_at', 'replies'] class FlavorSerializer(serializers.ModelSerializer): category_name = serializers.CharField(source='category.name', read_only=True) diff --git a/backend/api/views.py b/backend/api/views.py index 1f0541c..a2bb444 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -4,11 +4,10 @@ from rest_framework.response import Response from rest_framework.decorators import action from django.db.models import Avg from django_filters.rest_framework import DjangoFilterBackend -from .models import User, Flavor, Category, Rating -from .serializers import UserSerializer, FlavorSerializer, CategorySerializer, RatingSerializer +from .models import User, Flavor, Category, Rating, Reply +from .serializers import UserSerializer, FlavorSerializer, CategorySerializer, RatingSerializer, ReplySerializer class FlavorViewSet(viewsets.ModelViewSet): -# ... (rest of FlavorViewSet) queryset = Flavor.objects.annotate(average_rating=Avg('ratings__score')).order_by('-average_rating') serializer_class = FlavorSerializer filter_backends = [DjangoFilterBackend] @@ -28,12 +27,35 @@ class RatingViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticatedOrReadOnly] def perform_create(self, serializer): + # Ensure user can only have one rating per flavor + flavor = serializer.validated_data['flavor'] + if Rating.objects.filter(user=self.request.user, flavor=flavor).exists(): + raise serializers.ValidationError('You have already rated this flavor.') serializer.save(user=self.request.user) def get_queryset(self): - if self.action == 'list': - return Rating.objects.select_related('user', 'flavor') - return Rating.objects.filter(user=self.request.user) + if self.action in ['list', 'retrieve']: + return Rating.objects.select_related('user', 'flavor').prefetch_related('replies', 'replies__user') + # Allow users to see only their own ratings when editing/deleting? + # No, for detail views they need to find it. + # The permission class IsOwnerOrReadOnly (custom) would be better, + # but standard ViewSet logic handles "get_object" which we can protect. + return Rating.objects.select_related('user', 'flavor').prefetch_related('replies', 'replies__user') + + def check_object_permissions(self, request, obj): + super().check_object_permissions(request, obj) + if request.method in ['PUT', 'PATCH', 'DELETE'] and obj.user != request.user: + self.permission_denied(request, message='You cannot edit/delete this rating.') + + @action(detail=True, methods=['post'], permission_classes=[permissions.IsAuthenticated]) + def reply(self, request, pk=None): + rating = self.get_object() + text = request.data.get('text') + if not text: + return Response({'error': 'Text is required'}, status=status.HTTP_400_BAD_REQUEST) + + reply = Reply.objects.create(user=request.user, rating=rating, text=text) + return Response(ReplySerializer(reply).data, status=status.HTTP_201_CREATED) class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all() @@ -105,45 +127,6 @@ class UserViewSet(viewsets.ModelViewSet): else: return Response({'error': 'Invalid verification code'}, status=status.HTTP_400_BAD_REQUEST) - @action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny]) - def request_password_reset(self, request): - email = request.data.get('email') - try: - user = User.objects.get(email=email, is_active=True) - except User.DoesNotExist: - return Response({'status': 'If an account exists with this email, a reset code has been sent.'}) - - import secrets - import string - code = ''.join(secrets.choice(string.digits) for _ in range(6)) - user.email_confirmation_code = code - user.save() - - send_mail( - 'Password Reset Request', - f'Hi {user.username},\n\nYour password reset code is: {code}', - 'noreply@holyflavors.com', - [email], - fail_silently=False, - ) - return Response({'status': 'If an account exists with this email, a reset code has been sent.'}) - - @action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny]) - def complete_password_reset(self, request): - email = request.data.get('email') - code = request.data.get('code') - new_password = request.data.get('password') - - try: - user = User.objects.get(email=email, email_confirmation_code=code) - except User.DoesNotExist: - return Response({'error': 'Invalid email or code'}, status=status.HTTP_400_BAD_REQUEST) - - user.set_password(new_password) - user.email_confirmation_code = None - user.save() - return Response({'status': 'Password reset successful!'}) - @action(detail=False, methods=['get']) def me(self, request): serializer = self.get_serializer(request.user) @@ -217,6 +200,45 @@ class UserViewSet(viewsets.ModelViewSet): user.save() return Response({'status': 'Email confirmed', 'email': user.email}) + @action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny]) + def request_password_reset(self, request): + email = request.data.get('email') + try: + user = User.objects.get(email=email, is_active=True) + except User.DoesNotExist: + return Response({'status': 'If an account exists with this email, a reset code has been sent.'}) + + import secrets + import string + code = ''.join(secrets.choice(string.digits) for _ in range(6)) + user.email_confirmation_code = code + user.save() + + send_mail( + 'Password Reset Request', + f'Hi {user.username},\n\nYour password reset code is: {code}', + 'noreply@holyflavors.com', + [email], + fail_silently=False, + ) + return Response({'status': 'If an account exists with this email, a reset code has been sent.'}) + + @action(detail=False, methods=['post'], permission_classes=[permissions.AllowAny]) + def complete_password_reset(self, request): + email = request.data.get('email') + code = request.data.get('code') + new_password = request.data.get('password') + + try: + user = User.objects.get(email=email, email_confirmation_code=code) + except User.DoesNotExist: + return Response({'error': 'Invalid email or code'}, status=status.HTTP_400_BAD_REQUEST) + + user.set_password(new_password) + user.email_confirmation_code = None + user.save() + return Response({'status': 'Password reset successful!'}) + @action(detail=False, methods=['get']) def dashboard(self, request): user = request.user diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3900588..d5c7b19 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -25,6 +25,7 @@ import MenuIcon from '@mui/icons-material/Menu'; import { getTheme, type CatppuccinTheme } from './theme'; import CategoryList from './pages/CategoryList'; import CategoryFlavors from './pages/categories/CategoryFlavors'; +import FlavorDetail from './pages/flavors/FlavorDetail'; import Dashboard from './pages/Dashboard'; import Login from './pages/Login'; import Settings from './pages/Settings'; @@ -154,10 +155,12 @@ const App: React.FC = () => { onClose={() => setAnchorEl(null)} > handleThemeChange('latte')}>Latte - handleThemeChange('frappe')}>Frappé - handleThemeChange('macchiato')}>Macchiato - handleThemeChange('mocha')}>Mocha - + handleThemeChange('frappe')}>Frappé + handleThemeChange('macchiato')}>Macchiato + handleThemeChange('mocha')}>Mocha + handleThemeChange('pink')}>Pastel Pink + + @@ -181,6 +184,7 @@ const App: React.FC = () => { } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/categories/CategoryFlavors.tsx b/frontend/src/pages/categories/CategoryFlavors.tsx index 3dc7388..e572d6a 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 } from 'react-router-dom'; +import { useParams, Link } from 'react-router-dom'; import { Typography, Box, @@ -7,13 +7,8 @@ import { CardContent, Rating as MuiRating, TextField, - Button, - Accordion, - AccordionSummary, - AccordionDetails, - InputAdornment + 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'; @@ -46,7 +41,6 @@ const CategoryFlavors: React.FC = () => { 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 () => { @@ -65,19 +59,6 @@ const CategoryFlavors: React.FC = () => { 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()) @@ -108,60 +89,27 @@ const CategoryFlavors: React.FC = () => { {filteredFlavors.map(flavor => ( - - {!flavor.is_available && ( - - Out of Stock - - )} - {flavor.image_url && ( - - )} - - {flavor.name} - {flavor.description} - {flavor.shop_url && ( - + + + {!flavor.is_available && ( + + Out of Stock + )} - - Avg: - - ({(flavor.average_rating || 0).toFixed(1)}) - - {flavor.ratings && flavor.ratings.length > 0 && ( - - }> - Comments ({flavor.ratings.filter(r => r.comment).length}) - - - {flavor.ratings.filter(r => r.comment).map(r => ( - - {r.user} ({r.score}/10): - {r.comment} - - ))} - - + {flavor.image_url && ( + )} - {flavor.user_rating ? ( - Your Rating: {flavor.user_rating}/10 - ) : ( - - }> - Rate this flavor - - - - setRatingInput({...ratingInput, [flavor.id]: {...(ratingInput[flavor.id] || {comment: ''}), score: val || 0}})} /> - setRatingInput({...ratingInput, [flavor.id]: {...(ratingInput[flavor.id] || {score: 0}), comment: e.target.value}})} /> - - - - - )} - + + {flavor.name} + {flavor.description.substring(0, 100)}... + + + + ({(flavor.average_rating || 0).toFixed(1)}) + + {flavor.ratings.length} reviews + + ))} diff --git a/frontend/src/pages/flavors/FlavorDetail.tsx b/frontend/src/pages/flavors/FlavorDetail.tsx new file mode 100644 index 0000000..687c0d4 --- /dev/null +++ b/frontend/src/pages/flavors/FlavorDetail.tsx @@ -0,0 +1,297 @@ +import React, { useEffect, useState } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { + Typography, + Box, + Card, + CardContent, + Rating as MuiRating, + Button, + TextField, + Avatar +} from '@mui/material'; +import api from '../../api'; +import { useTitle } from '../../hooks/useTitle'; + +interface Reply { + id: number; + user: string; + text: string; + created_at: string; +} + +interface Rating { + id: number; + user: string; + score: number; + comment: string; + created_at: string; + replies: Reply[]; +} + +interface Flavor { + id: number; + name: string; + category_name: string; + category_slug: string; + description: string; + average_rating: number; + user_rating: number | null; + ratings: Rating[]; + image_url: string | null; + is_available: boolean; + shop_url: string | null; +} + +const FlavorDetail: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const [flavor, setFlavor] = useState(null); + const [loading, setLoading] = useState(true); + const [replyInputs, setReplyInputs] = useState<{[key: number]: string}>({}); + const [currentUser, setCurrentUser] = useState(null); + const [editMode, setEditMode] = useState(null); + const [editScore, setEditScore] = useState(0); + const [editComment, setEditComment] = useState(''); + + useEffect(() => { + const getUser = async () => { + try { + const res = await api.get('users/me/'); + setCurrentUser(res.data.username); + } catch (e) { /* ignore */ } + }; + getUser(); + }, []); + + const fetchFlavor = async () => { + try { + const res = await api.get(`flavors/${id}/`); + setFlavor(res.data); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchFlavor(); + }, [id]); + + useTitle(flavor?.name || 'Flavor Detail'); + + const handleReplySubmit = async (ratingId: number) => { + const text = replyInputs[ratingId]; + if (!text) return; + + try { + await api.post(`ratings/${ratingId}/reply/`, { text }); + setReplyInputs({ ...replyInputs, [ratingId]: '' }); + fetchFlavor(); // Refresh to show new reply + } catch (err) { + alert('Failed to submit reply'); + } + }; + + const handleDeleteRating = async (ratingId: number) => { + if (!confirm('Are you sure you want to delete your review?')) return; + try { + await api.delete(`ratings/${ratingId}/`); + fetchFlavor(); + } catch (err) { + alert('Failed to delete review'); + } + }; + + const startEdit = (rating: Rating) => { + setEditMode(rating.id); + setEditScore(rating.score); + setEditComment(rating.comment || ''); + }; + + const handleUpdateRating = async (ratingId: number) => { + try { + await api.patch(`ratings/${ratingId}/`, { score: editScore, comment: editComment }); + setEditMode(null); + fetchFlavor(); + } catch (err) { + alert('Failed to update review'); + } + }; + + if (loading) return Loading...; + if (!flavor) return Flavor not found; + + return ( + + + + + {flavor.image_url && ( + + + + )} + + {!flavor.is_available && ( + + Out of Stock + + )} + {flavor.name} + {flavor.category_name} + + + + {(flavor.average_rating || 0).toFixed(1)} / 10 + ({flavor.ratings.length} ratings) + + + + {flavor.description} + + + {flavor.shop_url && ( + + )} + + + + Community Reviews + + + {flavor.ratings.length === 0 ? ( + No reviews yet. Be the first to rate this flavor! + ) : ( + flavor.ratings.map((rating: Rating) => ( + + + {editMode === rating.id ? ( + + Edit Review + setEditScore(val || 0)} /> + setEditComment(e.target.value)} + /> + + + + + + ) : ( + <> + + + {rating.user.charAt(0).toUpperCase()} + + {rating.user} + + + {rating.score}/10 + + + {rating.comment && ( + {rating.comment} + )} + + + + {new Date(rating.created_at).toLocaleDateString()} + + {currentUser === rating.user && ( + + + + + )} + + + )} + + {/* Replies Section */} + {(rating.replies.length > 0 || true) && ( // Always show reply section essentially + + {rating.replies.map((reply: Reply) => ( + + + {reply.user} + + {reply.text} + + {new Date(reply.created_at).toLocaleDateString()} + + + ))} + + + setReplyInputs({ ...replyInputs, [rating.id]: e.target.value })} + /> + + + + )} + + + )) + )} + + + ); +}; + +export default FlavorDetail; diff --git a/frontend/src/theme.ts b/frontend/src/theme.ts index a02f4c1..815325d 100644 --- a/frontend/src/theme.ts +++ b/frontend/src/theme.ts @@ -1,27 +1,41 @@ import { createTheme, type ThemeOptions } from '@mui/material/styles'; import { flavors } from '@catppuccin/palette'; -export type CatppuccinTheme = 'latte' | 'frappe' | 'macchiato' | 'mocha'; +export type CatppuccinTheme = 'latte' | 'frappe' | 'macchiato' | 'mocha' | 'pink'; export const getTheme = (mode: CatppuccinTheme) => { - const flavor = flavors[mode].colors; + let palette; + + if (mode === 'pink') { + palette = { + base: { hex: '#fff0f5' }, // Lavender Blush + surface0: { hex: '#ffe4e1' }, // Misty Rose + crust: { hex: '#ffc0cb' }, // Pink + text: { hex: '#5c4b51' }, // Darker text for contrast + subtext0: { hex: '#8b6b7a' }, + mauve: { hex: '#ff69b4' }, // Hot Pink (Primary) + pink: { hex: '#db7093' }, // Pale Violet Red (Secondary) + }; + } else { + palette = flavors[mode].colors; + } const themeOptions: ThemeOptions = { palette: { - mode: mode === 'latte' ? 'light' : 'dark', + mode: (mode === 'latte' || mode === 'pink') ? 'light' : 'dark', primary: { - main: flavor.mauve.hex, + main: palette.mauve.hex, }, secondary: { - main: flavor.pink.hex, + main: palette.pink.hex, }, background: { - default: flavor.base.hex, - paper: flavor.surface0.hex, + default: palette.base.hex, + paper: palette.surface0.hex, }, text: { - primary: flavor.text.hex, - secondary: flavor.subtext0.hex, + primary: palette.text.hex, + secondary: palette.subtext0.hex, }, }, typography: { @@ -31,16 +45,16 @@ export const getTheme = (mode: CatppuccinTheme) => { MuiCard: { styleOverrides: { root: { - backgroundColor: flavor.surface0.hex, - color: flavor.text.hex, + backgroundColor: palette.surface0.hex, + color: palette.text.hex, } } }, MuiAppBar: { styleOverrides: { root: { - backgroundColor: flavor.crust.hex, - color: flavor.text.hex, + backgroundColor: palette.crust.hex, + color: palette.text.hex, } } }