diff --git a/backend/api/management/commands/seed_legacy_flavors.py b/backend/api/management/commands/seed_legacy_flavors.py index 0f466bc..42475f9 100644 --- a/backend/api/management/commands/seed_legacy_flavors.py +++ b/backend/api/management/commands/seed_legacy_flavors.py @@ -1,144 +1,95 @@ +import json +import os +import urllib.request from django.core.management.base import BaseCommand +from django.core.files import File +from tempfile import NamedTemporaryFile +from django.conf import settings from api.models import Category, Flavor class Command(BaseCommand): - help = 'Adds legacy flavors that are no longer in the official API or are old versions' + help = 'Adds legacy flavors from JSON files in the legacy/ folder' + + def download_image(self, url, flavor_name): + if not url: + return None + try: + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) + with urllib.request.urlopen(req) as response: + img_temp = NamedTemporaryFile(delete=True) + img_temp.write(response.read()) + img_temp.flush() + + ext = url.split('.')[-1].split('?')[0] + if len(ext) > 4: ext = 'png' + filename = f"legacy_{flavor_name.replace(' ', '_').lower()}.{ext}" + return File(img_temp, name=filename) + except Exception as e: + self.stdout.write(self.style.WARNING(f"Could not download image for {flavor_name}: {e}")) + return None def handle(self, *args, **options): - # Ensure categories exist - energy = Category.objects.get(slug='energy') - hydration = Category.objects.get(slug='hydration') - iced_tea = Category.objects.get(slug='iced-tea') + # 1. Remove old legacy flavors (ones without external_id or marked as legacy) + # To be safe, we only remove those marked as legacy or with no external_id + removed_count, _ = Flavor.objects.filter(is_legacy=True).delete() + Flavor.objects.filter(external_id__isnull=True).delete() + self.stdout.write(self.style.SUCCESS(f"Removed {removed_count} old legacy flavors.")) - legacy_data = [ - { - "Name": "Hitschies Saure Drachenzunge (Legacy)", - "Geschmack": "Blaue Himbeere", - "Beschreibung": "Eine legendäre Kollaboration mit Hitschies. Schmeckt wie die blauen sauren Drachenzungen – süß, fruchtig und sauer.", - "Status": "Limited Edition / Vaulted", - "Bild_URL": "https://weareholy.com/cdn/shop/files/Holy_Energy_Hitschies_Sour_Dragon_Tongue_Tub_720x.png" - }, - { - "Name": "Hitschies Saure Drachenzunge Rot (Legacy)", - "Geschmack": "Erdbeere & Kirsche", - "Beschreibung": "Die rote Variante der Hitschies-Kollaboration. Fruchtiger Mix aus roten Beeren, angelehnt an die roten Drachenzungen.", - "Status": "Limited Edition", - "Bild_URL": "https://weareholy.com/cdn/shop/files/Holy_Energy_Hitschies_Sour_Dragon_Tongue_Red_Tub_720x.png" - }, - { - "Name": "Lion's Lemonade (Legacy)", - "Geschmack": "Mango & Kiwi", - "Beschreibung": "Einer der beliebtesten Klassiker. Ein tropischer, fruchtiger Mix für maximalen Fokus. Oft von Fans zurückgefordert.", - "Status": "Vaulted", - "Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Lions_Lemonade_Tub_720x.png" - }, - { - "Name": "Lollipop Lovebird (Legacy)", - "Geschmack": "Kirsch-Lolli", - "Beschreibung": "Erinnert an die klassischen roten Kirsch-Lollies aus der Kindheit. Sehr süß und intensiv.", - "Status": "Limited Edition", - "Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Lollipop_Lovebird_Tub_720x.png" - }, - { - "Name": "Frosty Fox (Legacy)", - "Geschmack": "Eisbonbon", - "Beschreibung": "Ein kühler, süßer Geschmack, der an die klassischen Eisbonbons erinnert. Erfrischend mit einer 'kühlen' Note.", - "Status": "Limited Edition", - "Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Frosty_Fox_Tub_720x.png" - }, - { - "Name": "Bubble Gum Butterfly (Legacy)", - "Geschmack": "Kaugummi", - "Beschreibung": "Der typische rosa Kaugummi-Geschmack (Bubblegum). Sehr süß und nostalgisch.", - "Status": "Limited Edition", - "Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Bubblegum_Butterfly_Tub_720x.png" - }, - { - "Name": "Woodruff Wolf (Legacy)", - "Geschmack": "Waldmeister", - "Beschreibung": "Ein traditioneller deutscher Geschmack. Süß-würzig, wie Wackelpudding oder Waldmeister-Sirup.", - "Status": "Limited Edition", - "Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Woodruff_Wolf_Tub_720x.png" - }, - { - "Name": "Baked Apple Boar (Legacy)", - "Geschmack": "Bratapfel", - "Beschreibung": "Eine winterliche Sonderedition mit warmen Noten von Apfel und Zimt.", - "Status": "Seasonal / Vaulted", - "Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Baked_Apple_Boar_Tub_720x.png" - }, - { - "Name": "Kölle Kamelle (Legacy)", - "Geschmack": "Fruchtbonbons (Karnevalsmischung)", - "Beschreibung": "Spezialedition zum Kölner Karneval. Schmeckt nach einem Mix aus verschiedenen Fruchtbonbons ('Kamelle').", - "Status": "Limited Edition", - "Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Koelle_Kamelle_Tub_720x.png" - }, - { - "Name": "Strawberry Shark (Legacy)", - "Geschmack": "Erdbeere & Mandarine", - "Beschreibung": "Ein fruchtiger Mix aus süßer Erdbeere und spritziger Mandarine. Teil des ursprünglichen 'Squads'.", - "Status": "Discontinued", - "Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Strawberry_Shark_Tub_720x.png" - }, - { - "Name": "Blueberry Bear (Legacy)", - "Geschmack": "Blaubeere & Kokosnuss", - "Beschreibung": "Eine weiche Beeren-Note mit einem exotischen Kokos-Abgang.", - "Status": "Discontinued", - "Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Blueberry_Bear_Tub_720x.png" - }, - { - "Name": "Thai Lime Toucan (Legacy)", - "Geschmack": "Kaffir-Limette", - "Beschreibung": "Sehr aromatisch und zesty. Unterscheidet sich von normaler Zitrone durch die herbe Kaffir-Note.", - "Status": "Discontinued", - "Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Thai_Lime_Toucan_Tub_720x.png" - }, - { - "Name": "Cactus Camel (Legacy)", - "Geschmack": "Kaktusfeige", - "Beschreibung": "Erfrischend, leicht melonenartig und nicht zu süß. Ein sehr eigener, beliebter Geschmack.", - "Status": "Discontinued", - "Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Cactus_Camel_Tub_720x.png" - }, - { - "Name": "Apple Alligator (Legacy)", - "Geschmack": "Grüner Apfel", - "Beschreibung": "Knackiger, saurer Apfelgeschmack. Sehr erfrischend.", - "Status": "Discontinued", - "Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Apple_Alligator_Tub_720x.png" - }, - { - "Name": "Cherry Cheetah (Legacy)", - "Geschmack": "Kirsche", - "Beschreibung": "Klassischer, fruchtiger Kirschgeschmack.", - "Status": "Discontinued", - "Bild_URL": "https://weareholy.com/cdn/shop/files/HOLY_Energy_Cherry_Cheetah_Tub_720x.png" - } - ] + # Ensure main categories exist + 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] + packs = Category.objects.get_or_create(name='Packs and other', defaults={'slug': 'packs-and-other'})[0] - for data in legacy_data: - full_description = f"**Geschmack:** {data['Geschmack']}\n\n{data['Beschreibung']}\n\n**Status:** {data['Status']}" + legacy_dir = os.path.join(settings.BASE_DIR, '..', 'legacy') + if not os.path.exists(legacy_dir): + self.stdout.write(self.style.ERROR(f"Legacy directory not found at {legacy_dir}")) + return + + json_files = [f for f in os.listdir(legacy_dir) if f.endswith('.json')] + + created_count = 0 + + for json_file in json_files: + file_path = os.path.join(legacy_dir, json_file) + with open(file_path, 'r', encoding='utf-8') as f: + data_list = json.load(f) - cat = energy # Default - if "hydration" in data['Name'].lower() or "hydration" in data['Beschreibung'].lower(): - cat = hydration - elif "iced tea" in data['Name'].lower() or "eistee" in data['Name'].lower(): - cat = iced_tea + for data in data_list: + name = data.get('Name') + desc = data.get('Beschreibung') + taste = data.get('Geschmack') + status_text = data.get('Status') + image_url = data.get('Bild_URL') - flavor, created = Flavor.objects.update_or_create( - name=data['Name'], - defaults={ - 'category': cat, - 'description': full_description, - 'image_url': data['Bild_URL'], - 'is_available': False, - 'external_id': None - } - ) + full_desc = f"**Geschmack:** {taste}\n\n{desc}\n\n**Status:** {status_text}" + + # Determine category + cat = energy + if "hydration" in name.lower() or "hydration" in desc.lower(): + cat = hydration + elif "iced tea" in name.lower() or "eistee" in name.lower(): + cat = iced_tea + elif any(k in name.lower() for k in ['bundle', 'set', 'box', 'probe', 'sample', 'taster', 'shaker', 'starter']): + cat = packs - if created: - self.stdout.write(self.style.SUCCESS(f"Added legacy flavor: {data['Name']}")) - else: - self.stdout.write(self.style.SUCCESS(f"Updated legacy flavor: {data['Name']}")) + flavor = Flavor( + name=name, + category=cat, + description=full_desc, + image_url=image_url, + is_available=False, + is_legacy=True, + external_id=None + ) + + if image_url: + img_file = self.download_image(image_url, name) + if img_file: + flavor.image.save(img_file.name, img_file, save=False) + + flavor.save() + created_count += 1 + self.stdout.write(f"Added legacy flavor: {name}") + + self.stdout.write(self.style.SUCCESS(f"Finished! Created {created_count} legacy flavors from JSON.")) diff --git a/backend/api/management/commands/sync_flavors.py b/backend/api/management/commands/sync_flavors.py index c6ed03b..a1943fd 100644 --- a/backend/api/management/commands/sync_flavors.py +++ b/backend/api/management/commands/sync_flavors.py @@ -1,8 +1,10 @@ import json import urllib.request import re +import os from django.core.management.base import BaseCommand -from django.utils.text import slugify +from django.core.files import File +from tempfile import NamedTemporaryFile from api.models import Category, Flavor class Command(BaseCommand): @@ -15,12 +17,34 @@ class Command(BaseCommand): cleantext = re.sub(cleanr, '', raw_html) return cleantext.strip() + def download_image(self, url, flavor_name): + if not url: + return None + try: + # Simple caching: check if we already have a local image for this URL + # (In a real app, you might compare filenames or hashes) + img_temp = NamedTemporaryFile(delete=True) + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) + with urllib.request.urlopen(req) as response: + img_temp.write(response.read()) + img_temp.flush() + + # Use extension from URL or default to .png + ext = url.split('.')[-1].split('?')[0] + if len(ext) > 4: ext = 'png' + filename = f"{flavor_name.replace(' ', '_').lower()}.{ext}" + return File(img_temp, name=filename) + except Exception as e: + self.stdout.write(self.style.WARNING(f"Could not download image for {flavor_name}: {e}")) + return None + 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: + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) + with urllib.request.urlopen(req) as response: data = json.loads(response.read().decode()) except Exception as e: self.stdout.write(self.style.ERROR(f"Failed to fetch data: {e}")) @@ -54,7 +78,6 @@ class Command(BaseCommand): tags_lower = [t.lower() for t in tags] title_lower = title.lower() - # Check for Packs/Bundles/Samples is_pack = False pack_keywords = ['bundle', 'set', 'box', 'probe', 'sample', 'taster', 'starter', 'collection', 'probier'] if any(k in title_lower for k in pack_keywords): @@ -62,7 +85,6 @@ class Command(BaseCommand): if 'shaker' in title_lower or 'merch' in tags_lower: is_pack = True - # Determine Category if is_pack: category = cat_map['packs'] elif 'holy energy' in tags_lower or 'energy' in tags_lower: @@ -80,52 +102,59 @@ class Command(BaseCommand): else: 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 flavor = Flavor.objects.filter(external_id=p['id']).first() if not flavor: - flavor = Flavor.objects.filter(name=title).first() + flavor = Flavor.objects.filter(name=title, is_legacy=False).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.is_legacy = False # API flavors are not legacy + + # Cache image if URL changed or local image missing + if image_url and (flavor.image_url != image_url or not flavor.image): + img_file = self.download_image(image_url, title) + if img_file: + flavor.image.save(img_file.name, img_file, save=False) + + flavor.image_url = image_url flavor.save() updated_count += 1 else: - Flavor.objects.create( + new_flavor = Flavor( external_id=p['id'], name=title, category=category, description=description, image_url=image_url, shop_url=shop_url, - is_available=is_available + is_available=is_available, + is_legacy=False ) + if image_url: + img_file = self.download_image(image_url, title) + if img_file: + new_flavor.image.save(img_file.name, img_file, save=False) + new_flavor.save() created_count += 1 synced_external_ids.append(p['id']) # Mark flavors NOT in the sync as unavailable (only for those that have an external_id) - # This keeps manually added flavors (with external_id=None) as they are. + # These are discontinued API flavors. discontinued_count = Flavor.objects.filter(external_id__isnull=False).exclude(external_id__in=synced_external_ids).update(is_available=False) self.stdout.write(self.style.SUCCESS(f"Finished! Created: {created_count}, Updated: {updated_count}, Marked Unavailable: {discontinued_count}")) diff --git a/backend/api/migrations/0007_flavor_image_flavor_is_legacy_alter_flavor_image_url.py b/backend/api/migrations/0007_flavor_image_flavor_is_legacy_alter_flavor_image_url.py new file mode 100644 index 0000000..c28d7c0 --- /dev/null +++ b/backend/api/migrations/0007_flavor_image_flavor_is_legacy_alter_flavor_image_url.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.2 on 2026-02-16 12:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0006_reply'), + ] + + operations = [ + migrations.AddField( + model_name='flavor', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='flavors/'), + ), + migrations.AddField( + model_name='flavor', + name='is_legacy', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='flavor', + name='image_url', + field=models.URLField(blank=True, max_length=1000, null=True), + ), + ] diff --git a/backend/api/models.py b/backend/api/models.py index ebe67dd..34b9b82 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -31,9 +31,11 @@ class Flavor(models.Model): category = models.ForeignKey(Category, related_name='flavors', on_delete=models.CASCADE) name = models.CharField(max_length=100, unique=True) description = models.TextField(blank=True) - image_url = models.URLField(max_length=500, blank=True, null=True) + image_url = models.URLField(max_length=1000, blank=True, null=True) + image = models.ImageField(upload_to='flavors/', blank=True, null=True) shop_url = models.URLField(max_length=500, blank=True, null=True) is_available = models.BooleanField(default=True) + is_legacy = models.BooleanField(default=False) external_id = models.BigIntegerField(unique=True, null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) @@ -47,6 +49,12 @@ class Flavor(models.Model): return sum(r.score for r in ratings) / ratings.count() return 0.0 + @property + def cached_image_url(self): + if self.image: + return self.image.url + return self.image_url + class Rating(models.Model): user = models.ForeignKey(User, related_name='ratings', on_delete=models.CASCADE) flavor = models.ForeignKey(Flavor, related_name='ratings', on_delete=models.CASCADE) diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 99ae14a..ea8688d 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -22,23 +22,40 @@ class ReplySerializer(serializers.ModelSerializer): 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) + flavor_image = serializers.SerializerMethodField() replies = ReplySerializer(many=True, read_only=True) class Meta: model = Rating fields = ['id', 'user', 'flavor', 'flavor_name', 'flavor_image', 'score', 'comment', 'created_at', 'replies'] + def get_flavor_image(self, obj): + if obj.flavor.image: + request = self.context.get('request') + if request: + return request.build_absolute_uri(obj.flavor.image.url) + return obj.flavor.image.url + return obj.flavor.image_url + class FlavorSerializer(serializers.ModelSerializer): category_name = serializers.CharField(source='category.name', read_only=True) category_slug = serializers.CharField(source='category.slug', read_only=True) average_rating = serializers.FloatField(read_only=True) user_rating = serializers.SerializerMethodField() ratings = RatingSerializer(many=True, read_only=True) + image_url = serializers.SerializerMethodField() class Meta: model = Flavor - fields = ['id', 'name', 'category', 'category_name', 'category_slug', 'description', 'average_rating', 'user_rating', 'ratings', 'image_url', 'is_available', 'shop_url'] + fields = ['id', 'name', 'category', 'category_name', 'category_slug', 'description', 'average_rating', 'user_rating', 'ratings', 'image_url', 'is_available', 'is_legacy', 'shop_url'] + + def get_image_url(self, obj): + if obj.image: + request = self.context.get('request') + if request: + return request.build_absolute_uri(obj.image.url) + return obj.image.url + return obj.image_url def get_user_rating(self, obj): request = self.context.get('request') diff --git a/backend/holy_backend/settings.py b/backend/holy_backend/settings.py index 76e2342..84c3e38 100644 --- a/backend/holy_backend/settings.py +++ b/backend/holy_backend/settings.py @@ -130,6 +130,9 @@ STATIC_URL = 'static/' STATIC_ROOT = BASE_DIR / 'staticfiles' STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' +MEDIA_URL = 'media/' +MEDIA_ROOT = BASE_DIR / 'media' + # --- Reverse Proxy Configuration --- # These settings are crucial for running behind a proxy like Traefik USE_X_FORWARDED_HOST = True diff --git a/backend/holy_backend/urls.py b/backend/holy_backend/urls.py index c7d1dfb..9b828e3 100644 --- a/backend/holy_backend/urls.py +++ b/backend/holy_backend/urls.py @@ -2,6 +2,8 @@ from django.contrib import admin from django.urls import path, include from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView from rest_framework.authtoken.views import obtain_auth_token +from django.conf import settings +from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), @@ -11,4 +13,4 @@ urlpatterns = [ path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), -] +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/backend/media/flavors/3er_box_energy_cherry_cheetah.png b/backend/media/flavors/3er_box_energy_cherry_cheetah.png new file mode 100644 index 0000000..46d71a0 Binary files /dev/null and b/backend/media/flavors/3er_box_energy_cherry_cheetah.png differ diff --git a/backend/media/flavors/acai.jpg b/backend/media/flavors/acai.jpg new file mode 100644 index 0000000..5d95956 Binary files /dev/null and b/backend/media/flavors/acai.jpg differ diff --git a/backend/media/flavors/acai_anaconda.jpg b/backend/media/flavors/acai_anaconda.jpg new file mode 100644 index 0000000..d45273d Binary files /dev/null and b/backend/media/flavors/acai_anaconda.jpg differ diff --git a/backend/media/flavors/ananas.png b/backend/media/flavors/ananas.png new file mode 100644 index 0000000..0723a7a Binary files /dev/null and b/backend/media/flavors/ananas.png differ diff --git a/backend/media/flavors/apfel.jpg b/backend/media/flavors/apfel.jpg new file mode 100644 index 0000000..40ce5bf Binary files /dev/null and b/backend/media/flavors/apfel.jpg differ diff --git a/backend/media/flavors/apple_alligator.jpg b/backend/media/flavors/apple_alligator.jpg new file mode 100644 index 0000000..7378e70 Binary files /dev/null and b/backend/media/flavors/apple_alligator.jpg differ diff --git a/backend/media/flavors/badeschlappen.png b/backend/media/flavors/badeschlappen.png new file mode 100644 index 0000000..a962eb2 Binary files /dev/null and b/backend/media/flavors/badeschlappen.png differ diff --git a/backend/media/flavors/banane.jpg b/backend/media/flavors/banane.jpg new file mode 100644 index 0000000..4e81c47 Binary files /dev/null and b/backend/media/flavors/banane.jpg differ diff --git a/backend/media/flavors/birne.jpg b/backend/media/flavors/birne.jpg new file mode 100644 index 0000000..05feb8d Binary files /dev/null and b/backend/media/flavors/birne.jpg differ diff --git a/backend/media/flavors/blood_orange_bat.jpg b/backend/media/flavors/blood_orange_bat.jpg new file mode 100644 index 0000000..6fc5c44 Binary files /dev/null and b/backend/media/flavors/blood_orange_bat.jpg differ diff --git a/backend/media/flavors/blueberry_bear.jpg b/backend/media/flavors/blueberry_bear.jpg new file mode 100644 index 0000000..6babf34 Binary files /dev/null and b/backend/media/flavors/blueberry_bear.jpg differ diff --git a/backend/media/flavors/bratapfel.jpg b/backend/media/flavors/bratapfel.jpg new file mode 100644 index 0000000..1c25551 Binary files /dev/null and b/backend/media/flavors/bratapfel.jpg differ diff --git a/backend/media/flavors/brombeere.png b/backend/media/flavors/brombeere.png new file mode 100644 index 0000000..81cd3c3 Binary files /dev/null and b/backend/media/flavors/brombeere.png differ diff --git a/backend/media/flavors/bubblegum_butterfly.jpg b/backend/media/flavors/bubblegum_butterfly.jpg new file mode 100644 index 0000000..33adcdd Binary files /dev/null and b/backend/media/flavors/bubblegum_butterfly.jpg differ diff --git a/backend/media/flavors/burrsday_shaker.png b/backend/media/flavors/burrsday_shaker.png new file mode 100644 index 0000000..95a3141 Binary files /dev/null and b/backend/media/flavors/burrsday_shaker.png differ diff --git a/backend/media/flavors/cactus_camel.jpg b/backend/media/flavors/cactus_camel.jpg new file mode 100644 index 0000000..81ecb3e Binary files /dev/null and b/backend/media/flavors/cactus_camel.jpg differ diff --git a/backend/media/flavors/cherry_cheetah.jpg b/backend/media/flavors/cherry_cheetah.jpg new file mode 100644 index 0000000..e307128 Binary files /dev/null and b/backend/media/flavors/cherry_cheetah.jpg differ diff --git a/backend/media/flavors/citrus_cobra.jpg b/backend/media/flavors/citrus_cobra.jpg new file mode 100644 index 0000000..a806999 Binary files /dev/null and b/backend/media/flavors/citrus_cobra.jpg differ diff --git a/backend/media/flavors/community_thermo_shaker.jpg b/backend/media/flavors/community_thermo_shaker.jpg new file mode 100644 index 0000000..42a1781 Binary files /dev/null and b/backend/media/flavors/community_thermo_shaker.jpg differ diff --git a/backend/media/flavors/cranberry.jpg b/backend/media/flavors/cranberry.jpg new file mode 100644 index 0000000..caea8f9 Binary files /dev/null and b/backend/media/flavors/cranberry.jpg differ diff --git a/backend/media/flavors/darjeeling_x_honig.jpg b/backend/media/flavors/darjeeling_x_honig.jpg new file mode 100644 index 0000000..c5ab399 Binary files /dev/null and b/backend/media/flavors/darjeeling_x_honig.jpg differ diff --git a/backend/media/flavors/dosierlöffel__metall.png b/backend/media/flavors/dosierlöffel__metall.png new file mode 100644 index 0000000..93f0e7c Binary files /dev/null and b/backend/media/flavors/dosierlöffel__metall.png differ diff --git a/backend/media/flavors/drachenfrucht.jpg b/backend/media/flavors/drachenfrucht.jpg new file mode 100644 index 0000000..52353c0 Binary files /dev/null and b/backend/media/flavors/drachenfrucht.jpg differ diff --git a/backend/media/flavors/dragon_fruit_dragon.jpg b/backend/media/flavors/dragon_fruit_dragon.jpg new file mode 100644 index 0000000..bdf73f3 Binary files /dev/null and b/backend/media/flavors/dragon_fruit_dragon.jpg differ diff --git a/backend/media/flavors/einzelsorten-box_energy.jpg b/backend/media/flavors/einzelsorten-box_energy.jpg new file mode 100644 index 0000000..671a5bb Binary files /dev/null and b/backend/media/flavors/einzelsorten-box_energy.jpg differ diff --git a/backend/media/flavors/einzelsorten-box_hydration.jpg b/backend/media/flavors/einzelsorten-box_hydration.jpg new file mode 100644 index 0000000..d55703e Binary files /dev/null and b/backend/media/flavors/einzelsorten-box_hydration.jpg differ diff --git a/backend/media/flavors/einzelsorten-box_iced_tea.webp b/backend/media/flavors/einzelsorten-box_iced_tea.webp new file mode 100644 index 0000000..17bbee2 Binary files /dev/null and b/backend/media/flavors/einzelsorten-box_iced_tea.webp differ diff --git a/backend/media/flavors/einzelsorten-box_milkshake.jpg b/backend/media/flavors/einzelsorten-box_milkshake.jpg new file mode 100644 index 0000000..bd8208d Binary files /dev/null and b/backend/media/flavors/einzelsorten-box_milkshake.jpg differ diff --git a/backend/media/flavors/energy-probe.png b/backend/media/flavors/energy-probe.png new file mode 100644 index 0000000..49778d5 Binary files /dev/null and b/backend/media/flavors/energy-probe.png differ diff --git a/backend/media/flavors/energy_starter_set.jpg b/backend/media/flavors/energy_starter_set.jpg new file mode 100644 index 0000000..25a885a Binary files /dev/null and b/backend/media/flavors/energy_starter_set.jpg differ diff --git a/backend/media/flavors/erdbeere.jpg b/backend/media/flavors/erdbeere.jpg new file mode 100644 index 0000000..b3e36fd Binary files /dev/null and b/backend/media/flavors/erdbeere.jpg differ diff --git a/backend/media/flavors/erdbeere.webp b/backend/media/flavors/erdbeere.webp new file mode 100644 index 0000000..085d885 Binary files /dev/null and b/backend/media/flavors/erdbeere.webp differ diff --git a/backend/media/flavors/erdbeere_x_kiwi.jpg b/backend/media/flavors/erdbeere_x_kiwi.jpg new file mode 100644 index 0000000..a35a8a2 Binary files /dev/null and b/backend/media/flavors/erdbeere_x_kiwi.jpg differ diff --git a/backend/media/flavors/ersatzdichtung__glasflasche.png b/backend/media/flavors/ersatzdichtung__glasflasche.png new file mode 100644 index 0000000..0fea288 Binary files /dev/null and b/backend/media/flavors/ersatzdichtung__glasflasche.png differ diff --git a/backend/media/flavors/ersatzteil__shaker.png b/backend/media/flavors/ersatzteil__shaker.png new file mode 100644 index 0000000..a4b78c3 Binary files /dev/null and b/backend/media/flavors/ersatzteil__shaker.png differ diff --git a/backend/media/flavors/ersatzteile__thermo_shaker.webp b/backend/media/flavors/ersatzteile__thermo_shaker.webp new file mode 100644 index 0000000..77c7aec Binary files /dev/null and b/backend/media/flavors/ersatzteile__thermo_shaker.webp differ diff --git a/backend/media/flavors/ersatzteile__thermo_shaker_1l.webp b/backend/media/flavors/ersatzteile__thermo_shaker_1l.webp new file mode 100644 index 0000000..77c7aec Binary files /dev/null and b/backend/media/flavors/ersatzteile__thermo_shaker_1l.webp differ diff --git a/backend/media/flavors/ersatzteile_thermo_shaker.png b/backend/media/flavors/ersatzteile_thermo_shaker.png new file mode 100644 index 0000000..5477849 Binary files /dev/null and b/backend/media/flavors/ersatzteile_thermo_shaker.png differ diff --git a/backend/media/flavors/fanschal__1._fc_köln.webp b/backend/media/flavors/fanschal__1._fc_köln.webp new file mode 100644 index 0000000..8f96328 Binary files /dev/null and b/backend/media/flavors/fanschal__1._fc_köln.webp differ diff --git a/backend/media/flavors/frosty_fox.jpg b/backend/media/flavors/frosty_fox.jpg new file mode 100644 index 0000000..afb5d50 Binary files /dev/null and b/backend/media/flavors/frosty_fox.jpg differ diff --git a/backend/media/flavors/fruity_frog.png b/backend/media/flavors/fruity_frog.png new file mode 100644 index 0000000..8c75be8 Binary files /dev/null and b/backend/media/flavors/fruity_frog.png differ diff --git a/backend/media/flavors/geschenkgutschein.png b/backend/media/flavors/geschenkgutschein.png new file mode 100644 index 0000000..d3d120a Binary files /dev/null and b/backend/media/flavors/geschenkgutschein.png differ diff --git a/backend/media/flavors/giga_gallon_weiß.png b/backend/media/flavors/giga_gallon_weiß.png new file mode 100644 index 0000000..8f0e217 Binary files /dev/null and b/backend/media/flavors/giga_gallon_weiß.png differ diff --git a/backend/media/flavors/gorillas_grape.jpg b/backend/media/flavors/gorillas_grape.jpg new file mode 100644 index 0000000..fcf1049 Binary files /dev/null and b/backend/media/flavors/gorillas_grape.jpg differ diff --git a/backend/media/flavors/grapefruit.jpg b/backend/media/flavors/grapefruit.jpg new file mode 100644 index 0000000..e197829 Binary files /dev/null and b/backend/media/flavors/grapefruit.jpg differ diff --git a/backend/media/flavors/grapefruit_giraffe.jpg b/backend/media/flavors/grapefruit_giraffe.jpg new file mode 100644 index 0000000..c90e924 Binary files /dev/null and b/backend/media/flavors/grapefruit_giraffe.jpg differ diff --git a/backend/media/flavors/grüner_tee.png b/backend/media/flavors/grüner_tee.png new file mode 100644 index 0000000..c732f7a Binary files /dev/null and b/backend/media/flavors/grüner_tee.png differ diff --git a/backend/media/flavors/guave.jpg b/backend/media/flavors/guave.jpg new file mode 100644 index 0000000..213d118 Binary files /dev/null and b/backend/media/flavors/guave.jpg differ diff --git a/backend/media/flavors/haselnuss.jpg b/backend/media/flavors/haselnuss.jpg new file mode 100644 index 0000000..88d1980 Binary files /dev/null and b/backend/media/flavors/haselnuss.jpg differ diff --git a/backend/media/flavors/hibiskus.png b/backend/media/flavors/hibiskus.png new file mode 100644 index 0000000..c2c1419 Binary files /dev/null and b/backend/media/flavors/hibiskus.png differ diff --git a/backend/media/flavors/himbeere_x_vanille.jpg b/backend/media/flavors/himbeere_x_vanille.jpg new file mode 100644 index 0000000..099d106 Binary files /dev/null and b/backend/media/flavors/himbeere_x_vanille.jpg differ diff --git a/backend/media/flavors/holy_brushed_logo_thermo_shaker_adventcalendar.png b/backend/media/flavors/holy_brushed_logo_thermo_shaker_adventcalendar.png new file mode 100644 index 0000000..bb3856e Binary files /dev/null and b/backend/media/flavors/holy_brushed_logo_thermo_shaker_adventcalendar.png differ diff --git a/backend/media/flavors/holy_geschenkbox.png b/backend/media/flavors/holy_geschenkbox.png new file mode 100644 index 0000000..2261efe Binary files /dev/null and b/backend/media/flavors/holy_geschenkbox.png differ diff --git a/backend/media/flavors/holy_holy_holy_shaker.png b/backend/media/flavors/holy_holy_holy_shaker.png new file mode 100644 index 0000000..5da30fe Binary files /dev/null and b/backend/media/flavors/holy_holy_holy_shaker.png differ diff --git a/backend/media/flavors/holy_löwen_frankfurt_shaker.png b/backend/media/flavors/holy_löwen_frankfurt_shaker.png new file mode 100644 index 0000000..47fea1f Binary files /dev/null and b/backend/media/flavors/holy_löwen_frankfurt_shaker.png differ diff --git a/backend/media/flavors/holy_pflege_shaker.png b/backend/media/flavors/holy_pflege_shaker.png new file mode 100644 index 0000000..6668fa8 Binary files /dev/null and b/backend/media/flavors/holy_pflege_shaker.png differ diff --git a/backend/media/flavors/holy_squad_shaker.jpg b/backend/media/flavors/holy_squad_shaker.jpg new file mode 100644 index 0000000..cd78e6b Binary files /dev/null and b/backend/media/flavors/holy_squad_shaker.jpg differ diff --git a/backend/media/flavors/holy_x_7_vs._wild_thermo_shaker_2025.png b/backend/media/flavors/holy_x_7_vs._wild_thermo_shaker_2025.png new file mode 100644 index 0000000..b9457bf Binary files /dev/null and b/backend/media/flavors/holy_x_7_vs._wild_thermo_shaker_2025.png differ diff --git a/backend/media/flavors/holy_x_mission_unknown_shaker.png b/backend/media/flavors/holy_x_mission_unknown_shaker.png new file mode 100644 index 0000000..37d205f Binary files /dev/null and b/backend/media/flavors/holy_x_mission_unknown_shaker.png differ diff --git a/backend/media/flavors/honigmelone.jpg b/backend/media/flavors/honigmelone.jpg new file mode 100644 index 0000000..da8fb61 Binary files /dev/null and b/backend/media/flavors/honigmelone.jpg differ diff --git a/backend/media/flavors/hotfoot_run_turnbeutel.png b/backend/media/flavors/hotfoot_run_turnbeutel.png new file mode 100644 index 0000000..77edda0 Binary files /dev/null and b/backend/media/flavors/hotfoot_run_turnbeutel.png differ diff --git a/backend/media/flavors/hydration-probe.png b/backend/media/flavors/hydration-probe.png new file mode 100644 index 0000000..7c59cf0 Binary files /dev/null and b/backend/media/flavors/hydration-probe.png differ diff --git a/backend/media/flavors/hydration_starter_set.jpg b/backend/media/flavors/hydration_starter_set.jpg new file mode 100644 index 0000000..898153e Binary files /dev/null and b/backend/media/flavors/hydration_starter_set.jpg differ diff --git a/backend/media/flavors/iced_tea-probe.webp b/backend/media/flavors/iced_tea-probe.webp new file mode 100644 index 0000000..e100b39 Binary files /dev/null and b/backend/media/flavors/iced_tea-probe.webp differ diff --git a/backend/media/flavors/iced_tea_starter_set.jpg b/backend/media/flavors/iced_tea_starter_set.jpg new file mode 100644 index 0000000..820f7c4 Binary files /dev/null and b/backend/media/flavors/iced_tea_starter_set.jpg differ diff --git a/backend/media/flavors/kappe_-_hotfoot_run.png b/backend/media/flavors/kappe_-_hotfoot_run.png new file mode 100644 index 0000000..06a1db3 Binary files /dev/null and b/backend/media/flavors/kappe_-_hotfoot_run.png differ diff --git a/backend/media/flavors/karamell.jpg b/backend/media/flavors/karamell.jpg new file mode 100644 index 0000000..1a4af65 Binary files /dev/null and b/backend/media/flavors/karamell.jpg differ diff --git a/backend/media/flavors/kiwi_komodo.jpg b/backend/media/flavors/kiwi_komodo.jpg new file mode 100644 index 0000000..6f2a19f Binary files /dev/null and b/backend/media/flavors/kiwi_komodo.jpg differ diff --git a/backend/media/flavors/kokosnuss.png b/backend/media/flavors/kokosnuss.png new file mode 100644 index 0000000..6facb00 Binary files /dev/null and b/backend/media/flavors/kokosnuss.png differ diff --git a/backend/media/flavors/kölle_kamelle.webp b/backend/media/flavors/kölle_kamelle.webp new file mode 100644 index 0000000..fb83f7e Binary files /dev/null and b/backend/media/flavors/kölle_kamelle.webp differ diff --git a/backend/media/flavors/kühltasche.png b/backend/media/flavors/kühltasche.png new file mode 100644 index 0000000..1cd428d Binary files /dev/null and b/backend/media/flavors/kühltasche.png differ diff --git a/backend/media/flavors/legacy_dolphin_daiquiri.png b/backend/media/flavors/legacy_dolphin_daiquiri.png new file mode 100644 index 0000000..9adcf21 Binary files /dev/null and b/backend/media/flavors/legacy_dolphin_daiquiri.png differ diff --git a/backend/media/flavors/legacy_hitschies_saure_drachenzunge_blau.png b/backend/media/flavors/legacy_hitschies_saure_drachenzunge_blau.png new file mode 100644 index 0000000..83176ae Binary files /dev/null and b/backend/media/flavors/legacy_hitschies_saure_drachenzunge_blau.png differ diff --git a/backend/media/flavors/legacy_hitschies_saure_krakenarme.png b/backend/media/flavors/legacy_hitschies_saure_krakenarme.png new file mode 100644 index 0000000..d71a1d8 Binary files /dev/null and b/backend/media/flavors/legacy_hitschies_saure_krakenarme.png differ diff --git a/backend/media/flavors/legacy_hitschies_saure_spinnenbeine.png b/backend/media/flavors/legacy_hitschies_saure_spinnenbeine.png new file mode 100644 index 0000000..336929c Binary files /dev/null and b/backend/media/flavors/legacy_hitschies_saure_spinnenbeine.png differ diff --git a/backend/media/flavors/lemon_lizard.jpg b/backend/media/flavors/lemon_lizard.jpg new file mode 100644 index 0000000..f6993d8 Binary files /dev/null and b/backend/media/flavors/lemon_lizard.jpg differ diff --git a/backend/media/flavors/limette_x_matcha_x_minze.jpg b/backend/media/flavors/limette_x_matcha_x_minze.jpg new file mode 100644 index 0000000..8f24ab6 Binary files /dev/null and b/backend/media/flavors/limette_x_matcha_x_minze.jpg differ diff --git a/backend/media/flavors/limited_strawberry_shark.png b/backend/media/flavors/limited_strawberry_shark.png new file mode 100644 index 0000000..cfea85c Binary files /dev/null and b/backend/media/flavors/limited_strawberry_shark.png differ diff --git a/backend/media/flavors/lions_lemonade.jpg b/backend/media/flavors/lions_lemonade.jpg new file mode 100644 index 0000000..2c5f232 Binary files /dev/null and b/backend/media/flavors/lions_lemonade.jpg differ diff --git a/backend/media/flavors/logo_shaker.png b/backend/media/flavors/logo_shaker.png new file mode 100644 index 0000000..6f8964f Binary files /dev/null and b/backend/media/flavors/logo_shaker.png differ diff --git a/backend/media/flavors/lollipop_lovebird.webp b/backend/media/flavors/lollipop_lovebird.webp new file mode 100644 index 0000000..e8ed170 Binary files /dev/null and b/backend/media/flavors/lollipop_lovebird.webp differ diff --git a/backend/media/flavors/mango_x_passionsfrucht.png b/backend/media/flavors/mango_x_passionsfrucht.png new file mode 100644 index 0000000..9c2e418 Binary files /dev/null and b/backend/media/flavors/mango_x_passionsfrucht.png differ diff --git a/backend/media/flavors/milkshake-probe.png b/backend/media/flavors/milkshake-probe.png new file mode 100644 index 0000000..aa4ac5e Binary files /dev/null and b/backend/media/flavors/milkshake-probe.png differ diff --git a/backend/media/flavors/milkshake_starter_set.png b/backend/media/flavors/milkshake_starter_set.png new file mode 100644 index 0000000..0681110 Binary files /dev/null and b/backend/media/flavors/milkshake_starter_set.png differ diff --git a/backend/media/flavors/mix_probier-box_energy.png b/backend/media/flavors/mix_probier-box_energy.png new file mode 100644 index 0000000..0f0fa7f Binary files /dev/null and b/backend/media/flavors/mix_probier-box_energy.png differ diff --git a/backend/media/flavors/mix_probier-box_hydration.png b/backend/media/flavors/mix_probier-box_hydration.png new file mode 100644 index 0000000..755cfb8 Binary files /dev/null and b/backend/media/flavors/mix_probier-box_hydration.png differ diff --git a/backend/media/flavors/mix_probier-box_iced_tea.png b/backend/media/flavors/mix_probier-box_iced_tea.png new file mode 100644 index 0000000..31e31c2 Binary files /dev/null and b/backend/media/flavors/mix_probier-box_iced_tea.png differ diff --git a/backend/media/flavors/mix_probier-box_iced_tea_classics.webp b/backend/media/flavors/mix_probier-box_iced_tea_classics.webp new file mode 100644 index 0000000..608a48b Binary files /dev/null and b/backend/media/flavors/mix_probier-box_iced_tea_classics.webp differ diff --git a/backend/media/flavors/mix_probier-box_milkshake.png b/backend/media/flavors/mix_probier-box_milkshake.png new file mode 100644 index 0000000..f4dc887 Binary files /dev/null and b/backend/media/flavors/mix_probier-box_milkshake.png differ diff --git a/backend/media/flavors/multivitamin.jpg b/backend/media/flavors/multivitamin.jpg new file mode 100644 index 0000000..941cc2d Binary files /dev/null and b/backend/media/flavors/multivitamin.jpg differ diff --git a/backend/media/flavors/orange.jpg b/backend/media/flavors/orange.jpg new file mode 100644 index 0000000..2abf847 Binary files /dev/null and b/backend/media/flavors/orange.jpg differ diff --git a/backend/media/flavors/passionsfrucht.jpg b/backend/media/flavors/passionsfrucht.jpg new file mode 100644 index 0000000..f24c980 Binary files /dev/null and b/backend/media/flavors/passionsfrucht.jpg differ diff --git a/backend/media/flavors/peach_panther.jpg b/backend/media/flavors/peach_panther.jpg new file mode 100644 index 0000000..ce445bd Binary files /dev/null and b/backend/media/flavors/peach_panther.jpg differ diff --git a/backend/media/flavors/peacocks_punch.jpg b/backend/media/flavors/peacocks_punch.jpg new file mode 100644 index 0000000..6822c43 Binary files /dev/null and b/backend/media/flavors/peacocks_punch.jpg differ diff --git a/backend/media/flavors/pfirsich.jpg b/backend/media/flavors/pfirsich.jpg new file mode 100644 index 0000000..a0c21d1 Binary files /dev/null and b/backend/media/flavors/pfirsich.jpg differ diff --git a/backend/media/flavors/pomegranate_piranha.jpg b/backend/media/flavors/pomegranate_piranha.jpg new file mode 100644 index 0000000..2f08cf7 Binary files /dev/null and b/backend/media/flavors/pomegranate_piranha.jpg differ diff --git a/backend/media/flavors/premium_hoodie_-_blackweek.png b/backend/media/flavors/premium_hoodie_-_blackweek.png new file mode 100644 index 0000000..b839239 Binary files /dev/null and b/backend/media/flavors/premium_hoodie_-_blackweek.png differ diff --git a/backend/media/flavors/raspberry_raptor.jpg b/backend/media/flavors/raspberry_raptor.jpg new file mode 100644 index 0000000..4a31f5f Binary files /dev/null and b/backend/media/flavors/raspberry_raptor.jpg differ diff --git a/backend/media/flavors/rote_traube.jpg b/backend/media/flavors/rote_traube.jpg new file mode 100644 index 0000000..934a1c4 Binary files /dev/null and b/backend/media/flavors/rote_traube.jpg differ diff --git a/backend/media/flavors/sauerkirsche.png b/backend/media/flavors/sauerkirsche.png new file mode 100644 index 0000000..9b028e6 Binary files /dev/null and b/backend/media/flavors/sauerkirsche.png differ diff --git a/backend/media/flavors/schwarzer_tee.png b/backend/media/flavors/schwarzer_tee.png new file mode 100644 index 0000000..744960b Binary files /dev/null and b/backend/media/flavors/schwarzer_tee.png differ diff --git a/backend/media/flavors/shaker__1._fc_köln.webp b/backend/media/flavors/shaker__1._fc_köln.webp new file mode 100644 index 0000000..bea18d7 Binary files /dev/null and b/backend/media/flavors/shaker__1._fc_köln.webp differ diff --git a/backend/media/flavors/shaker__hydration.jpg b/backend/media/flavors/shaker__hydration.jpg new file mode 100644 index 0000000..e197829 Binary files /dev/null and b/backend/media/flavors/shaker__hydration.jpg differ diff --git a/backend/media/flavors/shaker__iced_tea.jpg b/backend/media/flavors/shaker__iced_tea.jpg new file mode 100644 index 0000000..e8dcec4 Binary files /dev/null and b/backend/media/flavors/shaker__iced_tea.jpg differ diff --git a/backend/media/flavors/shaker__milkshake.jpg b/backend/media/flavors/shaker__milkshake.jpg new file mode 100644 index 0000000..ed6630c Binary files /dev/null and b/backend/media/flavors/shaker__milkshake.jpg differ diff --git a/backend/media/flavors/shaker_energy.webp b/backend/media/flavors/shaker_energy.webp new file mode 100644 index 0000000..a7e2254 Binary files /dev/null and b/backend/media/flavors/shaker_energy.webp differ diff --git a/backend/media/flavors/spekulatius.jpg b/backend/media/flavors/spekulatius.jpg new file mode 100644 index 0000000..81f1e71 Binary files /dev/null and b/backend/media/flavors/spekulatius.jpg differ diff --git a/backend/media/flavors/spongebob__friends_shaker.png b/backend/media/flavors/spongebob__friends_shaker.png new file mode 100644 index 0000000..8228064 Binary files /dev/null and b/backend/media/flavors/spongebob__friends_shaker.png differ diff --git a/backend/media/flavors/squad_shaker_xl.png b/backend/media/flavors/squad_shaker_xl.png new file mode 100644 index 0000000..7330189 Binary files /dev/null and b/backend/media/flavors/squad_shaker_xl.png differ diff --git a/backend/media/flavors/starter_set_deluxe.jpg b/backend/media/flavors/starter_set_deluxe.jpg new file mode 100644 index 0000000..60d7977 Binary files /dev/null and b/backend/media/flavors/starter_set_deluxe.jpg differ diff --git a/backend/media/flavors/starter_set_deluxe.png b/backend/media/flavors/starter_set_deluxe.png new file mode 100644 index 0000000..11ae4db Binary files /dev/null and b/backend/media/flavors/starter_set_deluxe.png differ diff --git a/backend/media/flavors/starter_set_deluxe_4DZjmGt.jpg b/backend/media/flavors/starter_set_deluxe_4DZjmGt.jpg new file mode 100644 index 0000000..d3e1fe5 Binary files /dev/null and b/backend/media/flavors/starter_set_deluxe_4DZjmGt.jpg differ diff --git a/backend/media/flavors/starter_set_deluxe__thermo_shaker.png b/backend/media/flavors/starter_set_deluxe__thermo_shaker.png new file mode 100644 index 0000000..4b265bc Binary files /dev/null and b/backend/media/flavors/starter_set_deluxe__thermo_shaker.png differ diff --git a/backend/media/flavors/starter_set_deluxe__thermo_shaker_Kbno0U5.png b/backend/media/flavors/starter_set_deluxe__thermo_shaker_Kbno0U5.png new file mode 100644 index 0000000..01408d8 Binary files /dev/null and b/backend/media/flavors/starter_set_deluxe__thermo_shaker_Kbno0U5.png differ diff --git a/backend/media/flavors/starter_set_deluxe_löwen_frankfurt.jpg b/backend/media/flavors/starter_set_deluxe_löwen_frankfurt.jpg new file mode 100644 index 0000000..e4646aa Binary files /dev/null and b/backend/media/flavors/starter_set_deluxe_löwen_frankfurt.jpg differ diff --git a/backend/media/flavors/stell_dir_dein_starter_set_zusammen.jpg b/backend/media/flavors/stell_dir_dein_starter_set_zusammen.jpg new file mode 100644 index 0000000..2084b0e Binary files /dev/null and b/backend/media/flavors/stell_dir_dein_starter_set_zusammen.jpg differ diff --git a/backend/media/flavors/strawberry_shark.jpg b/backend/media/flavors/strawberry_shark.jpg new file mode 100644 index 0000000..fb066bf Binary files /dev/null and b/backend/media/flavors/strawberry_shark.jpg differ diff --git a/backend/media/flavors/tangerine_tarantula.png b/backend/media/flavors/tangerine_tarantula.png new file mode 100644 index 0000000..449444a Binary files /dev/null and b/backend/media/flavors/tangerine_tarantula.png differ diff --git a/backend/media/flavors/thai_lime_toucan.jpg b/backend/media/flavors/thai_lime_toucan.jpg new file mode 100644 index 0000000..4a84948 Binary files /dev/null and b/backend/media/flavors/thai_lime_toucan.jpg differ diff --git a/backend/media/flavors/thermo_shaker.jpg b/backend/media/flavors/thermo_shaker.jpg new file mode 100644 index 0000000..f21ef3f Binary files /dev/null and b/backend/media/flavors/thermo_shaker.jpg differ diff --git a/backend/media/flavors/thermo_shaker.png b/backend/media/flavors/thermo_shaker.png new file mode 100644 index 0000000..5f63da4 Binary files /dev/null and b/backend/media/flavors/thermo_shaker.png differ diff --git a/backend/media/flavors/thermo_shaker_-_1l.webp b/backend/media/flavors/thermo_shaker_-_1l.webp new file mode 100644 index 0000000..8c665e0 Binary files /dev/null and b/backend/media/flavors/thermo_shaker_-_1l.webp differ diff --git a/backend/media/flavors/thermo_shaker_1l_blau.png b/backend/media/flavors/thermo_shaker_1l_blau.png new file mode 100644 index 0000000..dcca6fe Binary files /dev/null and b/backend/media/flavors/thermo_shaker_1l_blau.png differ diff --git a/backend/media/flavors/thermo_shaker__1._fc_köln.webp b/backend/media/flavors/thermo_shaker__1._fc_köln.webp new file mode 100644 index 0000000..d94394b Binary files /dev/null and b/backend/media/flavors/thermo_shaker__1._fc_köln.webp differ diff --git a/backend/media/flavors/thermo_shaker__1l_new_year.png b/backend/media/flavors/thermo_shaker__1l_new_year.png new file mode 100644 index 0000000..d47cdf2 Binary files /dev/null and b/backend/media/flavors/thermo_shaker__1l_new_year.png differ diff --git a/backend/media/flavors/thermo_shaker__700ml_new_year.png b/backend/media/flavors/thermo_shaker__700ml_new_year.png new file mode 100644 index 0000000..0d6ae41 Binary files /dev/null and b/backend/media/flavors/thermo_shaker__700ml_new_year.png differ diff --git a/backend/media/flavors/thermo_shaker__700ml_new_year_mit_sport-deckel.png b/backend/media/flavors/thermo_shaker__700ml_new_year_mit_sport-deckel.png new file mode 100644 index 0000000..f666f90 Binary files /dev/null and b/backend/media/flavors/thermo_shaker__700ml_new_year_mit_sport-deckel.png differ diff --git a/backend/media/flavors/thermo_shaker__spongebob.jpg b/backend/media/flavors/thermo_shaker__spongebob.jpg new file mode 100644 index 0000000..62aab15 Binary files /dev/null and b/backend/media/flavors/thermo_shaker__spongebob.jpg differ diff --git a/backend/media/flavors/thermo_shaker__sports_lid.jpg b/backend/media/flavors/thermo_shaker__sports_lid.jpg new file mode 100644 index 0000000..ac03d71 Binary files /dev/null and b/backend/media/flavors/thermo_shaker__sports_lid.jpg differ diff --git a/backend/media/flavors/thermo_shaker_multichrom.png b/backend/media/flavors/thermo_shaker_multichrom.png new file mode 100644 index 0000000..758bc2a Binary files /dev/null and b/backend/media/flavors/thermo_shaker_multichrom.png differ diff --git a/backend/media/flavors/thermo_shaker_sportsfreund.png b/backend/media/flavors/thermo_shaker_sportsfreund.png new file mode 100644 index 0000000..4698365 Binary files /dev/null and b/backend/media/flavors/thermo_shaker_sportsfreund.png differ diff --git a/backend/media/flavors/tropical_tiger.jpg b/backend/media/flavors/tropical_tiger.jpg new file mode 100644 index 0000000..d5b510d Binary files /dev/null and b/backend/media/flavors/tropical_tiger.jpg differ diff --git a/backend/media/flavors/vanille.jpg b/backend/media/flavors/vanille.jpg new file mode 100644 index 0000000..518d73a Binary files /dev/null and b/backend/media/flavors/vanille.jpg differ diff --git a/backend/media/flavors/wassermelone.jpg b/backend/media/flavors/wassermelone.jpg new file mode 100644 index 0000000..bc337c8 Binary files /dev/null and b/backend/media/flavors/wassermelone.jpg differ diff --git a/backend/media/flavors/wassermelone_y1gSuob.jpg b/backend/media/flavors/wassermelone_y1gSuob.jpg new file mode 100644 index 0000000..f4b7493 Binary files /dev/null and b/backend/media/flavors/wassermelone_y1gSuob.jpg differ diff --git a/backend/media/flavors/watermelon_whale.jpg b/backend/media/flavors/watermelon_whale.jpg new file mode 100644 index 0000000..8738004 Binary files /dev/null and b/backend/media/flavors/watermelon_whale.jpg differ diff --git a/backend/media/flavors/weißer_pfirsich.jpg b/backend/media/flavors/weißer_pfirsich.jpg new file mode 100644 index 0000000..2b260c9 Binary files /dev/null and b/backend/media/flavors/weißer_pfirsich.jpg differ diff --git a/backend/media/flavors/wildberry_wolf.jpg b/backend/media/flavors/wildberry_wolf.jpg new file mode 100644 index 0000000..8684ec7 Binary files /dev/null and b/backend/media/flavors/wildberry_wolf.jpg differ diff --git a/backend/media/flavors/winter_punch.jpg b/backend/media/flavors/winter_punch.jpg new file mode 100644 index 0000000..95a6335 Binary files /dev/null and b/backend/media/flavors/winter_punch.jpg differ diff --git a/backend/media/flavors/woodruff_wolf.jpg b/backend/media/flavors/woodruff_wolf.jpg new file mode 100644 index 0000000..8e9e94e Binary files /dev/null and b/backend/media/flavors/woodruff_wolf.jpg differ diff --git a/backend/media/flavors/zitrone.jpg b/backend/media/flavors/zitrone.jpg new file mode 100644 index 0000000..0eb1f4f Binary files /dev/null and b/backend/media/flavors/zitrone.jpg differ diff --git a/backend/media/flavors/zitrone_x_honig.jpg b/backend/media/flavors/zitrone_x_honig.jpg new file mode 100644 index 0000000..9ba952c Binary files /dev/null and b/backend/media/flavors/zitrone_x_honig.jpg differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a828726..04da845 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useMemo } from 'react'; -import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'; +import { BrowserRouter as Router, Routes, Route, Link, useNavigate, useLocation } from 'react-router-dom'; import { ThemeProvider, CssBaseline, @@ -18,10 +18,13 @@ import { ListItem, ListItemButton, ListItemText, - useMediaQuery + useMediaQuery, + TextField, + InputAdornment } from '@mui/material'; import PaletteIcon from '@mui/icons-material/Palette'; import MenuIcon from '@mui/icons-material/Menu'; +import SearchIcon from '@mui/icons-material/Search'; import { getTheme, type CatppuccinTheme } from './theme'; import CategoryList from './pages/CategoryList'; import CategoryFlavors from './pages/categories/CategoryFlavors'; @@ -32,13 +35,61 @@ import Login from './pages/Login'; import Settings from './pages/Settings'; import api from './api'; +const GlobalSearch = () => { + const [query, setQuery] = useState(''); + const navigate = useNavigate(); + const location = useLocation(); + + useEffect(() => { + const params = new URLSearchParams(location.search); + setQuery(params.get('q') || ''); + }, [location.search]); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + // Redirect to categories page with search query + if (query.trim()) { + navigate(`/?q=${encodeURIComponent(query.trim())}`); + } else { + navigate('/'); + } + }; + + return ( +
+ setQuery(e.target.value)} + sx={{ + bgcolor: 'rgba(255,255,255,0.1)', + borderRadius: 1, + '& .MuiOutlinedInput-root': { + color: 'white', + '& fieldset': { border: 'none' }, + } + }} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + ); +}; + const App: React.FC = () => { const [themeName, setThemeName] = useState((localStorage.getItem('theme') as CatppuccinTheme) || 'mocha'); const [anchorEl, setAnchorEl] = useState(null); const [user, setUser] = useState<{username: string} | null>(null); const [loadingUser, setLoadingUser] = useState(true); const [drawerOpen, setDrawerOpen] = useState(false); - const isMobile = useMediaQuery('(max-width:600px)'); + const isMobile = useMediaQuery('(max-width:900px)'); useEffect(() => { const fetchUser = async () => { @@ -83,11 +134,11 @@ const App: React.FC = () => { }; const navItems = user ? [ - { label: 'Categories', path: '/' }, + { label: 'Catalog', path: '/' }, { label: 'Dashboard', path: '/dashboard' }, { label: 'Settings', path: '/settings' }, ] : [ - { label: 'Categories', path: '/' }, + { label: 'Catalog', path: '/' }, { label: 'Login', path: '/login' }, ]; @@ -120,21 +171,22 @@ const App: React.FC = () => { - {isMobile && ( - setDrawerOpen(true)} - sx={{ mr: 2 }} - > - - - )} - + setDrawerOpen(true)} + sx={{ mr: 2, display: { md: 'none' } }} + > + + + + Holy Flavors + + {!isMobile && !loadingUser && ( @@ -156,16 +208,14 @@ const App: React.FC = () => { onClose={() => setAnchorEl(null)} > handleThemeChange('latte')}>Latte - handleThemeChange('frappe')}>Frappé - handleThemeChange('macchiato')}>Macchiato - handleThemeChange('mocha')}>Mocha - handleThemeChange('pink')}>Pastel Pink - - + handleThemeChange('frappe')}>Frappé + handleThemeChange('macchiato')}>Macchiato + handleThemeChange('mocha')}>Mocha + handleThemeChange('pink')}>Pastel Pink + - {/* Spacer for the fixed AppBar */} { onClose={() => setDrawerOpen(false)} ModalProps={{ keepMounted: true }} sx={{ - display: { xs: 'block', sm: 'none' }, + display: { xs: 'block', md: 'none' }, '& .MuiDrawer-paper': { boxSizing: 'border-box', width: 240 }, }} > diff --git a/frontend/src/pages/CategoryList.tsx b/frontend/src/pages/CategoryList.tsx index b087e20..65d13a4 100644 --- a/frontend/src/pages/CategoryList.tsx +++ b/frontend/src/pages/CategoryList.tsx @@ -4,11 +4,10 @@ import { Box, Card, CardContent, - TextField, - InputAdornment + Rating as MuiRating, + CircularProgress } from '@mui/material'; -import SearchIcon from '@mui/icons-material/Search'; -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import api from '../api'; import { useTitle } from '../hooks/useTitle'; @@ -18,54 +17,103 @@ interface Category { slug: string; } +interface Flavor { + id: number; + name: string; + category_name: string; + description: string; + average_rating: number; + image_url: string | null; + is_available: boolean; + is_legacy: boolean; +} + const CategoryList: React.FC = () => { - useTitle('Categories'); const [categories, setCategories] = useState([]); + const [flavors, setFlavors] = useState([]); const [loading, setLoading] = useState(true); - const [search, setSearch] = useState(''); + const location = useLocation(); + + const query = new URLSearchParams(location.search).get('q') || ''; useEffect(() => { - const fetchCategories = async () => { + const fetchData = async () => { + setLoading(true); try { - const res = await api.get('categories/'); - setCategories(res.data); + if (query) { + const res = await api.get(`flavors/`); + setFlavors(res.data); + } else { + const res = await api.get('categories/'); + setCategories(res.data); + } } catch (err) { console.error(err); } finally { setLoading(false); } }; - fetchCategories(); - }, []); + fetchData(); + }, [query]); - const filteredCategories = categories.filter(c => - c.name.toLowerCase().includes(search.toLowerCase()) - ); + useTitle(query ? `Search: ${query}` : 'Catalog'); - if (loading) return Loading Categories...; + if (loading) return ; + + if (query) { + const filteredFlavors = flavors.filter(f => + f.name.toLowerCase().includes(query.toLowerCase()) || + f.description.toLowerCase().includes(query.toLowerCase()) || + f.category_name.toLowerCase().includes(query.toLowerCase()) + ); + + return ( + + Search Results for "{query}" + + Found {filteredFlavors.length} items + + + + {filteredFlavors.map(flavor => ( + + + + {!flavor.is_available && ( + + {flavor.is_legacy ? 'Unavailable' : 'Out of Stock'} + + )} + {flavor.image_url && ( + + )} + + {flavor.name} + {flavor.category_name} + + + ({(flavor.average_rating || 0).toFixed(1)}) + + + + + + ))} + + + ); + } return ( - Flavor Categories + Catalog - setSearch(e.target.value)} - sx={{ mb: 4 }} - InputProps={{ - startAdornment: ( - - - - ), - }} - /> - - {filteredCategories.map(category => ( + {categories.map(category => ( { const { slug } = useParams<{ slug: string }>(); const [flavors, setFlavors] = useState([]); const [categoryName, setCategoryName] = useState(''); - useTitle(categoryName || 'Flavors'); const [loading, setLoading] = useState(true); - const [search, setSearch] = useState(''); useEffect(() => { const fetchFlavors = async () => { + setLoading(true); try { const res = await api.get(`flavors/?category__slug=${slug}`); setFlavors(res.data); @@ -59,53 +57,42 @@ const CategoryFlavors: React.FC = () => { fetchFlavors(); }, [slug]); - const filteredFlavors = flavors.filter(f => - f.name.toLowerCase().includes(search.toLowerCase()) || - f.description.toLowerCase().includes(search.toLowerCase()) - ); + useTitle(categoryName || 'Flavors'); - if (loading) return Loading Flavors...; + if (loading) return ; return ( - {categoryName || 'Flavors'} - - setSearch(e.target.value)} - sx={{ mb: 4 }} - InputProps={{ - startAdornment: ( - - - - ), - }} - /> + + {categoryName || 'Flavors'} + + ← Back to all categories + + - {filteredFlavors.map(flavor => ( + {flavors.map(flavor => ( {!flavor.is_available && ( - - Out of Stock + + {flavor.is_legacy ? 'Unavailable' : 'Out of Stock'} )} {flavor.image_url && ( )} - {flavor.name} - {flavor.description.substring(0, 100)}... + {flavor.name} - ({(flavor.average_rating || 0).toFixed(1)}) + ({(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 index cf4ef37..b81ae3f 100644 --- a/frontend/src/pages/flavors/FlavorDetail.tsx +++ b/frontend/src/pages/flavors/FlavorDetail.tsx @@ -10,7 +10,8 @@ import { TextField, Avatar, Alert, - Paper + Paper, + CircularProgress } from '@mui/material'; import api from '../../api'; import { useTitle } from '../../hooks/useTitle'; @@ -42,6 +43,7 @@ interface Flavor { ratings: Rating[]; image_url: string | null; is_available: boolean; + is_legacy: boolean; shop_url: string | null; } @@ -139,7 +141,7 @@ const FlavorDetail: React.FC = () => { } }; - if (loading) return Loading...; + if (loading) return ; if (!flavor) return Flavor not found; return ( @@ -156,7 +158,9 @@ const FlavorDetail: React.FC = () => { )} {!flavor.is_available && ( - Out of Stock + + {flavor.is_legacy ? "Unavailable (Legacy Flavor)" : "Out of Stock"} + )} {flavor.name} {flavor.category_name} @@ -239,7 +243,7 @@ const FlavorDetail: React.FC = () => { )} - {rating.replies.map((reply: Reply) => ( + {rating.replies.map((reply: any) => ( {reply.user} {reply.text} diff --git a/legacy/hitschies_flavors.json b/legacy/hitschies_flavors.json new file mode 100644 index 0000000..4509d08 --- /dev/null +++ b/legacy/hitschies_flavors.json @@ -0,0 +1,23 @@ +[ + { + "Name": "Hitschies Saure Drachenzunge (Blau)", + "Geschmack": "Blaue Himbeere", + "Beschreibung": "Der Klassiker der Kollaboration. Schmeckt wie die blauen sauren Drachenzungen – süß, fruchtig und sauer.", + "Status": "Limited Edition", + "Bild_URL": "https://pbs.twimg.com/media/GRfMeG3b0AAafs-?format=jpg&name=small" + }, + { + "Name": "Hitschies Saure Krakenarme", + "Geschmack": "Orange & Vanille", + "Beschreibung": "Cremige Vanille trifft auf saure Orange. Erinnert stark an die originalen Krakenarme-Süßigkeiten oder Solero-Eis.", + "Status": "Limited Edition", + "Bild_URL": "https://scontent-fra5-2.xx.fbcdn.net/v/t39.30808-6/481459525_933111892304551_6012720927773596920_n.jpg?stp=dst-jpg_s640x640_tt6&_nc_cat=106&ccb=1-7&_nc_sid=127cfc&_nc_ohc=Cdh-KooVUtAQ7kNvwG7p0fQ&_nc_oc=Adl2bJS36wdLnSlo6JGyNrshZYY59GMvEx1CKDuvj-K_Cc_fEBP1H-bNuGX1_B9DH8U&_nc_zt=23&_nc_ht=scontent-fra5-2.xx&_nc_gid=70krAfOzN_hAUKL6i9aY_Q&oh=00_Afsk9sfWUdcMSaepQWZufWTlg314EkrwAAIbQbJMcrrGQA&oe=6998B737" + }, + { + "Name": "Hitschies Saure Spinnenbeine", + "Geschmack": "Cola & Zitrone", + "Beschreibung": "Saurer Cola-Geschmack, angelehnt an die Hitschies Spinnenbeine Fruchtgummis.", + "Status": "Limited Edition", + "Bild_URL": "https://pbs.twimg.com/media/GRjfH6NWIAAB8lZ?format=jpg&name=small" + } +] diff --git a/legacy/other_flavors.json b/legacy/other_flavors.json new file mode 100644 index 0000000..304b213 --- /dev/null +++ b/legacy/other_flavors.json @@ -0,0 +1,9 @@ +[ + { + "Name": "Dolphin Daiquiri", + "Geschmack": "Erdbeere & Limette", + "Beschreibung": "Ein sommerlicher Klassiker aus der alten Squad-Reihe. Nachempfunden dem Strawberry Daiquiri Cocktail (alkoholfrei). Eine süße Erdbeernote gemischt mit spritziger Limette.", + "Status": "Discontinued", + "Bild_URL": "https://pbs.twimg.com/media/GOq80SWXAAEnLGG?format=jpg&name=large" + } +]