13 Commits

20 changed files with 434 additions and 26 deletions

3
.gitignore vendored
View File

@@ -25,4 +25,5 @@ pyvenv.cfg
.coverage .coverage
coverage.xml coverage.xml
htmlcov/ htmlcov/
venv/ venv/
.venv/

View File

@@ -0,0 +1,182 @@
{
"ocyan_plugins": [
"ocyan.plugin.contact_form",
"ocyan.plugin.cookie_jar",
"ocyan.plugin.django",
"ocyan.plugin.newsletter",
"ocyan.plugin.oscar",
"ocyan.plugin.oscar_basket",
"ocyan.plugin.oscar_catalogue",
"ocyan.plugin.oscar_catalogue_dashboard",
"ocyan.plugin.oscar_checkout",
"ocyan.plugin.oscar_elasticsearch",
"ocyan.plugin.oscar_order",
"ocyan.plugin.oscar_partner",
"ocyan.plugin.oscar_shipping",
"ocyan.plugin.oscar_sequential_order_numbers",
"ocyan.plugin.payment_mollie",
"ocyan.plugin.roadrunner_bs5",
"ocyan.plugin.template_engine",
"ocyan.plugin.roadrunner_productchooser",
"ocyan.plugin.carbasa",
"ocyan.plugin.coyote",
"ocyan.plugin.sentry_logging",
"ocyan.plugin.seo",
"oxyan.themes",
"ocyan.plugin.varnish",
"ocyan.plugin.wagtail",
"ocyan.plugin.i18n",
"ocyan.plugin.ai_auto_translate",
"ocyan.plugin.wagtail_blog",
"ocyan.plugin.wagtail_content_page",
"ocyan.plugin.wagtail_forms",
"ocyan.plugin.wagtail_oscar_integration",
"ocyan.plugin.roadrunner_highlight_slider",
"ocyan.plugin.wordspinner"
],
"settings": {
"cookie_jar": {
"analytical": true,
"functional": true,
"google_analytics": "",
"google_tag_manager": "",
"marketing": false,
"social": false,
"trusted": ""
},
"django": {
"description": "",
"domain": "mandelstudio.nl",
"email_from": "webshop@mandelblog.com",
"email_host": "vps.transip.email",
"email_host_password": "CHANGE_ME",
"email_host_user": "noreply@mandelblog.com",
"email_port": "587",
"email_to": "info@mandelstudio.nl",
"email_use_tls": true,
"language_code": "nl",
"name": "mandelstudio",
"username": "administrator"
},
"i18n": {
"languages": [
"nl",
"en"
]
},
"payment_mollie": {
"api_key": "CHANGE_ME",
"ideal": true,
"creditcard": true,
"paypal": true,
"bancontact": true,
"sofort": true,
"banktransfer": false,
"belfius": false,
"bitcoin": false,
"directdebit": false,
"eps": false,
"giftcard": false,
"giropay": false,
"inghomepay": false,
"kbc": false,
"mistercash": false
},
"oscar": {
"allow_anon_checkout": true,
"cancelled_order_status": "cancelled",
"complete_order_status": "complete",
"dashboard_items_per_page": 21,
"default_currency": "EUR",
"delayed_payment_status": "delayed-payment",
"enable_cost_prices": false,
"enable_long_description": true,
"enable_retail_prices": false,
"enable_reviews": true,
"enable_wishlist": true,
"homepage": true,
"initial_order_status": "new",
"moderate_reviews": true,
"order_pipeline": [],
"paid_order_status": "paid",
"product_image_geometry": "x230",
"refund_order_status": "refund",
"shop_base_url": "shop",
"show_tax_everywhere": true,
"tax_rates": [
"high"
],
"use_price_incl_tax": true,
"waiting_for_payment_order_status": "pending-payment"
},
"oscar_catalogue": {
"minimum_quantity_attribute_code": "min_quantity",
"slug_id_separator": "-"
},
"oscar_elasticsearch": {
"facet_bucket_size": 10,
"facets": [],
"filter_available": false,
"price_ranges": "25, 100, 500, 1000",
"query_page_size": 100
},
"oscar_importexport": {
"category_extra_fields": [],
"category_separator": "|",
"product_extra_fields": [],
"stockrecord_extra_fields": []
},
"sentry logging": {
"dsn_secret": "https://309733f5d10b9210a99e269db8b95520:112999435d89a49657fc417fd42dbbec@sentry.mandelblog.com/34"
},
"shipping": {
"enable_charged_shipping": true,
"enable_free_shipping": true,
"enable_weightbased_shipping": true,
"paid_shipping_first": true
},
"themes": {
"theme": "default",
"theme-switcher": false
},
"theme": {
"category_navigation_depth": 1,
"danger_color": "",
"header": "header5",
"info_color": "",
"menu_depth": 2,
"name": "template9",
"primary_color": "#da0627",
"secondary_color": "",
"secondary_text_color": "",
"success_color": "",
"warning_color": "",
"dark_color": "#333333"
},
"wagtail": {
"wagtailuserbar_position": "bottom-right"
},
"wagtail content page": {
"actionbuttons": false,
"add_to_cart": false,
"heading": true,
"html": false,
"image": true,
"paragraph": true,
"table": true
},
"wagtail_blog": {
"items_per_page": 10
},
"wagtail_oscar": {
"sitemap_include_child_products": false
},
"ai_auto_translate": {
"auto_translated_fields": [
"catalogue.product.title",
"catalogue.product.description"
]
}
}
}

