Files
ps_shop/templates/product.templ
T
2026-05-16 00:20:34 +02:00

313 lines
19 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package templates
import (
"fmt"
"git.ma-al.com/goc_marek/ps_shop/internal/viewmodel"
)
templ ProductPage(data viewmodel.ProductPageData, cssPath string, jsPath string) {
@Layout(data.Product.Name, cssPath, jsPath, data.Menu, data.Locale, layoutCartItems(data.CartSummary)) {
<main class="min-h-screen bg-[#fdfbf7]">
<div class="site-container flex flex-col gap-8 py-6 sm:py-8 lg:py-10">
<nav class="rounded-sm bg-[#eceae7] px-4 py-3 text-[0.82rem] text-stone-500 sm:px-5">
<div class="flex flex-wrap items-center gap-x-2 gap-y-1">
<a class="transition hover:text-amber-600" href={ data.ShopBaseURL }>9b-plus</a>
if data.CategoryURL != "" && data.Product.CategoryName != "" {
<span>/</span>
<a class="transition hover:text-amber-600" href={ data.CategoryURL }>{ data.Product.CategoryName }</a>
}
<span>/</span>
<span class="text-stone-700">{ data.Product.Name }</span>
</div>
</nav>
<header class="flex flex-col gap-4 border-b border-stone-200 pb-6 lg:flex-row lg:items-end lg:justify-between">
<div>
<p class="text-[0.72rem] font-semibold uppercase tracking-[0.26em] text-stone-400">Product</p>
<h1 class="mt-4 text-3xl font-medium text-stone-800 sm:text-[2.6rem]">{ data.Product.Name }</h1>
if data.Product.CategoryName != "" {
<p class="mt-3 text-sm text-stone-500">{ data.Product.CategoryName }</p>
}
</div>
<div class="text-sm text-stone-500 lg:text-right">
if data.Customer != nil {
<p>{ fmt.Sprintf("%s %s", data.Customer.FirstName, data.Customer.LastName) }</p>
} else {
<p>Guest session</p>
}
if data.CartSummary != nil {
<p>{ fmt.Sprintf("Cart items: %d", data.CartSummary.TotalItems) }</p>
}
</div>
</header>
<section class="grid gap-8 xl:grid-cols-[minmax(0,1.2fr)_25rem]">
<div class="rounded-sm border border-stone-200 bg-white p-4 shadow-[0_18px_42px_rgba(20,33,61,0.06)] sm:p-6 lg:p-8">
if len(data.Product.GalleryImages) > 1 {
<div class="splide" aria-label="Product gallery" data-product-gallery-main>
<div class="splide__track">
<ul class="splide__list">
for i, image := range data.Product.GalleryImages {
if image.URL != "" {
<li class="splide__slide">
<button class="flex min-h-[16rem] w-full items-center justify-center overflow-hidden bg-white text-left sm:min-h-[22rem] lg:min-h-[30rem]" type="button" data-gallery-open="" data-gallery-index={ fmt.Sprintf("%d", i) }>
<img class="max-h-[20rem] w-auto max-w-full object-contain sm:max-h-[28rem] lg:max-h-[34rem]" src={ image.URL } alt={ data.Product.Name } data-product-gallery-image="" data-image-url={ image.URL } data-default-image={ data.Product.ImageURL }/>
</button>
</li>
}
}
</ul>
</div>
</div>
<div class="mt-6 flex items-center gap-3">
<button class="hidden h-11 w-11 shrink-0 items-center justify-center border border-stone-300 bg-white text-lg text-stone-700 transition hover:border-amber-500 hover:text-amber-600 sm:inline-flex" type="button" aria-label="Previous thumbnails" data-product-thumb-prev>
</button>
<div class="min-w-0 flex-1">
<div class="splide" aria-label="Product gallery thumbnails" data-product-gallery-thumbs>
<div class="splide__track">
<ul class="splide__list">
for _, image := range data.Product.GalleryImages {
if image.ThumbURL != "" && image.URL != "" {
<li class="splide__slide border border-stone-200 bg-white">
<img class="block h-16 w-16 object-cover sm:h-20 sm:w-20 lg:h-24 lg:w-24" src={ image.ThumbURL } alt={ data.Product.Name }/>
</li>
}
}
</ul>
</div>
</div>
</div>
<button class="hidden h-11 w-11 shrink-0 items-center justify-center border border-stone-300 bg-white text-lg text-stone-700 transition hover:border-amber-500 hover:text-amber-600 sm:inline-flex" type="button" aria-label="Next thumbnails" data-product-thumb-next>
</button>
</div>
} else if data.Product.ImageURL != "" {
<button class="flex min-h-[16rem] w-full items-center justify-center overflow-hidden bg-white text-left sm:min-h-[22rem] lg:min-h-[30rem]" type="button" data-gallery-open data-gallery-index="0">
<img class="max-h-[20rem] w-auto max-w-full object-contain sm:max-h-[28rem] lg:max-h-[34rem]" src={ data.Product.ImageURL } alt={ data.Product.Name } data-product-main-image="" data-default-image={ data.Product.ImageURL }/>
</button>
}
</div>
<aside class="rounded-sm border border-stone-200 bg-white p-6 shadow-[0_18px_42px_rgba(20,33,61,0.06)] sm:p-8 xl:sticky xl:top-28 xl:self-start">
<div class="border-b border-stone-200 pb-5">
<p class="text-[0.72rem] font-semibold uppercase tracking-[0.22em] text-stone-400">Price</p>
<p class="mt-4 text-4xl font-semibold text-stone-900" data-product-price-gross="" data-default-price-gross={ moneyWithCurrency(data.Product.PriceTaxIncl, data.Product.CurrencySign, data.Product.CurrencyCode) }>{ moneyWithCurrency(data.Product.PriceTaxIncl, data.Product.CurrencySign, data.Product.CurrencyCode) }</p>
<p class="mt-2 text-sm text-stone-500">Including VAT { fmt.Sprintf("%.0f%%", data.Product.TaxRate) }</p>
<p class="mt-2 text-sm text-stone-500" data-product-price-net="" data-default-price-net={ "Net " + moneyWithCurrency(data.Product.Price, data.Product.CurrencySign, data.Product.CurrencyCode) }>Net { moneyWithCurrency(data.Product.Price, data.Product.CurrencySign, data.Product.CurrencyCode) }</p>
<p class="mt-2 text-sm text-stone-500">{ conversionRateLabel(data.Product.ConversionRate, data.Product.CurrencyCode) }</p>
</div>
<form class="mt-6 flex flex-col gap-4" method="post" action={ localizedCartPath(data.Locale) }>
<input type="hidden" name="action" value="add"/>
<input type="hidden" name="id_product" value={ fmt.Sprintf("%d", data.Product.ID) }/>
if len(data.Product.Combinations) > 0 {
<input type="hidden" name="id_product_attribute" value={ fmt.Sprintf("%d", data.Product.DefaultAttribute) } data-variant-combination/>
<div class="rounded-sm border border-stone-200 bg-[#fcfbf8] p-5 text-stone-950">
<div class="hidden" aria-hidden="true">
for _, combination := range data.Product.Combinations {
<span
data-variant-combination-image={ fmt.Sprintf("%d", combination.ID) }
data-image-large={ combination.ImageURL }
data-price-gross={ moneyWithCurrency(combination.PriceTaxIncl, data.Product.CurrencySign, data.Product.CurrencyCode) }
data-price-net={ "Net " + moneyWithCurrency(combination.Price, data.Product.CurrencySign, data.Product.CurrencyCode) }></span>
}
</div>
<div class="border border-stone-200 bg-white px-4 py-3">
<p class="text-[0.72rem] font-medium uppercase tracking-[0.18em] text-stone-500">Current selection</p>
<p class="mt-2 text-base font-medium text-stone-900" data-variant-selection-summary>Choose product options</p>
</div>
<div class="mt-4 space-y-5" data-variant-picker>
for _, group := range productVariantGroups(data.Product.Combinations, data.Product.DefaultAttribute) {
<div class="space-y-3">
<div class="flex items-center justify-between gap-4">
<p class="text-sm font-medium uppercase tracking-[0.16em] text-stone-700">{ group.Label }</p>
<p class="text-sm text-stone-500" data-variant-current={ group.Key }></p>
</div>
if group.GroupType == "select" {
<div class="relative" data-variant-group={ group.Key } data-variant-select="">
<button class="flex w-full items-center justify-between gap-4 border-b border-stone-300 px-0 py-2 text-left text-lg text-stone-900 transition hover:text-stone-700" type="button" data-variant-select-trigger="" aria-expanded="false">
<span data-variant-select-value="">{ selectedVariantOptionValue(group.Options) }</span>
<span class="text-base text-stone-700">▼</span>
</button>
<div class="absolute left-0 right-0 top-full z-20 mt-3 hidden border border-stone-200 bg-white p-2 shadow-[0_24px_60px_rgba(0,0,0,0.12)]" data-variant-select-menu="">
for _, option := range group.Options {
<button class={ variantSelectOptionClass(option.Selected) } type="button" data-variant-option="" data-variant-presentation="select" data-combination-ids={ option.CombinationIDs } data-selected={ fmt.Sprintf("%t", option.Selected) }>
{ option.Value }
</button>
}
</div>
</div>
} else if group.GroupType == "color" {
<div class="mt-1 flex flex-wrap gap-1.5" data-variant-group={ group.Key }>
for _, option := range group.Options {
<button class={ variantColorOptionClass(option.Selected) } type="button" data-variant-option="" data-variant-presentation="color" data-combination-ids={ option.CombinationIDs } aria-label={ option.Value } title={ option.Value } data-selected={ fmt.Sprintf("%t", option.Selected) }>
if option.ColorStyle != "" {
<span class="block h-6 w-8 border border-stone-300" style={ "background-color:" + option.ColorStyle }></span>
} else {
<span class="px-2 text-xs font-medium text-stone-900">{ option.Value }</span>
}
</button>
}
</div>
} else {
<div class="mt-1 flex flex-wrap gap-2" data-variant-group={ group.Key }>
for _, option := range group.Options {
<button class={ variantRadioOptionClass(option.Selected) } type="button" data-variant-option="" data-variant-presentation="radio" data-combination-ids={ option.CombinationIDs } data-selected={ fmt.Sprintf("%t", option.Selected) }>
{ option.Value }
</button>
}
</div>
}
</div>
}
</div>
<p class="mt-5 text-sm leading-7 text-stone-600">Selected combination will be added directly to the PrestaShop cart.</p>
</div>
}
<input type="hidden" name="qty" value="1"/>
<button class="min-h-12 bg-amber-500 px-5 py-3 text-sm font-semibold uppercase tracking-[0.2em] text-white transition hover:bg-amber-600" type="submit">
Add to cart
</button>
<a class="text-sm text-stone-500 underline underline-offset-4" href={ data.ShopBaseURL + "/login" }>Account and login remain on PrestaShop</a>
</form>
</aside>
</section>
if data.Product.ShortDescription != "" {
<section class="rounded-sm border border-stone-200 bg-white p-6 shadow-[0_12px_30px_rgba(20,33,61,0.05)] sm:p-8">
<div class="flex flex-col gap-2 border-b border-stone-200 pb-5">
<p class="text-[0.72rem] font-semibold uppercase tracking-[0.22em] text-stone-400">Summary</p>
<h2 class="text-2xl font-medium text-stone-800">At a glance</h2>
</div>
<p class="mt-6 max-w-none text-lg leading-8 text-stone-600">{ plainTextHTML(data.Product.ShortDescription) }</p>
</section>
}
if data.Product.Description != "" {
<section class="rounded-sm border border-stone-200 bg-white p-6 shadow-[0_12px_30px_rgba(20,33,61,0.05)] sm:p-8">
<div class="flex flex-col gap-2 border-b border-stone-200 pb-5">
<p class="text-[0.72rem] font-semibold uppercase tracking-[0.22em] text-stone-400">Description</p>
<h2 class="text-2xl font-medium text-stone-800">About this product</h2>
</div>
<div class="category-description mt-6 max-w-none text-sm leading-7 text-stone-600">
@templ.Raw(data.Product.Description)
</div>
</section>
}
if len(data.Product.Features) > 0 {
<section class="rounded-sm border border-stone-200 bg-white p-6 shadow-[0_12px_30px_rgba(20,33,61,0.05)] sm:p-8">
<div class="flex flex-col gap-2 border-b border-stone-200 pb-5">
<p class="text-[0.72rem] font-semibold uppercase tracking-[0.22em] text-stone-400">Features</p>
<h2 class="text-2xl font-medium text-stone-800">Product details</h2>
</div>
<div class="mt-6 grid gap-3 md:grid-cols-2">
for _, feature := range data.Product.Features {
<div class="border border-stone-200 bg-[#fcfbf8] px-5 py-4">
<p class="text-[0.7rem] uppercase tracking-[0.24em] text-stone-500">{ feature.Name }</p>
<p class="mt-2 text-sm leading-7 text-stone-700">{ plainTextHTML(feature.Value) }</p>
</div>
}
</div>
</section>
}
if len(data.Product.Accessories) > 0 {
<section class="rounded-sm border border-stone-200 bg-white p-6 shadow-[0_12px_30px_rgba(20,33,61,0.05)] sm:p-8">
<div class="flex flex-col gap-2 border-b border-stone-200 pb-5">
<p class="text-[0.72rem] font-semibold uppercase tracking-[0.22em] text-stone-400">Related products</p>
<h2 class="text-2xl font-medium text-stone-800">Accessories</h2>
</div>
<div class="mt-8 grid gap-x-6 gap-y-10 md:grid-cols-2 xl:grid-cols-3">
for _, product := range data.Product.Accessories {
<article class="group flex h-full flex-col items-center text-center">
<a class="flex h-full w-full flex-col items-center px-3 pb-4 pt-2 transition hover:-translate-y-1" href={ product.URL }>
if product.ImageURL != "" {
<div class="flex h-[16rem] w-full items-center justify-center overflow-hidden bg-white">
<img class="max-h-[12rem] w-auto max-w-[82%] object-contain transition duration-500 group-hover:scale-[1.04]" src={ product.ImageURL } alt={ product.Name }/>
</div>
}
<h3 class="mt-5 text-[1.02rem] font-medium leading-6 text-stone-800">{ product.Name }</h3>
<p class="mt-3 max-w-[17rem] text-[0.92rem] leading-6 text-stone-400">{ truncatedPlainTextHTML(product.ShortDescription, 90) }</p>
<p class="mt-5 text-[1.5rem] font-semibold leading-none text-stone-900">{ moneyWithCurrency(product.PriceTaxIncl, product.CurrencySign, product.CurrencyCode) }</p>
<p class="mt-3 text-[0.72rem] uppercase tracking-[0.18em] text-stone-400">{ conversionRateLabel(product.ConversionRate, product.CurrencyCode) }</p>
</a>
</article>
}
</div>
</section>
}
if len(data.Product.GalleryImages) > 0 {
<div class="fixed inset-0 z-[70] hidden bg-black/55 px-3 py-3 backdrop-blur-sm sm:px-4 sm:py-6" aria-hidden="true" data-gallery-modal>
<div class="mx-auto flex h-full max-h-full w-full max-w-[104rem] flex-col overflow-hidden border border-stone-200 bg-white p-3 shadow-[0_32px_120px_rgba(0,0,0,0.28)] sm:p-4 md:p-6">
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-stone-200 pb-4">
<div>
<p class="text-[0.72rem] font-semibold uppercase tracking-[0.22em] text-stone-400">Gallery</p>
<p class="mt-1 line-clamp-2 text-base font-semibold text-stone-900 sm:text-lg">{ data.Product.Name }</p>
</div>
<button class="border border-stone-300 px-4 py-2 text-xs font-semibold uppercase tracking-[0.22em] text-stone-700 transition hover:border-amber-500 hover:text-amber-600" type="button" data-gallery-close>
Close
</button>
</div>
<div class="mt-4 flex min-h-0 flex-1 flex-col gap-4 lg:mt-5">
<div class="relative flex min-h-[16rem] flex-1 items-center justify-center overflow-hidden border border-stone-200 bg-[#fcfbf8] sm:min-h-[22rem]">
if len(data.Product.GalleryImages) > 1 {
<div class="splide w-full" aria-label="Expanded product gallery" data-gallery-main-splide>
<div class="splide__track">
<ul class="splide__list">
for _, image := range data.Product.GalleryImages {
if image.URL != "" {
<li class="splide__slide">
<div class="flex min-h-[16rem] w-full items-center justify-center sm:min-h-[22rem]">
<img class="max-h-[65vh] w-auto max-w-full object-contain" src={ image.URL } alt={ data.Product.Name } data-gallery-image="" data-image-url={ image.URL }/>
</div>
</li>
}
}
</ul>
</div>
</div>
<button class="absolute left-2 top-1/2 inline-flex h-10 w-10 -translate-y-1/2 items-center justify-center border border-stone-300 bg-white text-lg text-stone-700 transition hover:border-amber-500 hover:text-amber-600 sm:left-4 sm:h-11 sm:w-11" type="button" aria-label="Previous image" data-gallery-prev>
</button>
<button class="absolute right-2 top-1/2 inline-flex h-10 w-10 -translate-y-1/2 items-center justify-center border border-stone-300 bg-white text-lg text-stone-700 transition hover:border-amber-500 hover:text-amber-600 sm:right-4 sm:h-11 sm:w-11" type="button" aria-label="Next image" data-gallery-next>
</button>
} else {
<img class="max-h-[65vh] w-auto max-w-full object-contain" src={ data.Product.GalleryImages[0].URL } alt={ data.Product.Name } data-gallery-main/>
}
</div>
if len(data.Product.GalleryImages) > 1 {
<div class="flex items-center gap-3">
<button class="hidden h-11 w-11 shrink-0 items-center justify-center border border-stone-300 bg-white text-lg text-stone-700 transition hover:border-amber-500 hover:text-amber-600 sm:inline-flex" type="button" aria-label="Previous gallery thumbnails" data-gallery-thumb-prev>
</button>
<div class="min-w-0 flex-1">
<div class="splide" aria-label="Expanded gallery thumbnails" data-gallery-thumb-splide>
<div class="splide__track">
<ul class="splide__list">
for _, image := range data.Product.GalleryImages {
if image.ThumbURL != "" && image.URL != "" {
<li class="splide__slide border border-stone-200 bg-white">
<img class="block h-16 w-16 object-cover sm:h-20 sm:w-20 lg:h-24 lg:w-24" src={ image.ThumbURL } alt={ data.Product.Name }/>
</li>
}
}
</ul>
</div>
</div>
</div>
<button class="hidden h-11 w-11 shrink-0 items-center justify-center border border-stone-300 bg-white text-lg text-stone-700 transition hover:border-amber-500 hover:text-amber-600 sm:inline-flex" type="button" aria-label="Next gallery thumbnails" data-gallery-thumb-next>
</button>
</div>
}
</div>
</div>
</div>
}
</div>
</main>
}
}