Add white label template for starter
This commit is contained in:
+2
-2
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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))),
|
||||
),
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><polygon points="0,100 50,0 100,100" fill="rgba(255,255,255,0.1)"/></svg>') 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);
|
||||
}
|
||||
Reference in New Issue
Block a user