View File

@@ -21,6 +21,7 @@ BASE_DIR = str(BASE_PATH)
setup_search_paths("/etc/ocyan/", str(_project_app_path)) setup_search_paths("/etc/ocyan/", str(_project_app_path))
from ocyan.main.settings import * # pylint:disable=W0401,W0614 from ocyan.main.settings import * # pylint:disable=W0401,W0614
from ocyan.core.fender import config as ocyan_config
INSTALLED_APPS = [ INSTALLED_APPS = [
"mandelblog_content_guard.apps.MandelblogContentGuardConfig", "mandelblog_content_guard.apps.MandelblogContentGuardConfig",
@@ -60,6 +61,21 @@ _ensure_required_app(
"ocyan.plugin.coyote", "ocyan.plugin.coyote",
) )
def _ensure_installed_app(app_label: str, *, before: str | None = None) -> None:
"""Ensure an app is present in INSTALLED_APPS with optional ordering."""
if app_label in INSTALLED_APPS:
INSTALLED_APPS.remove(app_label)
if before and before in INSTALLED_APPS:
INSTALLED_APPS.insert(INSTALLED_APPS.index(before), app_label)
else:
INSTALLED_APPS.append(app_label)
# Prefer Carbasa's webshop templates whenever this project runs as a webshop.
# Ensures the full Carbasa webshop header (search, user bar, cart, megamenu).
if ocyan_config.is_webshop and importlib.util.find_spec("ocyan.plugin.carbasa.webshop"):
_ensure_installed_app("ocyan.plugin.carbasa.webshop", before="ocyan.plugin.carbasa")
# Keep Carbasa/Coyote defaults stable even when plugin settings are not # Keep Carbasa/Coyote defaults stable even when plugin settings are not
# injected early enough during startup on this deployment. # injected early enough during startup on this deployment.
OXYAN_HEADER_OPTIONS = globals().get( OXYAN_HEADER_OPTIONS = globals().get(

View File

@@ -1 +1 @@
{% include "carbasa/headers/header.html" %} {% include "oxyan/headers/mega.html" %}

View File

@@ -3,4 +3,4 @@ Project-level header override:
force engine pages to render the Carbasa header instead of force engine pages to render the Carbasa header instead of
the template_engine fallback header. the template_engine fallback header.
{% endcomment %} {% endcomment %}
{% include "carbasa/headers/header.html" %} {% include "oxyan/headers/mega.html" %}

View File

@@ -1 +1 @@
{% include "carbasa/headers/header.html" %} {% include "oxyan/headers/mega.html" %}

View File

@@ -1 +1 @@
{% include "carbasa/headers/header.html" %} {% include "oxyan/headers/mega.html" %}

View File

@@ -11,6 +11,12 @@
{% block title %}{% firstof page.seo_title self.seo_title page.title self.title shop_name %}{% endblock %} {% block title %}{% firstof page.seo_title self.seo_title page.title self.title shop_name %}{% endblock %}
{% block description %}{% firstof page.search_description self.search_description "" %}{% endblock %} {% block description %}{% firstof page.search_description self.search_description "" %}{% endblock %}
{% block base_css %}
{{ block.super }}
{# Ensure Carbasa webshop styling is present so responsive header/footer render correctly. #}
<link rel="stylesheet" type="text/x-scss" href="{% static 'carbasa/webshop_base.scss' %}">
{% endblock %}
{% block extrahead %} {% block extrahead %}
{% if cookie_jar.settings.google_tag_manager and cookie_jar.functional.is_allowed %} {% if cookie_jar.settings.google_tag_manager and cookie_jar.functional.is_allowed %}
<link rel="preconnect" href="https://www.googletagmanager.com"/> <link rel="preconnect" href="https://www.googletagmanager.com"/>
@@ -19,6 +25,69 @@
<link rel="preconnect" href="https://www.google-analytics.com/"> <link rel="preconnect" href="https://www.google-analytics.com/">
{% endif %} {% endif %}
{{ block.super }} {{ block.super }}
<style>
header .language-dropdown .dropdown-toggle::after { display: none; }
header .language-dropdown .dropdown-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 1px;
color: #fff;
line-height: 1;
transition: transform 120ms ease, box-shadow 120ms ease, background-color 120ms ease;
}
header .language-dropdown .dropdown-toggle:hover,
header .language-dropdown .dropdown-toggle:focus-visible {
transform: translateY(-1px);
box-shadow: 0 10px 22px rgba(15, 23, 42, .18);
}
header .language-dropdown .dropdown-toggle .language-icon,
header .language-dropdown .dropdown-toggle .language-chevron {
display: block;
}
header .language-dropdown .dropdown-toggle .language-icon {
width: 18px;
height: 18px;
}
header .language-dropdown .dropdown-toggle .language-chevron {
width: 10px;
height: 10px;
opacity: .9;
transition: transform 120ms ease, opacity 120ms ease;
}
header .language-dropdown .dropdown-toggle.show .language-chevron {
transform: rotate(180deg);
opacity: 1;
}
header .language-dropdown .dropdown-menu {
min-width: 15rem;
padding: .5rem;
border-radius: 0.9rem;
border: 1px solid rgba(15, 23, 42, .08);
box-shadow: 0 16px 44px rgba(15, 23, 42, .18);
}
header .language-dropdown .dropdown-menu .dropdown-item {
border-radius: .65rem;
padding: .55rem .7rem;
font-weight: 600;
color: #0f172a;
transition: background-color 120ms ease, color 120ms ease;
}
header .language-dropdown .dropdown-menu .dropdown-item:hover,
header .language-dropdown .dropdown-menu .dropdown-item:focus-visible {
background: rgba(2, 132, 199, .10);
color: #0b5aa3;
}
header .language-dropdown .dropdown-menu svg {
width: 1.35rem;
height: auto;
border-radius: .2rem;
box-shadow: 0 1px 0 rgba(15, 23, 42, .06);
flex: 0 0 auto;
}
</style>
{% if cookie_jar.needs_approval %} {% if cookie_jar.needs_approval %}
<link rel="stylesheet" type="text/css" href="{% static 'cookie_jar/css/cookie_jar.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'cookie_jar/css/cookie_jar.css' %}">
{% endif %} {% endif %}
@@ -33,7 +102,7 @@
{% endif %} {% endif %}
{% block navbar %} {% block navbar %}
{% include "carbasa/headers/header.html" %} {% include "oxyan/headers/mega.html" %}
{% endblock %} {% endblock %}
{% block content_wrapper %} {% block content_wrapper %}
@@ -78,6 +147,8 @@ oxyan.initImageZoom()
{% block cdn_scripts %} {% block cdn_scripts %}
{{ block.super }} {{ block.super }}
<script type="text/javascript" src="{% static 'carbasa/js/carbasa.js' %}"></script>
{% include "partials/search_modal.html" %}
{% ocyanjson "wagtail" "wagtailuserbar_position" as position %} {% ocyanjson "wagtail" "wagtailuserbar_position" as position %}
{% if position %} {% if position %}
{% wagtailuserbar position %} {% wagtailuserbar position %}

View File

@@ -1,26 +1,62 @@
{% load i18n mandelstudio_i18n %} {% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
<div class="header-right">
<form action="{% url 'set_language' %}" method="post" class="ms-lang-switcher me-2" aria-label="Language switcher">
{% csrf_token %}
<input name="next" type="hidden" value="{{ language_neutral_url_path|default:request.path|language_neutral_path }}">
<label for="header-language-switcher" class="visually-hidden">{% trans "Language" %}</label>
<select id="header-language-switcher" name="language" class="form-select form-select-sm" onchange="this.form.submit()">
<option value="nl" {% if LANGUAGE_CODE == 'nl' %}selected{% endif %}>NL</option>
<option value="en" {% if LANGUAGE_CODE == 'en' %}selected{% endif %}>EN</option>
<option value="de" {% if LANGUAGE_CODE == 'de' %}selected{% endif %}>DE</option>
<option value="fr" {% if LANGUAGE_CODE == 'fr' %}selected{% endif %}>FR</option>
<option value="es" {% if LANGUAGE_CODE == 'es' %}selected{% endif %}>ES</option>
<option value="it" {% if LANGUAGE_CODE == 'it' %}selected{% endif %}>IT</option>
<option value="pt" {% if LANGUAGE_CODE == 'pt' %}selected{% endif %}>PT</option>
<option value="ru" {% if LANGUAGE_CODE == 'ru' %}selected{% endif %}>RU</option>
</select>
</form>
<a tabindex="0" aria-label="Open Search" role="search" class="search-toggler user-button menu-circle"> <div class="header-right">
{% get_current_language as current_language %}
{% get_available_languages as available_languages %}
{% get_language_info_list for available_languages as languages %}
<div class="dropdown language-dropdown me-2">
<button
type="button"
class="dropdown-toggle user-button menu-circle"
id="header-language-switcher"
data-bs-toggle="dropdown"
aria-expanded="false"
aria-label="{% trans 'Language switcher' %}"
>
<svg class="language-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false">
<g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="9" />
<path d="M3 12h18" />
<path d="M12 3c3 3.5 3 14.5 0 18" />
<path d="M12 3c-3 3.5-3 14.5 0 18" />
</g>
</svg>
<svg class="language-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="10" height="10" aria-hidden="true" focusable="false">
<path d="M5 7.5 10 12.5 15 7.5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="header-language-switcher">
<form action="{% url 'set_language' %}" method="post" class="language_form">
{% csrf_token %}
<input name="next" type="hidden" value="{{ request.path }}"/>
{% for language in languages %}
<li>
<button class="dropdown-item d-flex align-items-center gap-2" type="submit" value="{{ language.code }}" name="language">
{% include "oxyan/partials/flags/"|add:language.code|add:".svg" %}
<span>{{ language.name_local|title }}</span>
</button>
</li>
{% endfor %}
</form>
</ul>
</div>
<a
tabindex="0"
aria-label="{% trans 'Open Search' %}"
role="button"
class="user-button menu-circle"
data-bs-toggle="modal"
data-bs-target="#siteSearchModal"
>
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
</a> </a>
<a href="{% url 'customer:summary' %}" aria-label="{% trans 'Customer summary' %}" class="user-button menu-circle"><i class="fa fa-user-solid"></i></a>
<a href="{% url 'customer:summary' %}" aria-label="{% trans 'Customer summary' %}" class="user-button menu-circle">
<i class="fa fa-user-solid"></i>
</a>
{% include "oxyan/headers/partials/mini_basket.html" %} {% include "oxyan/headers/partials/mini_basket.html" %}
</div> </div>

View File

@@ -0,0 +1,16 @@
{% load i18n ocyanjson %}
{# Project-level override: ensure Carbasa basket dropdown UI is used even when other themes provide a fallback. #}
<div class="dropdown basket-dropdown">
<button class="dropdown-toggle nav-link menu-circle" data-bs-toggle="dropdown" aria-label="{% trans 'Basket button' %}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M253.3 35.1c6.1-11.8 1.5-26.3-10.2-32.4s-26.3-1.5-32.4 10.2L117.6 192 32 192c-17.7 0-32 14.3-32 32s14.3 32 32 32L83.9 463.5C91 492 116.6 512 146 512L430 512c29.4 0 55-20 62.1-48.5L544 256c17.7 0 32-14.3 32-32s-14.3-32-32-32l-85.6 0L365.3 12.9C359.2 1.2 344.7-3.4 332.9 2.7s-16.3 20.6-10.2 32.4L404.3 192l-232.6 0L253.3 35.1zM192 304l0 96c0 8.8-7.2 16-16 16s-16-7.2-16-16l0-96c0-8.8 7.2-16 16-16s16 7.2 16 16zm96-16c8.8 0 16 7.2 16 16l0 96c0 8.8-7.2 16-16 16s-16-7.2-16-16l0-96c0-8.8 7.2-16 16-16zm128 16l0 96c0 8.8-7.2 16-16 16s-16-7.2-16-16l0-96c0-8.8 7.2-16 16-16s16 7.2 16 16z"/></svg>
{% if request.basket.num_items %}<span class="icon-label">{{ request.basket.num_items }}</span>{% endif %}
</button>
<div class="dropdown-menu dropdown-menu-end">
<span class="overlay"></span>
{% include "oxyan/headers/partials/mini_in_basket.html" %}
</div>
</div>

View File

@@ -0,0 +1 @@
{# Project override: use a Bootstrap modal popup search instead of the Carbasa inline search-wrapper dropdown. #}

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 3" width="18" height="12" aria-hidden="true" focusable="false">
<rect width="5" height="3" fill="#FFCE00"/>
<rect width="5" height="2" y="0" fill="#DD0000"/>
<rect width="5" height="1" y="0" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 274 B

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 30" width="18" height="12" aria-hidden="true" focusable="false">
<clipPath id="t">
<path d="M0 0v30h60V0z"/>
</clipPath>
<path d="M0 0v30h60V0z" fill="#012169"/>
<path d="M0 0l60 30m0-30L0 30" stroke="#FFF" stroke-width="6"/>
<path d="M0 0l60 30m0-30L0 30" clip-path="url(#t)" stroke="#C8102E" stroke-width="4"/>
<path d="M30 0v30M0 15h60" stroke="#FFF" stroke-width="10"/>
<path d="M30 0v30M0 15h60" stroke="#C8102E" stroke-width="6"/>
</svg>

After

Width:  |  Height:  |  Size: 519 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2" width="18" height="12" aria-hidden="true" focusable="false">
<rect width="3" height="2" fill="#AA151B"/>
<rect width="3" height="1" y="0.5" fill="#F1BF00"/>
</svg>

After

Width:  |  Height:  |  Size: 227 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2" width="18" height="12" aria-hidden="true" focusable="false">
<rect width="1" height="2" x="0" fill="#0055A4"/>
<rect width="1" height="2" x="1" fill="#FFF"/>
<rect width="1" height="2" x="2" fill="#EF4135"/>
</svg>

After

Width:  |  Height:  |  Size: 280 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2" width="18" height="12" aria-hidden="true" focusable="false">
<rect width="1" height="2" x="0" fill="#009246"/>
<rect width="1" height="2" x="1" fill="#FFF"/>
<rect width="1" height="2" x="2" fill="#CE2B37"/>
</svg>

After

Width:  |  Height:  |  Size: 280 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 9 6" width="18" height="12" aria-hidden="true" focusable="false">
<path fill="#21468B" d="M0 0h9v6H0z"/>
<path fill="#FFF" d="M0 0h9v4H0z"/>
<path fill="#AE1C28" d="M0 0h9v2H0z"/>
</svg>

After

Width:  |  Height:  |  Size: 247 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2" width="18" height="12" aria-hidden="true" focusable="false">
<rect width="3" height="2" fill="#D01C1F"/>
<rect width="1.2" height="2" x="0" fill="#006600"/>
</svg>

After

Width:  |  Height:  |  Size: 227 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2" width="18" height="12" aria-hidden="true" focusable="false">
<rect width="3" height="2" fill="#D52B1E"/>
<rect width="3" height="1.3333" y="0" fill="#0039A6"/>
<rect width="3" height="0.6667" y="0" fill="#FFF"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

View File

@@ -0,0 +1,34 @@
{% load i18n %}
<div class="modal fade" id="siteSearchModal" tabindex="-1" aria-labelledby="siteSearchModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header border-0 pb-0">
<h2 class="h4 modal-title" id="siteSearchModalLabel">{% trans "Search" %}</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans "Close" %}"></button>
</div>
<div class="modal-body pt-2">
<form method="get" rel="search" action="{% url 'search:search' %}" class="search_form" id="search_form">
<div class="search-input-wrapper">
<input type="search" name="q" placeholder="{% trans 'Search the whole site' %}" class="form-control form-control-lg" autocomplete="off" required="" id="id_q" title="{% trans 'Search' %}">
<button class="btn btn-primary btn-lg mt-3 w-100" type="submit">
{% trans "Search" %}
</button>
</div>
</form>
<p class="text-muted mt-3 mb-0">
{% trans "Tip: start typing to see suggestions." %}
</p>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('shown.bs.modal', function (event) {
if (event.target && event.target.id === 'siteSearchModal') {
const input = event.target.querySelector('#id_q');
if (input) input.focus();
}
});
</script>