Files
ps_shop/templates/product.templ
T
2026-05-14 01:56:02 +02:00

270 lines
18 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-[radial-gradient(circle_at_top,_rgba(245,158,11,0.28),_transparent_40%),linear-gradient(180deg,#0c0a09,#1c1917)]">
<div class="mx-auto flex w-full max-w-[104rem] flex-col gap-12 px-6 py-10 lg:px-8">
<header class="flex items-center justify-between border-b border-stone-800 pb-6">
<a class="text-sm uppercase tracking-[0.32em] text-amber-300" href={ data.ShopBaseURL }>Prestashop Proxy</a>
<div class="text-right text-sm text-stone-400">
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 lg:grid-cols-[1fr_1fr]">
<div class="rounded-3xl border border-stone-800 bg-stone-900/70 p-8 shadow-2xl shadow-amber-950/20">
<p class="text-xs uppercase tracking-[0.28em] text-stone-500">Product</p>
if data.Product.ImageURL != "" {
<button class="mt-5 block w-full overflow-hidden rounded-[2rem] border border-stone-800 bg-stone-950/60 text-left shadow-[0_24px_60px_rgba(0,0,0,0.28)]" type="button" data-gallery-open>
<img class="block h-80 w-full object-cover md:h-[28rem]" src={ data.Product.ImageURL } alt={ data.Product.Name } data-product-main-image="" data-default-image={ data.Product.ImageURL }/>
</button>
}
if len(data.Product.GalleryImages) > 1 {
<div class="mt-4 rounded-[1.6rem] border border-stone-800 bg-stone-950/55 p-3" data-product-thumb-carousel>
<div class="flex items-center gap-3">
<button class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-full border border-stone-700 bg-stone-950/80 text-lg text-stone-100 transition hover:border-amber-400/40 hover:text-amber-200" type="button" aria-label="Previous thumbnails" data-product-thumb-prev>
</button>
<div class="min-w-0 flex-1 overflow-hidden" data-product-thumb-viewport>
<div class="flex gap-3" data-product-thumb-track>
for i, image := range data.Product.GalleryImages {
if image.ThumbURL != "" && image.URL != "" {
<button class="group/thumb shrink-0 overflow-hidden rounded-[1.1rem] border border-stone-800 bg-stone-950/80 transition hover:border-amber-400/40" type="button" data-product-thumb-index={ fmt.Sprintf("%d", i) } data-product-thumb-large={ image.URL } data-product-thumb-large-fallback={ image.URL } data-product-thumb-alt={ data.Product.Name } data-gallery-open="">
<img class="block h-20 w-24 object-cover transition duration-300 group-hover/thumb:scale-[1.03]" src={ image.ThumbURL } alt={ data.Product.Name }/>
</button>
}
}
</div>
</div>
<button class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-full border border-stone-700 bg-stone-950/80 text-lg text-stone-100 transition hover:border-amber-400/40 hover:text-amber-200" type="button" aria-label="Next thumbnails" data-product-thumb-next>
</button>
</div>
</div>
}
if data.CategoryURL != "" && data.Product.CategoryName != "" {
<a class="mt-4 inline-flex text-sm uppercase tracking-[0.24em] text-amber-300 underline underline-offset-4" href={ data.CategoryURL }>{ data.Product.CategoryName }</a>
}
<h1 class="mt-4 font-serif text-4xl text-stone-50">{ data.Product.Name }</h1>
</div>
<aside class="flex flex-col justify-between rounded-3xl border border-amber-500/30 bg-amber-400/10 p-8">
<div>
<p class="text-xs uppercase tracking-[0.28em] text-amber-200">Price</p>
<p class="mt-4 text-5xl font-semibold text-stone-50" 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-300">Including VAT { fmt.Sprintf("%.0f%%", data.Product.TaxRate) }</p>
<p class="mt-2 text-sm text-stone-300" 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-300">{ conversionRateLabel(data.Product.ConversionRate, data.Product.CurrencyCode) }</p>
</div>
<form class="mt-10 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-[1.5rem] border border-stone-200 bg-stone-50 p-5 text-stone-950 shadow-[0_18px_40px_rgba(0,0,0,0.12)]">
<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="rounded-[1.25rem] border border-stone-200 bg-stone-100 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-900/80 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 rounded-[1.5rem] border border-stone-200 bg-white p-2 shadow-[0_24px_60px_rgba(0,0,0,0.18)]" 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-base text-stone-700">Selected combination will be added directly to the PrestaShop cart.</p>
</div>
}
<input type="hidden" name="qty" value="1"/>
<button class="rounded-full bg-amber-300 px-5 py-3 text-sm font-semibold uppercase tracking-[0.2em] text-stone-950 transition hover:bg-amber-200" type="submit">
Add to cart
</button>
<a class="text-sm text-stone-300 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-3xl border border-stone-800 bg-stone-900/70 p-8 shadow-2xl shadow-amber-950/10">
<div class="flex flex-col gap-2 border-b border-stone-800 pb-5">
<p class="text-xs uppercase tracking-[0.28em] text-amber-300">Summary</p>
<h2 class="font-serif text-3xl text-stone-50">At a glance</h2>
</div>
<p class="mt-8 max-w-none text-lg leading-8 text-stone-300">{ plainTextHTML(data.Product.ShortDescription) }</p>
</section>
}
if data.Product.Description != "" {
<section class="rounded-3xl border border-stone-800 bg-stone-900/70 p-8 shadow-2xl shadow-amber-950/10">
<div class="flex flex-col gap-2 border-b border-stone-800 pb-5">
<p class="text-xs uppercase tracking-[0.28em] text-amber-300">Description</p>
<h2 class="font-serif text-3xl text-stone-50">About this product</h2>
</div>
<div class="prose prose-invert mt-8 max-w-none text-stone-300">
@templ.Raw(data.Product.Description)
</div>
</section>
}
if len(data.Product.Features) > 0 {
<section class="rounded-3xl border border-stone-800 bg-stone-900/70 p-8 shadow-2xl shadow-amber-950/10">
<div class="flex flex-col gap-2 border-b border-stone-800 pb-5">
<p class="text-xs uppercase tracking-[0.28em] text-amber-300">Features</p>
<h2 class="font-serif text-3xl text-stone-50">Product details</h2>
</div>
<div class="mt-6 grid gap-3 md:grid-cols-2">
for _, feature := range data.Product.Features {
<div class="rounded-[1.5rem] border border-stone-800 bg-stone-950/60 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-200">{ plainTextHTML(feature.Value) }</p>
</div>
}
</div>
</section>
}
if len(data.Product.Accessories) > 0 {
<section class="rounded-3xl border border-stone-800 bg-stone-900/70 p-8 shadow-2xl shadow-amber-950/10">
<div class="flex flex-col gap-2 border-b border-stone-800 pb-5">
<p class="text-xs uppercase tracking-[0.28em] text-amber-300">Related products</p>
<h2 class="font-serif text-3xl text-stone-50">Accessories</h2>
</div>
<div class="mt-6 grid gap-5 md:grid-cols-2 xl:grid-cols-3">
for _, product := range data.Product.Accessories {
<article class="group rounded-[1.75rem] border border-stone-800 bg-stone-950/55 p-5 shadow-[0_24px_80px_rgba(0,0,0,0.28)] transition hover:-translate-y-1 hover:border-amber-400/40">
if product.ImageURL != "" {
<a class="mb-5 block overflow-hidden rounded-[1.25rem] border border-stone-800 bg-stone-950/80 shadow-[0_16px_40px_rgba(0,0,0,0.24)]" href={ product.URL }>
<img class="block h-56 w-full object-cover transition duration-500 group-hover:scale-[1.03]" src={ product.ImageURL } alt={ product.Name }/>
</a>
}
<p class="text-xs uppercase tracking-[0.28em] text-amber-300">Accessory</p>
<h3 class="mt-3 text-2xl font-semibold text-stone-50">{ product.Name }</h3>
<p class="mt-4 text-sm leading-7 text-stone-300">{ truncatedPlainTextHTML(product.ShortDescription, 180) }</p>
<div class="mt-8 flex items-center justify-between gap-4">
<div>
<p class="text-2xl font-semibold text-stone-50">{ moneyWithCurrency(product.PriceTaxIncl, product.CurrencySign, product.CurrencyCode) }</p>
<p class="mt-1 text-xs uppercase tracking-[0.2em] text-stone-500">{ taxLabel(product.TaxRate) } · { conversionRateLabel(product.ConversionRate, product.CurrencyCode) }</p>
</div>
<a class="rounded-full border border-amber-400/40 px-4 py-2 text-xs font-semibold uppercase tracking-[0.22em] text-amber-200 transition hover:bg-amber-300 hover:text-stone-950" href={ product.URL }>
View Product
</a>
</div>
</article>
}
</div>
</section>
}
if len(data.Product.GalleryImages) > 0 {
<div class="fixed inset-0 z-[70] hidden bg-black/80 px-4 py-6 backdrop-blur-sm" aria-hidden="true" data-gallery-modal>
<div class="mx-auto flex h-full w-full max-w-[104rem] flex-col rounded-[2rem] border border-stone-800 bg-[linear-gradient(180deg,rgba(28,25,23,0.98),rgba(12,10,9,0.98))] p-4 shadow-[0_32px_120px_rgba(0,0,0,0.55)] md:p-6">
<div class="flex items-center justify-between gap-4 border-b border-stone-800 pb-4">
<div>
<p class="text-xs uppercase tracking-[0.26em] text-amber-300">Gallery</p>
<p class="mt-1 text-lg font-semibold text-stone-50">{ data.Product.Name }</p>
</div>
<button class="rounded-full border border-stone-700 px-4 py-2 text-xs font-semibold uppercase tracking-[0.22em] text-stone-200 transition hover:border-amber-400/40 hover:text-stone-50" type="button" data-gallery-close>
Close
</button>
</div>
<div class="mt-5 flex min-h-0 flex-1 flex-col gap-5 lg:flex-row">
<div class="relative flex min-h-0 flex-1 items-center justify-center overflow-hidden rounded-[1.75rem] border border-stone-800 bg-stone-950/70">
<img class="max-h-full w-full object-contain" src={ data.Product.GalleryImages[0].URL } alt={ data.Product.Name } data-gallery-main/>
if len(data.Product.GalleryImages) > 1 {
<button class="absolute left-4 top-1/2 inline-flex h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full border border-stone-700 bg-stone-950/75 text-lg text-stone-100 transition hover:border-amber-400/40 hover:text-amber-200" type="button" aria-label="Previous image" data-gallery-prev>
</button>
<button class="absolute right-4 top-1/2 inline-flex h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full border border-stone-700 bg-stone-950/75 text-lg text-stone-100 transition hover:border-amber-400/40 hover:text-amber-200" type="button" aria-label="Next image" data-gallery-next>
</button>
}
</div>
<div class="rounded-[1.6rem] border border-stone-800 bg-stone-950/55 p-3 lg:w-44" data-gallery-thumb-carousel>
<div class="flex items-center gap-3 lg:h-full lg:flex-col">
<button class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-full border border-stone-700 bg-stone-950/80 text-lg text-stone-100 transition hover:border-amber-400/40 hover:text-amber-200" type="button" aria-label="Previous gallery thumbnails" data-gallery-thumb-prev>
</button>
<div class="min-w-0 flex-1 overflow-hidden lg:w-full" data-gallery-thumb-viewport>
<div class="flex gap-3 lg:flex-col" data-gallery-thumb-track>
for _, image := range data.Product.GalleryImages {
if image.ThumbURL != "" && image.URL != "" {
<button class="shrink-0 overflow-hidden rounded-[1.1rem] border border-stone-800 bg-stone-950/80 transition hover:border-amber-400/40" type="button" data-gallery-thumb={ image.URL } data-gallery-alt={ data.Product.Name }>
<img class="block h-20 w-24 object-cover lg:h-24 lg:w-full" src={ image.ThumbURL } alt={ data.Product.Name }/>
</button>
}
}
</div>
</div>
<button class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-full border border-stone-700 bg-stone-950/80 text-lg text-stone-100 transition hover:border-amber-400/40 hover:text-amber-200" type="button" aria-label="Next gallery thumbnails" data-gallery-thumb-next>
</button>
</div>
</div>
</div>
</div>
</div>
}
</div>
</main>
}
}