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/80 pl-4 text-sm" } func menuLinkClass(depth int) string { if depth == 0 { return "flex items-center justify-between gap-3 rounded-[1.1rem] border border-stone-200/80 bg-stone-50/90 px-4 py-3 text-[1rem] font-medium text-stone-900 shadow-[0_10px_24px_rgba(20,33,61,0.05)] transition hover:border-amber-300/60 hover:bg-white hover:text-amber-700" } return "inline-flex items-center gap-2 py-1 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 }