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)}
>
-
-
-
-
+
+
+
+
+
+
@@ -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,
}
}
}