Add white label template for starter
This commit is contained in:
+2
-2
@@ -4,13 +4,14 @@ go 1.25.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
ersteller-lib v0.0.0
|
ersteller-lib v0.0.0
|
||||||
|
github.com/jackc/pgx/v5 v5.7.6
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
maragu.dev/gomponents v1.2.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/labstack/echo/v4 v4.13.4 // indirect
|
github.com/labstack/echo/v4 v4.13.4 // indirect
|
||||||
github.com/labstack/gommon v0.4.2 // 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/sync v0.17.0 // indirect
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
golang.org/x/text v0.29.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
|
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/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 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
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 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||||
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
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=
|
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-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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
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=
|
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/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 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
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 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
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 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
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 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
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.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 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
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 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
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/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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 h1:H7/N5htz1GCnhu0HB1GasluWeU2rJZOYztVEyN61iTc=
|
||||||
maragu.dev/gomponents v1.2.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=
|
maragu.dev/gomponents v1.2.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=
|
||||||
maragu.dev/gomponents-htmx v0.6.1 h1:vXXOkvqEDKYxSwD1UwqmVp12YwFSuM6u8lsRn7Evyng=
|
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 (
|
import (
|
||||||
. "ersteller-lib"
|
. "ersteller-lib"
|
||||||
"ersteller-lib/starter/env"
|
"ersteller-lib/starter/env"
|
||||||
|
"ersteller-lib/starter/routes"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
GlobalI18n = GlobalI18nImplementation{}
|
GlobalI18n = GlobalI18nImplementation{}
|
||||||
server := NewHtmxServer()
|
|
||||||
|
|
||||||
environment := env.LoadEnvironment()
|
environment := env.LoadEnvironment()
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
Debug("starting rest api on port 8089")
|
Debug("starting white label app on port 8090")
|
||||||
serverWithMiddleWare := UseMiddleware(server, LoggingMiddleware, MakeGzipHandler)
|
handler := routes.CreateApi(db)
|
||||||
log.Fatal(http.ListenAndServe(":8090", serverWithMiddleWare))
|
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