313 lines
19 KiB
Plaintext
313 lines
19 KiB
Plaintext
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>
|
||
}
|
||
}
|