diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 896bb4f..f0d36cc 100644 Binary files a/config/__pycache__/__init__.cpython-311.pyc and b/config/__pycache__/__init__.cpython-311.pyc differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index d79d6a7..91c91d6 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 8cf22af..ef63af5 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index a1b4aa7..59f60c7 100644 Binary files a/config/__pycache__/wsgi.cpython-311.pyc and b/config/__pycache__/wsgi.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..7b53c10 100644 --- a/config/settings.py +++ b/config/settings.py @@ -23,6 +23,7 @@ DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true" ALLOWED_HOSTS = [ "127.0.0.1", "localhost", + "testserver", os.getenv("HOST_FQDN", ""), ] @@ -150,9 +151,13 @@ STATIC_URL = 'static/' STATIC_ROOT = BASE_DIR / 'staticfiles' STATICFILES_DIRS = [ - BASE_DIR / 'static', - BASE_DIR / 'assets', - BASE_DIR / 'node_modules', + directory + for directory in [ + BASE_DIR / 'static', + BASE_DIR / 'assets', + BASE_DIR / 'node_modules', + ] + if directory.exists() ] # Email diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc index 3f553f6..d4d602b 100644 Binary files a/core/__pycache__/__init__.cpython-311.pyc and b/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5e8987a..727fc13 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 2fa4a49..c656fb9 100644 Binary files a/core/__pycache__/apps.cpython-311.pyc and b/core/__pycache__/apps.cpython-311.pyc differ diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf223..558bb2a 100644 Binary files a/core/__pycache__/context_processors.cpython-311.pyc and b/core/__pycache__/context_processors.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000..c208736 Binary files /dev/null and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index a251b5f..0c3eda3 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/tests.cpython-311.pyc b/core/__pycache__/tests.cpython-311.pyc new file mode 100644 index 0000000..c2315bf Binary files /dev/null and b/core/__pycache__/tests.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index f705988..886b598 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2f0989c..c124bd6 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..5b2d8a2 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,34 @@ from django.contrib import admin -# Register your models here. +from .models import Category, ContactInquiry, Product + + +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ("name", "sort_order") + prepopulated_fields = {"slug": ("name",)} + search_fields = ("name",) + ordering = ("sort_order", "name") + + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ("name", "category", "price_from", "is_featured", "is_active") + list_filter = ("category", "is_featured", "is_active") + list_editable = ("is_featured", "is_active") + prepopulated_fields = {"slug": ("name",)} + search_fields = ("name", "short_description", "description", "material") + + +@admin.register(ContactInquiry) +class ContactInquiryAdmin(admin.ModelAdmin): + list_display = ("name", "subject", "preferred_contact_method", "status", "created_at") + list_filter = ("preferred_contact_method", "status", "created_at") + list_editable = ("status",) + search_fields = ("name", "email", "phone", "subject", "message") + autocomplete_fields = ("product",) + + +admin.site.site_header = "Thelen Atelier Admin" +admin.site.site_title = "Thelen Atelier" +admin.site.index_title = "Katalog & Anfragen verwalten" diff --git a/core/context_processors.py b/core/context_processors.py index 0bf87c3..72b929d 100644 --- a/core/context_processors.py +++ b/core/context_processors.py @@ -1,13 +1,22 @@ import os +import re import time + def project_context(request): """ Adds project-specific environment variables to the template context globally. """ + store_phone_display = os.getenv("STORE_PHONE", "0214 41243") + store_phone_link = re.sub(r"[^\d+]", "", store_phone_display) + return { "project_description": os.getenv("PROJECT_DESCRIPTION", ""), "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), - # Used for cache-busting static assets + "store_name": os.getenv("STORE_NAME", "Juwelier Thelen"), + "store_phone_display": store_phone_display, + "store_phone_link": store_phone_link, + "store_email": os.getenv("STORE_EMAIL", "info@juwelierthelen.de"), + "store_address": os.getenv("STORE_ADDRESS", "Wiesdorfer Platz 59, 51373 Leverkusen"), "deployment_timestamp": int(time.time()), } diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..eeff850 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,65 @@ +from django import forms + +from .models import ContactInquiry, Product + + +class ContactInquiryForm(forms.ModelForm): + class Meta: + model = ContactInquiry + fields = [ + "product", + "name", + "email", + "phone", + "preferred_contact_method", + "subject", + "message", + ] + widgets = { + "message": forms.Textarea(attrs={"rows": 5}), + } + labels = { + "product": "Produkt / Kollektion", + "name": "Ihr Name", + "email": "E-Mail-Adresse", + "phone": "Telefonnummer", + "preferred_contact_method": "Bevorzugte Kontaktart", + "subject": "Betreff", + "message": "Nachricht", + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["product"].queryset = Product.objects.filter(is_active=True).select_related("category") + self.fields["product"].required = False + self.fields["product"].empty_label = "Allgemeine Beratung" + + placeholders = { + "name": "z. B. Anna Becker", + "email": "anna@example.com", + "phone": "+49 ...", + "subject": "z. B. Beratung zu Trauringen", + "message": "Worum geht es? Teilen Sie uns kurz Ihre Wünsche mit.", + } + for name, field in self.fields.items(): + widget = field.widget + css_class = "form-select" if isinstance(widget, forms.Select) else "form-control" + existing = widget.attrs.get("class", "") + widget.attrs["class"] = f"{existing} {css_class}".strip() + if name in placeholders: + widget.attrs["placeholder"] = placeholders[name] + + def clean(self): + cleaned_data = super().clean() + preferred_contact_method = cleaned_data.get("preferred_contact_method") + phone = (cleaned_data.get("phone") or "").strip() + product = cleaned_data.get("product") + subject = (cleaned_data.get("subject") or "").strip() + + if preferred_contact_method == ContactInquiry.PreferredContactMethod.PHONE and not phone: + self.add_error("phone", "Bitte geben Sie eine Telefonnummer an, wenn Sie einen Rückruf wünschen.") + + if product and not subject: + cleaned_data["subject"] = f"Anfrage zu {product.name}" + + return cleaned_data diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..b938c73 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,67 @@ +# Generated by Django 5.2.7 on 2026-04-06 09:11 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=120, unique=True)), + ('slug', models.SlugField(unique=True)), + ('description', models.TextField(blank=True)), + ('sort_order', models.PositiveIntegerField(default=0)), + ], + options={ + 'ordering': ['sort_order', 'name'], + }, + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=160)), + ('slug', models.SlugField(unique=True)), + ('short_description', models.CharField(max_length=220)), + ('description', models.TextField()), + ('material', models.CharField(blank=True, max_length=120)), + ('price_from', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('collection_note', models.CharField(blank=True, max_length=180)), + ('is_featured', models.BooleanField(default=False)), + ('is_active', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='core.category')), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='ContactInquiry', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=120)), + ('email', models.EmailField(max_length=254)), + ('phone', models.CharField(blank=True, max_length=40)), + ('subject', models.CharField(blank=True, max_length=160)), + ('message', models.TextField()), + ('preferred_contact_method', models.CharField(choices=[('phone', 'Telefon'), ('email', 'E-Mail')], default='phone', max_length=10)), + ('status', models.CharField(choices=[('new', 'Neu'), ('contacted', 'Kontaktiert'), ('closed', 'Abgeschlossen')], default='new', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='core.product')), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/core/migrations/0002_seed_catalog.py b/core/migrations/0002_seed_catalog.py new file mode 100644 index 0000000..63667d6 --- /dev/null +++ b/core/migrations/0002_seed_catalog.py @@ -0,0 +1,120 @@ +from django.db import migrations + + +def seed_catalog(apps, schema_editor): + Category = apps.get_model("core", "Category") + Product = apps.get_model("core", "Product") + + categories = { + "trauringe": Category.objects.update_or_create( + slug="trauringe", + defaults={ + "name": "Trauringe", + "description": "Beratung für Trauringe, Materialien, Oberflächen und Gravuren.", + "sort_order": 1, + }, + )[0], + "schmuck": Category.objects.update_or_create( + slug="schmuck", + defaults={ + "name": "Schmuck", + "description": "Ringe, Ohrschmuck und Perlenstücke für besondere Momente.", + "sort_order": 2, + }, + )[0], + "uhren": Category.objects.update_or_create( + slug="uhren", + defaults={ + "name": "Uhren", + "description": "Ausgewählte Uhren mit persönlicher Beratung und Service.", + "sort_order": 3, + }, + )[0], + "service": Category.objects.update_or_create( + slug="service", + defaults={ + "name": "Services", + "description": "Goldschmiedearbeiten, Reparaturen, Reinigung und Extras.", + "sort_order": 4, + }, + )[0], + } + + products = [ + { + "slug": "signature-trauringe", + "category": categories["trauringe"], + "name": "Signature Trauringe", + "short_description": "Warme Goldtöne, klare Linien und Platz für Ihre persönliche Gravur.", + "description": "Diese Beispielkollektion steht für die Trauring-Beratung von Juwelier Thelen: verschiedene Legierungen, angenehme Profile und eine ruhige, persönliche Auswahl im Geschäft.", + "material": "Gelb- oder Roségold", + "price_from": "1290.00", + "collection_note": "Gravur und Größenanpassung möglich", + "is_featured": True, + }, + { + "slug": "perlen-ohrschmuck-edit", + "category": categories["schmuck"], + "name": "Perlen & Ohrschmuck Edit", + "short_description": "Leichte, elegante Schmuckstücke mit klassischer Note und moderner Wirkung.", + "description": "Inspiriert von den Serviceleistungen rund um Perlen und Ohrlochstechen: eine kuratierte Auswahl an Ohrschmuck und Perlendesigns für Alltag und Anlass.", + "material": "Perlen, Gold, Silber", + "price_from": "149.00", + "collection_note": "Ideal für Geschenkberatung", + "is_featured": True, + }, + { + "slug": "atelier-automatik-uhr", + "category": categories["uhren"], + "name": "Atelier Automatik Uhr", + "short_description": "Zeitloses Zifferblatt, ausgewogenes Gehäuse und Beratung zu Pflege & Revision.", + "description": "Die neue Katalogstrecke macht Uhrenpräsentation und Service greifbarer: ausgewählte Modelle, klare Preisindikationen und direkter Kontakt für Rückfragen.", + "material": "Edelstahl", + "price_from": "895.00", + "collection_note": "Revision und Armbandservice auf Anfrage", + "is_featured": True, + }, + { + "slug": "goldschmiede-service-paket", + "category": categories["service"], + "name": "Goldschmiede Servicepaket", + "short_description": "Ringweiten, Reinigung, Umarbeitung und Reparatur – digital angefragt, persönlich umgesetzt.", + "description": "Auch ohne Checkout bleibt der Ablauf komplett: Leistung entdecken, Kontakt aufnehmen und den Auftrag im Geschäft besprechen. Dieses Beispiel zeigt den Service-Fokus der Website.", + "material": "Service & Beratung", + "price_from": None, + "collection_note": "Preis nach Sichtprüfung", + "is_featured": True, + }, + ] + + for item in products: + Product.objects.update_or_create( + slug=item["slug"], + defaults=item, + ) + + +def unseed_catalog(apps, schema_editor): + Product = apps.get_model("core", "Product") + Category = apps.get_model("core", "Category") + + Product.objects.filter( + slug__in=[ + "signature-trauringe", + "perlen-ohrschmuck-edit", + "atelier-automatik-uhr", + "goldschmiede-service-paket", + ] + ).delete() + Category.objects.filter(slug__in=["trauringe", "schmuck", "uhren", "service"]).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0001_initial"), + ] + + operations = [ + migrations.RunPython(seed_catalog, unseed_catalog), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..90add7c Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_seed_catalog.cpython-311.pyc b/core/migrations/__pycache__/0002_seed_catalog.cpython-311.pyc new file mode 100644 index 0000000..79eb48e Binary files /dev/null and b/core/migrations/__pycache__/0002_seed_catalog.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc index 7995815..39dfe21 100644 Binary files a/core/migrations/__pycache__/__init__.cpython-311.pyc and b/core/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..a13c606 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,81 @@ +from decimal import Decimal + from django.db import models -# Create your models here. + +class Category(models.Model): + name = models.CharField(max_length=120, unique=True) + slug = models.SlugField(unique=True) + description = models.TextField(blank=True) + sort_order = models.PositiveIntegerField(default=0) + + class Meta: + ordering = ["sort_order", "name"] + + def __str__(self): + return self.name + + +class Product(models.Model): + category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name="products") + name = models.CharField(max_length=160) + slug = models.SlugField(unique=True) + short_description = models.CharField(max_length=220) + description = models.TextField() + material = models.CharField(max_length=120, blank=True) + price_from = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) + collection_note = models.CharField(max_length=180, blank=True) + is_featured = models.BooleanField(default=False) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ["name"] + + def __str__(self): + return self.name + + @property + def price_label(self): + if self.price_from is None: + return "Preis auf Anfrage" + quantized = self.price_from.quantize(Decimal("1.00")) + return f"ab {quantized:,.2f} €".replace(",", "X").replace(".", ",").replace("X", ".") + + +class ContactInquiry(models.Model): + class PreferredContactMethod(models.TextChoices): + PHONE = "phone", "Telefon" + EMAIL = "email", "E-Mail" + + class Status(models.TextChoices): + NEW = "new", "Neu" + CONTACTED = "contacted", "Kontaktiert" + CLOSED = "closed", "Abgeschlossen" + + product = models.ForeignKey( + Product, + on_delete=models.SET_NULL, + related_name="inquiries", + blank=True, + null=True, + ) + name = models.CharField(max_length=120) + email = models.EmailField() + phone = models.CharField(max_length=40, blank=True) + subject = models.CharField(max_length=160, blank=True) + message = models.TextField() + preferred_contact_method = models.CharField( + max_length=10, + choices=PreferredContactMethod.choices, + default=PreferredContactMethod.PHONE, + ) + status = models.CharField(max_length=20, choices=Status.choices, default=Status.NEW) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ["-created_at"] + + def __str__(self): + return f"{self.name} – {self.subject or 'Allgemeine Anfrage'}" diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..7ee44b3 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,81 @@ +{% load static %} - - + - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} + + {{ meta_title|default:store_name }} + {% if project_image_url %} {% endif %} - {% load static %} + + + + {% block head %}{% endblock %} - - {% block content %}{% endblock %} - +
+ +
+ {% block content %}{% endblock %} +
+ + +
+ + + diff --git a/core/templates/core/catalog.html b/core/templates/core/catalog.html new file mode 100644 index 0000000..41bc786 --- /dev/null +++ b/core/templates/core/catalog.html @@ -0,0 +1,73 @@ +{% extends "base.html" %} + +{% block content %} +
+
+ Katalog +

Entdecken Sie Schmuck, Uhren und Trauringe.

+

Filterbar, fokussiert und bereit für die direkte Kontaktaufnahme – ohne komplizierten Checkout.

+
+
+ +
+
+
+
+
+ + +
+
+ + +
+
+ + Zurücksetzen +
+
+
+ +
+ {% for product in products %} +
+
+
+ {{ product.category.name }} +
+
+

{{ product.category.name }}

+

{{ product.name }}

+

{{ product.short_description }}

+
+ {{ product.price_label }} + {% if product.material %}{{ product.material }}{% endif %} +
+ +
+
+
+ {% empty %} +
+
+

Keine Treffer gefunden

+

Passen Sie die Filter an oder nehmen Sie direkt Kontakt auf, damit wir Ihnen passende Stücke empfehlen können.

+ +
+
+ {% endfor %} +
+
+
+{% endblock %} diff --git a/core/templates/core/contact.html b/core/templates/core/contact.html new file mode 100644 index 0000000..2174c82 --- /dev/null +++ b/core/templates/core/contact.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} + +{% block content %} +
+
+ Kontakt +

Schnell anfragen, persönlich beraten lassen.

+

Die Anfrage wird gespeichert, im Admin sichtbar gemacht und auf Wunsch zusätzlich per E-Mail versendet.

+
+
+ +
+
+
+
+ +
+
+
+
+ Anfrageformular +

Per E-Mail anfragen

+
+
+ {% csrf_token %} + {% if form.non_field_errors %} +
{{ form.non_field_errors }}
+ {% endif %} +
+ {% for field in form %} +
+ + {{ field }} + {% if field.help_text %}
{{ field.help_text }}
{% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} +
+
+ +

Wir melden uns zeitnah telefonisch oder per E-Mail zurück.

+
+
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/contact_success.html b/core/templates/core/contact_success.html new file mode 100644 index 0000000..6338cd9 --- /dev/null +++ b/core/templates/core/contact_success.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+ Vielen Dank +

Ihre Anfrage ist eingegangen.

+

Das Team von {{ store_name }} meldet sich so schnell wie möglich bei Ihnen.

+ {% if inquiry %} +
+
Kontakt: {{ inquiry.name }}
+
Rückmeldung via: {{ inquiry.get_preferred_contact_method_display }}
+
Produkt: {% if inquiry.product %}{{ inquiry.product.name }}{% else %}Allgemeine Beratung{% endif %}
+
+ {% endif %} + +
+
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..942ee3d 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,171 @@ {% extends "base.html" %} - -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% load static %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+
+
+ Seit über 80 Jahren Vertrauen, Qualität und persönliche Beratung +

Ein moderner Schmuckkatalog für Trauringe, Uhren und Goldschmiedeservice.

+

Entdecken Sie ausgewählte Kollektionen, lernen Sie unsere Services kennen und wechseln Sie mit einem Klick vom Stöbern zur persönlichen Beratung – telefonisch oder per E-Mail.

+ + +
+
+
+
+
+

Store Snapshot

+
+
+ 80+ + Jahre Erfahrung +
+
+ 4 + klare Katalogbereiche +
+
+ 1 + zentrale Anfrage pro Produkt +
+
+
+

Wiesdorfer Platz 59

+

51373 Leverkusen · Beratung per Telefon und E-Mail

+
+
+
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
- -{% endblock %} \ No newline at end of file + + +
+
+ +
+
+ +
+
+
+ Leistungen +

Die wichtigsten Inhalte der bisherigen Website – jetzt klarer, schneller, moderner.

+

Die neue Startseite bündelt Trauringe, Schmuck, Uhren und Serviceleistungen in einem ruhigen, leicht navigierbaren Ablauf.

+
+
+ {% for item in service_highlights %} +
+
+
{{ forloop.counter|stringformat:"02d" }}
+

{{ item.title }}

+

{{ item.text }}

+
+
+ {% endfor %} +
+
+
+ +
+
+
+
+ Auswahl +

Featured Katalog

+
+ Alle Produkte ansehen +
+
+ {% for product in featured_products %} +
+
+
+ {{ product.category.name }} +
+
+
+
+

{{ product.category.name }}

+

{{ product.name }}

+
+ {{ product.price_label }} +
+

{{ product.short_description }}

+ Details & Anfrage +
+
+
+ {% empty %} +
+
+

Der Katalog ist bereit.

+

Pflegen Sie jetzt Produkte im Admin ein, damit die Startseite automatisch die ersten Highlights zeigt.

+ Produkte im Admin anlegen +
+
+ {% endfor %} +
+
+
+ +
+
+
+
+ Ablauf +

Vom Produkt zur persönlichen Beratung in drei einfachen Schritten.

+
+
+ {% for step in process_steps %} +
+
+ {{ step.step }} +

{{ step.title }}

+

{{ step.text }}

+
+
+ {% endfor %} +
+
+
+
+ +
+
+
+
+ Kontakt +

Sie möchten lieber direkt sprechen?

+

Rufen Sie an oder senden Sie eine E-Mail – alle Produktseiten führen direkt in die Kontaktstrecke.

+
+ +
+
+
+{% endblock %} diff --git a/core/templates/core/product_detail.html b/core/templates/core/product_detail.html new file mode 100644 index 0000000..16d60a8 --- /dev/null +++ b/core/templates/core/product_detail.html @@ -0,0 +1,76 @@ +{% extends "base.html" %} + +{% block content %} +
+
+ ← Zurück zum Katalog +
+
+ {{ product.category.name }} +

{{ product.name }}

+

{{ product.short_description }}

+
+
+
+

{{ product.price_label }}

+ {% if product.material %}

Material · {{ product.material }}

{% endif %} + {% if product.collection_note %}

{{ product.collection_note }}

{% endif %} + +
+
+
+
+
+ +
+
+
+
+
+

Über dieses Stück

+

{{ product.description }}

+
+
+
+ +
+
+
+
+ +{% if related_products %} +
+
+
+ Passend dazu +

Weitere Stücke aus {{ product.category.name }}

+
+
+ {% for item in related_products %} +
+
+
{{ item.category.name }}
+
+

{{ item.category.name }}

+

{{ item.name }}

+

{{ item.short_description }}

+ Details ansehen +
+
+
+ {% endfor %} +
+
+
+{% endif %} +{% endblock %} diff --git a/core/tests.py b/core/tests.py index 7ce503c..931b5ce 100644 --- a/core/tests.py +++ b/core/tests.py @@ -1,3 +1,47 @@ from django.test import TestCase +from django.urls import reverse -# Create your tests here. +from .models import Category, ContactInquiry, Product + + +class CatalogFlowTests(TestCase): + def setUp(self): + self.category = Category.objects.create( + name="Trauringe", + slug="trauringe", + description="Individuelle Ringe und Beratung.", + sort_order=1, + ) + self.product = Product.objects.create( + category=self.category, + name="Signature Trauring", + slug="signature-trauring", + short_description="Ein klassischer Ring mit warmem Goldton.", + description="Ausgewählte Trauringe mit Gravuroption und persönlicher Beratung.", + material="Gold", + price_from="1290.00", + is_featured=True, + ) + + def test_homepage_uses_catalog_content(self): + response = self.client.get(reverse("home")) + self.assertContains(response, self.product.name) + self.assertContains(response, self.category.name) + + def test_contact_form_creates_inquiry(self): + response = self.client.post( + reverse("contact"), + { + "product": self.product.pk, + "name": "Anna Becker", + "email": "anna@example.com", + "phone": "+49 123", + "preferred_contact_method": "phone", + "subject": "Beratung", + "message": "Ich möchte einen Termin für Trauringe.", + }, + ) + self.assertRedirects(response, reverse("contact_success")) + self.assertEqual(ContactInquiry.objects.count(), 1) + inquiry = ContactInquiry.objects.first() + self.assertEqual(inquiry.product, self.product) diff --git a/core/urls.py b/core/urls.py index 6299e3d..442aad9 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,11 @@ from django.urls import path -from .views import home +from .views import catalog, contact, contact_success, home, product_detail urlpatterns = [ path("", home, name="home"), + path("katalog/", catalog, name="catalog"), + path("katalog//", product_detail, name="product_detail"), + path("kontakt/", contact, name="contact"), + path("kontakt/danke/", contact_success, name="contact_success"), ] diff --git a/core/views.py b/core/views.py index c9aed12..c2fc1ff 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,186 @@ -import os -import platform +from django.conf import settings +from django.core.mail import send_mail +from django.db.models import Count, Q +from django.shortcuts import get_object_or_404, redirect, render -from django import get_version as django_version -from django.shortcuts import render -from django.utils import timezone +from .forms import ContactInquiryForm +from .models import Category, ContactInquiry, Product + + +SERVICE_HIGHLIGHTS = [ + { + "title": "Trauringe & Beratung", + "text": "Individuelle Beratung für Trauringe, Materialien und Gravuren – persönlich im Geschäft oder per Anfrage.", + }, + { + "title": "Goldschmiedeservice", + "text": "Ringweitenänderung, Reinigung, Umarbeitung und Reparaturen mit viel Erfahrung und Fingerspitzengefühl.", + }, + { + "title": "Uhren & Revision", + "text": "Ausgewählte Uhren und fachkundige Unterstützung bei Pflege, Wartung und Reparatur wertvoller Zeitmesser.", + }, + { + "title": "Perlen, Ohrschmuck & Extras", + "text": "Perlkettenservice, Ohrlochstechen mit STUDEX-System sowie Geschenkgutscheine für besondere Anlässe.", + }, +] + + +PROCESS_STEPS = [ + { + "step": "01", + "title": "Kollektion entdecken", + "text": "Filtern Sie die Kollektion nach Kategorie und lassen Sie sich inspirieren.", + }, + { + "step": "02", + "title": "Per Telefon oder E-Mail anfragen", + "text": "Zu jedem Produkt gelangen Sie in wenigen Klicks zur passenden Kontaktmöglichkeit.", + }, + { + "step": "03", + "title": "Persönliche Beratung im Store", + "text": "Im Geschäft in Leverkusen klärt das Team Details, Anpassungen und Verfügbarkeit.", + }, +] + + +def _catalog_categories(): + return Category.objects.annotate( + product_total=Count("products", filter=Q(products__is_active=True)) + ) + + +def _send_inquiry_notification(inquiry): + if not settings.CONTACT_EMAIL_TO: + return + + subject = inquiry.subject or "Neue Anfrage über die Website" + lines = [ + f"Name: {inquiry.name}", + f"E-Mail: {inquiry.email}", + f"Telefon: {inquiry.phone or '-'}", + f"Bevorzugte Kontaktart: {inquiry.get_preferred_contact_method_display()}", + f"Produkt: {inquiry.product.name if inquiry.product else 'Allgemeine Beratung'}", + "", + inquiry.message, + ] + + try: + send_mail( + subject=f"Website-Anfrage: {subject}", + message="\n".join(lines), + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=settings.CONTACT_EMAIL_TO, + fail_silently=True, + reply_to=[inquiry.email], + ) + except Exception: + return def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() + featured_products = Product.objects.filter(is_active=True, is_featured=True).select_related("category")[:4] + categories = _catalog_categories().filter(product_total__gt=0) context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + "meta_title": "Juwelier Thelen | Moderner Schmuck- & Trauringkatalog", + "meta_description": "Moderner Katalog für Trauringe, Schmuck, Uhren und Goldschmiedeservice – mit schneller Kontaktaufnahme per Telefon oder E-Mail.", + "featured_products": featured_products, + "categories": categories, + "service_highlights": SERVICE_HIGHLIGHTS, + "process_steps": PROCESS_STEPS, } return render(request, "core/index.html", context) + + +def catalog(request): + selected_category = request.GET.get("category", "").strip() + query = request.GET.get("q", "").strip() + + products = Product.objects.filter(is_active=True).select_related("category") + if selected_category: + products = products.filter(category__slug=selected_category) + if query: + products = products.filter( + Q(name__icontains=query) + | Q(short_description__icontains=query) + | Q(description__icontains=query) + | Q(material__icontains=query) + | Q(category__name__icontains=query) + ) + + context = { + "meta_title": "Katalog | Juwelier Thelen", + "meta_description": "Entdecken Sie Trauringe, Schmuck und Uhren – klar gefiltert und bereit für eine persönliche Beratung.", + "products": products, + "categories": _catalog_categories(), + "selected_category": selected_category, + "query": query, + } + return render(request, "core/catalog.html", context) + + +def product_detail(request, slug): + product = get_object_or_404( + Product.objects.filter(is_active=True).select_related("category"), + slug=slug, + ) + related_products = ( + Product.objects.filter(is_active=True, category=product.category) + .exclude(pk=product.pk) + .select_related("category")[:3] + ) + + context = { + "meta_title": f"{product.name} | Juwelier Thelen", + "meta_description": product.short_description, + "product": product, + "related_products": related_products, + } + return render(request, "core/product_detail.html", context) + + +def contact(request): + selected_product = None + product_slug = request.GET.get("product", "").strip() + + if product_slug: + selected_product = Product.objects.filter(is_active=True, slug=product_slug).first() + + if request.method == "POST": + form = ContactInquiryForm(request.POST) + if form.is_valid(): + inquiry = form.save() + _send_inquiry_notification(inquiry) + request.session["contact_inquiry_id"] = inquiry.pk + return redirect("contact_success") + else: + initial = {} + if selected_product: + initial["product"] = selected_product + initial["subject"] = f"Anfrage zu {selected_product.name}" + form = ContactInquiryForm(initial=initial) + + context = { + "meta_title": "Kontakt | Juwelier Thelen", + "meta_description": "Fragen zu Trauringen, Schmuck, Uhren oder Serviceleistungen? Kontaktieren Sie Juwelier Thelen per Telefon oder E-Mail.", + "form": form, + "selected_product": selected_product, + } + return render(request, "core/contact.html", context) + + +def contact_success(request): + inquiry_id = request.session.get("contact_inquiry_id") + inquiry = None + if inquiry_id: + inquiry = ContactInquiry.objects.filter(pk=inquiry_id).select_related("product").first() + + context = { + "meta_title": "Anfrage erhalten | Juwelier Thelen", + "meta_description": "Vielen Dank für Ihre Anfrage. Das Team meldet sich zeitnah telefonisch oder per E-Mail zurück.", + "inquiry": inquiry, + } + return render(request, "core/contact_success.html", context) diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..ce06a33 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,632 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +/* Brand system */ +:root { + --color-bg: #f5efe7; + --color-bg-soft: #ece4d7; + --color-surface: rgba(255, 250, 244, 0.82); + --color-surface-strong: #fffaf4; + --color-ink: #171616; + --color-muted: #5f605f; + --color-brand: #b08a52; + --color-brand-dark: #8d6a37; + --color-accent: #1d4339; + --color-accent-soft: #dce9e3; + --color-line: rgba(23, 22, 22, 0.08); + --shadow-soft: 0 24px 70px rgba(28, 24, 19, 0.12); + --shadow-card: 0 18px 40px rgba(43, 31, 18, 0.1); + --radius-xl: 32px; + --radius-lg: 24px; + --radius-md: 18px; + --radius-sm: 14px; + --space-section: clamp(4.5rem, 7vw, 7rem); +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + font-family: 'Inter', system-ui, -apple-system, sans-serif; + color: var(--color-ink); + background: + radial-gradient(circle at top left, rgba(176, 138, 82, 0.18), transparent 30%), + radial-gradient(circle at 85% 15%, rgba(29, 67, 57, 0.14), transparent 26%), + linear-gradient(180deg, #fbf7f1 0%, var(--color-bg) 46%, #efe8dc 100%); + line-height: 1.6; +} + +body::before, +body::after { + content: ""; + position: fixed; + inset: auto; + border-radius: 50%; + filter: blur(10px); + pointer-events: none; + z-index: -1; +} + +body::before { + width: 18rem; + height: 18rem; + top: 6rem; + right: 4vw; + background: rgba(176, 138, 82, 0.12); +} + +body::after { + width: 12rem; + height: 12rem; + bottom: 10rem; + left: 3vw; + background: rgba(29, 67, 57, 0.1); +} + +h1, +h2, +h3, +h4, +h5, +h6, +.navbar-brand strong, +.footer-title { + font-family: 'Cormorant Garamond', Georgia, serif; + letter-spacing: -0.02em; + line-height: 1.05; +} + +a { + color: inherit; + text-decoration: none; +} + +a:hover { + color: var(--color-brand-dark); +} + +.site-header { + background: rgba(251, 247, 241, 0.78); + backdrop-filter: blur(24px); + border-bottom: 1px solid rgba(23, 22, 22, 0.05); +} + +.navbar-brand { + display: inline-flex; + align-items: center; + gap: 0.85rem; + font-size: 1.05rem; +} + +.navbar-brand small { + display: block; + color: var(--color-muted); + font-size: 0.78rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.brand-mark { + width: 2.6rem; + height: 2.6rem; + display: inline-grid; + place-items: center; + border-radius: 50%; + background: linear-gradient(135deg, var(--color-brand), #ddc08b); + color: white; + font-family: 'Cormorant Garamond', Georgia, serif; + font-size: 1.45rem; + box-shadow: 0 12px 30px rgba(176, 138, 82, 0.3); +} + +.nav-link { + color: var(--color-ink); + font-weight: 500; + padding: 0.75rem 1rem !important; +} + +.nav-link:hover, +.nav-link:focus { + color: var(--color-brand-dark); +} + +.section-shell { + padding: var(--space-section) 0; +} + +.page-hero-compact { + padding-bottom: 2.5rem; +} + +.hero-section { + position: relative; + overflow: hidden; +} + +.hero-title, +.page-title { + font-size: clamp(3rem, 5vw, 5.4rem); + margin-bottom: 1.4rem; +} + +.hero-copy, +.page-copy, +.section-heading p, +.product-body p, +.service-card p, +.process-card p, +.contact-info p, +.detail-copy p, +.detail-side p, +.form-note, +.footer-title + p { + color: var(--color-muted); + font-size: 1.05rem; +} + +.hero-copy, +.page-copy { + max-width: 44rem; +} + +.eyebrow { + display: inline-block; + margin-bottom: 1rem; + color: var(--color-accent); + text-transform: uppercase; + letter-spacing: 0.18em; + font-size: 0.77rem; + font-weight: 700; +} + +.glass-panel, +.product-card, +.service-card, +.category-card, +.empty-state, +.process-shell, +.success-card { + background: linear-gradient(180deg, rgba(255, 250, 244, 0.9), rgba(255, 255, 255, 0.72)); + border: 1px solid rgba(255, 255, 255, 0.7); + box-shadow: var(--shadow-soft); + backdrop-filter: blur(18px); + border-radius: var(--radius-xl); +} + +.hero-search, +.filter-panel, +.form-panel { + padding: 1.35rem; +} + +.hero-actions { + margin-top: 1.8rem; +} + +.hero-showcase { + padding: 2rem; + min-height: 27rem; + position: relative; + overflow: hidden; +} + +.hero-orb { + position: absolute; + border-radius: 50%; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(176, 138, 82, 0.12)); + border: 1px solid rgba(176, 138, 82, 0.16); +} + +.hero-orb-lg { + width: 13rem; + height: 13rem; + top: 2rem; + right: -1rem; +} + +.hero-orb-sm { + width: 5rem; + height: 5rem; + left: 1rem; + bottom: 2rem; +} + +.stat-grid { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.stat-card { + padding: 1.4rem; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(23, 22, 22, 0.05); + box-shadow: var(--shadow-card); +} + +.stat-card strong { + display: block; + font-family: 'Cormorant Garamond', Georgia, serif; + font-size: 2.4rem; +} + +.stat-card span, +.showcase-note, +.product-category, +.detail-label { + color: var(--color-muted); +} + +.showcase-note { + position: absolute; + left: 2rem; + right: 2rem; + bottom: 2rem; + padding: 1.1rem 1.2rem; + border-radius: var(--radius-md); + background: rgba(29, 67, 57, 0.08); +} + +.category-card, +.service-card, +.product-card { + padding: 1.6rem; + display: flex; + flex-direction: column; + height: 100%; + transition: transform 0.25s ease, box-shadow 0.25s ease; +} + +.category-card:hover, +.service-card:hover, +.product-card:hover, +.contact-banner:hover { + transform: translateY(-4px); + box-shadow: 0 28px 70px rgba(28, 24, 19, 0.16); +} + +.category-chip, +.product-visual span, +.process-step { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + padding: 0.35rem 0.75rem; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.06em; +} + +.category-chip, +.process-step { + background: rgba(29, 67, 57, 0.09); + color: var(--color-accent); +} + +.product-visual { + min-height: 12rem; + border-radius: var(--radius-lg); + margin-bottom: 1.25rem; + background: + radial-gradient(circle at 28% 28%, rgba(255, 255, 255, 0.95), transparent 36%), + linear-gradient(140deg, rgba(176, 138, 82, 0.22), rgba(29, 67, 57, 0.18)); + position: relative; + overflow: hidden; +} + +.product-visual::before, +.product-visual::after { + content: ""; + position: absolute; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.75); +} + +.product-visual::before { + width: 7rem; + height: 7rem; + right: 1.25rem; + top: 1.25rem; +} + +.product-visual::after { + width: 3.6rem; + height: 3.6rem; + left: 1.4rem; + bottom: 1.1rem; +} + +.product-visual span { + position: absolute; + left: 1rem; + top: 1rem; + background: rgba(255, 250, 244, 0.9); + color: var(--color-accent); +} + +.product-body { + display: flex; + flex-direction: column; + flex: 1; +} + +.product-body h2, +.product-body h3, +.service-card h3, +.category-card h2, +.process-card h3, +.contact-banner h2, +.contact-info h2, +.detail-copy h2, +.detail-side h2, +.form-panel h2 { + font-size: clamp(1.8rem, 2vw, 2.4rem); + margin-bottom: 0.9rem; +} + +.product-category { + margin-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.12em; + font-size: 0.75rem; + font-weight: 700; +} + +.product-price, +.detail-price { + color: var(--color-brand-dark); + font-size: 1.1rem; +} + +.product-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-top: auto; + padding-top: 1rem; + color: var(--color-muted); +} + +.text-link, +.back-link { + color: var(--color-accent); + font-weight: 700; +} + +.text-link:hover, +.back-link:hover { + color: var(--color-brand-dark); +} + +.section-heading { + max-width: 42rem; + margin-bottom: 2rem; +} + +.section-heading h2, +.footer-title { + font-size: clamp(2.4rem, 3vw, 3.6rem); + margin-bottom: 0.8rem; +} + +.service-icon { + width: 3rem; + height: 3rem; + display: inline-grid; + place-items: center; + border-radius: 50%; + margin-bottom: 1rem; + background: rgba(176, 138, 82, 0.12); + color: var(--color-brand-dark); + font-weight: 700; +} + +.process-shell, +.contact-banner, +.success-card, +.detail-card, +.detail-copy, +.detail-side, +.contact-info { + padding: 2rem; +} + +.process-card { + padding: 1.2rem; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.55); + border: 1px solid rgba(23, 22, 22, 0.05); + height: 100%; +} + +.contact-banner { + padding: 2rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 2rem; +} + +.contact-method { + margin-bottom: 1.5rem; +} + +.contact-method span, +.footer-heading { + display: block; + margin-bottom: 0.35rem; + color: var(--color-muted); + font-size: 0.84rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; +} + +.contact-method a, +.site-footer a { + font-size: 1.05rem; + font-weight: 600; +} + +.selected-product-note { + margin-top: 2rem; + padding: 1.25rem; + border-radius: var(--radius-md); + background: rgba(29, 67, 57, 0.08); +} + +.site-footer { + border-top: 1px solid rgba(23, 22, 22, 0.06); + background: rgba(255, 250, 244, 0.65); + backdrop-filter: blur(18px); +} + +.btn { + border-radius: 999px; + padding: 0.9rem 1.45rem; + font-weight: 700; + letter-spacing: 0.01em; + border-width: 1px; + transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; +} + +.btn:hover, +.btn:focus { + transform: translateY(-1px); +} + +.btn-brand { + color: #fff; + background: linear-gradient(135deg, var(--color-brand-dark), var(--color-brand)); + border-color: transparent; + box-shadow: 0 16px 32px rgba(176, 138, 82, 0.28); +} + +.btn-brand:hover, +.btn-brand:focus { + color: #fff; + background: linear-gradient(135deg, #7a5b2f, var(--color-brand-dark)); +} + +.btn-ghost { + color: var(--color-ink); + background: rgba(255, 255, 255, 0.58); + border-color: rgba(23, 22, 22, 0.08); +} + +.btn-ghost:hover, +.btn-ghost:focus { + background: rgba(255, 255, 255, 0.88); + border-color: rgba(23, 22, 22, 0.14); +} + +.form-control, +.form-select { + min-height: 3.4rem; + border-radius: 1rem; + border: 1px solid rgba(23, 22, 22, 0.1); + background: rgba(255, 255, 255, 0.92); + color: var(--color-ink); + padding: 0.85rem 1rem; +} + +textarea.form-control { + min-height: 9rem; +} + +.form-control:focus, +.form-select:focus, +.btn:focus, +.nav-link:focus, +a:focus { + outline: none; + border-color: rgba(176, 138, 82, 0.5); + box-shadow: 0 0 0 0.2rem rgba(176, 138, 82, 0.16); +} + +.field-error { + color: #9b2d2d; + font-size: 0.88rem; + margin-top: 0.45rem; +} + +.empty-state, +.success-card { + text-align: center; + padding: 3rem 2rem; +} + +.success-card { + max-width: 48rem; +} + +.success-summary { + margin-top: 1.5rem; + padding: 1.2rem; + border-radius: var(--radius-md); + background: rgba(29, 67, 57, 0.08); +} + +.filter-panel { + position: relative; + z-index: 2; +} + +.detail-list { + padding-left: 1.1rem; + margin: 0; +} + +.detail-list li { + margin-bottom: 0.8rem; + color: var(--color-muted); +} + +.back-link { + display: inline-block; + margin-bottom: 1.25rem; +} + +@media (max-width: 991.98px) { + .hero-showcase { + min-height: auto; + } + + .contact-banner { + flex-direction: column; + align-items: flex-start; + } +} + +@media (max-width: 767.98px) { + .section-shell { + padding: 4rem 0; + } + + .hero-title, + .page-title { + font-size: 2.6rem; + } + + .glass-panel, + .product-card, + .service-card, + .category-card, + .contact-banner, + .success-card, + .process-shell, + .detail-card, + .detail-copy, + .detail-side, + .contact-info { + border-radius: 1.5rem; + } } diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..ce06a33 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,632 @@ - +/* Brand system */ :root { - --bg-color-start: #6a11cb; - --bg-color-end: #2575fc; - --text-color: #ffffff; - --card-bg-color: rgba(255, 255, 255, 0.01); - --card-border-color: rgba(255, 255, 255, 0.1); + --color-bg: #f5efe7; + --color-bg-soft: #ece4d7; + --color-surface: rgba(255, 250, 244, 0.82); + --color-surface-strong: #fffaf4; + --color-ink: #171616; + --color-muted: #5f605f; + --color-brand: #b08a52; + --color-brand-dark: #8d6a37; + --color-accent: #1d4339; + --color-accent-soft: #dce9e3; + --color-line: rgba(23, 22, 22, 0.08); + --shadow-soft: 0 24px 70px rgba(28, 24, 19, 0.12); + --shadow-card: 0 18px 40px rgba(43, 31, 18, 0.1); + --radius-xl: 32px; + --radius-lg: 24px; + --radius-md: 18px; + --radius-sm: 14px; + --space-section: clamp(4.5rem, 7vw, 7rem); } + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + body { margin: 0; - font-family: 'Inter', sans-serif; - background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); - color: var(--text-color); - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; - text-align: center; - overflow: hidden; - position: relative; + font-family: 'Inter', system-ui, -apple-system, sans-serif; + color: var(--color-ink); + background: + radial-gradient(circle at top left, rgba(176, 138, 82, 0.18), transparent 30%), + radial-gradient(circle at 85% 15%, rgba(29, 67, 57, 0.14), transparent 26%), + linear-gradient(180deg, #fbf7f1 0%, var(--color-bg) 46%, #efe8dc 100%); + line-height: 1.6; +} + +body::before, +body::after { + content: ""; + position: fixed; + inset: auto; + border-radius: 50%; + filter: blur(10px); + pointer-events: none; + z-index: -1; +} + +body::before { + width: 18rem; + height: 18rem; + top: 6rem; + right: 4vw; + background: rgba(176, 138, 82, 0.12); +} + +body::after { + width: 12rem; + height: 12rem; + bottom: 10rem; + left: 3vw; + background: rgba(29, 67, 57, 0.1); +} + +h1, +h2, +h3, +h4, +h5, +h6, +.navbar-brand strong, +.footer-title { + font-family: 'Cormorant Garamond', Georgia, serif; + letter-spacing: -0.02em; + line-height: 1.05; +} + +a { + color: inherit; + text-decoration: none; +} + +a:hover { + color: var(--color-brand-dark); +} + +.site-header { + background: rgba(251, 247, 241, 0.78); + backdrop-filter: blur(24px); + border-bottom: 1px solid rgba(23, 22, 22, 0.05); +} + +.navbar-brand { + display: inline-flex; + align-items: center; + gap: 0.85rem; + font-size: 1.05rem; +} + +.navbar-brand small { + display: block; + color: var(--color-muted); + font-size: 0.78rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.brand-mark { + width: 2.6rem; + height: 2.6rem; + display: inline-grid; + place-items: center; + border-radius: 50%; + background: linear-gradient(135deg, var(--color-brand), #ddc08b); + color: white; + font-family: 'Cormorant Garamond', Georgia, serif; + font-size: 1.45rem; + box-shadow: 0 12px 30px rgba(176, 138, 82, 0.3); +} + +.nav-link { + color: var(--color-ink); + font-weight: 500; + padding: 0.75rem 1rem !important; +} + +.nav-link:hover, +.nav-link:focus { + color: var(--color-brand-dark); +} + +.section-shell { + padding: var(--space-section) 0; +} + +.page-hero-compact { + padding-bottom: 2.5rem; +} + +.hero-section { + position: relative; + overflow: hidden; +} + +.hero-title, +.page-title { + font-size: clamp(3rem, 5vw, 5.4rem); + margin-bottom: 1.4rem; +} + +.hero-copy, +.page-copy, +.section-heading p, +.product-body p, +.service-card p, +.process-card p, +.contact-info p, +.detail-copy p, +.detail-side p, +.form-note, +.footer-title + p { + color: var(--color-muted); + font-size: 1.05rem; +} + +.hero-copy, +.page-copy { + max-width: 44rem; +} + +.eyebrow { + display: inline-block; + margin-bottom: 1rem; + color: var(--color-accent); + text-transform: uppercase; + letter-spacing: 0.18em; + font-size: 0.77rem; + font-weight: 700; +} + +.glass-panel, +.product-card, +.service-card, +.category-card, +.empty-state, +.process-shell, +.success-card { + background: linear-gradient(180deg, rgba(255, 250, 244, 0.9), rgba(255, 255, 255, 0.72)); + border: 1px solid rgba(255, 255, 255, 0.7); + box-shadow: var(--shadow-soft); + backdrop-filter: blur(18px); + border-radius: var(--radius-xl); +} + +.hero-search, +.filter-panel, +.form-panel { + padding: 1.35rem; +} + +.hero-actions { + margin-top: 1.8rem; +} + +.hero-showcase { + padding: 2rem; + min-height: 27rem; + position: relative; + overflow: hidden; +} + +.hero-orb { + position: absolute; + border-radius: 50%; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(176, 138, 82, 0.12)); + border: 1px solid rgba(176, 138, 82, 0.16); +} + +.hero-orb-lg { + width: 13rem; + height: 13rem; + top: 2rem; + right: -1rem; +} + +.hero-orb-sm { + width: 5rem; + height: 5rem; + left: 1rem; + bottom: 2rem; +} + +.stat-grid { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.stat-card { + padding: 1.4rem; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(23, 22, 22, 0.05); + box-shadow: var(--shadow-card); +} + +.stat-card strong { + display: block; + font-family: 'Cormorant Garamond', Georgia, serif; + font-size: 2.4rem; +} + +.stat-card span, +.showcase-note, +.product-category, +.detail-label { + color: var(--color-muted); +} + +.showcase-note { + position: absolute; + left: 2rem; + right: 2rem; + bottom: 2rem; + padding: 1.1rem 1.2rem; + border-radius: var(--radius-md); + background: rgba(29, 67, 57, 0.08); +} + +.category-card, +.service-card, +.product-card { + padding: 1.6rem; + display: flex; + flex-direction: column; + height: 100%; + transition: transform 0.25s ease, box-shadow 0.25s ease; +} + +.category-card:hover, +.service-card:hover, +.product-card:hover, +.contact-banner:hover { + transform: translateY(-4px); + box-shadow: 0 28px 70px rgba(28, 24, 19, 0.16); +} + +.category-chip, +.product-visual span, +.process-step { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + padding: 0.35rem 0.75rem; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.06em; +} + +.category-chip, +.process-step { + background: rgba(29, 67, 57, 0.09); + color: var(--color-accent); +} + +.product-visual { + min-height: 12rem; + border-radius: var(--radius-lg); + margin-bottom: 1.25rem; + background: + radial-gradient(circle at 28% 28%, rgba(255, 255, 255, 0.95), transparent 36%), + linear-gradient(140deg, rgba(176, 138, 82, 0.22), rgba(29, 67, 57, 0.18)); + position: relative; + overflow: hidden; +} + +.product-visual::before, +.product-visual::after { + content: ""; + position: absolute; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.75); +} + +.product-visual::before { + width: 7rem; + height: 7rem; + right: 1.25rem; + top: 1.25rem; +} + +.product-visual::after { + width: 3.6rem; + height: 3.6rem; + left: 1.4rem; + bottom: 1.1rem; +} + +.product-visual span { + position: absolute; + left: 1rem; + top: 1rem; + background: rgba(255, 250, 244, 0.9); + color: var(--color-accent); +} + +.product-body { + display: flex; + flex-direction: column; + flex: 1; +} + +.product-body h2, +.product-body h3, +.service-card h3, +.category-card h2, +.process-card h3, +.contact-banner h2, +.contact-info h2, +.detail-copy h2, +.detail-side h2, +.form-panel h2 { + font-size: clamp(1.8rem, 2vw, 2.4rem); + margin-bottom: 0.9rem; +} + +.product-category { + margin-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.12em; + font-size: 0.75rem; + font-weight: 700; +} + +.product-price, +.detail-price { + color: var(--color-brand-dark); + font-size: 1.1rem; +} + +.product-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-top: auto; + padding-top: 1rem; + color: var(--color-muted); +} + +.text-link, +.back-link { + color: var(--color-accent); + font-weight: 700; +} + +.text-link:hover, +.back-link:hover { + color: var(--color-brand-dark); +} + +.section-heading { + max-width: 42rem; + margin-bottom: 2rem; +} + +.section-heading h2, +.footer-title { + font-size: clamp(2.4rem, 3vw, 3.6rem); + margin-bottom: 0.8rem; +} + +.service-icon { + width: 3rem; + height: 3rem; + display: inline-grid; + place-items: center; + border-radius: 50%; + margin-bottom: 1rem; + background: rgba(176, 138, 82, 0.12); + color: var(--color-brand-dark); + font-weight: 700; +} + +.process-shell, +.contact-banner, +.success-card, +.detail-card, +.detail-copy, +.detail-side, +.contact-info { + padding: 2rem; +} + +.process-card { + padding: 1.2rem; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.55); + border: 1px solid rgba(23, 22, 22, 0.05); + height: 100%; +} + +.contact-banner { + padding: 2rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 2rem; +} + +.contact-method { + margin-bottom: 1.5rem; +} + +.contact-method span, +.footer-heading { + display: block; + margin-bottom: 0.35rem; + color: var(--color-muted); + font-size: 0.84rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; +} + +.contact-method a, +.site-footer a { + font-size: 1.05rem; + font-weight: 600; +} + +.selected-product-note { + margin-top: 2rem; + padding: 1.25rem; + border-radius: var(--radius-md); + background: rgba(29, 67, 57, 0.08); +} + +.site-footer { + border-top: 1px solid rgba(23, 22, 22, 0.06); + background: rgba(255, 250, 244, 0.65); + backdrop-filter: blur(18px); +} + +.btn { + border-radius: 999px; + padding: 0.9rem 1.45rem; + font-weight: 700; + letter-spacing: 0.01em; + border-width: 1px; + transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease; +} + +.btn:hover, +.btn:focus { + transform: translateY(-1px); +} + +.btn-brand { + color: #fff; + background: linear-gradient(135deg, var(--color-brand-dark), var(--color-brand)); + border-color: transparent; + box-shadow: 0 16px 32px rgba(176, 138, 82, 0.28); +} + +.btn-brand:hover, +.btn-brand:focus { + color: #fff; + background: linear-gradient(135deg, #7a5b2f, var(--color-brand-dark)); +} + +.btn-ghost { + color: var(--color-ink); + background: rgba(255, 255, 255, 0.58); + border-color: rgba(23, 22, 22, 0.08); +} + +.btn-ghost:hover, +.btn-ghost:focus { + background: rgba(255, 255, 255, 0.88); + border-color: rgba(23, 22, 22, 0.14); +} + +.form-control, +.form-select { + min-height: 3.4rem; + border-radius: 1rem; + border: 1px solid rgba(23, 22, 22, 0.1); + background: rgba(255, 255, 255, 0.92); + color: var(--color-ink); + padding: 0.85rem 1rem; +} + +textarea.form-control { + min-height: 9rem; +} + +.form-control:focus, +.form-select:focus, +.btn:focus, +.nav-link:focus, +a:focus { + outline: none; + border-color: rgba(176, 138, 82, 0.5); + box-shadow: 0 0 0 0.2rem rgba(176, 138, 82, 0.16); +} + +.field-error { + color: #9b2d2d; + font-size: 0.88rem; + margin-top: 0.45rem; +} + +.empty-state, +.success-card { + text-align: center; + padding: 3rem 2rem; +} + +.success-card { + max-width: 48rem; +} + +.success-summary { + margin-top: 1.5rem; + padding: 1.2rem; + border-radius: var(--radius-md); + background: rgba(29, 67, 57, 0.08); +} + +.filter-panel { + position: relative; + z-index: 2; +} + +.detail-list { + padding-left: 1.1rem; + margin: 0; +} + +.detail-list li { + margin-bottom: 0.8rem; + color: var(--color-muted); +} + +.back-link { + display: inline-block; + margin-bottom: 1.25rem; +} + +@media (max-width: 991.98px) { + .hero-showcase { + min-height: auto; + } + + .contact-banner { + flex-direction: column; + align-items: flex-start; + } +} + +@media (max-width: 767.98px) { + .section-shell { + padding: 4rem 0; + } + + .hero-title, + .page-title { + font-size: 2.6rem; + } + + .glass-panel, + .product-card, + .service-card, + .category-card, + .contact-banner, + .success-card, + .process-shell, + .detail-card, + .detail-copy, + .detail-side, + .contact-info { + border-radius: 1.5rem; + } }