refactor: complete module-driven architecture migration
This commit completes the migration to a fully module-driven architecture: ## Models Migration - Moved all domain models from models/database/ to their respective modules: - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc. - cms: MediaFile, VendorTheme - messaging: Email, VendorEmailSettings, VendorEmailTemplate - core: AdminMenuConfig - models/database/ now only contains Base and TimestampMixin (infrastructure) ## Schemas Migration - Moved all domain schemas from models/schema/ to their respective modules: - tenancy: company, vendor, admin, team, vendor_domain - cms: media, image, vendor_theme - messaging: email - models/schema/ now only contains base.py and auth.py (infrastructure) ## Routes Migration - Moved admin routes from app/api/v1/admin/ to modules: - menu_config.py -> core module - modules.py -> tenancy module - module_config.py -> tenancy module - app/api/v1/admin/ now only aggregates auto-discovered module routes ## Menu System - Implemented module-driven menu system with MenuDiscoveryService - Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT - Added MenuItemDefinition and MenuSectionDefinition dataclasses - Each module now defines its own menu items in definition.py - MenuService integrates with MenuDiscoveryService for template rendering ## Documentation - Updated docs/architecture/models-structure.md - Updated docs/architecture/menu-management.md - Updated architecture validation rules for new exceptions ## Architecture Validation - Updated MOD-019 rule to allow base.py in models/schema/ - Created core module exceptions.py and schemas/ directory - All validation errors resolved (only warnings remain) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,8 @@ This is a self-contained module with:
|
||||
- Templates: app.modules.cms.templates (namespaced as cms/)
|
||||
"""
|
||||
|
||||
from app.modules.base import ModuleDefinition
|
||||
from models.database.admin_menu_config import FrontendType
|
||||
from app.modules.base import MenuItemDefinition, MenuSectionDefinition, ModuleDefinition
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
def _get_admin_router():
|
||||
@@ -53,6 +53,57 @@ cms_module = ModuleDefinition(
|
||||
"media", # Media library
|
||||
],
|
||||
},
|
||||
# New module-driven menu definitions
|
||||
menus={
|
||||
FrontendType.ADMIN: [
|
||||
MenuSectionDefinition(
|
||||
id="contentMgmt",
|
||||
label_key="cms.menu.content_management",
|
||||
icon="document-text",
|
||||
order=70,
|
||||
items=[
|
||||
MenuItemDefinition(
|
||||
id="content-pages",
|
||||
label_key="cms.menu.content_pages",
|
||||
icon="document-text",
|
||||
route="/admin/content-pages",
|
||||
order=20,
|
||||
),
|
||||
MenuItemDefinition(
|
||||
id="vendor-themes",
|
||||
label_key="cms.menu.vendor_themes",
|
||||
icon="color-swatch",
|
||||
route="/admin/vendor-themes",
|
||||
order=30,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
FrontendType.VENDOR: [
|
||||
MenuSectionDefinition(
|
||||
id="shop",
|
||||
label_key="cms.menu.shop_content",
|
||||
icon="document-text",
|
||||
order=40,
|
||||
items=[
|
||||
MenuItemDefinition(
|
||||
id="content-pages",
|
||||
label_key="cms.menu.content_pages",
|
||||
icon="document-text",
|
||||
route="/vendor/{vendor_code}/content-pages",
|
||||
order=10,
|
||||
),
|
||||
MenuItemDefinition(
|
||||
id="media",
|
||||
label_key="cms.menu.media_library",
|
||||
icon="photograph",
|
||||
route="/vendor/{vendor_code}/media",
|
||||
order=20,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
},
|
||||
is_core=True, # CMS is a core module - content management is fundamental
|
||||
# Self-contained module configuration
|
||||
is_self_contained=True,
|
||||
|
||||
@@ -1,126 +1,203 @@
|
||||
{
|
||||
"title": "Content-Verwaltung",
|
||||
"description": "Verwalten Sie Inhaltsseiten, Medienbibliothek und Händler-Themes",
|
||||
"pages": {
|
||||
"title": "Inhaltsseiten",
|
||||
"subtitle": "Verwalten Sie Plattform- und Händler-Inhaltsseiten",
|
||||
"create": "Seite erstellen",
|
||||
"edit": "Seite bearbeiten",
|
||||
"delete": "Seite löschen",
|
||||
"list": "Alle Seiten",
|
||||
"empty": "Keine Seiten gefunden",
|
||||
"empty_search": "Keine Seiten entsprechen Ihrer Suche",
|
||||
"create_first": "Erste Seite erstellen"
|
||||
},
|
||||
"page": {
|
||||
"title": "Seitentitel",
|
||||
"slug": "Slug",
|
||||
"slug_help": "URL-sichere Kennung (Kleinbuchstaben, Zahlen, Bindestriche)",
|
||||
"content": "Inhalt",
|
||||
"content_format": "Inhaltsformat",
|
||||
"format_html": "HTML",
|
||||
"format_markdown": "Markdown",
|
||||
"platform": "Plattform",
|
||||
"vendor_override": "Händler-Überschreibung",
|
||||
"vendor_override_none": "Keine (Plattform-Standard)",
|
||||
"vendor_override_help_default": "Dies ist eine plattformweite Standardseite",
|
||||
"vendor_override_help_vendor": "Diese Seite überschreibt den Standard nur für den ausgewählten Händler"
|
||||
},
|
||||
"tiers": {
|
||||
"platform": "Plattform-Marketing",
|
||||
"vendor_default": "Händler-Standard",
|
||||
"vendor_override": "Händler-Überschreibung"
|
||||
},
|
||||
"seo": {
|
||||
"title": "SEO & Metadaten",
|
||||
"meta_description": "Meta-Beschreibung",
|
||||
"meta_description_help": "Zeichen (150-160 empfohlen)",
|
||||
"meta_keywords": "Meta-Schlüsselwörter",
|
||||
"meta_keywords_placeholder": "schlüsselwort1, schlüsselwort2, schlüsselwort3"
|
||||
},
|
||||
"navigation": {
|
||||
"title": "Navigation & Anzeige",
|
||||
"display_order": "Anzeigereihenfolge",
|
||||
"display_order_help": "Niedriger = zuerst",
|
||||
"show_in_header": "Im Header anzeigen",
|
||||
"show_in_footer": "Im Footer anzeigen",
|
||||
"show_in_legal": "Im Rechtsbereich anzeigen",
|
||||
"show_in_legal_help": "Untere Leiste neben dem Copyright"
|
||||
},
|
||||
"publishing": {
|
||||
"published": "Veröffentlicht",
|
||||
"draft": "Entwurf",
|
||||
"publish_help": "Diese Seite öffentlich sichtbar machen"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Startseiten-Abschnitte",
|
||||
"subtitle": "Mehrsprachiger Inhalt",
|
||||
"loading": "Abschnitte werden geladen...",
|
||||
"hero": {
|
||||
"title": "Hero-Abschnitt",
|
||||
"badge_text": "Badge-Text",
|
||||
"main_title": "Titel",
|
||||
"subtitle": "Untertitel",
|
||||
"buttons": "Schaltflächen",
|
||||
"add_button": "Schaltfläche hinzufügen"
|
||||
"platform": {
|
||||
"nav": {
|
||||
"pricing": "Preise",
|
||||
"find_shop": "Finden Sie Ihren Shop",
|
||||
"start_trial": "Kostenlos testen",
|
||||
"admin_login": "Admin-Login",
|
||||
"vendor_login": "Händler-Login",
|
||||
"toggle_menu": "Menü umschalten",
|
||||
"toggle_dark_mode": "Dunkelmodus umschalten"
|
||||
},
|
||||
"features": {
|
||||
"title": "Funktionen-Abschnitt",
|
||||
"section_title": "Abschnittstitel",
|
||||
"cards": "Funktionskarten",
|
||||
"add_card": "Karte hinzufügen",
|
||||
"icon": "Icon-Name",
|
||||
"feature_title": "Titel",
|
||||
"feature_description": "Beschreibung"
|
||||
"hero": {
|
||||
"badge": "{trial_days}-Tage kostenlose Testversion - Keine Kreditkarte erforderlich",
|
||||
"title": "Leichtes OMS für Letzshop-Verkäufer",
|
||||
"subtitle": "Bestellverwaltung, Lager und Rechnungsstellung für den luxemburgischen E-Commerce. Schluss mit Tabellenkalkulationen. Führen Sie Ihr Geschäft.",
|
||||
"cta_trial": "Kostenlos testen",
|
||||
"cta_find_shop": "Finden Sie Ihren Letzshop"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Preise-Abschnitt",
|
||||
"section_title": "Abschnittstitel",
|
||||
"use_tiers": "Abonnement-Stufen aus der Datenbank verwenden",
|
||||
"use_tiers_help": "Wenn aktiviert, werden Preiskarten dynamisch aus Ihrer Abonnement-Stufenkonfiguration abgerufen."
|
||||
"title": "Einfache, transparente Preise",
|
||||
"subtitle": "Wählen Sie den Plan, der zu Ihrem Unternehmen passt. Alle Pläne beinhalten eine {trial_days}-tägige kostenlose Testversion.",
|
||||
"monthly": "Monatlich",
|
||||
"annual": "Jährlich",
|
||||
"save_months": "Sparen Sie 2 Monate!",
|
||||
"most_popular": "AM BELIEBTESTEN",
|
||||
"recommended": "EMPFOHLEN",
|
||||
"contact_sales": "Kontaktieren Sie uns",
|
||||
"start_trial": "Kostenlos testen",
|
||||
"per_month": "/Monat",
|
||||
"per_year": "/Jahr",
|
||||
"custom": "Individuell",
|
||||
"orders_per_month": "{count} Bestellungen/Monat",
|
||||
"unlimited_orders": "Unbegrenzte Bestellungen",
|
||||
"products_limit": "{count} Produkte",
|
||||
"unlimited_products": "Unbegrenzte Produkte",
|
||||
"team_members": "{count} Teammitglieder",
|
||||
"unlimited_team": "Unbegrenztes Team",
|
||||
"letzshop_sync": "Letzshop-Synchronisierung",
|
||||
"eu_vat_invoicing": "EU-MwSt-Rechnungen",
|
||||
"analytics_dashboard": "Analyse-Dashboard",
|
||||
"api_access": "API-Zugang",
|
||||
"multi_channel": "Multi-Channel-Integration",
|
||||
"products": "Produkte",
|
||||
"team_member": "Teammitglied",
|
||||
"unlimited": "Unbegrenzt",
|
||||
"order_history": "Monate Bestellhistorie",
|
||||
"trial_note": "Alle Pläne beinhalten eine {trial_days}-tägige kostenlose Testversion. Keine Kreditkarte erforderlich.",
|
||||
"back_home": "Zurück zur Startseite"
|
||||
},
|
||||
"features": {
|
||||
"letzshop_sync": "Letzshop-Synchronisierung",
|
||||
"inventory_basic": "Grundlegende Lagerverwaltung",
|
||||
"inventory_locations": "Lagerstandorte",
|
||||
"inventory_purchase_orders": "Bestellungen",
|
||||
"invoice_lu": "Luxemburg-MwSt-Rechnungen",
|
||||
"invoice_eu_vat": "EU-MwSt-Rechnungen",
|
||||
"invoice_bulk": "Massenrechnungen",
|
||||
"customer_view": "Kundenliste",
|
||||
"customer_export": "Kundenexport",
|
||||
"analytics_dashboard": "Analyse-Dashboard",
|
||||
"accounting_export": "Buchhaltungsexport",
|
||||
"api_access": "API-Zugang",
|
||||
"automation_rules": "Automatisierungsregeln",
|
||||
"team_roles": "Teamrollen und Berechtigungen",
|
||||
"white_label": "White-Label-Option",
|
||||
"multi_vendor": "Multi-Händler-Unterstützung",
|
||||
"custom_integrations": "Individuelle Integrationen",
|
||||
"sla_guarantee": "SLA-Garantie",
|
||||
"dedicated_support": "Dedizierter Kundenbetreuer"
|
||||
},
|
||||
"addons": {
|
||||
"title": "Erweitern Sie Ihre Plattform",
|
||||
"subtitle": "Fügen Sie Ihre Marke, professionelle E-Mail und erweiterte Sicherheit hinzu.",
|
||||
"per_year": "/Jahr",
|
||||
"per_month": "/Monat",
|
||||
"custom_domain": "Eigene Domain",
|
||||
"custom_domain_desc": "Nutzen Sie Ihre eigene Domain (meinedomain.com)",
|
||||
"premium_ssl": "Premium SSL",
|
||||
"premium_ssl_desc": "EV-Zertifikat für Vertrauenssiegel",
|
||||
"email_package": "E-Mail-Paket",
|
||||
"email_package_desc": "Professionelle E-Mail-Adressen"
|
||||
},
|
||||
"find_shop": {
|
||||
"title": "Finden Sie Ihren Letzshop",
|
||||
"subtitle": "Verkaufen Sie bereits auf Letzshop? Geben Sie Ihre Shop-URL ein, um zu beginnen.",
|
||||
"placeholder": "Geben Sie Ihre Letzshop-URL ein (z.B. letzshop.lu/vendors/mein-shop)",
|
||||
"button": "Meinen Shop finden",
|
||||
"claim_shop": "Diesen Shop beanspruchen",
|
||||
"already_claimed": "Bereits beansprucht",
|
||||
"no_account": "Sie haben kein Letzshop-Konto?",
|
||||
"signup_letzshop": "Registrieren Sie sich zuerst bei Letzshop",
|
||||
"then_connect": ", dann kommen Sie zurück, um Ihren Shop zu verbinden.",
|
||||
"search_placeholder": "Letzshop-URL oder Shopname eingeben...",
|
||||
"search_button": "Suchen",
|
||||
"examples": "Beispiele:",
|
||||
"claim_button": "Diesen Shop beanspruchen und kostenlos testen",
|
||||
"not_found": "Wir konnten keinen Letzshop mit dieser URL finden. Bitte überprüfen Sie und versuchen Sie es erneut.",
|
||||
"or_signup": "Oder registrieren Sie sich ohne Letzshop-Verbindung",
|
||||
"need_help": "Brauchen Sie Hilfe?",
|
||||
"no_account_yet": "Sie haben noch kein Letzshop-Konto? Kein Problem!",
|
||||
"create_letzshop": "Letzshop-Konto erstellen",
|
||||
"signup_without": "Ohne Letzshop registrieren",
|
||||
"looking_up": "Suche Ihren Shop...",
|
||||
"found": "Gefunden:",
|
||||
"claimed_badge": "Bereits beansprucht"
|
||||
},
|
||||
"signup": {
|
||||
"step_plan": "Plan wählen",
|
||||
"step_shop": "Shop beanspruchen",
|
||||
"step_account": "Konto",
|
||||
"step_payment": "Zahlung",
|
||||
"choose_plan": "Wählen Sie Ihren Plan",
|
||||
"save_percent": "Sparen Sie {percent}%",
|
||||
"trial_info": "Wir erfassen Ihre Zahlungsdaten, aber Sie werden erst nach Ende der Testphase belastet.",
|
||||
"connect_shop": "Verbinden Sie Ihren Letzshop",
|
||||
"connect_optional": "Optional: Verknüpfen Sie Ihr Letzshop-Konto, um Bestellungen automatisch zu synchronisieren.",
|
||||
"connect_continue": "Verbinden und fortfahren",
|
||||
"skip_step": "Diesen Schritt überspringen",
|
||||
"create_account": "Erstellen Sie Ihr Konto",
|
||||
"first_name": "Vorname",
|
||||
"last_name": "Nachname",
|
||||
"company_name": "Firmenname",
|
||||
"email": "E-Mail",
|
||||
"password": "Passwort",
|
||||
"password_hint": "Mindestens 8 Zeichen",
|
||||
"continue": "Weiter",
|
||||
"continue_payment": "Weiter zur Zahlung",
|
||||
"back": "Zurück",
|
||||
"add_payment": "Zahlungsmethode hinzufügen",
|
||||
"no_charge_note": "Sie werden erst nach Ablauf Ihrer {trial_days}-tägigen Testphase belastet.",
|
||||
"processing": "Verarbeitung...",
|
||||
"start_trial": "Kostenlose Testversion starten",
|
||||
"creating_account": "Erstelle Ihr Konto..."
|
||||
},
|
||||
"success": {
|
||||
"title": "Willkommen bei Wizamart!",
|
||||
"subtitle": "Ihr Konto wurde erstellt und Ihre {trial_days}-tägige kostenlose Testphase hat begonnen.",
|
||||
"what_next": "Was kommt als Nächstes?",
|
||||
"step_connect": "Letzshop verbinden:",
|
||||
"step_connect_desc": "Fügen Sie Ihren API-Schlüssel hinzu, um Bestellungen automatisch zu synchronisieren.",
|
||||
"step_invoicing": "Rechnungsstellung einrichten:",
|
||||
"step_invoicing_desc": "Konfigurieren Sie Ihre Rechnungseinstellungen für die luxemburgische Compliance.",
|
||||
"step_products": "Produkte importieren:",
|
||||
"step_products_desc": "Synchronisieren Sie Ihren Produktkatalog von Letzshop.",
|
||||
"go_to_dashboard": "Zum Dashboard",
|
||||
"login_dashboard": "Zum Dashboard anmelden",
|
||||
"need_help": "Brauchen Sie Hilfe beim Einstieg?",
|
||||
"contact_support": "Kontaktieren Sie unser Support-Team"
|
||||
},
|
||||
"cta": {
|
||||
"title": "Call-to-Action-Abschnitt",
|
||||
"main_title": "Titel",
|
||||
"subtitle": "Untertitel",
|
||||
"buttons": "Schaltflächen",
|
||||
"add_button": "Schaltfläche hinzufügen"
|
||||
"title": "Bereit, Ihre Bestellungen zu optimieren?",
|
||||
"subtitle": "Schließen Sie sich Letzshop-Händlern an, die Wizamart für ihre Bestellverwaltung vertrauen. Starten Sie heute Ihre {trial_days}-tägige kostenlose Testversion.",
|
||||
"button": "Kostenlos testen"
|
||||
},
|
||||
"footer": {
|
||||
"tagline": "Leichtes OMS für Letzshop-Verkäufer. Verwalten Sie Bestellungen, Lager und Rechnungen.",
|
||||
"quick_links": "Schnelllinks",
|
||||
"platform": "Plattform",
|
||||
"contact": "Kontakt",
|
||||
"copyright": "© {year} Wizamart. Entwickelt für den luxemburgischen E-Commerce.",
|
||||
"privacy": "Datenschutzerklärung",
|
||||
"terms": "Nutzungsbedingungen",
|
||||
"about": "Über uns",
|
||||
"faq": "FAQ",
|
||||
"contact_us": "Kontaktieren Sie uns"
|
||||
},
|
||||
"modern": {
|
||||
"badge_integration": "Offizielle Integration",
|
||||
"badge_connect": "In 2 Minuten verbinden",
|
||||
"hero_title_1": "Für den luxemburgischen E-Commerce entwickelt",
|
||||
"hero_title_2": "Das Back-Office, das Letzshop Ihnen nicht gibt",
|
||||
"hero_subtitle": "Synchronisieren Sie Bestellungen, verwalten Sie Lager, erstellen Sie Rechnungen mit korrekter MwSt und besitzen Sie Ihre Kundendaten. Alles an einem Ort.",
|
||||
"cta_trial": "{trial_days}-Tage kostenlos testen",
|
||||
"cta_how": "Sehen Sie, wie es funktioniert",
|
||||
"hero_note": "Keine Kreditkarte erforderlich. Einrichtung in 5 Minuten. Jederzeit kündbar.",
|
||||
"pain_title": "Kommt Ihnen das bekannt vor?",
|
||||
"pain_subtitle": "Das sind die täglichen Frustrationen von Letzshop-Verkäufern",
|
||||
"pain_manual": "Manuelle Bestelleingabe",
|
||||
"pain_manual_desc": "Bestellungen von Letzshop in Tabellenkalkulationen kopieren. Jeden. Einzelnen. Tag.",
|
||||
"pain_inventory": "Lagerchaos",
|
||||
"pain_inventory_desc": "Der Bestand in Letzshop stimmt nicht mit der Realität überein. Überverkäufe passieren.",
|
||||
"pain_vat": "Falsche MwSt-Rechnungen",
|
||||
"pain_vat_desc": "EU-Kunden brauchen die korrekte MwSt. Ihr Buchhalter beschwert sich.",
|
||||
"pain_customers": "Verlorene Kunden",
|
||||
"pain_customers_desc": "Letzshop besitzt Ihre Kundendaten. Sie können nicht retargeten oder Loyalität aufbauen.",
|
||||
"how_title": "So funktioniert es",
|
||||
"how_subtitle": "Vom Chaos zur Kontrolle in 4 Schritten",
|
||||
"how_step1": "Letzshop verbinden",
|
||||
"how_step1_desc": "Geben Sie Ihre Letzshop-API-Zugangsdaten ein. In 2 Minuten erledigt, keine technischen Kenntnisse erforderlich.",
|
||||
"how_step2": "Bestellungen kommen rein",
|
||||
"how_step2_desc": "Bestellungen werden automatisch synchronisiert. Bestätigen und Tracking direkt von Wizamart hinzufügen.",
|
||||
"how_step3": "Rechnungen erstellen",
|
||||
"how_step3_desc": "Ein Klick, um konforme PDF-Rechnungen mit korrekter MwSt für jedes EU-Land zu erstellen.",
|
||||
"how_step4": "Ihr Geschäft ausbauen",
|
||||
"how_step4_desc": "Exportieren Sie Kunden für Marketing. Verfolgen Sie Lagerbestände. Konzentrieren Sie sich auf den Verkauf, nicht auf Tabellenkalkulationen.",
|
||||
"features_title": "Alles, was ein Letzshop-Verkäufer braucht",
|
||||
"features_subtitle": "Die operativen Tools, die Letzshop nicht bietet",
|
||||
"cta_final_title": "Bereit, die Kontrolle über Ihr Letzshop-Geschäft zu übernehmen?",
|
||||
"cta_final_subtitle": "Schließen Sie sich luxemburgischen Händlern an, die aufgehört haben, gegen Tabellenkalkulationen zu kämpfen, und begonnen haben, ihr Geschäft auszubauen.",
|
||||
"cta_final_note": "Keine Kreditkarte erforderlich. Einrichtung in 5 Minuten. Volle Professional-Funktionen während der Testphase."
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"title": "Medienbibliothek",
|
||||
"upload": "Hochladen",
|
||||
"upload_file": "Datei hochladen",
|
||||
"delete": "Löschen",
|
||||
"empty": "Keine Mediendateien",
|
||||
"upload_first": "Laden Sie Ihre erste Datei hoch"
|
||||
},
|
||||
"themes": {
|
||||
"title": "Händler-Themes",
|
||||
"subtitle": "Verwalten Sie Händler-Theme-Anpassungen"
|
||||
},
|
||||
"actions": {
|
||||
"save": "Speichern",
|
||||
"saving": "Speichern...",
|
||||
"update": "Seite aktualisieren",
|
||||
"create": "Seite erstellen",
|
||||
"cancel": "Abbrechen",
|
||||
"back_to_list": "Zurück zur Liste",
|
||||
"preview": "Vorschau",
|
||||
"revert_to_default": "Auf Standard zurücksetzen"
|
||||
},
|
||||
"messages": {
|
||||
"created": "Seite erfolgreich erstellt",
|
||||
"updated": "Seite erfolgreich aktualisiert",
|
||||
"deleted": "Seite erfolgreich gelöscht",
|
||||
"reverted": "Auf Standardseite zurückgesetzt",
|
||||
"error_loading": "Fehler beim Laden der Seite",
|
||||
"error_saving": "Fehler beim Speichern der Seite",
|
||||
"confirm_delete": "Sind Sie sicher, dass Sie diese Seite löschen möchten?"
|
||||
},
|
||||
"filters": {
|
||||
"all_pages": "Alle Seiten",
|
||||
"all_platforms": "Alle Plattformen",
|
||||
"search_placeholder": "Seiten suchen..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +1,203 @@
|
||||
{
|
||||
"title": "Gestion de contenu",
|
||||
"description": "Gestion des pages de contenu, de la bibliothèque de médias et des thèmes",
|
||||
"pages": {
|
||||
"title": "Pages de contenu",
|
||||
"subtitle": "Gérez les pages de contenu de la plateforme et des vendeurs",
|
||||
"create": "Créer une page",
|
||||
"edit": "Modifier la page",
|
||||
"delete": "Supprimer la page",
|
||||
"list": "Toutes les pages",
|
||||
"empty": "Aucune page trouvée",
|
||||
"empty_search": "Aucune page ne correspond à votre recherche",
|
||||
"create_first": "Créer la première page"
|
||||
},
|
||||
"page": {
|
||||
"title": "Titre de la page",
|
||||
"slug": "Slug",
|
||||
"slug_help": "Identifiant URL (minuscules, chiffres, tirets uniquement)",
|
||||
"content": "Contenu",
|
||||
"content_format": "Format du contenu",
|
||||
"format_html": "HTML",
|
||||
"format_markdown": "Markdown",
|
||||
"platform": "Plateforme",
|
||||
"vendor_override": "Remplacement vendeur",
|
||||
"vendor_override_none": "Aucun (page par défaut)",
|
||||
"vendor_override_help_default": "Ceci est une page par défaut pour toute la plateforme",
|
||||
"vendor_override_help_vendor": "Cette page remplace la page par défaut pour le vendeur sélectionné"
|
||||
},
|
||||
"tiers": {
|
||||
"platform": "Marketing plateforme",
|
||||
"vendor_default": "Défaut vendeur",
|
||||
"vendor_override": "Remplacement vendeur"
|
||||
},
|
||||
"seo": {
|
||||
"title": "SEO & Métadonnées",
|
||||
"meta_description": "Meta Description",
|
||||
"meta_description_help": "caractères (150-160 recommandés)",
|
||||
"meta_keywords": "Mots-clés",
|
||||
"meta_keywords_placeholder": "mot-clé1, mot-clé2, mot-clé3"
|
||||
},
|
||||
"navigation": {
|
||||
"title": "Navigation & Affichage",
|
||||
"display_order": "Ordre d'affichage",
|
||||
"display_order_help": "Plus bas = premier",
|
||||
"show_in_header": "Afficher dans l'en-tête",
|
||||
"show_in_footer": "Afficher dans le pied de page",
|
||||
"show_in_legal": "Afficher dans les mentions légales",
|
||||
"show_in_legal_help": "Barre en bas à côté du copyright"
|
||||
},
|
||||
"publishing": {
|
||||
"published": "Publié",
|
||||
"draft": "Brouillon",
|
||||
"publish_help": "Rendre cette page visible au public"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Sections de la page d'accueil",
|
||||
"subtitle": "Contenu multilingue",
|
||||
"loading": "Chargement des sections...",
|
||||
"hero": {
|
||||
"title": "Section Hero",
|
||||
"badge_text": "Texte du badge",
|
||||
"main_title": "Titre",
|
||||
"subtitle": "Sous-titre",
|
||||
"buttons": "Boutons",
|
||||
"add_button": "Ajouter un bouton"
|
||||
"platform": {
|
||||
"nav": {
|
||||
"pricing": "Tarifs",
|
||||
"find_shop": "Trouvez votre boutique",
|
||||
"start_trial": "Essai gratuit",
|
||||
"admin_login": "Connexion Admin",
|
||||
"vendor_login": "Connexion Vendeur",
|
||||
"toggle_menu": "Basculer le menu",
|
||||
"toggle_dark_mode": "Basculer le mode sombre"
|
||||
},
|
||||
"features": {
|
||||
"title": "Section Fonctionnalités",
|
||||
"section_title": "Titre de la section",
|
||||
"cards": "Cartes de fonctionnalités",
|
||||
"add_card": "Ajouter une carte",
|
||||
"icon": "Nom de l'icône",
|
||||
"feature_title": "Titre",
|
||||
"feature_description": "Description"
|
||||
"hero": {
|
||||
"badge": "Essai gratuit de {trial_days} jours - Aucune carte de crédit requise",
|
||||
"title": "OMS léger pour les vendeurs Letzshop",
|
||||
"subtitle": "Gestion des commandes, stocks et facturation conçue pour le e-commerce luxembourgeois. Arrêtez de jongler avec les tableurs. Gérez votre entreprise.",
|
||||
"cta_trial": "Essai gratuit",
|
||||
"cta_find_shop": "Trouvez votre boutique Letzshop"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Section Tarifs",
|
||||
"section_title": "Titre de la section",
|
||||
"use_tiers": "Utiliser les niveaux d'abonnement de la base de données",
|
||||
"use_tiers_help": "Si activé, les cartes de tarifs sont extraites dynamiquement de la configuration des niveaux d'abonnement."
|
||||
"title": "Tarification simple et transparente",
|
||||
"subtitle": "Choisissez le plan adapté à votre entreprise. Tous les plans incluent un essai gratuit de {trial_days} jours.",
|
||||
"monthly": "Mensuel",
|
||||
"annual": "Annuel",
|
||||
"save_months": "Économisez 2 mois !",
|
||||
"most_popular": "LE PLUS POPULAIRE",
|
||||
"recommended": "RECOMMANDÉ",
|
||||
"contact_sales": "Contactez-nous",
|
||||
"start_trial": "Essai gratuit",
|
||||
"per_month": "/mois",
|
||||
"per_year": "/an",
|
||||
"custom": "Sur mesure",
|
||||
"orders_per_month": "{count} commandes/mois",
|
||||
"unlimited_orders": "Commandes illimitées",
|
||||
"products_limit": "{count} produits",
|
||||
"unlimited_products": "Produits illimités",
|
||||
"team_members": "{count} membres d'équipe",
|
||||
"unlimited_team": "Équipe illimitée",
|
||||
"letzshop_sync": "Synchronisation Letzshop",
|
||||
"eu_vat_invoicing": "Facturation TVA UE",
|
||||
"analytics_dashboard": "Tableau de bord analytique",
|
||||
"api_access": "Accès API",
|
||||
"multi_channel": "Intégration multi-canal",
|
||||
"products": "produits",
|
||||
"team_member": "membre d'équipe",
|
||||
"unlimited": "Illimité",
|
||||
"order_history": "mois d'historique",
|
||||
"trial_note": "Tous les plans incluent un essai gratuit de {trial_days} jours. Aucune carte de crédit requise.",
|
||||
"back_home": "Retour à l'accueil"
|
||||
},
|
||||
"features": {
|
||||
"letzshop_sync": "Synchronisation Letzshop",
|
||||
"inventory_basic": "Gestion de stock de base",
|
||||
"inventory_locations": "Emplacements d'entrepôt",
|
||||
"inventory_purchase_orders": "Bons de commande",
|
||||
"invoice_lu": "Facturation TVA Luxembourg",
|
||||
"invoice_eu_vat": "Facturation TVA UE",
|
||||
"invoice_bulk": "Facturation en masse",
|
||||
"customer_view": "Liste des clients",
|
||||
"customer_export": "Export clients",
|
||||
"analytics_dashboard": "Tableau de bord analytique",
|
||||
"accounting_export": "Export comptable",
|
||||
"api_access": "Accès API",
|
||||
"automation_rules": "Règles d'automatisation",
|
||||
"team_roles": "Rôles et permissions",
|
||||
"white_label": "Option marque blanche",
|
||||
"multi_vendor": "Support multi-vendeurs",
|
||||
"custom_integrations": "Intégrations personnalisées",
|
||||
"sla_guarantee": "Garantie SLA",
|
||||
"dedicated_support": "Gestionnaire de compte dédié"
|
||||
},
|
||||
"addons": {
|
||||
"title": "Améliorez votre plateforme",
|
||||
"subtitle": "Ajoutez votre marque, e-mail professionnel et sécurité renforcée.",
|
||||
"per_year": "/an",
|
||||
"per_month": "/mois",
|
||||
"custom_domain": "Domaine personnalisé",
|
||||
"custom_domain_desc": "Utilisez votre propre domaine (mondomaine.com)",
|
||||
"premium_ssl": "SSL Premium",
|
||||
"premium_ssl_desc": "Certificat EV pour les badges de confiance",
|
||||
"email_package": "Pack Email",
|
||||
"email_package_desc": "Adresses e-mail professionnelles"
|
||||
},
|
||||
"find_shop": {
|
||||
"title": "Trouvez votre boutique Letzshop",
|
||||
"subtitle": "Vous vendez déjà sur Letzshop ? Entrez l'URL de votre boutique pour commencer.",
|
||||
"placeholder": "Entrez votre URL Letzshop (ex: letzshop.lu/vendors/ma-boutique)",
|
||||
"button": "Trouver ma boutique",
|
||||
"claim_shop": "Réclamer cette boutique",
|
||||
"already_claimed": "Déjà réclamée",
|
||||
"no_account": "Vous n'avez pas de compte Letzshop ?",
|
||||
"signup_letzshop": "Inscrivez-vous d'abord sur Letzshop",
|
||||
"then_connect": ", puis revenez connecter votre boutique.",
|
||||
"search_placeholder": "Entrez l'URL Letzshop ou le nom de la boutique...",
|
||||
"search_button": "Rechercher",
|
||||
"examples": "Exemples :",
|
||||
"claim_button": "Réclamez cette boutique et démarrez l'essai gratuit",
|
||||
"not_found": "Nous n'avons pas trouvé de boutique Letzshop avec cette URL. Vérifiez et réessayez.",
|
||||
"or_signup": "Ou inscrivez-vous sans connexion Letzshop",
|
||||
"need_help": "Besoin d'aide ?",
|
||||
"no_account_yet": "Vous n'avez pas encore de compte Letzshop ? Pas de problème !",
|
||||
"create_letzshop": "Créer un compte Letzshop",
|
||||
"signup_without": "S'inscrire sans Letzshop",
|
||||
"looking_up": "Recherche de votre boutique...",
|
||||
"found": "Trouvé :",
|
||||
"claimed_badge": "Déjà réclamée"
|
||||
},
|
||||
"signup": {
|
||||
"step_plan": "Choisir le plan",
|
||||
"step_shop": "Réclamer la boutique",
|
||||
"step_account": "Compte",
|
||||
"step_payment": "Paiement",
|
||||
"choose_plan": "Choisissez votre plan",
|
||||
"save_percent": "Économisez {percent}%",
|
||||
"trial_info": "Nous collecterons vos informations de paiement, mais vous ne serez pas débité avant la fin de l'essai.",
|
||||
"connect_shop": "Connectez votre boutique Letzshop",
|
||||
"connect_optional": "Optionnel : Liez votre compte Letzshop pour synchroniser automatiquement les commandes.",
|
||||
"connect_continue": "Connecter et continuer",
|
||||
"skip_step": "Passer cette étape",
|
||||
"create_account": "Créez votre compte",
|
||||
"first_name": "Prénom",
|
||||
"last_name": "Nom",
|
||||
"company_name": "Nom de l'entreprise",
|
||||
"email": "E-mail",
|
||||
"password": "Mot de passe",
|
||||
"password_hint": "Minimum 8 caractères",
|
||||
"continue": "Continuer",
|
||||
"continue_payment": "Continuer vers le paiement",
|
||||
"back": "Retour",
|
||||
"add_payment": "Ajouter un moyen de paiement",
|
||||
"no_charge_note": "Vous ne serez pas débité avant la fin de votre essai de {trial_days} jours.",
|
||||
"processing": "Traitement en cours...",
|
||||
"start_trial": "Démarrer l'essai gratuit",
|
||||
"creating_account": "Création de votre compte..."
|
||||
},
|
||||
"success": {
|
||||
"title": "Bienvenue sur Wizamart !",
|
||||
"subtitle": "Votre compte a été créé et votre essai gratuit de {trial_days} jours a commencé.",
|
||||
"what_next": "Et maintenant ?",
|
||||
"step_connect": "Connecter Letzshop :",
|
||||
"step_connect_desc": "Ajoutez votre clé API pour commencer à synchroniser automatiquement les commandes.",
|
||||
"step_invoicing": "Configurer la facturation :",
|
||||
"step_invoicing_desc": "Configurez vos paramètres de facturation pour la conformité luxembourgeoise.",
|
||||
"step_products": "Importer les produits :",
|
||||
"step_products_desc": "Synchronisez votre catalogue de produits depuis Letzshop.",
|
||||
"go_to_dashboard": "Aller au tableau de bord",
|
||||
"login_dashboard": "Connexion au tableau de bord",
|
||||
"need_help": "Besoin d'aide pour démarrer ?",
|
||||
"contact_support": "Contactez notre équipe support"
|
||||
},
|
||||
"cta": {
|
||||
"title": "Section Appel à l'action",
|
||||
"main_title": "Titre",
|
||||
"subtitle": "Sous-titre",
|
||||
"buttons": "Boutons",
|
||||
"add_button": "Ajouter un bouton"
|
||||
"title": "Prêt à optimiser vos commandes ?",
|
||||
"subtitle": "Rejoignez les vendeurs Letzshop qui font confiance à Wizamart pour leur gestion de commandes. Commencez votre essai gratuit de {trial_days} jours aujourd'hui.",
|
||||
"button": "Essai gratuit"
|
||||
},
|
||||
"footer": {
|
||||
"tagline": "OMS léger pour les vendeurs Letzshop. Gérez commandes, stocks et facturation.",
|
||||
"quick_links": "Liens rapides",
|
||||
"platform": "Plateforme",
|
||||
"contact": "Contact",
|
||||
"copyright": "© {year} Wizamart. Conçu pour le e-commerce luxembourgeois.",
|
||||
"privacy": "Politique de confidentialité",
|
||||
"terms": "Conditions d'utilisation",
|
||||
"about": "À propos",
|
||||
"faq": "FAQ",
|
||||
"contact_us": "Nous contacter"
|
||||
},
|
||||
"modern": {
|
||||
"badge_integration": "Intégration officielle",
|
||||
"badge_connect": "Connexion en 2 minutes",
|
||||
"hero_title_1": "Conçu pour le e-commerce luxembourgeois",
|
||||
"hero_title_2": "Le back-office que Letzshop ne vous donne pas",
|
||||
"hero_subtitle": "Synchronisez les commandes, gérez les stocks, générez des factures avec la TVA correcte et possédez vos données clients. Tout en un seul endroit.",
|
||||
"cta_trial": "Essai gratuit de {trial_days} jours",
|
||||
"cta_how": "Voir comment ça marche",
|
||||
"hero_note": "Aucune carte de crédit requise. Configuration en 5 minutes. Annulez à tout moment.",
|
||||
"pain_title": "Ça vous dit quelque chose ?",
|
||||
"pain_subtitle": "Ce sont les frustrations quotidiennes des vendeurs Letzshop",
|
||||
"pain_manual": "Saisie manuelle des commandes",
|
||||
"pain_manual_desc": "Copier-coller les commandes de Letzshop vers des tableurs. Chaque. Jour.",
|
||||
"pain_inventory": "Chaos des stocks",
|
||||
"pain_inventory_desc": "Le stock dans Letzshop ne correspond pas à la réalité. Les surventes arrivent.",
|
||||
"pain_vat": "Mauvaises factures TVA",
|
||||
"pain_vat_desc": "Les clients UE ont besoin de la TVA correcte. Votre comptable se plaint.",
|
||||
"pain_customers": "Clients perdus",
|
||||
"pain_customers_desc": "Letzshop possède vos données clients. Vous ne pouvez pas les recibler ou fidéliser.",
|
||||
"how_title": "Comment ça marche",
|
||||
"how_subtitle": "Du chaos au contrôle en 4 étapes",
|
||||
"how_step1": "Connecter Letzshop",
|
||||
"how_step1_desc": "Entrez vos identifiants API Letzshop. Fait en 2 minutes, aucune compétence technique requise.",
|
||||
"how_step2": "Les commandes arrivent",
|
||||
"how_step2_desc": "Les commandes se synchronisent automatiquement. Confirmez et ajoutez le suivi directement depuis Wizamart.",
|
||||
"how_step3": "Générer des factures",
|
||||
"how_step3_desc": "Un clic pour créer des factures PDF conformes avec la TVA correcte pour tout pays UE.",
|
||||
"how_step4": "Développez votre entreprise",
|
||||
"how_step4_desc": "Exportez les clients pour le marketing. Suivez les stocks. Concentrez-vous sur la vente, pas les tableurs.",
|
||||
"features_title": "Tout ce dont un vendeur Letzshop a besoin",
|
||||
"features_subtitle": "Les outils opérationnels que Letzshop ne fournit pas",
|
||||
"cta_final_title": "Prêt à prendre le contrôle de votre entreprise Letzshop ?",
|
||||
"cta_final_subtitle": "Rejoignez les vendeurs luxembourgeois qui ont arrêté de lutter contre les tableurs et ont commencé à développer leur entreprise.",
|
||||
"cta_final_note": "Aucune carte de crédit requise. Configuration en 5 minutes. Toutes les fonctionnalités Pro pendant l'essai."
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"title": "Bibliothèque de médias",
|
||||
"upload": "Télécharger",
|
||||
"upload_file": "Télécharger un fichier",
|
||||
"delete": "Supprimer",
|
||||
"empty": "Aucun fichier média",
|
||||
"upload_first": "Téléchargez votre premier fichier"
|
||||
},
|
||||
"themes": {
|
||||
"title": "Thèmes vendeurs",
|
||||
"subtitle": "Gérez les personnalisations de thèmes des vendeurs"
|
||||
},
|
||||
"actions": {
|
||||
"save": "Enregistrer",
|
||||
"saving": "Enregistrement...",
|
||||
"update": "Mettre à jour la page",
|
||||
"create": "Créer la page",
|
||||
"cancel": "Annuler",
|
||||
"back_to_list": "Retour à la liste",
|
||||
"preview": "Aperçu",
|
||||
"revert_to_default": "Revenir à la valeur par défaut"
|
||||
},
|
||||
"messages": {
|
||||
"created": "Page créée avec succès",
|
||||
"updated": "Page mise à jour avec succès",
|
||||
"deleted": "Page supprimée avec succès",
|
||||
"reverted": "Retour à la page par défaut",
|
||||
"error_loading": "Erreur lors du chargement de la page",
|
||||
"error_saving": "Erreur lors de l'enregistrement de la page",
|
||||
"confirm_delete": "Êtes-vous sûr de vouloir supprimer cette page ?"
|
||||
},
|
||||
"filters": {
|
||||
"all_pages": "Toutes les pages",
|
||||
"all_platforms": "Toutes les plateformes",
|
||||
"search_placeholder": "Rechercher des pages..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +1,203 @@
|
||||
{
|
||||
"title": "Inhalts-Verwaltung",
|
||||
"description": "Verwaltet Inhaltsäiten, Mediebibliothéik an Händler-Themen",
|
||||
"pages": {
|
||||
"title": "Inhaltsäiten",
|
||||
"subtitle": "Verwaltet Plattform- an Händler-Inhaltsäiten",
|
||||
"create": "Säit erstellen",
|
||||
"edit": "Säit änneren",
|
||||
"delete": "Säit läschen",
|
||||
"list": "All Säiten",
|
||||
"empty": "Keng Säite fonnt",
|
||||
"empty_search": "Keng Säite passen op Är Sich",
|
||||
"create_first": "Éischt Säit erstellen"
|
||||
},
|
||||
"page": {
|
||||
"title": "Säitentitel",
|
||||
"slug": "Slug",
|
||||
"slug_help": "URL-sécher Kennung (Klengbuschtawen, Zuelen, Bindestricher)",
|
||||
"content": "Inhalt",
|
||||
"content_format": "Inhaltsformat",
|
||||
"format_html": "HTML",
|
||||
"format_markdown": "Markdown",
|
||||
"platform": "Plattform",
|
||||
"vendor_override": "Händler-Iwwerschreiwung",
|
||||
"vendor_override_none": "Keng (Plattform-Standard)",
|
||||
"vendor_override_help_default": "Dëst ass eng plattformwäit Standardsäit",
|
||||
"vendor_override_help_vendor": "Dës Säit iwwerschreift de Standard nëmme fir de gewielte Händler"
|
||||
},
|
||||
"tiers": {
|
||||
"platform": "Plattform-Marketing",
|
||||
"vendor_default": "Händler-Standard",
|
||||
"vendor_override": "Händler-Iwwerschreiwung"
|
||||
},
|
||||
"seo": {
|
||||
"title": "SEO & Metadaten",
|
||||
"meta_description": "Meta-Beschreiwung",
|
||||
"meta_description_help": "Zeechen (150-160 recommandéiert)",
|
||||
"meta_keywords": "Meta-Schlësselwierder",
|
||||
"meta_keywords_placeholder": "schlësselwuert1, schlësselwuert2, schlësselwuert3"
|
||||
},
|
||||
"navigation": {
|
||||
"title": "Navigatioun & Affichage",
|
||||
"display_order": "Uweisungsreiefolleg",
|
||||
"display_order_help": "Méi niddreg = éischt",
|
||||
"show_in_header": "Am Header weisen",
|
||||
"show_in_footer": "Am Footer weisen",
|
||||
"show_in_legal": "Am Rechtsberäich weisen",
|
||||
"show_in_legal_help": "Ënnescht Leist nieft dem Copyright"
|
||||
},
|
||||
"publishing": {
|
||||
"published": "Verëffentlecht",
|
||||
"draft": "Entworf",
|
||||
"publish_help": "Dës Säit ëffentlech siichtbar maachen"
|
||||
},
|
||||
"homepage": {
|
||||
"title": "Haaptsäit-Sektiounen",
|
||||
"subtitle": "Méisproochegen Inhalt",
|
||||
"loading": "Sektiounen ginn gelueden...",
|
||||
"hero": {
|
||||
"title": "Hero-Sektioun",
|
||||
"badge_text": "Badge-Text",
|
||||
"main_title": "Titel",
|
||||
"subtitle": "Ënnertitel",
|
||||
"buttons": "Knäpp",
|
||||
"add_button": "Knapp derbäisetzen"
|
||||
"platform": {
|
||||
"nav": {
|
||||
"pricing": "Präisser",
|
||||
"find_shop": "Fannt Äre Buttek",
|
||||
"start_trial": "Gratis Testen",
|
||||
"admin_login": "Admin Login",
|
||||
"vendor_login": "Händler Login",
|
||||
"toggle_menu": "Menü wiesselen",
|
||||
"toggle_dark_mode": "Däischter Modus wiesselen"
|
||||
},
|
||||
"features": {
|
||||
"title": "Funktiounen-Sektioun",
|
||||
"section_title": "Sektiounstitel",
|
||||
"cards": "Funktiounskaarten",
|
||||
"add_card": "Kaart derbäisetzen",
|
||||
"icon": "Icon-Numm",
|
||||
"feature_title": "Titel",
|
||||
"feature_description": "Beschreiwung"
|
||||
"hero": {
|
||||
"badge": "{trial_days}-Deeg gratis Testversioun - Keng Kreditkaart néideg",
|
||||
"title": "Liichtt OMS fir Letzshop Verkeefer",
|
||||
"subtitle": "Bestellungsverwaltung, Lager an Rechnungsstellung fir de lëtzebuergeschen E-Commerce. Schluss mat Tabellen. Féiert Äert Geschäft.",
|
||||
"cta_trial": "Gratis Testen",
|
||||
"cta_find_shop": "Fannt Äre Letzshop Buttek"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Präisser-Sektioun",
|
||||
"section_title": "Sektiounstitel",
|
||||
"use_tiers": "Abonnement-Stufen aus der Datebank benotzen",
|
||||
"use_tiers_help": "Wann aktivéiert, ginn d'Präiskaarten dynamesch aus Ärer Abonnement-Stufekonfiguratioun ofgeruff."
|
||||
"title": "Einfach, transparent Präisser",
|
||||
"subtitle": "Wielt de Plang deen zu Ärer Firma passt. All Pläng enthalen eng {trial_days}-Deeg gratis Testversioun.",
|
||||
"monthly": "Monatslech",
|
||||
"annual": "Jäerlech",
|
||||
"save_months": "Spuert 2 Méint!",
|
||||
"most_popular": "AM BELÉIFSTEN",
|
||||
"recommended": "EMPFOHLEN",
|
||||
"contact_sales": "Kontaktéiert eis",
|
||||
"start_trial": "Gratis Testen",
|
||||
"per_month": "/Mount",
|
||||
"per_year": "/Joer",
|
||||
"custom": "Personnaliséiert",
|
||||
"orders_per_month": "{count} Bestellungen/Mount",
|
||||
"unlimited_orders": "Onbegrenzt Bestellungen",
|
||||
"products_limit": "{count} Produkter",
|
||||
"unlimited_products": "Onbegrenzt Produkter",
|
||||
"team_members": "{count} Teammemberen",
|
||||
"unlimited_team": "Onbegrenzt Team",
|
||||
"letzshop_sync": "Letzshop Synchronisatioun",
|
||||
"eu_vat_invoicing": "EU TVA Rechnungen",
|
||||
"analytics_dashboard": "Analyse Dashboard",
|
||||
"api_access": "API Zougang",
|
||||
"multi_channel": "Multi-Channel Integratioun",
|
||||
"products": "Produkter",
|
||||
"team_member": "Teammember",
|
||||
"unlimited": "Onbegrenzt",
|
||||
"order_history": "Méint Bestellungshistorique",
|
||||
"trial_note": "All Pläng enthalen eng {trial_days}-Deeg gratis Testversioun. Keng Kreditkaart néideg.",
|
||||
"back_home": "Zréck op d'Haaptsäit"
|
||||
},
|
||||
"features": {
|
||||
"letzshop_sync": "Letzshop Synchronisatioun",
|
||||
"inventory_basic": "Basis Lagerverwaltung",
|
||||
"inventory_locations": "Lagerstanduerten",
|
||||
"inventory_purchase_orders": "Bestellungen",
|
||||
"invoice_lu": "Lëtzebuerg TVA Rechnungen",
|
||||
"invoice_eu_vat": "EU TVA Rechnungen",
|
||||
"invoice_bulk": "Massrechnungen",
|
||||
"customer_view": "Clientelëscht",
|
||||
"customer_export": "Client Export",
|
||||
"analytics_dashboard": "Analyse Dashboard",
|
||||
"accounting_export": "Comptabilitéits Export",
|
||||
"api_access": "API Zougang",
|
||||
"automation_rules": "Automatiséierungsreegelen",
|
||||
"team_roles": "Team Rollen an Autorisatiounen",
|
||||
"white_label": "White-Label Optioun",
|
||||
"multi_vendor": "Multi-Händler Ënnerstëtzung",
|
||||
"custom_integrations": "Personnaliséiert Integratiounen",
|
||||
"sla_guarantee": "SLA Garantie",
|
||||
"dedicated_support": "Dedizéierte Kontobetreier"
|
||||
},
|
||||
"addons": {
|
||||
"title": "Erweidert Är Plattform",
|
||||
"subtitle": "Füügt Är Mark, professionell Email a verbessert Sécherheet derbäi.",
|
||||
"per_year": "/Joer",
|
||||
"per_month": "/Mount",
|
||||
"custom_domain": "Eegen Domain",
|
||||
"custom_domain_desc": "Benotzt Är eegen Domain (mengdomain.lu)",
|
||||
"premium_ssl": "Premium SSL",
|
||||
"premium_ssl_desc": "EV Zertifikat fir Vertrauensbadgen",
|
||||
"email_package": "Email Package",
|
||||
"email_package_desc": "Professionell Email Adressen"
|
||||
},
|
||||
"find_shop": {
|
||||
"title": "Fannt Äre Letzshop Buttek",
|
||||
"subtitle": "Verkaaft Dir schonn op Letzshop? Gitt Är Buttek URL an fir unzefänken.",
|
||||
"placeholder": "Gitt Är Letzshop URL an (z.B. letzshop.lu/vendors/mäi-buttek)",
|
||||
"button": "Mäi Buttek fannen",
|
||||
"claim_shop": "Dëse Buttek reklaméieren",
|
||||
"already_claimed": "Scho reklaméiert",
|
||||
"no_account": "Kee Letzshop Kont?",
|
||||
"signup_letzshop": "Registréiert Iech éischt bei Letzshop",
|
||||
"then_connect": ", dann kommt zréck fir Äre Buttek ze verbannen.",
|
||||
"search_placeholder": "Letzshop URL oder Butteknumm aginn...",
|
||||
"search_button": "Sichen",
|
||||
"examples": "Beispiller:",
|
||||
"claim_button": "Dëse Buttek reklaméieren a gratis testen",
|
||||
"not_found": "Mir konnten keen Letzshop Buttek mat dëser URL fannen. Iwwerpréift w.e.g. a probéiert nach eng Kéier.",
|
||||
"or_signup": "Oder registréiert Iech ouni Letzshop Verbindung",
|
||||
"need_help": "Braucht Dir Hëllef?",
|
||||
"no_account_yet": "Dir hutt nach keen Letzshop Kont? Keen Problem!",
|
||||
"create_letzshop": "Letzshop Kont erstellen",
|
||||
"signup_without": "Ouni Letzshop registréieren",
|
||||
"looking_up": "Sich Äre Buttek...",
|
||||
"found": "Fonnt:",
|
||||
"claimed_badge": "Scho reklaméiert"
|
||||
},
|
||||
"signup": {
|
||||
"step_plan": "Plang wielen",
|
||||
"step_shop": "Buttek reklaméieren",
|
||||
"step_account": "Kont",
|
||||
"step_payment": "Bezuelung",
|
||||
"choose_plan": "Wielt Äre Plang",
|
||||
"save_percent": "Spuert {percent}%",
|
||||
"trial_info": "Mir sammelen Är Bezuelungsinformatiounen, awer Dir gitt eréischt nom Enn vun der Testperiod belaaschtt.",
|
||||
"connect_shop": "Verbannt Äre Letzshop Buttek",
|
||||
"connect_optional": "Optional: Verlinkt Äre Letzshop Kont fir Bestellungen automatesch ze synchroniséieren.",
|
||||
"connect_continue": "Verbannen a weider",
|
||||
"skip_step": "Dëse Schrëtt iwwersprangen",
|
||||
"create_account": "Erstellt Äre Kont",
|
||||
"first_name": "Virnumm",
|
||||
"last_name": "Numm",
|
||||
"company_name": "Firmennumm",
|
||||
"email": "Email",
|
||||
"password": "Passwuert",
|
||||
"password_hint": "Mindestens 8 Zeechen",
|
||||
"continue": "Weider",
|
||||
"continue_payment": "Weider zur Bezuelung",
|
||||
"back": "Zréck",
|
||||
"add_payment": "Bezuelungsmethod derbäisetzen",
|
||||
"no_charge_note": "Dir gitt eréischt nom Enn vun Ärer {trial_days}-Deeg Testperiod belaaschtt.",
|
||||
"processing": "Veraarbechtung...",
|
||||
"start_trial": "Gratis Testversioun starten",
|
||||
"creating_account": "Erstellt Äre Kont..."
|
||||
},
|
||||
"success": {
|
||||
"title": "Wëllkomm bei Wizamart!",
|
||||
"subtitle": "Äre Kont gouf erstallt an Är {trial_days}-Deeg gratis Testversioun huet ugefaang.",
|
||||
"what_next": "Wat kënnt duerno?",
|
||||
"step_connect": "Letzshop verbannen:",
|
||||
"step_connect_desc": "Füügt Äre API Schlëssel derbäi fir Bestellungen automatesch ze synchroniséieren.",
|
||||
"step_invoicing": "Rechnungsstellung astellen:",
|
||||
"step_invoicing_desc": "Konfiguréiert Är Rechnungsastellungen fir Lëtzebuerger Konformitéit.",
|
||||
"step_products": "Produkter importéieren:",
|
||||
"step_products_desc": "Synchroniséiert Äre Produktkatalog vu Letzshop.",
|
||||
"go_to_dashboard": "Zum Dashboard",
|
||||
"login_dashboard": "Am Dashboard umellen",
|
||||
"need_help": "Braucht Dir Hëllef beim Ufänken?",
|
||||
"contact_support": "Kontaktéiert eist Support Team"
|
||||
},
|
||||
"cta": {
|
||||
"title": "Call-to-Action-Sektioun",
|
||||
"main_title": "Titel",
|
||||
"subtitle": "Ënnertitel",
|
||||
"buttons": "Knäpp",
|
||||
"add_button": "Knapp derbäisetzen"
|
||||
"title": "Prett fir Är Bestellungen ze optiméieren?",
|
||||
"subtitle": "Schléisst Iech Letzshop Händler un déi Wizamart fir hir Bestellungsverwaltung vertrauen. Fänkt haut Är {trial_days}-Deeg gratis Testversioun un.",
|
||||
"button": "Gratis Testen"
|
||||
},
|
||||
"footer": {
|
||||
"tagline": "Liichtt OMS fir Letzshop Verkeefer. Verwaltt Bestellungen, Lager an Rechnungen.",
|
||||
"quick_links": "Séier Linken",
|
||||
"platform": "Plattform",
|
||||
"contact": "Kontakt",
|
||||
"copyright": "© {year} Wizamart. Gemaach fir de lëtzebuergeschen E-Commerce.",
|
||||
"privacy": "Dateschutzrichtlinn",
|
||||
"terms": "Notzungsbedéngungen",
|
||||
"about": "Iwwer eis",
|
||||
"faq": "FAQ",
|
||||
"contact_us": "Kontaktéiert eis"
|
||||
},
|
||||
"modern": {
|
||||
"badge_integration": "Offiziell Integratioun",
|
||||
"badge_connect": "An 2 Minutten verbannen",
|
||||
"hero_title_1": "Gemaach fir de lëtzebuergeschen E-Commerce",
|
||||
"hero_title_2": "De Back-Office dee Letzshop Iech net gëtt",
|
||||
"hero_subtitle": "Synchroniséiert Bestellungen, verwaltt Lager, erstellt Rechnunge mat der korrekter TVA a besëtzt Är Clientsdaten. Alles un engem Plaz.",
|
||||
"cta_trial": "{trial_days}-Deeg gratis testen",
|
||||
"cta_how": "Kuckt wéi et funktionéiert",
|
||||
"hero_note": "Keng Kreditkaart néideg. Setup an 5 Minutten. Ëmmer kënnegen.",
|
||||
"pain_title": "Kënnt Iech dat bekannt vir?",
|
||||
"pain_subtitle": "Dat sinn d'deeglech Frustratioune vu Letzshop Verkeefer",
|
||||
"pain_manual": "Manuell Bestellungsagab",
|
||||
"pain_manual_desc": "Bestellunge vu Letzshop an Tabelle kopéieren. All. Eenzelen. Dag.",
|
||||
"pain_inventory": "Lager Chaos",
|
||||
"pain_inventory_desc": "De Stock an Letzshop stëmmt net mat der Realitéit iwwereneen. Iwwerverkeef passéieren.",
|
||||
"pain_vat": "Falsch TVA Rechnungen",
|
||||
"pain_vat_desc": "EU Cliente brauchen déi korrekt TVA. Äre Comptabel beschwéiert sech.",
|
||||
"pain_customers": "Verluer Clienten",
|
||||
"pain_customers_desc": "Letzshop besëtzt Är Clientsdaten. Dir kënnt se net retargeten oder Loyalitéit opbauen.",
|
||||
"how_title": "Wéi et funktionéiert",
|
||||
"how_subtitle": "Vum Chaos zur Kontroll an 4 Schrëtt",
|
||||
"how_step1": "Letzshop verbannen",
|
||||
"how_step1_desc": "Gitt Är Letzshop API Zougangsdaten an. An 2 Minutte fäerdeg, keng technesch Kenntnisser néideg.",
|
||||
"how_step2": "Bestellunge kommen eran",
|
||||
"how_step2_desc": "Bestellunge ginn automatesch synchroniséiert. Confirméiert an Tracking direkt vu Wizamart derbäisetzen.",
|
||||
"how_step3": "Rechnunge generéieren",
|
||||
"how_step3_desc": "Ee Klick fir konform PDF Rechnunge mat korrekter TVA fir all EU Land ze erstellen.",
|
||||
"how_step4": "Äert Geschäft ausbauen",
|
||||
"how_step4_desc": "Exportéiert Clientë fir Marketing. Verfolgt Lagerstänn. Konzentréiert Iech op de Verkaf, net op Tabellen.",
|
||||
"features_title": "Alles wat e Letzshop Verkeefer brauch",
|
||||
"features_subtitle": "D'operativ Tools déi Letzshop net bitt",
|
||||
"cta_final_title": "Prett fir d'Kontroll iwwer Äert Letzshop Geschäft ze iwwerhuelen?",
|
||||
"cta_final_subtitle": "Schléisst Iech lëtzebuerger Händler un déi opgehalen hunn géint Tabellen ze kämpfen an ugefaang hunn hiert Geschäft auszbauen.",
|
||||
"cta_final_note": "Keng Kreditkaart néideg. Setup an 5 Minutten. Voll Professional Fonctiounen während der Testperiod."
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"title": "Mediebibliothéik",
|
||||
"upload": "Eroplueden",
|
||||
"upload_file": "Fichier eroplueden",
|
||||
"delete": "Läschen",
|
||||
"empty": "Keng Mediefichieren",
|
||||
"upload_first": "Luet Äre éischte Fichier erop"
|
||||
},
|
||||
"themes": {
|
||||
"title": "Händler-Themen",
|
||||
"subtitle": "Verwaltet Händler-Theme-Personnalisatiounen"
|
||||
},
|
||||
"actions": {
|
||||
"save": "Späicheren",
|
||||
"saving": "Späicheren...",
|
||||
"update": "Säit aktualiséieren",
|
||||
"create": "Säit erstellen",
|
||||
"cancel": "Ofbriechen",
|
||||
"back_to_list": "Zréck op d'Lëscht",
|
||||
"preview": "Virschau",
|
||||
"revert_to_default": "Op Standard zrécksetzen"
|
||||
},
|
||||
"messages": {
|
||||
"created": "Säit erfollegräich erstallt",
|
||||
"updated": "Säit erfollegräich aktualiséiert",
|
||||
"deleted": "Säit erfollegräich geläscht",
|
||||
"reverted": "Op Standardsäit zréckgesat",
|
||||
"error_loading": "Feeler beim Lueden vun der Säit",
|
||||
"error_saving": "Feeler beim Späichere vun der Säit",
|
||||
"confirm_delete": "Sidd Dir sécher, datt Dir dës Säit läsche wëllt?"
|
||||
},
|
||||
"filters": {
|
||||
"all_pages": "All Säiten",
|
||||
"all_platforms": "All Plattformen",
|
||||
"search_placeholder": "Säite sichen..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,24 @@
|
||||
"""
|
||||
CMS module database models.
|
||||
|
||||
This is the canonical location for CMS models. Module models are automatically
|
||||
discovered and registered with SQLAlchemy's Base.metadata at startup.
|
||||
This is the canonical location for CMS models including:
|
||||
- ContentPage: CMS pages (marketing, vendor default pages)
|
||||
- MediaFile: Vendor media library
|
||||
- VendorTheme: Vendor storefront theme configuration
|
||||
|
||||
Usage:
|
||||
from app.modules.cms.models import ContentPage
|
||||
from app.modules.cms.models import ContentPage, MediaFile, VendorTheme
|
||||
|
||||
For media models:
|
||||
from models.database.media import MediaFile # Core media file storage
|
||||
from app.modules.catalog.models import ProductMedia # Product-media associations
|
||||
For product-media associations:
|
||||
from app.modules.catalog.models import ProductMedia
|
||||
"""
|
||||
|
||||
from app.modules.cms.models.content_page import ContentPage
|
||||
from app.modules.cms.models.media import MediaFile
|
||||
from app.modules.cms.models.vendor_theme import VendorTheme
|
||||
|
||||
__all__ = [
|
||||
"ContentPage",
|
||||
"MediaFile",
|
||||
"VendorTheme",
|
||||
]
|
||||
|
||||
124
app/modules/cms/models/media.py
Normal file
124
app/modules/cms/models/media.py
Normal file
@@ -0,0 +1,124 @@
|
||||
# app/modules/cms/models/media.py
|
||||
"""
|
||||
CORE media file model for vendor media library.
|
||||
|
||||
This is a CORE framework model used across multiple modules.
|
||||
MediaFile provides vendor-uploaded media files (images, documents, videos).
|
||||
|
||||
For product-media associations, use:
|
||||
from app.modules.catalog.models import ProductMedia
|
||||
|
||||
Files are stored in vendor-specific directories:
|
||||
uploads/vendors/{vendor_id}/{folder}/{filename}
|
||||
"""
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
)
|
||||
from sqlalchemy.dialects.sqlite import JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.core.database import Base
|
||||
from models.database.base import TimestampMixin
|
||||
|
||||
|
||||
class MediaFile(Base, TimestampMixin):
|
||||
"""Vendor media file record.
|
||||
|
||||
Stores metadata about uploaded files. Actual files are stored
|
||||
in the filesystem at uploads/vendors/{vendor_id}/{folder}/
|
||||
"""
|
||||
|
||||
__tablename__ = "media_files"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
|
||||
|
||||
# File identification
|
||||
filename = Column(String(255), nullable=False) # Stored filename (UUID-based)
|
||||
original_filename = Column(String(255)) # Original uploaded filename
|
||||
file_path = Column(String(500), nullable=False) # Relative path from uploads/
|
||||
|
||||
# File properties
|
||||
media_type = Column(String(20), nullable=False) # image, video, document
|
||||
mime_type = Column(String(100))
|
||||
file_size = Column(Integer) # bytes
|
||||
|
||||
# Image/video dimensions
|
||||
width = Column(Integer)
|
||||
height = Column(Integer)
|
||||
|
||||
# Thumbnail (for images/videos)
|
||||
thumbnail_path = Column(String(500))
|
||||
|
||||
# Metadata
|
||||
alt_text = Column(String(500))
|
||||
description = Column(Text)
|
||||
folder = Column(String(100), default="general") # products, general, etc.
|
||||
tags = Column(JSON) # List of tags for categorization
|
||||
extra_metadata = Column(JSON) # Additional metadata (EXIF, etc.)
|
||||
|
||||
# Status
|
||||
is_optimized = Column(Boolean, default=False)
|
||||
optimized_size = Column(Integer) # Size after optimization
|
||||
|
||||
# Usage tracking
|
||||
usage_count = Column(Integer, default=0) # How many times used
|
||||
|
||||
# Relationships
|
||||
vendor = relationship("Vendor", back_populates="media_files")
|
||||
# ProductMedia relationship uses string reference to avoid circular import
|
||||
product_associations = relationship(
|
||||
"ProductMedia",
|
||||
back_populates="media",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_media_vendor_id", "vendor_id"),
|
||||
Index("idx_media_vendor_folder", "vendor_id", "folder"),
|
||||
Index("idx_media_vendor_type", "vendor_id", "media_type"),
|
||||
Index("idx_media_filename", "filename"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<MediaFile(id={self.id}, vendor_id={self.vendor_id}, "
|
||||
f"filename='{self.filename}', type='{self.media_type}')>"
|
||||
)
|
||||
|
||||
@property
|
||||
def file_url(self) -> str:
|
||||
"""Get the public URL for this file."""
|
||||
return f"/uploads/{self.file_path}"
|
||||
|
||||
@property
|
||||
def thumbnail_url(self) -> str | None:
|
||||
"""Get the thumbnail URL if available."""
|
||||
if self.thumbnail_path:
|
||||
return f"/uploads/{self.thumbnail_path}"
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_image(self) -> bool:
|
||||
"""Check if this is an image file."""
|
||||
return self.media_type == "image"
|
||||
|
||||
@property
|
||||
def is_video(self) -> bool:
|
||||
"""Check if this is a video file."""
|
||||
return self.media_type == "video"
|
||||
|
||||
@property
|
||||
def is_document(self) -> bool:
|
||||
"""Check if this is a document file."""
|
||||
return self.media_type == "document"
|
||||
|
||||
|
||||
__all__ = ["MediaFile"]
|
||||
139
app/modules/cms/models/vendor_theme.py
Normal file
139
app/modules/cms/models/vendor_theme.py
Normal file
@@ -0,0 +1,139 @@
|
||||
# app/modules/cms/models/vendor_theme.py
|
||||
"""
|
||||
Vendor Theme Configuration Model
|
||||
Allows each vendor to customize their shop's appearance
|
||||
"""
|
||||
|
||||
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Integer, String, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.core.database import Base
|
||||
from models.database.base import TimestampMixin
|
||||
|
||||
|
||||
class VendorTheme(Base, TimestampMixin):
|
||||
"""
|
||||
Stores theme configuration for each vendor's shop.
|
||||
|
||||
Each vendor can have ONE active theme:
|
||||
- Custom colors (primary, secondary, accent)
|
||||
- Custom fonts
|
||||
- Custom logo and favicon
|
||||
- Custom CSS overrides
|
||||
- Layout preferences
|
||||
|
||||
Theme presets available: default, modern, classic, minimal, vibrant
|
||||
"""
|
||||
|
||||
__tablename__ = "vendor_themes"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
vendor_id = Column(
|
||||
Integer,
|
||||
ForeignKey("vendors.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
unique=True, # ONE vendor = ONE theme
|
||||
)
|
||||
|
||||
# Basic Theme Settings
|
||||
theme_name = Column(
|
||||
String(100), default="default"
|
||||
) # default, modern, classic, minimal, vibrant
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
# Color Scheme (JSON for flexibility)
|
||||
colors = Column(
|
||||
JSON,
|
||||
default={
|
||||
"primary": "#6366f1", # Indigo
|
||||
"secondary": "#8b5cf6", # Purple
|
||||
"accent": "#ec4899", # Pink
|
||||
"background": "#ffffff", # White
|
||||
"text": "#1f2937", # Gray-800
|
||||
"border": "#e5e7eb", # Gray-200
|
||||
},
|
||||
)
|
||||
|
||||
# Typography
|
||||
font_family_heading = Column(String(100), default="Inter, sans-serif")
|
||||
font_family_body = Column(String(100), default="Inter, sans-serif")
|
||||
|
||||
# Branding Assets
|
||||
logo_url = Column(String(500), nullable=True) # Path to vendor logo
|
||||
logo_dark_url = Column(String(500), nullable=True) # Dark mode logo
|
||||
favicon_url = Column(String(500), nullable=True) # Favicon
|
||||
banner_url = Column(String(500), nullable=True) # Homepage banner
|
||||
|
||||
# Layout Preferences
|
||||
layout_style = Column(String(50), default="grid") # grid, list, masonry
|
||||
header_style = Column(String(50), default="fixed") # fixed, static, transparent
|
||||
product_card_style = Column(
|
||||
String(50), default="modern"
|
||||
) # modern, classic, minimal
|
||||
|
||||
# Custom CSS (for advanced customization)
|
||||
custom_css = Column(Text, nullable=True)
|
||||
|
||||
# Social Media Links
|
||||
social_links = Column(JSON, default={}) # {facebook: "url", instagram: "url", etc.}
|
||||
|
||||
# SEO & Meta
|
||||
meta_title_template = Column(
|
||||
String(200), nullable=True
|
||||
) # e.g., "{product_name} - {shop_name}"
|
||||
meta_description = Column(Text, nullable=True)
|
||||
|
||||
# Relationships - FIXED: back_populates must match the relationship name in Vendor model
|
||||
vendor = relationship("Vendor", back_populates="vendor_theme")
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<VendorTheme(vendor_id={self.vendor_id}, theme_name='{self.theme_name}')>"
|
||||
)
|
||||
|
||||
@property
|
||||
def primary_color(self):
|
||||
"""Get primary color from JSON"""
|
||||
return self.colors.get("primary", "#6366f1")
|
||||
|
||||
@property
|
||||
def css_variables(self):
|
||||
"""Generate CSS custom properties from theme config"""
|
||||
return {
|
||||
"--color-primary": self.colors.get("primary", "#6366f1"),
|
||||
"--color-secondary": self.colors.get("secondary", "#8b5cf6"),
|
||||
"--color-accent": self.colors.get("accent", "#ec4899"),
|
||||
"--color-background": self.colors.get("background", "#ffffff"),
|
||||
"--color-text": self.colors.get("text", "#1f2937"),
|
||||
"--color-border": self.colors.get("border", "#e5e7eb"),
|
||||
"--font-heading": self.font_family_heading,
|
||||
"--font-body": self.font_family_body,
|
||||
}
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert theme to dictionary for template rendering"""
|
||||
return {
|
||||
"theme_name": self.theme_name,
|
||||
"colors": self.colors,
|
||||
"fonts": {
|
||||
"heading": self.font_family_heading,
|
||||
"body": self.font_family_body,
|
||||
},
|
||||
"branding": {
|
||||
"logo": self.logo_url,
|
||||
"logo_dark": self.logo_dark_url,
|
||||
"favicon": self.favicon_url,
|
||||
"banner": self.banner_url,
|
||||
},
|
||||
"layout": {
|
||||
"style": self.layout_style,
|
||||
"header": self.header_style,
|
||||
"product_card": self.product_card_style,
|
||||
},
|
||||
"social_links": self.social_links,
|
||||
"custom_css": self.custom_css,
|
||||
"css_variables": self.css_variables,
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["VendorTheme"]
|
||||
@@ -23,7 +23,7 @@ from app.modules.cms.schemas import (
|
||||
SectionUpdateResponse,
|
||||
)
|
||||
from app.modules.cms.services import content_page_service
|
||||
from models.database.user import User
|
||||
from app.modules.tenancy.models import User
|
||||
|
||||
admin_content_pages_router = APIRouter(prefix="/content-pages")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -15,7 +15,7 @@ from fastapi import APIRouter, Depends, File, Form, UploadFile
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.modules.core.services.image_service import image_service
|
||||
from models.schema.auth import UserContext
|
||||
from models.schema.image import (
|
||||
from app.modules.cms.schemas.image import (
|
||||
ImageDeleteResponse,
|
||||
ImageStorageStats,
|
||||
ImageUploadResponse,
|
||||
|
||||
@@ -14,7 +14,7 @@ from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.modules.cms.services.media_service import media_service
|
||||
from models.schema.auth import UserContext
|
||||
from models.schema.media import (
|
||||
from app.modules.cms.schemas.media import (
|
||||
MediaDetailResponse,
|
||||
MediaItemResponse,
|
||||
MediaListResponse,
|
||||
|
||||
@@ -20,7 +20,7 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_admin_api, get_db
|
||||
from app.modules.cms.services.vendor_theme_service import vendor_theme_service
|
||||
from models.schema.auth import UserContext
|
||||
from models.schema.vendor_theme import (
|
||||
from app.modules.cms.schemas.vendor_theme import (
|
||||
ThemeDeleteResponse,
|
||||
ThemePresetListResponse,
|
||||
ThemePresetResponse,
|
||||
|
||||
@@ -26,7 +26,7 @@ from app.modules.cms.schemas import (
|
||||
)
|
||||
from app.modules.cms.services import content_page_service
|
||||
from app.modules.tenancy.services.vendor_service import VendorService # noqa: MOD-004 - shared platform service
|
||||
from models.database.user import User
|
||||
from app.modules.tenancy.models import User
|
||||
|
||||
vendor_service = VendorService()
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from app.core.database import get_db
|
||||
from app.modules.cms.exceptions import MediaOptimizationException
|
||||
from app.modules.cms.services.media_service import media_service
|
||||
from models.schema.auth import UserContext
|
||||
from models.schema.media import (
|
||||
from app.modules.cms.schemas.media import (
|
||||
MediaDetailResponse,
|
||||
MediaItemResponse,
|
||||
MediaListResponse,
|
||||
|
||||
@@ -11,8 +11,8 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db, require_menu_access
|
||||
from app.templates_config import templates
|
||||
from models.database.admin_menu_config import FrontendType
|
||||
from models.database.user import User
|
||||
from app.modules.enums import FrontendType
|
||||
from app.modules.tenancy.models import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ from app.api.deps import get_current_vendor_from_cookie_or_header, get_db
|
||||
from app.modules.cms.services import content_page_service
|
||||
from app.modules.core.services.platform_settings_service import platform_settings_service # noqa: MOD-004 - shared platform service
|
||||
from app.templates_config import templates
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
from app.modules.tenancy.models import User
|
||||
from app.modules.tenancy.models import Vendor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -35,6 +35,44 @@ from app.modules.cms.schemas.homepage_sections import (
|
||||
HomepageSectionsResponse,
|
||||
)
|
||||
|
||||
# Media schemas
|
||||
from app.modules.cms.schemas.media import (
|
||||
FailedFileInfo,
|
||||
MediaDetailResponse,
|
||||
MediaItemResponse,
|
||||
MediaListResponse,
|
||||
MediaMetadataUpdate,
|
||||
MediaUploadResponse,
|
||||
MediaUsageResponse,
|
||||
MessageResponse,
|
||||
MultipleUploadResponse,
|
||||
OptimizationResultResponse,
|
||||
ProductUsageInfo,
|
||||
UploadedFileInfo,
|
||||
)
|
||||
|
||||
# Image schemas
|
||||
from app.modules.cms.schemas.image import (
|
||||
ImageDeleteResponse,
|
||||
ImageStorageStats,
|
||||
ImageUploadResponse,
|
||||
ImageUrls,
|
||||
)
|
||||
|
||||
# Theme schemas
|
||||
from app.modules.cms.schemas.vendor_theme import (
|
||||
ThemeDeleteResponse,
|
||||
ThemePresetListResponse,
|
||||
ThemePresetPreview,
|
||||
ThemePresetResponse,
|
||||
VendorThemeBranding,
|
||||
VendorThemeColors,
|
||||
VendorThemeFonts,
|
||||
VendorThemeLayout,
|
||||
VendorThemeResponse,
|
||||
VendorThemeUpdate,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Content Page - Admin
|
||||
"ContentPageCreate",
|
||||
@@ -60,4 +98,33 @@ __all__ = [
|
||||
"HomepageSections",
|
||||
"SectionUpdateRequest",
|
||||
"HomepageSectionsResponse",
|
||||
# Media
|
||||
"FailedFileInfo",
|
||||
"MediaDetailResponse",
|
||||
"MediaItemResponse",
|
||||
"MediaListResponse",
|
||||
"MediaMetadataUpdate",
|
||||
"MediaUploadResponse",
|
||||
"MediaUsageResponse",
|
||||
"MessageResponse",
|
||||
"MultipleUploadResponse",
|
||||
"OptimizationResultResponse",
|
||||
"ProductUsageInfo",
|
||||
"UploadedFileInfo",
|
||||
# Image
|
||||
"ImageDeleteResponse",
|
||||
"ImageStorageStats",
|
||||
"ImageUploadResponse",
|
||||
"ImageUrls",
|
||||
# Theme
|
||||
"ThemeDeleteResponse",
|
||||
"ThemePresetListResponse",
|
||||
"ThemePresetPreview",
|
||||
"ThemePresetResponse",
|
||||
"VendorThemeBranding",
|
||||
"VendorThemeColors",
|
||||
"VendorThemeFonts",
|
||||
"VendorThemeLayout",
|
||||
"VendorThemeResponse",
|
||||
"VendorThemeUpdate",
|
||||
]
|
||||
|
||||
46
app/modules/cms/schemas/image.py
Normal file
46
app/modules/cms/schemas/image.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# app/modules/cms/schemas/image.py
|
||||
"""
|
||||
Pydantic schemas for image operations.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ImageUrls(BaseModel):
|
||||
"""URLs for image variants."""
|
||||
|
||||
original: str
|
||||
medium: str | None = None # 800px variant
|
||||
thumb: str | None = None # 200px variant
|
||||
|
||||
# Allow arbitrary keys for flexibility
|
||||
class Config:
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ImageUploadResponse(BaseModel):
|
||||
"""Response from image upload."""
|
||||
|
||||
success: bool
|
||||
image: dict | None = None
|
||||
error: str | None = None
|
||||
|
||||
|
||||
class ImageDeleteResponse(BaseModel):
|
||||
"""Response from image deletion."""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
|
||||
class ImageStorageStats(BaseModel):
|
||||
"""Image storage statistics."""
|
||||
|
||||
total_files: int
|
||||
total_size_bytes: int
|
||||
total_size_mb: float
|
||||
total_size_gb: float
|
||||
directory_count: int
|
||||
max_files_per_dir: int
|
||||
avg_files_per_dir: float
|
||||
products_estimated: int
|
||||
198
app/modules/cms/schemas/media.py
Normal file
198
app/modules/cms/schemas/media.py
Normal file
@@ -0,0 +1,198 @@
|
||||
# app/modules/cms/schemas/media.py
|
||||
"""
|
||||
Media/file management Pydantic schemas for API validation and responses.
|
||||
|
||||
This module provides schemas for:
|
||||
- Media library listing
|
||||
- File upload responses
|
||||
- Media metadata operations
|
||||
- Media usage tracking
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# ============================================================================
|
||||
# SHARED RESPONSE SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
"""Generic message response for simple operations."""
|
||||
|
||||
message: str
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MEDIA ITEM SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MediaItemResponse(BaseModel):
|
||||
"""Single media item response."""
|
||||
|
||||
id: int
|
||||
filename: str
|
||||
original_filename: str | None = None
|
||||
file_url: str
|
||||
url: str | None = None # Alias for file_url for JS compatibility
|
||||
thumbnail_url: str | None = None
|
||||
media_type: str # image, video, document
|
||||
mime_type: str | None = None
|
||||
file_size: int | None = None # bytes
|
||||
width: int | None = None # for images/videos
|
||||
height: int | None = None # for images/videos
|
||||
alt_text: str | None = None
|
||||
description: str | None = None
|
||||
folder: str | None = None
|
||||
extra_metadata: dict[str, Any] | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
"""Set url from file_url if not provided."""
|
||||
if self.url is None:
|
||||
object.__setattr__(self, "url", self.file_url)
|
||||
|
||||
|
||||
class MediaListResponse(BaseModel):
|
||||
"""Paginated list of media items."""
|
||||
|
||||
media: list[MediaItemResponse] = []
|
||||
total: int = 0
|
||||
skip: int = 0
|
||||
limit: int = 100
|
||||
message: str | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# UPLOAD RESPONSE SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MediaUploadResponse(BaseModel):
|
||||
"""Response for single file upload."""
|
||||
|
||||
success: bool = True
|
||||
message: str | None = None
|
||||
media: MediaItemResponse | None = None
|
||||
# Legacy fields for backwards compatibility
|
||||
id: int | None = None
|
||||
file_url: str | None = None
|
||||
thumbnail_url: str | None = None
|
||||
filename: str | None = None
|
||||
file_size: int | None = None
|
||||
media_type: str | None = None
|
||||
|
||||
|
||||
class UploadedFileInfo(BaseModel):
|
||||
"""Information about a successfully uploaded file."""
|
||||
|
||||
id: int
|
||||
filename: str
|
||||
file_url: str
|
||||
thumbnail_url: str | None = None
|
||||
|
||||
|
||||
class FailedFileInfo(BaseModel):
|
||||
"""Information about a failed file upload."""
|
||||
|
||||
filename: str
|
||||
error: str
|
||||
|
||||
|
||||
class MultipleUploadResponse(BaseModel):
|
||||
"""Response for multiple file upload."""
|
||||
|
||||
uploaded_files: list[UploadedFileInfo] = []
|
||||
failed_files: list[FailedFileInfo] = []
|
||||
total_uploaded: int = 0
|
||||
total_failed: int = 0
|
||||
message: str | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MEDIA DETAIL SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MediaDetailResponse(BaseModel):
|
||||
"""Detailed media item response with usage info."""
|
||||
|
||||
id: int | None = None
|
||||
filename: str | None = None
|
||||
original_filename: str | None = None
|
||||
file_url: str | None = None
|
||||
thumbnail_url: str | None = None
|
||||
media_type: str | None = None
|
||||
mime_type: str | None = None
|
||||
file_size: int | None = None
|
||||
width: int | None = None
|
||||
height: int | None = None
|
||||
alt_text: str | None = None
|
||||
description: str | None = None
|
||||
folder: str | None = None
|
||||
extra_metadata: dict[str, Any] | None = None
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
message: str | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MEDIA UPDATE SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MediaMetadataUpdate(BaseModel):
|
||||
"""Request model for updating media metadata."""
|
||||
|
||||
filename: str | None = Field(None, max_length=255)
|
||||
alt_text: str | None = Field(None, max_length=500)
|
||||
description: str | None = None
|
||||
folder: str | None = Field(None, max_length=100)
|
||||
metadata: dict[str, Any] | None = None # Named 'metadata' in API, stored as 'extra_metadata'
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MEDIA USAGE SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ProductUsageInfo(BaseModel):
|
||||
"""Information about product using this media."""
|
||||
|
||||
product_id: int
|
||||
product_name: str
|
||||
usage_type: str # main_image, gallery, variant, etc.
|
||||
|
||||
|
||||
class MediaUsageResponse(BaseModel):
|
||||
"""Response showing where media is being used."""
|
||||
|
||||
media_id: int | None = None
|
||||
products: list[ProductUsageInfo] = []
|
||||
other_usage: list[dict[str, Any]] = []
|
||||
total_usage_count: int = 0
|
||||
message: str | None = None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MEDIA OPTIMIZATION SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class OptimizationResultResponse(BaseModel):
|
||||
"""Response for media optimization operation."""
|
||||
|
||||
media_id: int | None = None
|
||||
original_size: int | None = None
|
||||
optimized_size: int | None = None
|
||||
savings_percent: float | None = None
|
||||
optimized_url: str | None = None
|
||||
message: str | None = None
|
||||
108
app/modules/cms/schemas/vendor_theme.py
Normal file
108
app/modules/cms/schemas/vendor_theme.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# app/modules/cms/schemas/vendor_theme.py
|
||||
"""
|
||||
Pydantic schemas for vendor theme operations.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class VendorThemeColors(BaseModel):
|
||||
"""Color scheme for vendor theme."""
|
||||
|
||||
primary: str | None = Field(None, description="Primary brand color")
|
||||
secondary: str | None = Field(None, description="Secondary color")
|
||||
accent: str | None = Field(None, description="Accent/CTA color")
|
||||
background: str | None = Field(None, description="Background color")
|
||||
text: str | None = Field(None, description="Text color")
|
||||
border: str | None = Field(None, description="Border color")
|
||||
|
||||
|
||||
class VendorThemeFonts(BaseModel):
|
||||
"""Typography settings for vendor theme."""
|
||||
|
||||
heading: str | None = Field(None, description="Font for headings")
|
||||
body: str | None = Field(None, description="Font for body text")
|
||||
|
||||
|
||||
class VendorThemeBranding(BaseModel):
|
||||
"""Branding assets for vendor theme."""
|
||||
|
||||
logo: str | None = Field(None, description="Logo URL")
|
||||
logo_dark: str | None = Field(None, description="Dark mode logo URL")
|
||||
favicon: str | None = Field(None, description="Favicon URL")
|
||||
banner: str | None = Field(None, description="Banner image URL")
|
||||
|
||||
|
||||
class VendorThemeLayout(BaseModel):
|
||||
"""Layout settings for vendor theme."""
|
||||
|
||||
style: str | None = Field(
|
||||
None, description="Product layout style (grid, list, masonry)"
|
||||
)
|
||||
header: str | None = Field(
|
||||
None, description="Header style (fixed, static, transparent)"
|
||||
)
|
||||
product_card: str | None = Field(
|
||||
None, description="Product card style (modern, classic, minimal)"
|
||||
)
|
||||
|
||||
|
||||
class VendorThemeUpdate(BaseModel):
|
||||
"""Schema for updating vendor theme (partial updates allowed)."""
|
||||
|
||||
theme_name: str | None = Field(None, description="Theme preset name")
|
||||
colors: dict[str, str] | None = Field(None, description="Color scheme")
|
||||
fonts: dict[str, str] | None = Field(None, description="Font settings")
|
||||
branding: dict[str, str | None] | None = Field(None, description="Branding assets")
|
||||
layout: dict[str, str] | None = Field(None, description="Layout settings")
|
||||
custom_css: str | None = Field(None, description="Custom CSS rules")
|
||||
social_links: dict[str, str] | None = Field(None, description="Social media links")
|
||||
|
||||
|
||||
class VendorThemeResponse(BaseModel):
|
||||
"""Schema for vendor theme response."""
|
||||
|
||||
theme_name: str = Field(..., description="Theme name")
|
||||
colors: dict[str, str] = Field(..., description="Color scheme")
|
||||
fonts: dict[str, str] = Field(..., description="Font settings")
|
||||
branding: dict[str, str | None] = Field(..., description="Branding assets")
|
||||
layout: dict[str, str] = Field(..., description="Layout settings")
|
||||
social_links: dict[str, str] | None = Field(
|
||||
default_factory=dict, description="Social links"
|
||||
)
|
||||
custom_css: str | None = Field(None, description="Custom CSS")
|
||||
css_variables: dict[str, str] | None = Field(
|
||||
None, description="CSS custom properties"
|
||||
)
|
||||
|
||||
|
||||
class ThemePresetPreview(BaseModel):
|
||||
"""Preview information for a theme preset."""
|
||||
|
||||
name: str = Field(..., description="Preset name")
|
||||
description: str = Field(..., description="Preset description")
|
||||
primary_color: str = Field(..., description="Primary color")
|
||||
secondary_color: str = Field(..., description="Secondary color")
|
||||
accent_color: str = Field(..., description="Accent color")
|
||||
heading_font: str = Field(..., description="Heading font")
|
||||
body_font: str = Field(..., description="Body font")
|
||||
layout_style: str = Field(..., description="Layout style")
|
||||
|
||||
|
||||
class ThemePresetResponse(BaseModel):
|
||||
"""Response after applying a preset."""
|
||||
|
||||
message: str = Field(..., description="Success message")
|
||||
theme: VendorThemeResponse = Field(..., description="Applied theme")
|
||||
|
||||
|
||||
class ThemePresetListResponse(BaseModel):
|
||||
"""List of available theme presets."""
|
||||
|
||||
presets: list[ThemePresetPreview] = Field(..., description="Available presets")
|
||||
|
||||
|
||||
class ThemeDeleteResponse(BaseModel):
|
||||
"""Response after deleting a theme."""
|
||||
|
||||
message: str = Field(..., description="Success message")
|
||||
@@ -27,7 +27,7 @@ from app.modules.cms.exceptions import (
|
||||
UnsupportedMediaTypeException,
|
||||
MediaFileTooLargeException,
|
||||
)
|
||||
from models.database.media import MediaFile
|
||||
from app.modules.cms.models import MediaFile
|
||||
from app.modules.catalog.models import ProductMedia
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,14 +24,13 @@ from app.exceptions import (
|
||||
ValidationException,
|
||||
ExternalServiceException,
|
||||
)
|
||||
from models.database import (
|
||||
Vendor,
|
||||
from app.modules.tenancy.models import Vendor
|
||||
from app.modules.messaging.models import (
|
||||
VendorEmailSettings,
|
||||
EmailProvider,
|
||||
PREMIUM_EMAIL_PROVIDERS,
|
||||
VendorSubscription,
|
||||
TierCode,
|
||||
)
|
||||
from app.modules.billing.models import VendorSubscription, TierCode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ from app.modules.cms.exceptions import (
|
||||
ThemeValidationException,
|
||||
VendorThemeNotFoundException,
|
||||
)
|
||||
from models.database.vendor import Vendor
|
||||
from models.database.vendor_theme import VendorTheme
|
||||
from models.schema.vendor_theme import ThemePresetPreview, VendorThemeUpdate
|
||||
from app.modules.tenancy.models import Vendor
|
||||
from app.modules.cms.models import VendorTheme
|
||||
from app.modules.cms.schemas.vendor_theme import ThemePresetPreview, VendorThemeUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ function contentPagesManager() {
|
||||
|
||||
// Initialize
|
||||
async init() {
|
||||
// Load i18n translations
|
||||
await I18n.loadModule('cms');
|
||||
|
||||
contentPagesLog.info('=== CONTENT PAGES MANAGER INITIALIZING ===');
|
||||
|
||||
// Prevent multiple initializations
|
||||
@@ -235,7 +238,7 @@ function contentPagesManager() {
|
||||
|
||||
} catch (err) {
|
||||
contentPagesLog.error('Error deleting page:', err);
|
||||
Utils.showToast(`Failed to delete page: ${err.message}`, 'error');
|
||||
Utils.showToast(I18n.t('cms.messages.failed_to_delete_page', { error: err.message }), 'error');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -20,24 +20,24 @@
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{{ _("platform.hero.badge", trial_days=trial_days) }}
|
||||
{{ _("cms.platform.hero.badge", trial_days=trial_days) }}
|
||||
</div>
|
||||
|
||||
{# Headline #}
|
||||
<h1 class="text-4xl md:text-5xl lg:text-6xl font-extrabold text-gray-900 dark:text-white leading-tight mb-6">
|
||||
{{ _("platform.hero.title") }}
|
||||
{{ _("cms.platform.hero.title") }}
|
||||
</h1>
|
||||
|
||||
{# Subheadline #}
|
||||
<p class="text-xl text-gray-600 dark:text-gray-400 max-w-3xl mx-auto mb-10">
|
||||
{{ _("platform.hero.subtitle") }}
|
||||
{{ _("cms.platform.hero.subtitle") }}
|
||||
</p>
|
||||
|
||||
{# CTA Buttons #}
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="/signup"
|
||||
class="inline-flex items-center justify-center px-8 py-4 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold rounded-xl shadow-lg shadow-indigo-500/30 transition-all hover:scale-105">
|
||||
{{ _("platform.hero.cta_trial") }}
|
||||
{{ _("cms.platform.hero.cta_trial") }}
|
||||
<svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
|
||||
</svg>
|
||||
@@ -47,7 +47,7 @@
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
{{ _("platform.hero.cta_find_shop") }}
|
||||
{{ _("cms.platform.hero.cta_find_shop") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,19 +68,19 @@
|
||||
{# Section Header #}
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
{{ _("platform.pricing.title") }}
|
||||
{{ _("cms.platform.pricing.title") }}
|
||||
</h2>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
{{ _("platform.pricing.subtitle", trial_days=trial_days) }}
|
||||
{{ _("cms.platform.pricing.subtitle", trial_days=trial_days) }}
|
||||
</p>
|
||||
|
||||
{# Billing Toggle #}
|
||||
<div class="flex justify-center mt-8">
|
||||
{{ toggle_switch(
|
||||
model='annual',
|
||||
left_label=_("platform.pricing.monthly"),
|
||||
right_label=_("platform.pricing.annual"),
|
||||
right_badge=_("platform.pricing.save_months")
|
||||
left_label=_("cms.platform.pricing.monthly"),
|
||||
right_label=_("cms.platform.pricing.annual"),
|
||||
right_badge=_("cms.platform.pricing.save_months")
|
||||
) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,7 +95,7 @@
|
||||
{% if tier.is_popular %}
|
||||
<div class="absolute -top-3 left-1/2 -translate-x-1/2">
|
||||
<span class="bg-indigo-600 text-white text-xs font-bold px-3 py-1 rounded-full">
|
||||
{{ _("platform.pricing.most_popular") }}
|
||||
{{ _("cms.platform.pricing.most_popular") }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -108,19 +108,19 @@
|
||||
<template x-if="!annual">
|
||||
<div>
|
||||
<span class="text-4xl font-extrabold text-gray-900 dark:text-white">{{ tier.price_monthly|int }}€</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ _("platform.pricing.per_month") }}</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ _("cms.platform.pricing.per_month") }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="annual">
|
||||
<div>
|
||||
{% if tier.price_annual %}
|
||||
<span class="text-4xl font-extrabold text-gray-900 dark:text-white">{{ (tier.price_annual / 12)|round(0)|int }}€</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ _("platform.pricing.per_month") }}</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ _("cms.platform.pricing.per_month") }}</span>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ tier.price_annual|int }}€ {{ _("platform.pricing.per_year") }}
|
||||
{{ tier.price_annual|int }}€ {{ _("cms.platform.pricing.per_year") }}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-2xl font-bold text-gray-900 dark:text-white">{{ _("platform.pricing.custom") }}</span>
|
||||
<span class="text-2xl font-bold text-gray-900 dark:text-white">{{ _("cms.platform.pricing.custom") }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</template>
|
||||
@@ -133,28 +133,28 @@
|
||||
<svg class="w-4 h-4 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% if tier.orders_per_month %}{{ _("platform.pricing.orders_per_month", count=tier.orders_per_month) }}{% else %}{{ _("platform.pricing.unlimited_orders") }}{% endif %}
|
||||
{% if tier.orders_per_month %}{{ _("cms.platform.pricing.orders_per_month", count=tier.orders_per_month) }}{% else %}{{ _("cms.platform.pricing.unlimited_orders") }}{% endif %}
|
||||
</li>
|
||||
{# Products #}
|
||||
<li class="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-4 h-4 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% if tier.products_limit %}{{ _("platform.pricing.products_limit", count=tier.products_limit) }}{% else %}{{ _("platform.pricing.unlimited_products") }}{% endif %}
|
||||
{% if tier.products_limit %}{{ _("cms.platform.pricing.products_limit", count=tier.products_limit) }}{% else %}{{ _("cms.platform.pricing.unlimited_products") }}{% endif %}
|
||||
</li>
|
||||
{# Team Members #}
|
||||
<li class="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-4 h-4 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% if tier.team_members %}{{ _("platform.pricing.team_members", count=tier.team_members) }}{% else %}{{ _("platform.pricing.unlimited_team") }}{% endif %}
|
||||
{% if tier.team_members %}{{ _("cms.platform.pricing.team_members", count=tier.team_members) }}{% else %}{{ _("cms.platform.pricing.unlimited_team") }}{% endif %}
|
||||
</li>
|
||||
{# Letzshop Sync - always included #}
|
||||
<li class="flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-4 h-4 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{{ _("platform.pricing.letzshop_sync") }}
|
||||
{{ _("cms.platform.pricing.letzshop_sync") }}
|
||||
</li>
|
||||
{# EU VAT Invoicing #}
|
||||
<li class="flex items-center {% if 'invoice_eu_vat' in tier.features %}text-gray-700 dark:text-gray-300{% else %}text-gray-400 dark:text-gray-600{% endif %}">
|
||||
@@ -167,7 +167,7 @@
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
{{ _("platform.pricing.eu_vat_invoicing") }}
|
||||
{{ _("cms.platform.pricing.eu_vat_invoicing") }}
|
||||
</li>
|
||||
{# Analytics Dashboard #}
|
||||
<li class="flex items-center {% if 'analytics_dashboard' in tier.features %}text-gray-700 dark:text-gray-300{% else %}text-gray-400 dark:text-gray-600{% endif %}">
|
||||
@@ -180,7 +180,7 @@
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
{{ _("platform.pricing.analytics_dashboard") }}
|
||||
{{ _("cms.platform.pricing.analytics_dashboard") }}
|
||||
</li>
|
||||
{# API Access #}
|
||||
<li class="flex items-center {% if 'api_access' in tier.features %}text-gray-700 dark:text-gray-300{% else %}text-gray-400 dark:text-gray-600{% endif %}">
|
||||
@@ -193,7 +193,7 @@
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
{{ _("platform.pricing.api_access") }}
|
||||
{{ _("cms.platform.pricing.api_access") }}
|
||||
</li>
|
||||
{# Multi-channel Integration - Enterprise only #}
|
||||
<li class="flex items-center {% if tier.is_enterprise %}text-gray-700 dark:text-gray-300{% else %}text-gray-400 dark:text-gray-600{% endif %}">
|
||||
@@ -206,7 +206,7 @@
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
{{ _("platform.pricing.multi_channel") }}
|
||||
{{ _("cms.platform.pricing.multi_channel") }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -214,14 +214,14 @@
|
||||
{% if tier.is_enterprise %}
|
||||
<a href="mailto:sales@wizamart.com?subject=Enterprise%20Plan%20Inquiry"
|
||||
class="block w-full py-3 px-4 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white font-semibold rounded-xl text-center hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
|
||||
{{ _("platform.pricing.contact_sales") }}
|
||||
{{ _("cms.platform.pricing.contact_sales") }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/signup?tier={{ tier.code }}"
|
||||
:href="'/signup?tier={{ tier.code }}&annual=' + annual"
|
||||
class="block w-full py-3 px-4 font-semibold rounded-xl text-center transition-colors
|
||||
{% if tier.is_popular %}bg-indigo-600 hover:bg-indigo-700 text-white{% else %}bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-200 dark:hover:bg-indigo-900/50{% endif %}">
|
||||
{{ _("platform.pricing.start_trial") }}
|
||||
{{ _("cms.platform.pricing.start_trial") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -238,10 +238,10 @@
|
||||
{# Section Header #}
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
{{ _("platform.addons.title") }}
|
||||
{{ _("cms.platform.addons.title") }}
|
||||
</h2>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
{{ _("platform.addons.subtitle") }}
|
||||
{{ _("cms.platform.addons.subtitle") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -300,10 +300,10 @@
|
||||
{# Section Header #}
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
{{ _("platform.find_shop.title") }}
|
||||
{{ _("cms.platform.find_shop.title") }}
|
||||
</h2>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-400">
|
||||
{{ _("platform.find_shop.subtitle") }}
|
||||
{{ _("cms.platform.find_shop.subtitle") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -313,7 +313,7 @@
|
||||
<input
|
||||
type="text"
|
||||
x-model="shopUrl"
|
||||
placeholder="{{ _('platform.find_shop.placeholder') }}"
|
||||
placeholder="{{ _('cms.platform.find_shop.placeholder') }}"
|
||||
class="flex-1 px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
/>
|
||||
<button
|
||||
@@ -326,7 +326,7 @@
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
|
||||
</svg>
|
||||
</template>
|
||||
{{ _("platform.find_shop.button") }}
|
||||
{{ _("cms.platform.find_shop.button") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -342,12 +342,12 @@
|
||||
<template x-if="!vendorResult.vendor.is_claimed">
|
||||
<a :href="'/signup?letzshop=' + vendorResult.vendor.slug"
|
||||
class="px-6 py-2 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-lg transition-colors">
|
||||
{{ _("platform.find_shop.claim_shop") }}
|
||||
{{ _("cms.platform.find_shop.claim_shop") }}
|
||||
</a>
|
||||
</template>
|
||||
<template x-if="vendorResult.vendor.is_claimed">
|
||||
<span class="px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded-lg">
|
||||
{{ _("platform.find_shop.already_claimed") }}
|
||||
{{ _("cms.platform.find_shop.already_claimed") }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
@@ -362,7 +362,7 @@
|
||||
|
||||
{# Help Text #}
|
||||
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400 text-center">
|
||||
{{ _("platform.find_shop.no_account") }} <a href="https://letzshop.lu" target="_blank" class="text-indigo-600 dark:text-indigo-400 hover:underline">{{ _("platform.find_shop.signup_letzshop") }}</a>{{ _("platform.find_shop.then_connect") }}
|
||||
{{ _("cms.platform.find_shop.no_account") }} <a href="https://letzshop.lu" target="_blank" class="text-indigo-600 dark:text-indigo-400 hover:underline">{{ _("cms.platform.find_shop.signup_letzshop") }}</a>{{ _("cms.platform.find_shop.then_connect") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -374,14 +374,14 @@
|
||||
<section class="py-16 lg:py-24 bg-gradient-to-r from-indigo-600 to-purple-600">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-white mb-6">
|
||||
{{ _("platform.cta.title") }}
|
||||
{{ _("cms.platform.cta.title") }}
|
||||
</h2>
|
||||
<p class="text-xl text-indigo-100 mb-10">
|
||||
{{ _("platform.cta.subtitle", trial_days=trial_days) }}
|
||||
{{ _("cms.platform.cta.subtitle", trial_days=trial_days) }}
|
||||
</p>
|
||||
<a href="/signup"
|
||||
class="inline-flex items-center px-10 py-4 bg-white text-indigo-600 font-bold rounded-xl shadow-lg hover:shadow-xl transition-all hover:scale-105">
|
||||
{{ _("platform.cta.button") }}
|
||||
{{ _("cms.platform.cta.button") }}
|
||||
<svg class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/>
|
||||
</svg>
|
||||
|
||||
Reference in New Issue
Block a user