Compare commits
5 Commits
29b2170448
...
2b8dc84584
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b8dc84584 | |||
| f6e224fb24 | |||
| 06a44e55e7 | |||
| caf1361291 | |||
| bdb613581c |
3
Makefile
3
Makefile
@@ -178,7 +178,8 @@ db-reset:
|
||||
@echo "Creating default CMS content pages..."
|
||||
$(PYTHON) scripts/seed/create_default_content_pages.py
|
||||
@echo "Seeding email templates..."
|
||||
$(PYTHON) scripts/seed/seed_email_templates.py
|
||||
$(PYTHON) scripts/seed/seed_email_templates_core.py
|
||||
$(PYTHON) scripts/seed/seed_email_templates_loyalty.py
|
||||
@echo "Seeding demo data..."
|
||||
ifeq ($(DETECTED_OS),Windows)
|
||||
@set SEED_MODE=reset&& set FORCE_RESET=true&& $(PYTHON) scripts/seed/seed_demo.py
|
||||
|
||||
@@ -381,6 +381,8 @@ def get_merchant_card(
|
||||
enrolled_at_store_id=card.enrolled_at_store_id,
|
||||
customer_name=customer.full_name if customer else None,
|
||||
customer_email=customer.email if customer else None,
|
||||
customer_phone=customer.phone if customer else None,
|
||||
customer_birthday=customer.birth_date if customer else None,
|
||||
merchant_name=card.merchant.name if card.merchant else None,
|
||||
qr_code_data=card.qr_code_data or card.card_number,
|
||||
program_name=program.display_name,
|
||||
|
||||
@@ -228,6 +228,8 @@ def get_card_detail(
|
||||
enrolled_at_store_id=card.enrolled_at_store_id,
|
||||
customer_name=customer.full_name if customer else None,
|
||||
customer_email=customer.email if customer else None,
|
||||
customer_phone=customer.phone if customer else None,
|
||||
customer_birthday=customer.birth_date if customer else None,
|
||||
merchant_name=card.merchant.name if card.merchant else None,
|
||||
qr_code_data=card.qr_code_data or card.card_number,
|
||||
program_name=program.display_name,
|
||||
|
||||
@@ -656,6 +656,8 @@ def get_card_detail(
|
||||
enrolled_at_store_name=enrolled_store_name,
|
||||
customer_name=customer.full_name if customer else None,
|
||||
customer_email=customer.email if customer else None,
|
||||
customer_phone=customer.phone if customer else None,
|
||||
customer_birthday=customer.birth_date if customer else None,
|
||||
merchant_name=card.merchant.name if card.merchant else None,
|
||||
qr_code_data=card.qr_code_data or card.card_number,
|
||||
program_name=program.display_name,
|
||||
|
||||
@@ -97,6 +97,8 @@ class CardDetailResponse(CardResponse):
|
||||
# Customer info
|
||||
customer_name: str | None = None
|
||||
customer_email: str | None = None
|
||||
customer_phone: str | None = None
|
||||
customer_birthday: date | None = None
|
||||
|
||||
# Merchant info
|
||||
merchant_name: str | None = None
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""add description_translations to stores
|
||||
|
||||
Revision ID: tenancy_005
|
||||
Revises: tenancy_004
|
||||
Create Date: 2026-05-16
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "tenancy_005"
|
||||
down_revision = "tenancy_004"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"stores",
|
||||
sa.Column(
|
||||
"description_translations",
|
||||
sa.JSON(),
|
||||
nullable=True,
|
||||
comment="Language-keyed description dict for multi-language support",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("stores", "description_translations")
|
||||
@@ -56,6 +56,11 @@ class Store(Base, TimestampMixin, SoftDeleteMixin):
|
||||
String, nullable=False
|
||||
) # Non-nullable name column for the store (brand name)
|
||||
description = Column(Text) # Optional text description column for the store
|
||||
description_translations = Column(
|
||||
JSON,
|
||||
nullable=True,
|
||||
comment="Language-keyed description dict for multi-language support",
|
||||
)
|
||||
|
||||
# Letzshop URLs - multi-language support (brand-specific marketplace feeds)
|
||||
letzshop_csv_url_fr = Column(String) # URL for French CSV in Letzshop
|
||||
@@ -320,6 +325,16 @@ class Store(Base, TimestampMixin, SoftDeleteMixin):
|
||||
"logo"
|
||||
) # Return None or the logo URL if found
|
||||
|
||||
def get_translated_description(self, lang: str, default_lang: str = "fr") -> str | None:
|
||||
"""Get description in the given language, falling back to default_lang then self.description."""
|
||||
if self.description_translations:
|
||||
return (
|
||||
self.description_translations.get(lang)
|
||||
or self.description_translations.get(default_lang)
|
||||
or self.description
|
||||
)
|
||||
return self.description
|
||||
|
||||
# ========================================================================
|
||||
# Domain Helper Methods
|
||||
# ========================================================================
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{# app/templates/storefront/base.html #}
|
||||
{# Base template for store shop frontend with theme support #}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" x-data="{% block alpine_data %}storefrontLayoutData(){% endblock %}" x-bind:class="{ 'dark': dark }">
|
||||
<html lang="{{ current_language|default('en') }}" x-data="{% block alpine_data %}storefrontLayoutData(){% endblock %}" x-bind:class="{ 'dark': dark }">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -13,7 +13,7 @@
|
||||
</title>
|
||||
|
||||
{# SEO Meta Tags #}
|
||||
<meta name="description" content="{% block meta_description %}{{ store.description or 'Shop at ' + store.name }}{% endblock %}">
|
||||
<meta name="description" content="{% block meta_description %}{{ store.get_translated_description(current_language) or 'Shop at ' + store.name }}{% endblock %}">
|
||||
|
||||
{# Favicon - store-specific or default #}
|
||||
{% if theme.branding.favicon %}
|
||||
@@ -89,7 +89,7 @@
|
||||
{# Navigation — Home is always shown, module items are dynamic #}
|
||||
<nav class="hidden md:flex space-x-8">
|
||||
<a href="{{ base_url }}" class="text-gray-700 dark:text-gray-300 hover:text-primary">
|
||||
Home
|
||||
{{ _('nav.home') }}
|
||||
</a>
|
||||
{% for item in storefront_nav.get('nav', []) %}
|
||||
<a href="{{ base_url }}{{ item.route }}" class="text-gray-700 dark:text-gray-300 hover:text-primary">
|
||||
@@ -244,7 +244,7 @@
|
||||
<nav class="max-w-7xl mx-auto px-4 py-4 space-y-1">
|
||||
<a href="{{ base_url }}" @click="closeMobileMenu()"
|
||||
class="block px-3 py-2 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 font-medium">
|
||||
Home
|
||||
{{ _('nav.home') }}
|
||||
</a>
|
||||
{% for item in storefront_nav.get('nav', []) %}
|
||||
<a href="{{ base_url }}{{ item.route }}" @click="closeMobileMenu()"
|
||||
@@ -279,7 +279,7 @@
|
||||
{{ store.name }}
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
{{ store.description }}
|
||||
{{ store.get_translated_description(current_language) }}
|
||||
</p>
|
||||
|
||||
{# Social Links from theme #}
|
||||
|
||||
@@ -5,14 +5,20 @@ from sqlalchemy import Column, DateTime, ForeignKey, Integer
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
def _utc_now() -> datetime:
|
||||
return datetime.now(UTC)
|
||||
|
||||
|
||||
class TimestampMixin:
|
||||
"""Mixin to add created_at and updated_at timestamps to models"""
|
||||
|
||||
created_at = Column(DateTime, default=datetime.now(UTC), nullable=False)
|
||||
# Pass the callable, not its result. Otherwise the default is evaluated
|
||||
# once at module import and every row gets the same timestamp.
|
||||
created_at = Column(DateTime, default=_utc_now, nullable=False)
|
||||
updated_at = Column(
|
||||
DateTime,
|
||||
default=datetime.now(UTC),
|
||||
onupdate=datetime.now(UTC),
|
||||
default=_utc_now,
|
||||
onupdate=_utc_now,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -2979,7 +2979,6 @@ def _create_page(
|
||||
template=page_data.get("template", "default"),
|
||||
sections=sections,
|
||||
meta_description=page_data.get("meta_description", ""),
|
||||
meta_keywords=page_data.get("meta_keywords", ""),
|
||||
is_platform_page=is_platform_page,
|
||||
is_published=True,
|
||||
published_at=datetime.now(UTC),
|
||||
|
||||
@@ -207,6 +207,12 @@ DEMO_STORES = [
|
||||
"name": "Fashion Hub",
|
||||
"subdomain": "fashionhub",
|
||||
"description": "Trendy clothing and accessories",
|
||||
"description_translations": {
|
||||
"en": "Trendy clothing and accessories",
|
||||
"fr": "Vêtements et accessoires tendance",
|
||||
"de": "Trendige Kleidung und Accessoires",
|
||||
"lb": "Trendeg Kleeder a Accessoiren",
|
||||
},
|
||||
"theme_preset": "vibrant",
|
||||
"custom_domain": "fashionhub.store",
|
||||
"custom_domain_platform": "loyalty", # Link domain to Loyalty platform
|
||||
@@ -217,6 +223,12 @@ DEMO_STORES = [
|
||||
"name": "Fashion Outlet",
|
||||
"subdomain": "fashionoutlet",
|
||||
"description": "Discounted designer fashion and seasonal clearance",
|
||||
"description_translations": {
|
||||
"en": "Discounted designer fashion and seasonal clearance",
|
||||
"fr": "Mode de créateurs à prix réduit et soldes de saison",
|
||||
"de": "Reduzierte Designermode und saisonale Restposten",
|
||||
"lb": "Reduzéiert Designermode a Saisonsoldes",
|
||||
},
|
||||
"theme_preset": "vibrant",
|
||||
"custom_domain": None,
|
||||
},
|
||||
@@ -932,6 +944,7 @@ def create_demo_stores(
|
||||
name=store_data["name"],
|
||||
subdomain=store_data["subdomain"],
|
||||
description=store_data["description"],
|
||||
description_translations=store_data.get("description_translations"),
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
created_at=datetime.now(UTC),
|
||||
|
||||
Reference in New Issue
Block a user