almost all ready

This commit is contained in:
2026-05-14 01:48:15 +02:00
parent 1b53c1c199
commit 2e1bec0e8b
29 changed files with 5442 additions and 642 deletions
+218 -28
View File
@@ -7,9 +7,9 @@ import (
)
templ ProductPage(data viewmodel.ProductPageData, cssPath string, jsPath string) {
@Layout(data.Product.Name, cssPath, jsPath, data.Menu, data.Locale) {
@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 max-w-6xl flex-col gap-12 px-6 py-10 lg:px-8">
<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">
@@ -24,55 +24,245 @@ templ ProductPage(data viewmodel.ProductPageData, cssPath string, jsPath string)
</div>
</header>
<section class="grid gap-8 lg:grid-cols-[1.1fr_0.9fr]">
<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>
<p class="mt-6 max-w-2xl text-lg leading-8 text-stone-300">{ data.Product.ShortDescription }</p>
<div class="prose prose-invert mt-8 max-w-none text-stone-300">
@templ.Raw(data.Product.Description)
</div>
</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">{ fmt.Sprintf("%.2f", data.Product.Price) }</p>
<p class="mt-2 text-sm text-stone-300">Live data loaded from PrestaShop storage, rendered by Go.</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={ data.ShopBaseURL + "/cart" }>
<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 in PrestaShop
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.Session != nil {
<section class="rounded-3xl border border-stone-800 bg-stone-950/80 p-8">
<p class="text-xs uppercase tracking-[0.28em] text-stone-500">Go Cookie Debug</p>
<div class="mt-6 grid gap-6 lg:grid-cols-2">
<div>
<p class="text-sm font-semibold text-stone-200">Raw Cookie</p>
<pre class="mt-3 overflow-x-auto rounded-2xl bg-black/30 p-4 text-xs leading-6 text-stone-300">{ data.Session.RawCookie }</pre>
</div>
<div>
<p class="text-sm font-semibold text-stone-200">Decoded Values</p>
<pre class="mt-3 overflow-x-auto rounded-2xl bg-black/30 p-4 text-xs leading-6 text-stone-300">
for _, line := range sessionCookieLines(data.Session) {
{ line }
{"\n"}
}
</pre>
</div>
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>
}