diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index 4713bb8..fd06ed6 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -14,19 +14,19 @@ import ( "github.com/labstack/echo/v4" - "prestaproxy/internal/assets" - "prestaproxy/internal/http/handlers" - appmiddleware "prestaproxy/internal/http/middleware" - httpproxy "prestaproxy/internal/http/proxy" - pscart "prestaproxy/internal/prestashop/cart" - pscatalog "prestaproxy/internal/prestashop/catalog" - psconfig "prestaproxy/internal/prestashop/config" - pscookie "prestaproxy/internal/prestashop/cookie" - pscustomer "prestaproxy/internal/prestashop/customer" - psroutes "prestaproxy/internal/prestashop/routes" - pssession "prestaproxy/internal/prestashop/session" - "prestaproxy/internal/render" - "prestaproxy/internal/store" + "git.ma-al.com/goc_marek/ps_shop/internal/assets" + "git.ma-al.com/goc_marek/ps_shop/internal/http/handlers" + appmiddleware "git.ma-al.com/goc_marek/ps_shop/internal/http/middleware" + httpproxy "git.ma-al.com/goc_marek/ps_shop/internal/http/proxy" + pscart "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cart" + pscatalog "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/catalog" + psconfig "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/config" + pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie" + pscustomer "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/customer" + psroutes "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/routes" + pssession "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/session" + "git.ma-al.com/goc_marek/ps_shop/internal/render" + "git.ma-al.com/goc_marek/ps_shop/internal/store" ) func main() { @@ -95,6 +95,7 @@ func run() error { render.New(assetManifest), cfg, productRoute, + categoryRoute, ) proxyHandler, err := httpproxy.New(cfg.PrestaShopProxyTarget) diff --git a/go.mod b/go.mod index 69cd708..1eba17c 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module prestaproxy +module git.ma-al.com/goc_marek/ps_shop go 1.25.0 diff --git a/internal/http/handlers/category.go b/internal/http/handlers/category.go index a33d2ec..ce12b9b 100644 --- a/internal/http/handlers/category.go +++ b/internal/http/handlers/category.go @@ -8,44 +8,43 @@ import ( "github.com/labstack/echo/v4" "gorm.io/gorm" - appmiddleware "prestaproxy/internal/http/middleware" - pscart "prestaproxy/internal/prestashop/cart" - pscatalog "prestaproxy/internal/prestashop/catalog" - psconfig "prestaproxy/internal/prestashop/config" - pscustomer "prestaproxy/internal/prestashop/customer" - psroutes "prestaproxy/internal/prestashop/routes" - "prestaproxy/internal/render" - "prestaproxy/internal/viewmodel" + appmiddleware "git.ma-al.com/goc_marek/ps_shop/internal/http/middleware" + pscart "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cart" + pscatalog "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/catalog" + psconfig "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/config" + pscustomer "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/customer" + psroutes "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/routes" + "git.ma-al.com/goc_marek/ps_shop/internal/render" + "git.ma-al.com/goc_marek/ps_shop/internal/viewmodel" ) const categorySlugContextKey = "category_slug" const categoryIDContextKey = "category_id" type CategoryHandler struct { - catalog *pscatalog.Service - customers *pscustomer.Service - carts *pscart.Service - renderer *render.Engine - config psconfig.Config - products *psroutes.ProductRoute + catalog *pscatalog.Service + customers *pscustomer.Service + carts *pscart.Service + renderer *render.Engine + config psconfig.Config + products *psroutes.ProductRoute + categories *psroutes.CategoryRoute } -func NewCategoryHandler(catalog *pscatalog.Service, customers *pscustomer.Service, carts *pscart.Service, renderer *render.Engine, cfg psconfig.Config, products *psroutes.ProductRoute) *CategoryHandler { +func NewCategoryHandler(catalog *pscatalog.Service, customers *pscustomer.Service, carts *pscart.Service, renderer *render.Engine, cfg psconfig.Config, products *psroutes.ProductRoute, categories *psroutes.CategoryRoute) *CategoryHandler { return &CategoryHandler{ - catalog: catalog, - customers: customers, - carts: carts, - renderer: renderer, - config: cfg, - products: products, + catalog: catalog, + customers: customers, + carts: carts, + renderer: renderer, + config: cfg, + products: products, + categories: categories, } } func (h *CategoryHandler) Show(c echo.Context) error { session := appmiddleware.GetSession(c) - if session == nil { - session = appmiddleware.GetSession(c) - } if h == nil || h.catalog == nil || h.renderer == nil { return echo.NewHTTPError(http.StatusInternalServerError, "category handler is not initialized") } @@ -91,6 +90,16 @@ func (h *CategoryHandler) Show(c echo.Context) error { ShopBaseURL: h.config.PrestaShopBaseURL, } assignCategoryProductLinks(c.Request(), h.products, &page) + menu, err := loadMenu(c.Request(), h.catalog, h.categories, languageID, shopID) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "menu query failed: "+err.Error()) + } + page.Menu = menu + locale, err := loadHeaderLocale(c.Request(), h.catalog, session, languageID) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "locale query failed: "+err.Error()) + } + page.Locale = locale if err := h.renderer.Category(c.Response(), c.Request(), page); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "category render failed: "+err.Error()) @@ -141,22 +150,3 @@ func assignCategoryProductLinks(req *http.Request, route *psroutes.ProductRoute, }) } } - -func requestLanguagePrefix(req *http.Request) string { - if req == nil || req.URL == nil { - return "" - } - path := strings.Trim(req.URL.Path, "/") - if path == "" { - return "" - } - first := path - if idx := strings.IndexByte(path, '/'); idx >= 0 { - first = path[:idx] - } - first = strings.TrimSpace(first) - if len(first) < 2 || len(first) > 5 { - return "" - } - return "/" + first -} diff --git a/internal/http/handlers/navigation.go b/internal/http/handlers/navigation.go new file mode 100644 index 0000000..5e5017a --- /dev/null +++ b/internal/http/handlers/navigation.go @@ -0,0 +1,151 @@ +package handlers + +import ( + "net/http" + "net/url" + "strconv" + "strings" + + pscatalog "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/catalog" + pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie" + psroutes "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/routes" +) + +func loadMenu(req *http.Request, catalog *pscatalog.Service, route *psroutes.CategoryRoute, languageID int64, shopID int64) ([]pscatalog.MenuItem, error) { + if catalog == nil || route == nil { + return nil, nil + } + menu, err := catalog.GetCategoryMenu(req.Context(), languageID, shopID) + if err != nil { + return nil, err + } + assignMenuLinks(req, route, menu) + return menu, nil +} + +func assignMenuLinks(req *http.Request, route *psroutes.CategoryRoute, items []pscatalog.MenuItem) { + langPrefix := requestLanguagePrefix(req) + for i := range items { + items[i].URL = route.BuildPath(psroutes.CategoryURLData{ + ID: items[i].ID, + Slug: items[i].Slug, + LanguagePrefix: langPrefix, + }) + if len(items[i].Children) > 0 { + assignMenuLinks(req, route, items[i].Children) + } + } +} + +func loadHeaderLocale(req *http.Request, catalog *pscatalog.Service, session *pscookie.SessionContext, languageID int64) (pscatalog.HeaderLocaleData, error) { + if catalog == nil || req == nil { + return pscatalog.HeaderLocaleData{}, nil + } + + var currencyID int64 + countryISO := "" + if session != nil { + currencyID = int64Default(session.CurrencyID, 0) + countryISO = strings.TrimSpace(session.Values["iso_code_country"]) + } + + locale, err := catalog.GetHeaderLocale(req.Context(), languageID, currencyID, countryISO) + if err != nil { + return pscatalog.HeaderLocaleData{}, err + } + assignLanguageSwitchLinks(req, &locale) + assignMarketSwitchLinks(req, &locale) + return locale, nil +} + +func assignLanguageSwitchLinks(req *http.Request, locale *pscatalog.HeaderLocaleData) { + if req == nil || req.URL == nil || locale == nil || len(locale.Languages) == 0 { + return + } + + basePath := stripLanguagePrefix(req.URL.Path, locale.Languages) + rawQuery := req.URL.RawQuery + for i := range locale.Languages { + code := strings.ToLower(strings.TrimSpace(locale.Languages[i].Code)) + path := "/" + code + if basePath != "/" { + path += basePath + } + if rawQuery != "" { + path += "?" + rawQuery + } + locale.Languages[i].URL = path + } +} + +func assignMarketSwitchLinks(req *http.Request, locale *pscatalog.HeaderLocaleData) { + if req == nil || req.URL == nil || locale == nil || len(locale.Countries) == 0 { + return + } + + for i := range locale.Countries { + marketCode := strings.ToUpper(strings.TrimSpace(locale.Countries[i].Code)) + if marketCode == "" || locale.Countries[i].CurrencyID == 0 { + continue + } + query := req.URL.Query() + query.Set("market", strconv.FormatInt(locale.Countries[i].ID, 10)+":"+marketCode+":"+strconv.FormatInt(locale.Countries[i].CurrencyID, 10)) + locale.Countries[i].URL = rebuildURL(req.URL.Path, query) + } +} + +func requestLanguagePrefix(req *http.Request) string { + if req == nil || req.URL == nil { + return "" + } + path := strings.Trim(req.URL.Path, "/") + if path == "" { + return "" + } + first := path + if idx := strings.IndexByte(path, '/'); idx >= 0 { + first = path[:idx] + } + first = strings.TrimSpace(first) + if len(first) < 2 || len(first) > 5 { + return "" + } + return "/" + first +} + +func stripLanguagePrefix(path string, languages []pscatalog.LocaleOption) string { + if path == "" { + return "/" + } + codes := make(map[string]struct{}, len(languages)) + for _, language := range languages { + code := strings.ToLower(strings.TrimSpace(language.Code)) + if code != "" { + codes[code] = struct{}{} + } + } + + trimmed := strings.Trim(path, "/") + if trimmed == "" { + return "/" + } + parts := strings.Split(trimmed, "/") + if _, ok := codes[strings.ToLower(parts[0])]; ok { + parts = parts[1:] + } + if len(parts) == 0 { + return "/" + } + return "/" + strings.Join(parts, "/") +} + +func rebuildURL(path string, query url.Values) string { + if path == "" { + path = "/" + } + encoded := query.Encode() + if encoded == "" { + return path + } + return path + "?" + encoded +} diff --git a/internal/http/handlers/product.go b/internal/http/handlers/product.go index f384abc..be27844 100644 --- a/internal/http/handlers/product.go +++ b/internal/http/handlers/product.go @@ -8,14 +8,14 @@ import ( "github.com/labstack/echo/v4" "gorm.io/gorm" - appmiddleware "prestaproxy/internal/http/middleware" - pscart "prestaproxy/internal/prestashop/cart" - pscatalog "prestaproxy/internal/prestashop/catalog" - psconfig "prestaproxy/internal/prestashop/config" - pscustomer "prestaproxy/internal/prestashop/customer" - psroutes "prestaproxy/internal/prestashop/routes" - "prestaproxy/internal/render" - "prestaproxy/internal/viewmodel" + appmiddleware "git.ma-al.com/goc_marek/ps_shop/internal/http/middleware" + pscart "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cart" + pscatalog "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/catalog" + psconfig "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/config" + pscustomer "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/customer" + psroutes "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/routes" + "git.ma-al.com/goc_marek/ps_shop/internal/render" + "git.ma-al.com/goc_marek/ps_shop/internal/viewmodel" ) const productSlugContextKey = "product_slug" @@ -43,9 +43,6 @@ func NewProductHandler(products *pscatalog.Service, customers *pscustomer.Servic func (h *ProductHandler) Show(c echo.Context) error { session := appmiddleware.GetSession(c) - if session == nil { - session = appmiddleware.GetSession(c) - } if h == nil || h.products == nil || h.renderer == nil { return echo.NewHTTPError(http.StatusInternalServerError, "product handler is not initialized") } @@ -91,6 +88,16 @@ func (h *ProductHandler) Show(c echo.Context) error { CartSummary: cartSummary, ShopBaseURL: h.config.PrestaShopBaseURL, } + menu, err := loadMenu(c.Request(), h.products, h.categories, languageID, shopID) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "menu query failed: "+err.Error()) + } + page.Menu = menu + locale, err := loadHeaderLocale(c.Request(), h.products, session, languageID) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "locale query failed: "+err.Error()) + } + page.Locale = locale return h.renderer.Product(c.Response(), c.Request(), page) } diff --git a/internal/http/middleware/context.go b/internal/http/middleware/context.go index cd8fa5f..8a32089 100644 --- a/internal/http/middleware/context.go +++ b/internal/http/middleware/context.go @@ -1,7 +1,7 @@ package middleware import ( - "prestaproxy/internal/prestashop/cookie" + "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie" "github.com/labstack/echo/v4" ) diff --git a/internal/http/middleware/session.go b/internal/http/middleware/session.go index 867a28b..95c5901 100644 --- a/internal/http/middleware/session.go +++ b/internal/http/middleware/session.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "hash/crc32" + "net" "net/http" "net/url" "path" @@ -12,8 +13,8 @@ import ( "github.com/labstack/echo/v4" - psconfig "prestaproxy/internal/prestashop/config" - pscookie "prestaproxy/internal/prestashop/cookie" + psconfig "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/config" + pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie" ) type AnonymousSessionInitializer interface { @@ -62,6 +63,7 @@ func Session(cfg psconfig.Config, codec pscookie.Codec, initializer AnonymousSes } if ownedRoute { applyRequestLanguage(session, resolveRequestLanguageID(c.Request().Context(), c.Request(), session, languageResolver)) + applyRequestMarket(session, requestMarketSelection(c.Request())) } if ownedRoute && shouldSetSessionCookie(rawCookie, session) { encoded, err := codec.Encode(session) @@ -70,6 +72,9 @@ func Session(cfg psconfig.Config, codec pscookie.Codec, initializer AnonymousSes } session.RawCookie = encoded setPrestaShopCookie(c.Request(), c.Response(), ownership.ProductPrefixes, cookieName, encoded) + if redirectURL, ok := clearMarketSelectionURL(c.Request()); ok { + return c.Redirect(http.StatusSeeOther, redirectURL) + } } SetSession(c, session) @@ -78,6 +83,12 @@ func Session(cfg psconfig.Config, codec pscookie.Codec, initializer AnonymousSes } } +type marketSelection struct { + CountryID int64 + CountryISO string + CurrencyID int64 +} + func resolveRequestLanguageID(ctx context.Context, req *http.Request, session *pscookie.SessionContext, resolver LanguageResolver) int64 { if resolver == nil { return 0 @@ -177,6 +188,50 @@ func applyRequestLanguage(session *pscookie.SessionContext, languageID int64) { session.RawCookie = "" } +func applyRequestMarket(session *pscookie.SessionContext, selection marketSelection) { + if session == nil || selection.CountryISO == "" || selection.CurrencyID == 0 { + return + } + + currentCountry := "" + currentCurrency := int64(0) + currentCountryID := int64(0) + if session.Values != nil { + currentCountry = strings.ToUpper(strings.TrimSpace(session.Values["iso_code_country"])) + if session.CurrencyID != nil { + currentCurrency = *session.CurrencyID + } + currentCountryID, _ = strconv.ParseInt(session.Values["id_country"], 10, 64) + } + if currentCountry == selection.CountryISO && currentCurrency == selection.CurrencyID && currentCountryID == selection.CountryID { + return + } + + if session.Values == nil { + session.Values = map[string]string{} + } + + session.CurrencyID = int64Ptr(selection.CurrencyID) + session.Values["iso_code_country"] = selection.CountryISO + if selection.CountryID > 0 { + session.Values["id_country"] = strconv.FormatInt(selection.CountryID, 10) + session.OrderedKeys = ensureOrderedKey(session.OrderedKeys, "id_country", 5) + } + session.Values["id_currency"] = strconv.FormatInt(selection.CurrencyID, 10) + session.OrderedKeys = ensureOrderedKey(session.OrderedKeys, "iso_code_country", 4) + session.OrderedKeys = ensureOrderedKey(session.OrderedKeys, "id_currency", 6) + + if !session.IsLoggedIn { + if checksum := anonymousSessionChecksum(session, sessionLanguageID(session)); checksum != "" { + session.Values["checksum"] = checksum + session.OrderedKeys = ensureOrderedKey(session.OrderedKeys, "checksum", len(session.OrderedKeys)) + } + } + + session.Plaintext = "" + session.RawCookie = "" +} + func sessionLanguageID(session *pscookie.SessionContext) int64 { if session == nil || session.LanguageID == nil { return 0 @@ -237,6 +292,67 @@ func int64Ptr(value int64) *int64 { return &v } +func requestMarketSelection(req *http.Request) marketSelection { + if req == nil || req.URL == nil { + return marketSelection{} + } + raw := strings.TrimSpace(req.URL.Query().Get("market")) + if raw == "" { + return marketSelection{} + } + parts := strings.Split(raw, ":") + if len(parts) != 2 && len(parts) != 3 { + return marketSelection{} + } + + selection := marketSelection{} + var countryISO string + var currencyValue string + if len(parts) == 3 { + countryID, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64) + if err != nil || countryID == 0 { + return marketSelection{} + } + selection.CountryID = countryID + countryISO = strings.ToUpper(strings.TrimSpace(parts[1])) + currencyValue = parts[2] + } else { + countryISO = strings.ToUpper(strings.TrimSpace(parts[0])) + currencyValue = parts[1] + } + + currencyID, err := strconv.ParseInt(strings.TrimSpace(currencyValue), 10, 64) + if err != nil || currencyID == 0 { + return marketSelection{} + } + if len(countryISO) < 2 || len(countryISO) > 5 { + return marketSelection{} + } + + selection.CountryISO = countryISO + selection.CurrencyID = currencyID + return selection +} + +func clearMarketSelectionURL(req *http.Request) (string, bool) { + if req == nil || req.URL == nil { + return "", false + } + query := req.URL.Query() + if query.Get("market") == "" { + return "", false + } + query.Del("market") + cleanPath := req.URL.Path + if cleanPath == "" { + cleanPath = "/" + } + if encoded := query.Encode(); encoded != "" { + return cleanPath + "?" + encoded, true + } + return cleanPath, true +} + func setPrestaShopCookie(req *http.Request, res *echo.Response, ownedPrefixes []string, name, value string) { http.SetCookie(res.Writer, &http.Cookie{ Name: name, @@ -277,7 +393,11 @@ func requestCookieDomain(req *http.Request) string { return "" } if parsed, err := url.Parse("http://" + host); err == nil { - return parsed.Hostname() + host = parsed.Hostname() + } + host = strings.TrimSpace(strings.TrimPrefix(host, ".")) + if host == "" || strings.EqualFold(host, "localhost") || net.ParseIP(host) != nil { + return "" } return host } diff --git a/internal/prestashop/catalog/service.go b/internal/prestashop/catalog/service.go index 592b5a5..bfc4271 100644 --- a/internal/prestashop/catalog/service.go +++ b/internal/prestashop/catalog/service.go @@ -55,6 +55,32 @@ type CategoryProductCard struct { EAN13 string } +type MenuItem struct { + ID int64 + ParentID int64 + Name string + Slug string + Depth int + URL string `gorm:"-"` + Children []MenuItem `gorm:"-"` +} + +type LocaleOption struct { + ID int64 + CurrencyID int64 `gorm:"column:currency_id"` + Label string + Code string + Meta string + URL string `gorm:"-"` +} + +type HeaderLocaleData struct { + CurrentLanguage LocaleOption + CurrentCountry LocaleOption + Languages []LocaleOption + Countries []LocaleOption +} + type Service struct { db *gorm.DB prefix string @@ -239,3 +265,237 @@ func (s *Service) ResolveLanguageID(ctx context.Context, req *http.Request, fall } return row.ID } + +func (s *Service) GetCategoryMenu(ctx context.Context, languageID int64, shopID int64) ([]MenuItem, error) { + rootCategoryID, err := s.rootCategoryID(ctx) + if err != nil { + return nil, err + } + + query := fmt.Sprintf(` +WITH RECURSIVE category_tree AS ( + SELECT c.id_category AS id, + c.id_parent AS parent_id, + cl.name AS name, + cl.link_rewrite AS slug, + 0 AS depth + FROM %scategory c + JOIN %scategory_shop cs ON cs.id_category = c.id_category + JOIN %scategory_lang cl ON cl.id_category = c.id_category + WHERE c.id_parent = ? + AND c.active = 1 + AND cs.id_shop = ? + AND cl.id_lang = ? + UNION ALL + SELECT c.id_category AS id, + c.id_parent AS parent_id, + cl.name AS name, + cl.link_rewrite AS slug, + tree.depth + 1 AS depth + FROM %scategory c + JOIN %scategory_shop cs ON cs.id_category = c.id_category + JOIN %scategory_lang cl ON cl.id_category = c.id_category + JOIN category_tree tree ON tree.id = c.id_parent + WHERE c.active = 1 + AND cs.id_shop = ? + AND cl.id_lang = ? +) +SELECT id, parent_id, name, slug, depth +FROM category_tree +ORDER BY depth ASC, parent_id ASC, name ASC +`, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix, s.prefix) + + var flat []MenuItem + if err := s.db.WithContext(ctx).Raw(strings.TrimSpace(query), rootCategoryID, shopID, languageID, shopID, languageID).Scan(&flat).Error; err != nil { + return nil, err + } + if len(flat) == 0 { + return nil, nil + } + + nodes := make(map[int64]MenuItem, len(flat)) + childrenByParent := make(map[int64][]int64, len(flat)) + for i := range flat { + item := flat[i] + nodes[item.ID] = item + childrenByParent[item.ParentID] = append(childrenByParent[item.ParentID], item.ID) + } + + rootIDs := childrenByParent[rootCategoryID] + roots := make([]MenuItem, 0, len(rootIDs)) + for _, id := range rootIDs { + if item, ok := buildMenuTree(id, nodes, childrenByParent); ok { + roots = append(roots, item) + } + } + return roots, nil +} + +func (s *Service) GetHeaderLocale(ctx context.Context, languageID int64, currencyID int64, countryISO string) (HeaderLocaleData, error) { + var locale HeaderLocaleData + defaultCurrencyID, err := s.defaultCurrencyID(ctx) + if err != nil { + return HeaderLocaleData{}, err + } + + languageQuery := fmt.Sprintf(` +SELECT id_lang AS id, + name AS label, + UPPER(iso_code) AS code, + COALESCE(NULLIF(language_code, ''), UPPER(iso_code)) AS meta +FROM %slang +WHERE active = 1 +ORDER BY id_lang ASC +`, s.prefix) + if err := s.db.WithContext(ctx).Raw(strings.TrimSpace(languageQuery)).Scan(&locale.Languages).Error; err != nil { + return HeaderLocaleData{}, err + } + + hasCountryCurrency, err := s.columnExists(ctx, s.prefix+"country", "id_currency") + if err != nil { + return HeaderLocaleData{}, err + } + + var countryQuery string + var countryArgs []any + if hasCountryCurrency { + countryQuery = fmt.Sprintf(` +SELECT c.id_country AS id, + COALESCE(c.id_currency, ?) AS currency_id, + cl.name AS label, + UPPER(c.iso_code) AS code, + TRIM(CONCAT(COALESCE(UPPER(cur.iso_code), ''), ' ', COALESCE(cur.sign, ''))) AS meta +FROM %scountry c +JOIN %scountry_lang cl ON cl.id_country = c.id_country +LEFT JOIN %scurrency cur ON cur.id_currency = c.id_currency +WHERE c.active = 1 + AND cl.id_lang = ? +ORDER BY cl.name ASC +`, s.prefix, s.prefix, s.prefix) + countryArgs = []any{defaultCurrencyID, languageID} + } else { + countryQuery = fmt.Sprintf(` +SELECT c.id_country AS id, + ? AS currency_id, + cl.name AS label, + UPPER(c.iso_code) AS code, + UPPER(c.iso_code) AS meta +FROM %scountry c +JOIN %scountry_lang cl ON cl.id_country = c.id_country +WHERE c.active = 1 + AND cl.id_lang = ? +ORDER BY cl.name ASC +`, s.prefix, s.prefix) + countryArgs = []any{defaultCurrencyID, languageID} + } + if err := s.db.WithContext(ctx).Raw(strings.TrimSpace(countryQuery), countryArgs...).Scan(&locale.Countries).Error; err != nil { + return HeaderLocaleData{}, err + } + + locale.CurrentLanguage = pickLocaleOptionByID(locale.Languages, languageID) + locale.CurrentCountry = pickLocaleOptionByCode(locale.Countries, countryISO) + return locale, nil +} + +func (s *Service) rootCategoryID(ctx context.Context) (int64, error) { + var row struct { + Value string `gorm:"column:value"` + } + query := fmt.Sprintf("SELECT value FROM %sconfiguration WHERE name = 'PS_ROOT_CATEGORY' LIMIT 1", s.prefix) + if err := s.db.WithContext(ctx).Raw(query).Scan(&row).Error; err != nil { + return 0, err + } + id := parseInt64(row.Value) + if id == 0 { + return 1, nil + } + return id, nil +} + +func (s *Service) defaultCurrencyID(ctx context.Context) (int64, error) { + var row struct { + Value string `gorm:"column:value"` + } + query := fmt.Sprintf("SELECT value FROM %sconfiguration WHERE name = 'PS_CURRENCY_DEFAULT' LIMIT 1", s.prefix) + if err := s.db.WithContext(ctx).Raw(query).Scan(&row).Error; err != nil { + return 0, err + } + id := parseInt64(row.Value) + if id == 0 { + return 1, nil + } + return id, nil +} + +func (s *Service) columnExists(ctx context.Context, tableName string, columnName string) (bool, error) { + var count int64 + query := ` +SELECT COUNT(*) +FROM information_schema.columns +WHERE table_schema = DATABASE() + AND table_name = ? + AND column_name = ? +` + if err := s.db.WithContext(ctx).Raw(strings.TrimSpace(query), tableName, columnName).Scan(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +func parseInt64(value string) int64 { + var n int64 + for _, r := range value { + if r < '0' || r > '9' { + return 0 + } + n = n*10 + int64(r-'0') + } + return n +} + +func buildMenuTree(id int64, nodes map[int64]MenuItem, childrenByParent map[int64][]int64) (MenuItem, bool) { + item, ok := nodes[id] + if !ok { + return MenuItem{}, false + } + childIDs := childrenByParent[id] + if len(childIDs) == 0 { + return item, true + } + item.Children = make([]MenuItem, 0, len(childIDs)) + for _, childID := range childIDs { + child, ok := buildMenuTree(childID, nodes, childrenByParent) + if ok { + item.Children = append(item.Children, child) + } + } + return item, true +} + +func pickLocaleOptionByID(options []LocaleOption, id int64) LocaleOption { + for _, option := range options { + if option.ID == id && id != 0 { + return option + } + } + if len(options) > 0 { + return options[0] + } + return LocaleOption{} +} + +func pickLocaleOptionByCode(options []LocaleOption, code string) LocaleOption { + code = strings.ToUpper(strings.TrimSpace(code)) + for _, option := range options { + if option.Code == code && code != "" { + return option + } + } + if len(options) > 0 { + return options[0] + } + if code == "" { + return LocaleOption{} + } + return LocaleOption{Code: code, Label: code, Meta: code} +} diff --git a/internal/prestashop/config/config.go b/internal/prestashop/config/config.go index e917b1c..d542a0b 100644 --- a/internal/prestashop/config/config.go +++ b/internal/prestashop/config/config.go @@ -13,7 +13,7 @@ import ( "regexp" "strings" - pscookie "prestaproxy/internal/prestashop/cookie" + pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie" ) type Config struct { diff --git a/internal/prestashop/session/service.go b/internal/prestashop/session/service.go index d2b3c7e..8b14402 100644 --- a/internal/prestashop/session/service.go +++ b/internal/prestashop/session/service.go @@ -10,7 +10,7 @@ import ( "strings" "time" - pscookie "prestaproxy/internal/prestashop/cookie" + pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie" "gorm.io/gorm" ) diff --git a/internal/render/engine.go b/internal/render/engine.go index a933732..ea3e45a 100644 --- a/internal/render/engine.go +++ b/internal/render/engine.go @@ -3,9 +3,9 @@ package render import ( "net/http" - "prestaproxy/internal/assets" - "prestaproxy/internal/viewmodel" - "prestaproxy/templates" + "git.ma-al.com/goc_marek/ps_shop/internal/assets" + "git.ma-al.com/goc_marek/ps_shop/internal/viewmodel" + "git.ma-al.com/goc_marek/ps_shop/templates" ) type Engine struct { diff --git a/internal/viewmodel/category.go b/internal/viewmodel/category.go index ae40676..8a1ec9b 100644 --- a/internal/viewmodel/category.go +++ b/internal/viewmodel/category.go @@ -1,14 +1,16 @@ package viewmodel import ( - pscart "prestaproxy/internal/prestashop/cart" - pscatalog "prestaproxy/internal/prestashop/catalog" - pscookie "prestaproxy/internal/prestashop/cookie" - pscustomer "prestaproxy/internal/prestashop/customer" + pscart "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cart" + pscatalog "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/catalog" + pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie" + pscustomer "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/customer" ) type CategoryPageData struct { Category pscatalog.CategoryPageData + Menu []pscatalog.MenuItem + Locale pscatalog.HeaderLocaleData Session *pscookie.SessionContext Customer *pscustomer.Profile CartSummary *pscart.Summary diff --git a/internal/viewmodel/product.go b/internal/viewmodel/product.go index e88557f..2151019 100644 --- a/internal/viewmodel/product.go +++ b/internal/viewmodel/product.go @@ -1,15 +1,17 @@ package viewmodel import ( - pscart "prestaproxy/internal/prestashop/cart" - pscatalog "prestaproxy/internal/prestashop/catalog" - pscookie "prestaproxy/internal/prestashop/cookie" - pscustomer "prestaproxy/internal/prestashop/customer" + pscart "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cart" + pscatalog "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/catalog" + pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie" + pscustomer "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/customer" ) type ProductPageData struct { Product pscatalog.ProductPageData CategoryURL string + Menu []pscatalog.MenuItem + Locale pscatalog.HeaderLocaleData Session *pscookie.SessionContext Customer *pscustomer.Profile CartSummary *pscart.Summary diff --git a/package.json b/package.json index 5fe2d19..23166ab 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { - "name": "prestaproxy", + "name": "ps_shop", "private": true, "scripts": { - "build:js": "bun build ./web/src/app.js --outdir ./web/dist --naming app.js", - "build:css": "bunx tailwindcss -i ./web/src/app.css -o ./web/dist/app.css --minify", + "build:js": "bun build ./web/src/app.js --outfile ./web/dist/app.js --target browser --minify", + "build:css": "env BUN_TMPDIR=/tmp bunx tailwindcss -i ./web/src/app.css -o ./web/dist/app.css --minify", "build:manifest": "bun ./web/write-manifest.mjs", "build": "bun run build:js && bun run build:css && bun run build:manifest", "dev": "bun run build" diff --git a/templates/category.templ b/templates/category.templ index ee81b7c..8373ec8 100644 --- a/templates/category.templ +++ b/templates/category.templ @@ -3,11 +3,11 @@ package templates import ( "fmt" - "prestaproxy/internal/viewmodel" + "git.ma-al.com/goc_marek/ps_shop/internal/viewmodel" ) templ CategoryPage(data viewmodel.CategoryPageData, cssPath string, jsPath string) { - @Layout(data.Category.Name, cssPath, jsPath) { + @Layout(data.Category.Name, cssPath, jsPath, data.Menu, data.Locale) {
diff --git a/templates/category_templ.go b/templates/category_templ.go index a8e203e..2e49b6f 100644 --- a/templates/category_templ.go +++ b/templates/category_templ.go @@ -11,7 +11,7 @@ import templruntime "github.com/a-h/templ/runtime" import ( "fmt" - "prestaproxy/internal/viewmodel" + "git.ma-al.com/goc_marek/ps_shop/internal/viewmodel" ) func CategoryPage(data viewmodel.CategoryPageData, cssPath string, jsPath string) templ.Component { @@ -187,7 +187,7 @@ func CategoryPage(data viewmodel.CategoryPageData, cssPath string, jsPath string } return nil }) - templ_7745c5c3_Err = Layout(data.Category.Name, cssPath, jsPath).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout(data.Category.Name, cssPath, jsPath, data.Menu, data.Locale).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/layout.templ b/templates/layout.templ index 3be200b..d9c7f37 100644 --- a/templates/layout.templ +++ b/templates/layout.templ @@ -1,8 +1,10 @@ package templates -templ Layout(title string, cssPath string, jsPath string) { +import pscatalog "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/catalog" + +templ Layout(title string, cssPath string, jsPath string, menu []pscatalog.MenuItem, locale pscatalog.HeaderLocaleData) { - + @@ -10,8 +12,183 @@ templ Layout(title string, cssPath string, jsPath string) { - + + { children... } } + +templ LocalePicker(title string, current pscatalog.LocaleOption, options []pscatalog.LocaleOption, navigable bool) { +
+ + + if current.Label != "" { + { current.Label } + } else { + { title } + } + + if current.Code != "" { + { current.Code } + } + + + if len(options) > 0 { +
+

{ title }

+ +
+ } +
+} + +templ MenuTree(items []pscatalog.MenuItem, depth int) { + +} + +templ MegaMenuBar(items []pscatalog.MenuItem) { +
    + for _, item := range items { +
  • 0) }> + if len(item.Children) > 0 { +
    + + +
    + } else { + { item.Name } + } + if len(item.Children) > 0 { + @MegaMenu(item.ID, item.URL, item.Name, item.Children) + } +
  • + } +
+} + +templ MegaMenu(id int64, href string, label string, columns []pscatalog.MenuItem) { + +} diff --git a/templates/layout_helpers.go b/templates/layout_helpers.go new file mode 100644 index 0000000..6f8d7c1 --- /dev/null +++ b/templates/layout_helpers.go @@ -0,0 +1,78 @@ +package templates + +import ( + "strconv" + "strings" + + pscatalog "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/catalog" +) + +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) +} diff --git a/templates/layout_templ.go b/templates/layout_templ.go index 19f72d3..912e81a 100644 --- a/templates/layout_templ.go +++ b/templates/layout_templ.go @@ -8,7 +8,9 @@ package templates import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" -func Layout(title string, cssPath string, jsPath string) templ.Component { +import pscatalog "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/catalog" + +func Layout(title string, cssPath string, jsPath string, menu []pscatalog.MenuItem, locale pscatalog.HeaderLocaleData) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -29,46 +31,107 @@ func Layout(title string, cssPath string, jsPath string) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var2 string - templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(pageLanguage(locale)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 9, Col: 17} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 7, Col: 34} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var3 templ.SafeURL - templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(cssPath) + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 10, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 11, Col: 17} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><script type=\"module\" src=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if hasHeaderLocale(locale) { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = LocalePicker("Market", locale.CurrentCountry, locale.Countries, true).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = LocalePicker("Language", locale.CurrentLanguage, locale.Languages, true).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
9b plus
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(menu) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -76,7 +139,830 @@ func Layout(title string, cssPath string, jsPath string) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func LocalePicker(title string, current pscatalog.LocaleOption, options []pscatalog.LocaleOption, navigable bool) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var6 := templ.GetChildren(ctx) + if templ_7745c5c3_Var6 == nil { + templ_7745c5c3_Var6 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if current.Label != "" { + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(current.Label) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 75, Col: 20} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 77, Col: 12} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if current.Code != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(current.Code) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 81, Col: 52} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(options) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 87, Col: 43} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func MenuTree(items []pscatalog.MenuItem, depth int) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var21 := templ.GetChildren(ctx) + if templ_7745c5c3_Var21 == nil { + templ_7745c5c3_Var21 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var22 = []any{menuListClass(depth)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func MegaMenuBar(items []pscatalog.MenuItem) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var30 := templ.GetChildren(ctx) + if templ_7745c5c3_Var30 == nil { + templ_7745c5c3_Var30 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func MegaMenu(id int64, href string, label string, columns []pscatalog.MenuItem) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var47 := templ.GetChildren(ctx) + if templ_7745c5c3_Var47 == nil { + templ_7745c5c3_Var47 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, column := range columns { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var50 string + templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(column.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 175, Col: 68} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(column.Children) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "View all") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/product.templ b/templates/product.templ index cd225f3..52780a6 100644 --- a/templates/product.templ +++ b/templates/product.templ @@ -3,11 +3,11 @@ package templates import ( "fmt" - "prestaproxy/internal/viewmodel" + "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) { + @Layout(data.Product.Name, cssPath, jsPath, data.Menu, data.Locale) {
diff --git a/templates/product_templ.go b/templates/product_templ.go index 85d9fb2..b74b74c 100644 --- a/templates/product_templ.go +++ b/templates/product_templ.go @@ -11,7 +11,7 @@ import templruntime "github.com/a-h/templ/runtime" import ( "fmt" - "prestaproxy/internal/viewmodel" + "git.ma-al.com/goc_marek/ps_shop/internal/viewmodel" ) func ProductPage(data viewmodel.ProductPageData, cssPath string, jsPath string) templ.Component { @@ -235,7 +235,7 @@ func ProductPage(data viewmodel.ProductPageData, cssPath string, jsPath string) } return nil }) - templ_7745c5c3_Err = Layout(data.Product.Name, cssPath, jsPath).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout(data.Product.Name, cssPath, jsPath, data.Menu, data.Locale).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/tmp/build-errors.log b/tmp/build-errors.log index 2f95017..3415563 100644 --- a/tmp/build-errors.log +++ b/tmp/build-errors.log @@ -1 +1 @@ -exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file +exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file diff --git a/tmp/main b/tmp/main index be9df8f..18b2c7a 100755 Binary files a/tmp/main and b/tmp/main differ diff --git a/web/dist/app.css b/web/dist/app.css index cbc9a8f..2b5f5e5 100644 --- a/web/dist/app.css +++ b/web/dist/app.css @@ -1 +1 @@ -body{margin:0;font-family:"IBM Plex Sans",sans-serif;background:#0c0a09;color:#f5f5f4}a{color:inherit}button{cursor:pointer} +*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--cta-glow:0 0 0 0 transparent}body{font-family:IBM Plex Sans,Avenir Next,Segoe UI,sans-serif;background:radial-gradient(circle at top left,hsla(39,82%,69%,.24),transparent 28%),radial-gradient(circle at top right,rgba(157,217,210,.16),transparent 34%),linear-gradient(180deg,#fbfaf6,#f5efe3 55%,#f7f3ea);--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1))}h1,h2,h3{font-family:Cormorant Garamond,IBM Plex Sans,serif}.site-container{margin-left:auto;margin-right:auto;width:100%;max-width:80rem;padding-left:1rem;padding-right:1rem}@media (min-width:640px){.site-container{padding-left:1.25rem;padding-right:1.25rem}}@media (min-width:1024px){.site-container{padding-left:2rem;padding-right:2rem}}.site-header{position:sticky;top:0;z-index:40;--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.utility-bar{background:linear-gradient(90deg,rgba(20,33,61,.98),rgba(37,58,89,.94));border-bottom-width:1px;border-color:hsla(0,0%,100%,.1)}.header-locale,.utility-bar{--tw-text-opacity:1;color:rgb(245 245 244/var(--tw-text-opacity,1))}.header-locale{display:flex;width:100%;flex-wrap:wrap;align-items:center;justify-content:flex-start;gap:.5rem;font-size:.875rem;line-height:1.25rem}@media (min-width:640px){.header-locale{width:auto;justify-content:flex-end}}.locale-picker{position:relative;width:100%}@media (min-width:640px){.locale-picker{width:auto}}.locale-picker__summary{display:flex;width:100%;cursor:pointer;list-style-type:none;align-items:center;justify-content:space-between;gap:.5rem;border-radius:9999px;border-width:1px;border-color:hsla(0,0%,100%,0);padding:.375rem .75rem;--tw-text-opacity:1;color:rgb(245 245 244/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.locale-picker__summary:hover{border-color:hsla(0,0%,100%,.2);background-color:hsla(0,0%,100%,.1);--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}@media (min-width:640px){.locale-picker__summary{width:auto;justify-content:flex-start}}.locale-picker[open] .locale-picker__summary{border-color:hsla(0,0%,100%,.2);background-color:hsla(0,0%,100%,.1);--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.locale-picker__summary::-webkit-details-marker{display:none}.locale-picker__value{font-weight:500}.locale-picker__chevron,.locale-picker__code,.locale-picker__item-meta{font-size:.74rem;text-transform:uppercase;letter-spacing:.16em;color:hsla(24,6%,83%,.8)}.locale-picker__panel{left:0;right:0;z-index:50;margin-top:.75rem;min-width:13rem;border-radius:1.5rem;border-width:1px;border-color:hsla(0,0%,100%,.7);background-color:hsla(0,0%,100%,.95);padding:.75rem;--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1));--tw-shadow:0 22px 48px rgba(20,33,61,.18);--tw-shadow-colored:0 22px 48px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}@media (min-width:640px){.locale-picker__panel{position:absolute;left:auto;right:0}}.locale-picker__title{margin-bottom:.5rem;padding-left:.5rem;padding-right:.5rem;font-size:.68rem;font-weight:600;text-transform:uppercase;letter-spacing:.2em;--tw-text-opacity:1;color:rgb(168 162 158/var(--tw-text-opacity,1))}.main-nav{background:rgba(255,251,245,.82);border-bottom-width:1px;border-color:hsla(0,0%,100%,.6);--tw-shadow:0 10px 30px rgba(20,33,61,.08);--tw-shadow-colored:0 10px 30px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.header-bar{display:flex;flex-wrap:wrap;align-items:center;gap:1rem;padding-top:1rem;padding-bottom:1rem}@media (min-width:640px){.header-bar{gap:1.25rem;padding-top:1.25rem;padding-bottom:1.25rem}}@media (min-width:1024px){.header-bar{flex-wrap:nowrap}}.brand-mark{display:inline-flex;align-items:flex-end;gap:.125rem;font-size:2.25rem;line-height:2.5rem;font-weight:900;line-height:1;letter-spacing:-.025em;--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1));transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.3s}.brand-mark:hover{--tw-translate-y:-0.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@media (min-width:640px){.brand-mark{font-size:3rem;line-height:1}}.brand-mark__accent{--tw-text-opacity:1;color:rgb(245 158 11/var(--tw-text-opacity,1))}.menu-toggle{display:inline-flex;height:2.75rem;align-items:center;justify-content:center;border-radius:9999px;border-width:1px;border-color:hsla(24,6%,83%,.7);background-color:hsla(0,0%,100%,.8);padding-left:1.25rem;padding-right:1.25rem;font-size:.75rem;line-height:1rem;font-weight:600;text-transform:uppercase;letter-spacing:.28em;--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.menu-toggle:hover{--tw-border-opacity:1;border-color:rgb(245 158 11/var(--tw-border-opacity,1));--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.menu-panel{order:9999;width:100%;border-radius:1.5rem;border-width:1px;border-color:hsla(0,0%,100%,.7);background-color:hsla(0,0%,100%,.95);padding:1rem;--tw-shadow:0 16px 34px rgba(20,33,61,.12);--tw-shadow-colored:0 16px 34px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}@media (min-width:1024px){.menu-panel{order:0;border-radius:0;border-width:0;background-color:transparent;padding:0;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}}.desktop-nav{display:flex;align-items:center;justify-content:center;gap:2rem}@media (min-width:1280px){.desktop-nav{gap:3rem}}.desktop-nav__toggle{display:inline-flex;height:2rem;width:1.25rem;align-items:center;justify-content:center;font-size:.875rem;line-height:1.25rem;--tw-text-opacity:1;color:rgb(168 162 158/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.desktop-nav__entry--open .desktop-nav__link,.desktop-nav__entry--open .desktop-nav__toggle,.desktop-nav__toggle:hover{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.mega-menu{background:linear-gradient(180deg,rgba(255,252,247,.98),hsla(40,67%,96%,.98));position:absolute;left:0;right:0;top:100%;z-index:50;border-top-width:1px;border-color:hsla(0,0%,100%,.7);--tw-shadow:0 28px 60px rgba(20,33,61,.16);--tw-shadow-colored:0 28px 60px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.mega-menu__grid{margin-left:auto;margin-right:auto;width:100%;max-width:80rem;padding-left:1rem;padding-right:1rem}@media (min-width:640px){.mega-menu__grid{padding-left:1.25rem;padding-right:1.25rem}}@media (min-width:1024px){.mega-menu__grid{padding-left:2rem;padding-right:2rem}}.mega-menu__grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));-moz-column-gap:3rem;column-gap:3rem;row-gap:2.5rem;padding-top:2.5rem;padding-bottom:2.5rem}@media (min-width:1024px){.mega-menu__grid{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:1280px){.mega-menu__grid{grid-template-columns:repeat(5,minmax(0,1fr))}}.mega-menu__heading{margin-bottom:1rem;display:inline-flex;font-size:.875rem;line-height:1.25rem;font-weight:600;text-transform:uppercase;letter-spacing:.12em;--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.mega-menu__heading:hover{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.mega-menu__link{display:inline-flex;font-size:1.02rem;line-height:1.75rem;--tw-text-opacity:1;color:rgb(68 64 60/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.mega-menu__link:hover{--tw-translate-x:0.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.mega-menu__all{grid-column:1/-1;display:flex;align-items:flex-start;justify-content:flex-end;border-top-width:1px;--tw-border-opacity:1;border-color:rgb(231 229 228/var(--tw-border-opacity,1));padding-top:1.25rem}.mega-menu__all-link{display:inline-flex;font-size:.875rem;line-height:1.25rem;font-weight:600;text-transform:uppercase;letter-spacing:.18em;--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.mega-menu__all-link:hover{--tw-text-opacity:1;color:rgb(180 83 9/var(--tw-text-opacity,1))}.header-actions{margin-left:0;display:flex;align-items:center;gap:1rem;font-size:1.5rem;line-height:2rem;--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1))}@media (min-width:1024px){.header-actions{margin-left:auto}}.nav-icon{display:flex;height:2.75rem;width:2.75rem;align-items:center;justify-content:center;border-radius:9999px;background-color:hsla(0,0%,100%,.7);font-size:1.35rem;--tw-shadow:0 10px 24px rgba(20,33,61,.08);--tw-shadow-colored:0 10px 24px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.nav-icon:hover{--tw-translate-y:-0.125rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}@media (max-width:1023px){.desktop-nav,.mega-menu{display:none}}button[type=submit]{box-shadow:var(--cta-glow)}.mx-auto{margin-right:auto}.ml-auto,.mx-auto{margin-left:auto}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.min-h-screen{min-height:100vh}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-none{max-width:none}.shrink-0{flex-shrink:0}.basis-full{flex-basis:100%}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-10{gap:2.5rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-2{row-gap:.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.rounded-3xl{border-radius:1.5rem}.rounded-\[1\.75rem\]{border-radius:1.75rem}.rounded-\[2rem\]{border-radius:2rem}.rounded-full{border-radius:9999px}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-amber-500\/30{border-color:rgba(245,158,11,.3)}.border-emerald-400\/40{border-color:rgba(52,211,153,.4)}.border-emerald-500\/20{border-color:rgba(16,185,129,.2)}.border-stone-800{--tw-border-opacity:1;border-color:rgb(41 37 36/var(--tw-border-opacity,1))}.border-white\/10{border-color:hsla(0,0%,100%,.1)}.bg-amber-300{--tw-bg-opacity:1;background-color:rgb(252 211 77/var(--tw-bg-opacity,1))}.bg-amber-400\/10{background-color:rgba(251,191,36,.1)}.bg-slate-900\/70{background-color:rgba(15,23,42,.7)}.bg-stone-900\/70{background-color:rgba(28,25,23,.7)}.bg-white\/5{background-color:hsla(0,0%,100%,.05)}.bg-\[radial-gradient\(circle_at_top\2c _rgba\(245\2c 158\2c 11\2c 0\.28\)\2c _transparent_40\%\)\2c linear-gradient\(180deg\2c \#0c0a09\2c \#1c1917\)\]{background-image:radial-gradient(circle at top,rgba(245,158,11,.28),transparent 40%),linear-gradient(180deg,#0c0a09,#1c1917)}.bg-\[radial-gradient\(circle_at_top_left\2c _rgba\(34\2c 197\2c 94\2c 0\.18\)\2c _transparent_35\%\)\2c linear-gradient\(180deg\2c \#0b1020\2c \#111827\)\]{background-image:radial-gradient(circle at top left,rgba(34,197,94,.18),transparent 35%),linear-gradient(180deg,#0b1020,#111827)}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.pb-6{padding-bottom:1.5rem}.text-right{text-align:right}.font-serif{font-family:ui-serif,Georgia,Cambria,Times New Roman,Times,serif}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.92rem\]{font-size:.92rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-7{line-height:1.75rem}.leading-8{line-height:2rem}.tracking-\[0\.22em\]{letter-spacing:.22em}.tracking-\[0\.24em\]{letter-spacing:.24em}.tracking-\[0\.28em\]{letter-spacing:.28em}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-\[0\.32em\]{letter-spacing:.32em}.text-amber-200{--tw-text-opacity:1;color:rgb(253 230 138/var(--tw-text-opacity,1))}.text-amber-300{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.text-amber-300\/80{color:rgba(252,211,77,.8)}.text-emerald-200{--tw-text-opacity:1;color:rgb(167 243 208/var(--tw-text-opacity,1))}.text-emerald-300{--tw-text-opacity:1;color:rgb(110 231 183/var(--tw-text-opacity,1))}.text-slate-300{--tw-text-opacity:1;color:rgb(203 213 225/var(--tw-text-opacity,1))}.text-stone-300{--tw-text-opacity:1;color:rgb(214 211 209/var(--tw-text-opacity,1))}.text-stone-400{--tw-text-opacity:1;color:rgb(168 162 158/var(--tw-text-opacity,1))}.text-stone-50{--tw-text-opacity:1;color:rgb(250 250 249/var(--tw-text-opacity,1))}.text-stone-500{--tw-text-opacity:1;color:rgb(120 113 108/var(--tw-text-opacity,1))}.text-stone-700{--tw-text-opacity:1;color:rgb(68 64 60/var(--tw-text-opacity,1))}.text-stone-900{--tw-text-opacity:1;color:rgb(28 25 23/var(--tw-text-opacity,1))}.text-stone-950{--tw-text-opacity:1;color:rgb(12 10 9/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.underline-offset-4{text-underline-offset:4px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-\[0_24px_80px_rgba\(0\2c 0\2c 0\2c 0\.35\)\]{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-\[0_24px_80px_rgba\(0\2c 0\2c 0\2c 0\.35\)\]{--tw-shadow:0 24px 80px rgba(0,0,0,.35);--tw-shadow-colored:0 24px 80px var(--tw-shadow-color)}.shadow-amber-950\/20{--tw-shadow-color:rgba(69,26,3,.2);--tw-shadow:var(--tw-shadow-colored)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:-translate-y-1:hover{--tw-translate-y:-0.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-emerald-400\/40:hover{border-color:rgba(52,211,153,.4)}.hover\:bg-amber-200:hover{--tw-bg-opacity:1;background-color:rgb(253 230 138/var(--tw-bg-opacity,1))}.hover\:bg-emerald-300:hover{--tw-bg-opacity:1;background-color:rgb(110 231 183/var(--tw-bg-opacity,1))}.hover\:text-amber-600:hover{--tw-text-opacity:1;color:rgb(217 119 6/var(--tw-text-opacity,1))}.hover\:text-slate-950:hover{--tw-text-opacity:1;color:rgb(2 6 23/var(--tw-text-opacity,1))}@media (min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:ml-10{margin-left:2.5rem}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:min-w-0{min-width:0}.lg\:flex-1{flex:1 1 0%}.lg\:basis-auto{flex-basis:auto}.lg\:grid-cols-\[1\.1fr_0\.9fr\]{grid-template-columns:1.1fr .9fr}.lg\:items-center{align-items:center}.lg\:justify-center{justify-content:center}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width:1280px){.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} \ No newline at end of file diff --git a/web/dist/app.js b/web/dist/app.js index 3df2ef7..adeb004 100644 --- a/web/dist/app.js +++ b/web/dist/app.js @@ -1 +1 @@ -document.documentElement.dataset.js="ready"; +var l=document.documentElement;l.dataset.js="ready";var a=document.querySelector("[data-menu-toggle]"),d=document.querySelector("[data-menu-panel]"),r=[...document.querySelectorAll("[data-mega-trigger]")],c=[...document.querySelectorAll("[data-mega-menu]")];if(a&&d){let t=()=>{d.classList.add("hidden"),a.setAttribute("aria-expanded","false")},i=()=>{d.classList.remove("hidden"),a.setAttribute("aria-expanded","true")};a.addEventListener("click",()=>{if(a.getAttribute("aria-expanded")==="true"){t();return}i()}),window.addEventListener("resize",()=>{if(window.innerWidth>=1024){d.classList.remove("hidden"),a.setAttribute("aria-expanded","true");return}t()})}if(r.length>0&&c.length>0){let t=()=>{r.forEach((e)=>{e.setAttribute("aria-expanded","false"),e.closest(".desktop-nav__entry")?.classList.remove("desktop-nav__entry--open")}),c.forEach((e)=>{e.classList.add("hidden")})},i=(e)=>{let n=e.dataset.megaTarget;if(!n)return;t(),e.setAttribute("aria-expanded","true"),e.closest(".desktop-nav__entry")?.classList.add("desktop-nav__entry--open"),document.getElementById(n)?.classList.remove("hidden")};r.forEach((e)=>{e.addEventListener("click",(n)=>{if(window.innerWidth<1024)return;let s=e.getAttribute("aria-expanded")==="true";if(e instanceof HTMLAnchorElement&&s)return;if(n.preventDefault(),s){t();return}i(e)})}),document.addEventListener("click",(e)=>{if(window.innerWidth<1024)return;let n=e.target;if(!(n instanceof Node))return;let s=r.some((o)=>o.contains(n)),m=c.some((o)=>o.contains(n));if(!s&&!m)t()}),document.addEventListener("keydown",(e)=>{if(e.key==="Escape")t()}),window.addEventListener("resize",()=>{if(window.innerWidth<1024)t()})}var u=document.querySelector("button[type='submit']");if(u)u.addEventListener("mouseenter",()=>{l.style.setProperty("--cta-glow","0 0 0 4px rgba(252, 211, 77, 0.12)")}),u.addEventListener("mouseleave",()=>{l.style.setProperty("--cta-glow","0 0 0 0 rgba(0,0,0,0)")}); diff --git a/web/dist/manifest.json b/web/dist/manifest.json index e923b42..a4edb43 100644 --- a/web/dist/manifest.json +++ b/web/dist/manifest.json @@ -1,4 +1,4 @@ { "app.css": "/dist/app.css", "app.js": "/dist/app.js" -} +} \ No newline at end of file diff --git a/web/src/app.css b/web/src/app.css index 7d9ed4e..d2ec6ae 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -8,15 +8,170 @@ } body { - font-family: "IBM Plex Sans", sans-serif; + font-family: + "IBM Plex Sans", + "Avenir Next", + "Segoe UI", + sans-serif; + background: + radial-gradient(circle at top left, rgba(241, 196, 110, 0.24), transparent 28%), + radial-gradient(circle at top right, rgba(157, 217, 210, 0.16), transparent 34%), + linear-gradient(180deg, #fbfaf6 0%, #f5efe3 55%, #f7f3ea 100%); + @apply text-stone-900; } - h1, h2, h3 { - font-family: "Cormorant Garamond", serif; + h1, + h2, + h3 { + font-family: + "Cormorant Garamond", + "IBM Plex Sans", + serif; } } @layer components { + .site-container { + @apply mx-auto w-full max-w-7xl px-4 sm:px-5 lg:px-8; + } + + .site-header { + @apply sticky top-0 z-40 backdrop-blur; + } + + .utility-bar { + background: linear-gradient(90deg, rgba(20, 33, 61, 0.98), rgba(37, 58, 89, 0.94)); + @apply border-b border-white/10 text-stone-100; + } + + .header-locale { + @apply flex w-full flex-wrap items-center justify-start gap-2 text-sm text-stone-100 sm:w-auto sm:justify-end; + } + + .locale-picker { + @apply relative w-full sm:w-auto; + } + + .locale-picker__summary { + @apply flex w-full cursor-pointer list-none items-center justify-between gap-2 rounded-full border border-white/0 px-3 py-1.5 text-stone-100 transition hover:border-white/20 hover:bg-white/10 hover:text-white sm:w-auto sm:justify-start; + } + + .locale-picker[open] .locale-picker__summary { + @apply border-white/20 bg-white/10 text-white; + } + + .locale-picker__summary::-webkit-details-marker { + display: none; + } + + .locale-picker__value { + @apply font-medium; + } + + .locale-picker__code, + .locale-picker__item-meta, + .locale-picker__chevron { + @apply text-[0.74rem] uppercase tracking-[0.16em] text-stone-300/80; + } + + .locale-picker__panel { + @apply left-0 right-0 z-50 mt-3 min-w-[13rem] rounded-3xl border border-white/70 bg-white/95 p-3 text-stone-900 shadow-[0_22px_48px_rgba(20,33,61,0.18)] backdrop-blur sm:absolute sm:left-auto sm:right-0; + } + + .locale-picker__title { + @apply mb-2 px-2 text-[0.68rem] font-semibold uppercase tracking-[0.2em] text-stone-400; + } + + .locale-picker__item { + @apply flex w-full items-center justify-between rounded-2xl px-3 py-2 text-sm text-stone-700 transition hover:bg-stone-100 hover:text-stone-900; + } + + .locale-picker__item--active { + @apply bg-stone-100 font-medium text-stone-900; + } + + .main-nav { + background: rgba(255, 251, 245, 0.82); + @apply border-b border-white/60 shadow-[0_10px_30px_rgba(20,33,61,0.08)]; + } + + .header-bar { + @apply flex flex-wrap items-center gap-4 py-4 sm:gap-5 sm:py-5 lg:flex-nowrap; + } + + .brand-mark { + @apply inline-flex items-end gap-0.5 text-4xl font-black leading-none tracking-tight text-black transition-transform duration-300 hover:-translate-y-0.5 sm:text-5xl; + } + + .brand-mark__accent { + @apply text-amber-500; + } + + .menu-toggle { + @apply inline-flex h-11 items-center justify-center rounded-full border border-stone-300/70 bg-white/80 px-5 text-xs font-semibold uppercase tracking-[0.28em] text-stone-900 transition hover:border-amber-500 hover:text-amber-600; + } + + .menu-panel { + @apply order-last w-full rounded-3xl border border-white/70 bg-white/95 p-4 shadow-[0_16px_34px_rgba(20,33,61,0.12)] backdrop-blur lg:order-none lg:rounded-none lg:border-0 lg:bg-transparent lg:p-0 lg:shadow-none; + } + + .desktop-nav { + @apply flex items-center justify-center gap-8 xl:gap-12; + } + + .desktop-nav__link { + @apply inline-flex items-center py-4 text-[1.04rem] font-medium text-stone-800 transition hover:text-amber-600; + } + + .desktop-nav__toggle { + @apply inline-flex h-8 w-5 items-center justify-center text-sm text-stone-400 transition hover:text-amber-600; + } + + .desktop-nav__entry--open .desktop-nav__link, + .desktop-nav__entry--open .desktop-nav__toggle { + @apply text-amber-600; + } + + .mega-menu { + background: linear-gradient(180deg, rgba(255, 252, 247, 0.98), rgba(252, 248, 240, 0.98)); + @apply absolute inset-x-0 top-full z-50 border-t border-white/70 shadow-[0_28px_60px_rgba(20,33,61,0.16)] backdrop-blur; + } + + .mega-menu__grid { + @apply site-container grid grid-cols-2 gap-x-12 gap-y-10 py-10 lg:grid-cols-4 xl:grid-cols-5; + } + + .mega-menu__heading { + @apply mb-4 inline-flex text-sm font-semibold uppercase tracking-[0.12em] text-stone-900 transition hover:text-amber-600; + } + + .mega-menu__link { + @apply inline-flex text-[1.02rem] leading-7 text-stone-700 transition hover:translate-x-1 hover:text-amber-600; + } + + .mega-menu__all { + @apply col-span-full flex items-start justify-end border-t border-stone-200 pt-5; + } + + .mega-menu__all-link { + @apply inline-flex text-sm font-semibold uppercase tracking-[0.18em] text-amber-600 transition hover:text-amber-700; + } + + .header-actions { + @apply ml-0 flex items-center gap-4 text-2xl text-stone-900 lg:ml-auto; + } + + .nav-icon { + @apply flex h-11 w-11 items-center justify-center rounded-full bg-white/70 text-[1.35rem] shadow-[0_10px_24px_rgba(20,33,61,0.08)] transition hover:-translate-y-0.5 hover:text-amber-600; + } + + @media (max-width: 1023px) { + .desktop-nav, + .mega-menu { + display: none; + } + } + button[type='submit'] { box-shadow: var(--cta-glow); } diff --git a/web/src/app.js b/web/src/app.js index 7f0b644..f7b1b27 100644 --- a/web/src/app.js +++ b/web/src/app.js @@ -1,6 +1,102 @@ const root = document.documentElement; root.dataset.js = "ready"; +const menuToggle = document.querySelector("[data-menu-toggle]"); +const menuPanel = document.querySelector("[data-menu-panel]"); +const megaTriggers = [...document.querySelectorAll("[data-mega-trigger]")]; +const megaMenus = [...document.querySelectorAll("[data-mega-menu]")]; + +if (menuToggle && menuPanel) { + const closeMenu = () => { + menuPanel.classList.add("hidden"); + menuToggle.setAttribute("aria-expanded", "false"); + }; + + const openMenu = () => { + menuPanel.classList.remove("hidden"); + menuToggle.setAttribute("aria-expanded", "true"); + }; + + menuToggle.addEventListener("click", () => { + const expanded = menuToggle.getAttribute("aria-expanded") === "true"; + if (expanded) { + closeMenu(); + return; + } + openMenu(); + }); + + window.addEventListener("resize", () => { + if (window.innerWidth >= 1024) { + menuPanel.classList.remove("hidden"); + menuToggle.setAttribute("aria-expanded", "true"); + return; + } + closeMenu(); + }); +} + +if (megaTriggers.length > 0 && megaMenus.length > 0) { + const closeMegaMenus = () => { + megaTriggers.forEach((trigger) => { + trigger.setAttribute("aria-expanded", "false"); + trigger.closest(".desktop-nav__entry")?.classList.remove("desktop-nav__entry--open"); + }); + megaMenus.forEach((menu) => { + menu.classList.add("hidden"); + }); + }; + + const openMegaMenu = (trigger) => { + const target = trigger.dataset.megaTarget; + if (!target) return; + closeMegaMenus(); + trigger.setAttribute("aria-expanded", "true"); + trigger.closest(".desktop-nav__entry")?.classList.add("desktop-nav__entry--open"); + document.getElementById(target)?.classList.remove("hidden"); + }; + + megaTriggers.forEach((trigger) => { + trigger.addEventListener("click", (event) => { + if (window.innerWidth < 1024) return; + const expanded = trigger.getAttribute("aria-expanded") === "true"; + const isLinkTrigger = trigger instanceof HTMLAnchorElement; + if (isLinkTrigger && expanded) { + return; + } + event.preventDefault(); + if (expanded) { + closeMegaMenus(); + return; + } + openMegaMenu(trigger); + }); + }); + + document.addEventListener("click", (event) => { + if (window.innerWidth < 1024) return; + const target = event.target; + if (!(target instanceof Node)) return; + const insideTrigger = megaTriggers.some((trigger) => trigger.contains(target)); + const insideMenu = megaMenus.some((menu) => menu.contains(target)); + if (!insideTrigger && !insideMenu) { + closeMegaMenus(); + } + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + closeMegaMenus(); + } + }); + + window.addEventListener("resize", () => { + if (window.innerWidth < 1024) { + closeMegaMenus(); + } + }); +} + const cartButton = document.querySelector("button[type='submit']"); if (cartButton) { cartButton.addEventListener("mouseenter", () => {