From 35ee2897657c56cdc621551d26764d5888fc9749 Mon Sep 17 00:00:00 2001 From: Achim Rohn Date: Fri, 12 Sep 2025 23:26:10 +0200 Subject: [PATCH] Add white label template for starter --- starter/go.mod | 4 +- starter/go.sum | 16 - starter/index/index.go | 111 +++++++ starter/main.go | 8 +- starter/page/page.go | 226 +++++++++++++ starter/routes/routing.go | 112 +++++++ starter/static/scripts/language-select.js | 39 +++ starter/static/styles.css | 380 ++++++++++++++++++++++ 8 files changed, 874 insertions(+), 22 deletions(-) create mode 100644 starter/index/index.go create mode 100644 starter/page/page.go create mode 100644 starter/routes/routing.go create mode 100644 starter/static/scripts/language-select.js create mode 100644 starter/static/styles.css diff --git a/starter/go.mod b/starter/go.mod index 4db9522..d27b92b 100644 --- a/starter/go.mod +++ b/starter/go.mod @@ -4,13 +4,14 @@ go 1.25.0 require ( ersteller-lib v0.0.0 + github.com/jackc/pgx/v5 v5.7.6 github.com/joho/godotenv v1.5.1 + maragu.dev/gomponents v1.2.0 ) require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.7.6 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/labstack/echo/v4 v4.13.4 // indirect github.com/labstack/gommon v0.4.2 // indirect @@ -24,7 +25,6 @@ require ( golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.29.0 // indirect - maragu.dev/gomponents v1.2.0 // indirect maragu.dev/gomponents-htmx v0.6.1 // indirect ) diff --git a/starter/go.sum b/starter/go.sum index 7660eae..d2d8466 100644 --- a/starter/go.sum +++ b/starter/go.sum @@ -5,8 +5,6 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= -github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= @@ -21,8 +19,6 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.29 h1:1O6nRLJKvsi1H2Sj0Hzdfojwt8GiGKm+LOfLaBFaouQ= -github.com/mattn/go-sqlite3 v1.14.29/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -36,33 +32,21 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -maragu.dev/gomponents v1.1.0 h1:iCybZZChHr1eSlvkWp/JP3CrZGzctLudQ/JI3sBcO4U= -maragu.dev/gomponents v1.1.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM= maragu.dev/gomponents v1.2.0 h1:H7/N5htz1GCnhu0HB1GasluWeU2rJZOYztVEyN61iTc= maragu.dev/gomponents v1.2.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM= maragu.dev/gomponents-htmx v0.6.1 h1:vXXOkvqEDKYxSwD1UwqmVp12YwFSuM6u8lsRn7Evyng= diff --git a/starter/index/index.go b/starter/index/index.go new file mode 100644 index 0000000..521c6f2 --- /dev/null +++ b/starter/index/index.go @@ -0,0 +1,111 @@ +package index + +import ( + . "ersteller-lib" + + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +const IndexPath = "/" +const IndexPathDe = "/" + +var indexTexts *IndexTexts + +type IndexTexts struct { + PageTitle I18nText + PageDescription I18nText + WelcomeTitle I18nText + WelcomeText I18nText + FeaturesTitle I18nText + FeaturesText I18nText + ContactTitle I18nText + ContactText I18nText +} + +type Page struct { + createPage CreateHtmxPageFunc + ViewRoute HtmxRoute +} + +func NewPage(createPage CreateHtmxPageFunc, server HtmxServer, path *ActivePath) *Page { + if indexTexts == nil { + createIndexTexts() + } + page := &Page{ + createPage: createPage, + } + page.ViewRoute = NewHtmxGetRoute(page.View, LanguagePaths{ + En: IndexPath, + De: IndexPathDe, + }).SetActivePath(path) + page.ViewRoute.Add(server) + return page +} + +func createIndexTexts() { + indexTexts = &IndexTexts{ + PageTitle: NewI18nText(map[Language]string{ + En: "Home", + De: "Startseite", + }), + PageDescription: NewI18nText(map[Language]string{ + En: "Welcome to our application - Your digital solution", + De: "Willkommen bei unserer Anwendung - Ihre digitale Lösung", + }), + WelcomeTitle: NewI18nText(map[Language]string{ + En: "Welcome to Your Application", + De: "Willkommen bei Ihrer Anwendung", + }), + WelcomeText: NewI18nText(map[Language]string{ + En: "This is your white label template starter kit. Customize this content to match your brand and requirements.", + De: "Dies ist Ihr White-Label-Template-Starter-Kit. Passen Sie diesen Inhalt an Ihre Marke und Anforderungen an.", + }), + FeaturesTitle: NewI18nText(map[Language]string{ + En: "Key Features", + De: "Hauptfunktionen", + }), + FeaturesText: NewI18nText(map[Language]string{ + En: "Built with modern web technologies, responsive design, and multi-language support. Perfect foundation for your next project.", + De: "Entwickelt mit modernen Web-Technologien, responsivem Design und mehrsprachiger Unterstützung. Perfekte Grundlage für Ihr nächstes Projekt.", + }), + ContactTitle: NewI18nText(map[Language]string{ + En: "Get Started", + De: "Erste Schritte", + }), + ContactText: NewI18nText(map[Language]string{ + En: "Ready to customize this template? Replace this placeholder content with your own information and branding.", + De: "Bereit, diese Vorlage anzupassen? Ersetzen Sie diesen Platzhalter-Inhalt durch Ihre eigenen Informationen und Ihr Branding.", + }), + } +} + +func (p *Page) getMetaData() PageWebsiteMetaData { + return PageWebsiteMetaData{ + Title: indexTexts.PageTitle, + Lang: En, + Description: indexTexts.PageDescription, + } +} + +func (p *Page) View(c HtmxContext) { + content := IndexContent(c.GetLanguage()) + p.createPage(c, p.getMetaData(), content) +} + +func IndexContent(language Language) Group { + return []Node{ + Div(Class("hero-section"), + H1(Class("hero-title"), Text(indexTexts.WelcomeTitle.FromLang(language))), + P(Class("hero-description"), Text(indexTexts.WelcomeText.FromLang(language))), + ), + Div(Class("content-section"), + H2(Class("section-title"), Text(indexTexts.FeaturesTitle.FromLang(language))), + P(Class("section-description"), Text(indexTexts.FeaturesText.FromLang(language))), + ), + Div(Class("content-section"), + H2(Class("section-title"), Text(indexTexts.ContactTitle.FromLang(language))), + P(Class("section-description"), Text(indexTexts.ContactText.FromLang(language))), + ), + } +} diff --git a/starter/main.go b/starter/main.go index c98b749..b24fecc 100644 --- a/starter/main.go +++ b/starter/main.go @@ -3,13 +3,13 @@ package main import ( . "ersteller-lib" "ersteller-lib/starter/env" + "ersteller-lib/starter/routes" "log" "net/http" ) func main() { GlobalI18n = GlobalI18nImplementation{} - server := NewHtmxServer() environment := env.LoadEnvironment() @@ -20,7 +20,7 @@ func main() { } defer db.Close() - Debug("starting rest api on port 8089") - serverWithMiddleWare := UseMiddleware(server, LoggingMiddleware, MakeGzipHandler) - log.Fatal(http.ListenAndServe(":8090", serverWithMiddleWare)) + Debug("starting white label app on port 8090") + handler := routes.CreateApi(db) + log.Fatal(http.ListenAndServe(":8090", handler)) } diff --git a/starter/page/page.go b/starter/page/page.go new file mode 100644 index 0000000..51986d6 --- /dev/null +++ b/starter/page/page.go @@ -0,0 +1,226 @@ +package page + +import ( + . "ersteller-lib" + "fmt" + "time" + + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/components" + . "maragu.dev/gomponents/html" +) + +const DeIndexUrl = "/de/index" +const EnIndexUrl = "/en/index" +const DefaultLanguage = En + +var _texts *Texts + +func CreatePage(req HtmxContext, metadata PageWebsiteMetaData, content ...Node) { + metadata.Lang = req.GetLanguage() + + var contentForFunction Node + if len(content) > 0 { + contentForFunction = Div(content...) + } else if len(content) == 0 { + contentForFunction = Div() + } else if len(content) == 1 { + contentForFunction = content[0] + } + + styles := []string{ + "/static/styles.css", + } + for _, src := range metadata.StyleSrcs { + styles = append(styles, src) + } + + scripts := []string{ + "/static/scripts/language-select.js", + "/static/htmx.js", + } + for _, src := range metadata.ScriptSrcs { + scripts = append(scripts, src) + } + + page := HTML5(HTML5Props{ + Title: "White Label App " + metadata.Title.From(req), + Language: string(metadata.Lang), + Description: metadata.Description.From(req), + Head: []Node{ + Map(styles, func(s string) Node { + return Link(Rel("stylesheet"), Href(s)) + }), + Map(scripts, func(s string) Node { + return Script(Type("text/javascript"), Src(s)) + }), + addLanguageSelectScript(metadata), + }, + Body: []Node{ + Header(Class("header"), + Div(Class("header-content"), + Div(Class("logo"), + Span(Class("logo-icon"), Text("🚀")), + Span(Class("logo-text"), Text("White Label App")), + ), + getNav(req, metadata), + getLanguageSwitcher(req, metadata), + ), + ), + Div(Class("container"), + contentForFunction, + ), + Footer(Class("footer"), + Div(Class("footer-menu"), Aria("label", "Footer Menu"), + getFooterMenu(req, metadata), + ), + P(Class("footer-disclaimer"), Text(getTexts().disclaimer.From(req))), + P(Text(getTexts().getCopyright(req.GetLanguage()))), + ), + }, + }) + req.Render(page) +} + +func addLanguageSelectScript(metadata PageWebsiteMetaData) Node { + script := InlineTemplate(` +(function() { + const langs = { + de: "$.DeUrl$", + en: "$.EnUrl$", + }; + const currentLang = "$.CurrentLang$"; + const defaultLang = "$.DefaultLang$"; + selectWebsiteLanguage(currentLang, langs, defaultLang); +})();`, struct { + CurrentLang string + DeUrl string + EnUrl string + DefaultLang string + }{ + CurrentLang: string(metadata.Lang), + DeUrl: DeIndexUrl, + EnUrl: EnIndexUrl, + DefaultLang: string(DefaultLanguage), + }) + return Script(Type("text/javascript"), Raw(script)) +} + +func getNav(c HtmxContext, metadata PageWebsiteMetaData) Node { + return Nav(Class("nav"), Aria("label", "Main Menu"), + Map(metadata.NavItems, func(path ActivePath) Node { + return createNavItem(c, path) + }), + ) +} + +func createNavItem(c HtmxContext, activePath ActivePath) Node { + isActive := activePath.IsActive(c) + return A( + Href(activePath.GetPath(c.GetLanguage())), + Text(activePath.From(c)), + If(isActive, Attr("aria-current", "page")), + If(isActive, Class("selected")), + ) +} + +func getLanguageSwitcher(c HtmxContext, metadata PageWebsiteMetaData) Node { + currentLang := c.GetLanguage() + + var currentRoute HtmxRoute + for _, route := range c.GetAllRoutes() { + if route.IsCurrentRoute(c) { + currentRoute = route + break + } + } + + return Div(Class("language-switcher"), + createLanguageButton(En, currentLang, currentRoute, c), + createLanguageButton(De, currentLang, currentRoute, c), + ) +} + +func getFooterMenu(c HtmxContext, metadata PageWebsiteMetaData) Node { + lang := c.GetLanguage() + return Div( + Map(metadata.FooterNavItems, func(path ActivePath) Node { + return Span( + A(Href(path.GetPath(lang)), Class("footer-link"), Text(path.From(c))), + ) + }), + ) +} + +func createLanguageButton(lang Language, currentLang Language, route HtmxRoute, c HtmxContext) Node { + isActive := lang == currentLang + var href string + + if route != nil { + href = route.ToUrlFromContext(c, lang) + } else { + // Fallback to index page if no active path found + if lang == En { + href = EnIndexUrl + } else { + href = DeIndexUrl + } + } + + var langText string + if lang == En { + langText = "EN" + } else { + langText = "DE" + } + + // Combine classes properly + var classes string + if isActive { + classes = "language-button active" + } else { + classes = "language-button inactive" + } + + return A( + Href(href), + Text(langText), + Class(classes), + ) +} + +type Texts struct { + disclaimer I18nText + copyright I18nText +} + +func NewTexts() *Texts { + return &Texts{ + disclaimer: NewI18nText(map[Language]string{ + De: "Dies ist eine White-Label-Vorlage. Passen Sie den Inhalt an Ihre Bedürfnisse an.", + En: "This is a white label template. Customize the content to fit your needs.", + }), + copyright: NewI18nText(map[Language]string{ + De: "© 2020 - $Year$ White Label App. Alle Rechte vorbehalten.", + En: "© 2020 - $Year$ White Label App. All rights reserved.", + }), + } +} + +func (t *Texts) getCopyright(lang Language) string { + currentYear := fmt.Sprint(time.Now().Year()) + text := t.copyright.FromLang(lang) + return InlineTemplate(text, struct { + Year string + }{ + Year: currentYear, + }) +} + +func getTexts() *Texts { + if _texts != nil { + return _texts + } + _texts = NewTexts() + return _texts +} diff --git a/starter/routes/routing.go b/starter/routes/routing.go new file mode 100644 index 0000000..6866fb5 --- /dev/null +++ b/starter/routes/routing.go @@ -0,0 +1,112 @@ +package routes + +import ( + . "ersteller-lib" + "ersteller-lib/starter/index" + "ersteller-lib/starter/page" + "net/http" + + "github.com/jackc/pgx/v5/pgxpool" + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +func CreateApi(db *pgxpool.Pool) http.Handler { + server := NewHtmxServer() + + HtmxRouteDebugTrace = true + + indexActivePath := NewActivePath(map[Language]string{ + En: "Home", + De: "Startseite", + }, LanguagePaths{ + En: index.IndexPath, + De: index.IndexPathDe, + }) + + aboutActivePath := NewActivePath(map[Language]string{ + En: "About", + De: "Über uns", + }, LanguagePaths{ + En: "/about", + De: "/de/ueber-uns", + }) + + contactActivePath := NewActivePath(map[Language]string{ + En: "Contact", + De: "Kontakt", + }, LanguagePaths{ + En: "/contact", + De: "/de/kontakt", + }) + + // Main navigation items + activePaths := []ActivePath{indexActivePath, aboutActivePath, contactActivePath} + + // Footer navigation items (placeholder - can be customized) + footerPaths := []ActivePath{} + + createPageFunc := createPage(activePaths, footerPaths) + indexPage := index.NewPage(createPageFunc, server, &indexActivePath) + server.HandleStaticAndDefaultPath(indexPage.ViewRoute, En) + + // Add placeholder routes for About and Contact pages + // These can be implemented as needed + aboutRoute := NewHtmxGetRoute(func(c HtmxContext) { + content := []Node{ + Div(Class("hero-section"), + H1(Class("hero-title"), Text("About Us")), + P(Class("hero-description"), Text("This is a placeholder about page. Customize this content to tell your story.")), + ), + } + createPageFunc(c, PageWebsiteMetaData{ + Title: NewI18nText(map[Language]string{ + En: "About", + De: "Über uns", + }), + Description: NewI18nText(map[Language]string{ + En: "Learn more about us", + De: "Erfahren Sie mehr über uns", + }), + }, content...) + }, LanguagePaths{ + En: "/about", + De: "/de/ueber-uns", + }).SetActivePath(&aboutActivePath) + aboutRoute.Add(server) + + contactRoute := NewHtmxGetRoute(func(c HtmxContext) { + content := []Node{ + Div(Class("hero-section"), + H1(Class("hero-title"), Text("Contact Us")), + P(Class("hero-description"), Text("This is a placeholder contact page. Add your contact information and forms here.")), + ), + } + createPageFunc(c, PageWebsiteMetaData{ + Title: NewI18nText(map[Language]string{ + En: "Contact", + De: "Kontakt", + }), + Description: NewI18nText(map[Language]string{ + En: "Get in touch with us", + De: "Nehmen Sie Kontakt mit uns auf", + }), + }, content...) + }, LanguagePaths{ + En: "/contact", + De: "/de/kontakt", + }).SetActivePath(&contactActivePath) + contactRoute.Add(server) + + serverWithMiddleWare := UseMiddleware(server, LoggingMiddleware, MakeGzipHandler) + + return serverWithMiddleWare +} + +func createPage(activePaths []ActivePath, footerPaths []ActivePath) CreateHtmxPageFunc { + return func(req HtmxContext, metadata PageWebsiteMetaData, content ...Node) { + metadata.NavItems = activePaths + metadata.FooterNavItems = footerPaths + page.CreatePage(req, metadata, content...) + } +} diff --git a/starter/static/scripts/language-select.js b/starter/static/scripts/language-select.js new file mode 100644 index 0000000..e6b3758 --- /dev/null +++ b/starter/static/scripts/language-select.js @@ -0,0 +1,39 @@ +// Language selection functionality for the white label template +function selectWebsiteLanguage(currentLang, langs, defaultLang) { + // This function handles language switching for the website + // It can be extended to handle URL routing and language persistence + + console.log('Current language:', currentLang); + console.log('Available languages:', langs); + console.log('Default language:', defaultLang); + + // Add any additional language switching logic here + // For example, saving language preference to localStorage + if (typeof Storage !== 'undefined') { + localStorage.setItem('preferredLanguage', currentLang); + } +} + +// Function to get preferred language from storage +function getPreferredLanguage() { + if (typeof Storage !== 'undefined') { + return localStorage.getItem('preferredLanguage'); + } + return null; +} + +// Initialize language selection on page load +document.addEventListener('DOMContentLoaded', function() { + const preferredLang = getPreferredLanguage(); + if (preferredLang) { + console.log('Preferred language from storage:', preferredLang); + } +}); + +// Export functions for potential module use +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + selectWebsiteLanguage, + getPreferredLanguage + }; +} \ No newline at end of file diff --git a/starter/static/styles.css b/starter/static/styles.css new file mode 100644 index 0000000..9595abc --- /dev/null +++ b/starter/static/styles.css @@ -0,0 +1,380 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Arial', sans-serif; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + color: #333333; + min-height: 100vh; +} + +.header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 20px 0; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + position: relative; + overflow: hidden; +} + +.header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml,') repeat-x; + background-size: 60px 100%; + opacity: 0.3; +} + +.header-content { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + z-index: 2; +} + +.logo { + display: flex; + align-items: center; + gap: 15px; +} + +.logo-icon { + font-size: 2.5rem; + background: linear-gradient(45deg, #667eea, #764ba2); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.logo-text { + font-size: 2rem; + font-weight: bold; + color: #ffffff; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); +} + +.nav { + display: flex; + gap: 30px; +} + +.nav a { + color: #ffffff; + text-decoration: none; + font-weight: bold; + padding: 10px 15px; + border-radius: 5px; + transition: all 0.3s ease; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); +} + +.nav a:hover { + background: rgba(255, 255, 255, 0.2); + transform: translateY(-2px); +} + +.nav a.selected { + background: linear-gradient(45deg, #667eea, #764ba2); + border: 2px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + transform: translateY(-1px); +} + +.language-switcher { + display: flex; + gap: 3px; + align-items: center; + background: rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 4px; + border: 1px solid rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); +} + +.language-button { + color: #ffffff; + text-decoration: none; + font-weight: bold; + padding: 8px 14px; + border-radius: 6px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); + font-size: 0.85rem; + min-width: 36px; + text-align: center; + display: block; +} + +.language-button.active { + background: rgba(255, 255, 255, 0.3); + transform: scale(1.05); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.language-button.inactive:hover { + background: rgba(255, 255, 255, 0.15); + transform: scale(1.02); +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 40px 20px; +} + +.hero-section { + text-align: center; + padding: 60px 0; + background: linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.7) 100%); + border-radius: 15px; + margin-bottom: 40px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); +} + +.hero-title { + font-size: 3rem; + font-weight: bold; + color: #333333; + margin-bottom: 20px; + background: linear-gradient(45deg, #667eea, #764ba2); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: none; +} + +.hero-description { + font-size: 1.2rem; + color: #666666; + line-height: 1.6; + max-width: 600px; + margin: 0 auto; +} + +.content-section { + background: rgba(255, 255, 255, 0.8); + padding: 40px; + border-radius: 10px; + margin-bottom: 30px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.content-section:hover { + transform: translateY(-5px); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); +} + +.section-title { + font-size: 2rem; + color: #333333; + margin-bottom: 15px; + background: linear-gradient(45deg, #667eea, #764ba2); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.section-description { + font-size: 1.1rem; + color: #555555; + line-height: 1.6; +} + +.footer { + background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); + color: #ffffff; + padding: 40px 0 20px 0; + margin-top: 60px; +} + +.footer-menu { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; + display: flex; + justify-content: center; + gap: 30px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.footer-link { + color: #ffffff; + text-decoration: none; + font-weight: 500; + padding: 8px 12px; + border-radius: 5px; + transition: all 0.3s ease; +} + +.footer-link:hover { + background: rgba(255, 255, 255, 0.1); + transform: translateY(-2px); +} + +.footer-disclaimer { + text-align: center; + color: rgba(255, 255, 255, 0.8); + font-size: 0.9rem; + margin: 15px 0 10px 0; + font-style: italic; + max-width: 1200px; + margin-left: auto; + margin-right: auto; + padding: 0 20px; +} + +.footer p:last-child { + text-align: center; + color: rgba(255, 255, 255, 0.7); + font-size: 0.85rem; + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +/* Mobile Responsive Styles */ +@media (max-width: 768px) { + .header-content { + flex-direction: column; + gap: 20px; + text-align: center; + } + + .nav { + gap: 15px; + flex-wrap: wrap; + justify-content: center; + } + + .nav a { + padding: 8px 12px; + font-size: 0.9rem; + } + + .logo-text { + font-size: 1.5rem; + } + + .logo-icon { + font-size: 2rem; + } + + .hero-title { + font-size: 2.2rem; + } + + .hero-description { + font-size: 1.1rem; + padding: 0 15px; + } + + .content-section { + padding: 25px 20px; + margin-bottom: 20px; + } + + .section-title { + font-size: 1.6rem; + } + + .section-description { + font-size: 1rem; + } + + .container { + padding: 20px 15px; + } + + .hero-section { + padding: 40px 20px; + margin-bottom: 30px; + } + + .footer-menu { + gap: 15px; + flex-direction: column; + align-items: center; + } + + .footer-link { + font-size: 0.9rem; + padding: 6px 10px; + } + + .footer-disclaimer { + font-size: 0.8rem; + margin: 12px 0 8px 0; + } +} + +/* Extra small screens */ +@media (max-width: 480px) { + .hero-title { + font-size: 1.8rem; + } + + .nav { + gap: 10px; + } + + .nav a { + padding: 6px 10px; + font-size: 0.85rem; + } + + .language-switcher { + padding: 2px; + gap: 2px; + } + + .language-button { + padding: 6px 10px; + font-size: 0.8rem; + min-width: 32px; + } +} + +/* Global link styles */ +a { + color: #667eea; + text-decoration: underline; + text-underline-offset: 0.2em; + text-decoration-thickness: 2px; + text-decoration-color: rgba(102, 126, 234, 0.4); + transition: color 0.2s ease, text-decoration-color 0.2s ease; +} + +a:hover { + color: #764ba2; + text-decoration-color: #764ba2; +} + +a:focus-visible { + outline: 2px solid #667eea; + outline-offset: 2px; + border-radius: 2px; +} + +a:active { + opacity: 0.9; +} + +/* Disabled state support */ +a[aria-disabled="true"], +a.disabled { + opacity: 0.6; + pointer-events: none; + text-decoration-color: rgba(0, 0, 0, 0.2); +} \ No newline at end of file