Files
ps_shop/templates/layout_helpers.go
T
2026-05-14 01:48:15 +02:00

364 lines
9.2 KiB
Go

package templates
import (
"fmt"
"html"
"regexp"
"strconv"
"strings"
pscart "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cart"
pscatalog "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/catalog"
"git.ma-al.com/goc_marek/ps_shop/internal/viewmodel"
)
var htmlTagPattern = regexp.MustCompile(`<[^>]+>`)
func menuListClass(depth int) string {
if depth == 0 {
return "flex min-w-0 flex-col gap-2 text-sm"
}
return "mt-2 space-y-2 border-l border-stone-200 pl-4 text-sm"
}
func menuLinkClass(depth int) string {
if depth == 0 {
return "inline-flex items-center gap-2 px-2 py-2 text-[1.02rem] font-medium text-stone-900 transition hover:text-amber-600"
}
return "inline-flex items-center gap-2 text-stone-700 transition hover:text-amber-600"
}
func menuItemClass(depth int, hasChildren bool) string {
if depth == 0 && hasChildren {
return "min-w-0"
}
return "min-w-0"
}
func desktopNavItemClass(hasChildren bool) string {
if hasChildren {
return "desktop-nav__entry desktop-nav__entry--has-children"
}
return "desktop-nav__entry"
}
func desktopNavLinkClass() string {
return "desktop-nav__link"
}
func hasHeaderLocale(locale pscatalog.HeaderLocaleData) bool {
return len(locale.Languages) > 0 || len(locale.Countries) > 0
}
func pageLanguage(locale pscatalog.HeaderLocaleData) string {
code := strings.TrimSpace(locale.CurrentLanguage.Code)
if code == "" {
return "en"
}
return strings.ToLower(code)
}
func localeOptionClass(option pscatalog.LocaleOption, current pscatalog.LocaleOption) string {
base := "locale-picker__item"
if option.ID != 0 && option.ID == current.ID {
return base + " locale-picker__item--active"
}
if option.Code != "" && option.Code == current.Code {
return base + " locale-picker__item--active"
}
return base
}
func productInitial(name string) string {
name = strings.TrimSpace(name)
if name == "" {
return "P"
}
return strings.ToUpper(name[:1])
}
func menuPanelID(id int64) string {
if id <= 0 {
return "mega-menu-panel"
}
return "mega-menu-panel-" + strconv.FormatInt(id, 10)
}
func moneyWithCurrency(amount float64, sign, code string) string {
formatted := fmt.Sprintf("%.2f", amount)
sign = strings.TrimSpace(sign)
switch {
case sign != "":
return formatted + " " + sign
default:
return formatted
}
}
func taxLabel(rate float64) string {
return fmt.Sprintf("Tax %.2f%%", rate)
}
func conversionRateLabel(rate float64, code string) string {
code = strings.TrimSpace(code)
if code == "" {
return fmt.Sprintf("Rate %.6f", rate)
}
return fmt.Sprintf("Rate %.6f %s", rate, code)
}
func localizedCartPath(locale pscatalog.HeaderLocaleData) string {
code := strings.ToLower(strings.TrimSpace(locale.CurrentLanguage.Code))
if code == "" {
return "/cart"
}
return "/" + code + "/cart"
}
func plainTextHTML(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
value = htmlTagPattern.ReplaceAllString(value, " ")
value = html.UnescapeString(value)
return strings.Join(strings.Fields(value), " ")
}
func truncatedPlainTextHTML(value string, maxChars int) string {
value = plainTextHTML(value)
if value == "" || maxChars <= 0 {
return value
}
runes := []rune(value)
if len(runes) <= maxChars {
return value
}
cut := strings.TrimSpace(string(runes[:maxChars]))
if idx := strings.LastIndex(cut, " "); idx >= maxChars/2 {
cut = strings.TrimSpace(cut[:idx])
}
if cut == "" {
return value
}
return cut + "..."
}
func combinationAttributeLabel(publicName, group string) string {
publicName = strings.TrimSpace(publicName)
if publicName != "" {
return publicName
}
return strings.TrimSpace(group)
}
func isHexColor(value string) bool {
value = strings.TrimSpace(value)
if value == "" {
return false
}
if !strings.HasPrefix(value, "#") {
value = "#" + value
}
if len(value) != 7 {
return false
}
for _, r := range value[1:] {
if (r < '0' || r > '9') && (r < 'a' || r > 'f') && (r < 'A' || r > 'F') {
return false
}
}
return true
}
func cartCurrencyCode(data viewmodel.CartPageData) string {
if code := cartCurrencyField(data.Cart.Items, func(item pscart.Item) string { return item.CurrencyCode }); code != "" {
return code
}
return ""
}
func cartCurrencySign(data viewmodel.CartPageData) string {
if sign := cartCurrencyField(data.Cart.Items, func(item pscart.Item) string { return item.CurrencySign }); sign != "" {
return sign
}
return ""
}
func cartCurrencyField(items []pscart.Item, pick func(pscart.Item) string) string {
for _, item := range items {
value := strings.TrimSpace(pick(item))
if value != "" {
return value
}
}
return ""
}
func cartItemAttributeLabel(item pscart.Item) string {
if len(item.Attributes) == 0 {
return ""
}
parts := make([]string, 0, len(item.Attributes))
for _, attribute := range item.Attributes {
group := strings.TrimSpace(attribute.Group)
value := strings.TrimSpace(attribute.Value)
if value == "" {
continue
}
if group != "" {
parts = append(parts, group+": "+value)
continue
}
parts = append(parts, value)
}
return strings.Join(parts, " • ")
}
func layoutCartItems(summary *pscart.Summary) int64 {
if summary == nil || summary.TotalItems <= 0 {
return 0
}
return summary.TotalItems
}
func categoryPageStart(pagination viewmodel.CategoryPagination) int64 {
if pagination.TotalItems <= 0 || pagination.Page <= 0 || pagination.PerPage <= 0 {
return 0
}
return int64((pagination.Page-1)*pagination.PerPage) + 1
}
func categoryPageEnd(pagination viewmodel.CategoryPagination, loaded int) int64 {
if pagination.TotalItems <= 0 || loaded <= 0 {
return 0
}
end := categoryPageStart(pagination) + int64(loaded) - 1
if end > pagination.TotalItems {
return pagination.TotalItems
}
return end
}
type productVariantGroupView struct {
Key string
Label string
GroupType string
Options []productVariantOptionView
}
type productVariantOptionView struct {
Value string
ColorStyle string
CombinationIDs string
Selected bool
}
func productVariantGroups(combinations []pscatalog.ProductCombination, defaultID int64) []productVariantGroupView {
groups := make([]productVariantGroupView, 0)
groupIndex := make(map[string]int)
optionIndex := make(map[string]map[string]int)
for _, combination := range combinations {
for _, attribute := range combination.Attributes {
label := combinationAttributeLabel(attribute.PublicName, attribute.Group)
if label == "" {
continue
}
key := strings.ToLower(strings.TrimSpace(label))
idx, exists := groupIndex[key]
if !exists {
groups = append(groups, productVariantGroupView{
Key: "variant-group-" + strconv.Itoa(len(groups)),
Label: label,
GroupType: normalizedGroupType(attribute.GroupType),
Options: make([]productVariantOptionView, 0),
})
idx = len(groups) - 1
groupIndex[key] = idx
optionIndex[key] = make(map[string]int)
}
optionKey := strings.ToLower(strings.TrimSpace(attribute.Value)) + "|" + normalizedColorStyle(attribute.Color)
optIdx, exists := optionIndex[key][optionKey]
if !exists {
groups[idx].Options = append(groups[idx].Options, productVariantOptionView{
Value: attribute.Value,
ColorStyle: normalizedColorStyle(attribute.Color),
CombinationIDs: strconv.FormatInt(combination.ID, 10),
Selected: combination.ID == defaultID,
})
optionIndex[key][optionKey] = len(groups[idx].Options) - 1
continue
}
option := &groups[idx].Options[optIdx]
option.CombinationIDs += "," + strconv.FormatInt(combination.ID, 10)
if combination.ID == defaultID {
option.Selected = true
}
}
}
return groups
}
func normalizedGroupType(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
switch value {
case "color", "radio", "select":
return value
default:
return "select"
}
}
func normalizedColorStyle(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
if !strings.HasPrefix(value, "#") {
value = "#" + value
}
if !isHexColor(value) {
return ""
}
return value
}
func variantColorOptionClass(selected bool) string {
if selected {
return "inline-flex min-h-10 min-w-10 items-center justify-center border border-stone-900 p-0.5 ring-1 ring-stone-900 transition"
}
return "inline-flex min-h-10 min-w-10 items-center justify-center border border-stone-300 p-0.5 transition hover:border-stone-700"
}
func variantRadioOptionClass(selected bool) string {
if selected {
return "rounded-full border border-stone-900 bg-stone-900 px-4 py-2 text-sm font-medium text-stone-50 transition"
}
return "rounded-full border border-stone-300 bg-white px-4 py-2 text-sm font-medium text-stone-900 transition hover:border-stone-700"
}
func variantSelectOptionClass(selected bool) string {
if selected {
return "w-full rounded-2xl bg-stone-900 px-4 py-3 text-left text-sm font-medium text-stone-50 transition"
}
return "w-full rounded-2xl px-4 py-3 text-left text-sm font-medium text-stone-700 transition hover:bg-stone-100 hover:text-stone-950"
}
func selectedVariantOptionValue(options []productVariantOptionView) string {
for _, option := range options {
if option.Selected {
return option.Value
}
}
if len(options) == 0 {
return ""
}
return options[0].Value
}