diff options
45 files changed, 1328 insertions, 862 deletions
@@ -19,8 +19,8 @@ COPY --from=build-stage /builddir/sayana-web /app/sayana-web COPY --from=build-stage /builddir/migrations /app/migrations COPY --from=build-stage /builddir/static /app/static COPY --from=build-stage /builddir/views /app/views -COPY --from=build-stage /builddir/locale/*.yaml /app/locale/ -COPY --from=build-stage /builddir/config/config*.yaml /app/config/ +COPY --from=build-stage /builddir/l10n/*.yaml /app/l10n/ +COPY --from=build-stage /builddir/config/config.*.yaml /app/config/ RUN apk add --no-cache tzdata sqlite diff --git a/config/config.go b/config/config.go index 0b1570a..8ab1bfc 100644 --- a/config/config.go +++ b/config/config.go @@ -22,7 +22,7 @@ type Config struct { CanonicalEndpoint string `json:"CanonicalEndpoint" yaml:"canonicalEndpoint" validate:"required"` Meta MetaConfig `json:"Meta" yaml:"meta"` PhotoStorage PhotoStorageConfig `json:"PhotoStorage" yaml:"photoStorage"` - StaticStorage PhotoTypeConfig `json:"StaticStorage" yaml:"staticStorage"` + StaticStorage StaticStorageConfig `json:"StaticStorage" yaml:"staticStorage" validate:"required"` AllowOrigins []string `json:"AllowOrigins" yaml:"allowOrigins"` } @@ -261,6 +261,16 @@ type HomePageGifsConfig struct { Indexes []string `json:"Indexes" yaml:"indexes"` } +type StaticStorageConfig struct { + BaseUrl string `json:"BaseUrl" yaml:"baseUrl" validate:"url,required"` + Map MapStorageConfig `json:"Map" yaml:"map" validate:"required"` +} + +type MapStorageConfig struct { + BaseUrl string `json:"BaseUrl" yaml:"baseUrl" validate:"url,required"` + PMTiles string `json:"PMTiles" yaml:"pmTiles" validate:"required"` +} + func LoadConfig(path string, config *Config) error { fileBytes, err := os.ReadFile(path) if err != nil { diff --git a/config/config.local.yaml b/config/config.local.yaml index d3e2e33..7fe612b 100644 --- a/config/config.local.yaml +++ b/config/config.local.yaml @@ -70,9 +70,13 @@ photoStorage: thumbnail320p: baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp320/%s.webp homePageGifs: - baseUrl: https://storage.yandexcloud.kz/sayauz-static/home-page-gifs/otter-%s.gif + baseUrl: https://cdn.saya.uz/static/home-page-gifs/otter-%s.gif indexes: ["1", "2", "3"] staticStorage: - baseUrl: https://storage.yandexcloud.kz/sayauz-static + baseUrl: https://cdn.saya.uz/static + map: + baseUrl: https://cdn.saya.uz/map + pmTiles: global.pmtiles allowOrigins: - https://storage.yandexcloud.kz + - https://cdn.saya.uz diff --git a/config/config.prod.yaml b/config/config.prod.yaml index e5785f8..5df5422 100644 --- a/config/config.prod.yaml +++ b/config/config.prod.yaml @@ -73,9 +73,13 @@ photoStorage: thumbnail320p: baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp320/%s.webp homePageGifs: - baseUrl: https://storage.yandexcloud.kz/sayauz-static/home-page-gifs/otter-%s.gif + baseUrl: https://cdn.saya.uz/static/home-page-gifs/otter-%s.gif indexes: ["1", "2", "3"] staticStorage: - baseUrl: https://storage.yandexcloud.kz/sayauz-static + baseUrl: https://cdn.saya.uz/static + map: + baseUrl: https://cdn.saya.uz/map + pmTiles: global.pmtiles allowOrigins: - https://storage.yandexcloud.kz + - https://cdn.saya.uz diff --git a/config/config.stage.yaml b/config/config.stage.yaml index c2e337b..30feead 100644 --- a/config/config.stage.yaml +++ b/config/config.stage.yaml @@ -31,11 +31,11 @@ localePath: ./locale/ availableLanguages: - name: ru alt: Русский - flag: https://storage.yandexcloud.kz/sayauz-static/flags/ru.svg + flag: https://cdn.saya.uz/static/flags/ru.svg locFile: localization.ru.yaml - name: en alt: English - flag: https://storage.yandexcloud.kz/sayauz-static/flags/gb.svg + flag: https://cdn.saya.uz/static/flags/gb.svg locFile: localization.en.yaml auth: db: @@ -56,23 +56,26 @@ mail: canonicalEndpoint: "https://${FQDN}" photoStorage: full: - baseUrl: https://storage.yandexcloud.kz/sayauz-photos/%s + baseUrl: https://photos.saya.uz/jpeg-full/%s webp: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp/%s.webp + baseUrl: https://photos.saya.uz/webp-full/%s.webp thumbnail1600p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp1600/%s.webp + baseUrl: https://photos.saya.uz/webp-1600p/%s.webp thumbnail1200p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp1200/%s.webp + baseUrl: https://photos.saya.uz/webp-1200p/%s.webp thumbnail800p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp800/%s.webp + baseUrl: https://photos.saya.uz/webp-800p/%s.webp thumbnail560p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp560/%s.webp + baseUrl: https://photos.saya.uz/webp-560p/%s.webp thumbnail320p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp320/%s.webp + baseUrl: https://photos.saya.uz/webp-320p/%s.webp homePageGifs: - baseUrl: https://storage.yandexcloud.kz/sayauz-static/home-page-gifs/otter-%s.gif + baseUrl: https://cdn.saya.uz/static/home-page-gifs/otter-%s.gif indexes: ["1", "2", "3"] staticStorage: - baseUrl: https://storage.yandexcloud.kz/sayauz-static + baseUrl: https://cdn.saya.uz/static + map: + baseUrl: https://cdn.saya.uz/map + pmTiles: global.pmtiles allowOrigins: - - https://storage.yandexcloud.kz + - https://cdn.saya.uz @@ -5,15 +5,15 @@ go 1.26 require ( github.com/Backblaze/blazer v0.7.2 github.com/dgraph-io/ristretto/v2 v2.4.0 - github.com/go-co-op/gocron/v2 v2.21.1 + github.com/go-co-op/gocron/v2 v2.21.2 github.com/go-playground/validator/v10 v10.30.2 github.com/gofiber/fiber/v2 v2.52.13 github.com/golang-migrate/migrate/v4 v4.19.1 github.com/mattn/go-sqlite3 v1.14.44 - github.com/valyala/fasthttp v1.70.0 - github.com/wneessen/go-mail v0.7.2 + github.com/valyala/fasthttp v1.71.0 + github.com/wneessen/go-mail v0.7.3 github.com/yuin/goldmark v1.8.2 - golang.org/x/crypto v0.50.0 + golang.org/x/crypto v0.51.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -40,7 +40,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 github.com/aws/smithy-go v1.25.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect @@ -58,6 +58,6 @@ require ( github.com/mattn/go-runewidth v0.0.23 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.36.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect ) @@ -26,8 +26,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/ku github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1 h1:mxuT1xE+dI54NW3RkNjP8DUT5HXqbkiAFvfdyDFwE5c= -github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4= github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= @@ -53,8 +53,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/go-co-op/gocron/v2 v2.21.1 h1:QYOK6iOQVCut+jDcs4zRdWRTBHRxRCEeeFi1TnAmgbU= -github.com/go-co-op/gocron/v2 v2.21.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U= +github.com/go-co-op/gocron/v2 v2.21.2 h1:bD8/YwkojYHgXFr3iEulL148KBdTbKVxUZzFKpXcdbY= +github.com/go-co-op/gocron/v2 v2.21.2/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -104,22 +104,22 @@ github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= -github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= -github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8= -github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= +github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k= +github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA= +github.com/wneessen/go-mail v0.7.3 h1:g3DravXC5SMlVdboFrQA8Jx95A8sOzoBeS5F+vzNRK0= +github.com/wneessen/go-mail v0.7.3/go.mod h1:QGhBX0yNbc1J+Mkjcu7z2rpj4B4l+BmDY8gYznPC9sk= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/blog/b2.go b/internal/blog/b2.go index 0caa0ba..f2ac890 100644 --- a/internal/blog/b2.go +++ b/internal/blog/b2.go @@ -2,6 +2,7 @@ package blog import ( "context" + "encoding/json" "fmt" "slices" "strings" @@ -37,6 +38,20 @@ func NewB2Client(cfg *config.StorageConfig) (Client, error) { return &B2Client{b2cl: b2cl, bucket: bucket, prefix: b2cfg.Prefix}, nil } +func (c *B2Client) GetMedleys() ([]MedleyEntry, error) { + idxRaw, err := c.readAll(MedleysIndexFileName) + if err != nil { + return nil, fmt.Errorf("read %s: %w", MedleysIndexFileName, err) + } + + var idx []MedleyEntry + if err := json.Unmarshal(idxRaw, &idx); err != nil { + return nil, fmt.Errorf("unmarshal %s: %w", MedleysIndexFileName, err) + } + + return idx, nil +} + func (c *B2Client) Scan(prefix string) ([]*Page, error) { filePaths := []*Page{} @@ -70,17 +85,15 @@ func (c *B2Client) Scan(prefix string) ([]*Page, error) { return nil, fmt.Errorf("failed to parse published time metadata field: %w", err) } - linkParts := strings.Split(obj.Name(), "/") - nameParts := strings.Split(linkParts[len(linkParts)-1], ".") - fileName := strings.Join(nameParts[:len(linkParts)-1], ".") - + link := obj.Name() + fileName := link[strings.LastIndex(link, "/")+1 : strings.LastIndex(link, ".")] tags := strings.Split(attrs.Info["tags"], ",") slices.Sort(tags) - lang, _ := strings.CutPrefix(linkParts[0], c.prefix) + lang, _ := strings.CutPrefix(link[0:strings.Index(link, "/")], c.prefix) filePaths = append(filePaths, &Page{ - Link: obj.Name(), + Link: link, FileName: fileName, Lang: lang, ModifiedTime: attrs.LastModified, @@ -104,7 +117,11 @@ func (c *B2Client) Scan(prefix string) ([]*Page, error) { } func (c *B2Client) ReadAll(path string) ([]byte, error) { - obj := c.bucket.Object(c.prefix + path) + return c.readAll(c.prefix + path) +} + +func (c *B2Client) readAll(path string) ([]byte, error) { + obj := c.bucket.Object(path) if obj == nil { return nil, fmt.Errorf("failed to reference object in B2 bucket") } diff --git a/internal/blog/client.go b/internal/blog/client.go index 56bbee4..53eb0fd 100644 --- a/internal/blog/client.go +++ b/internal/blog/client.go @@ -17,6 +17,7 @@ type Page struct { type Client interface { Scan(prefix string) ([]*Page, error) + GetMedleys() ([]MedleyEntry, error) ReadAll(path string) ([]byte, error) ReadFrontmatter(path string) (metadata *frontmatter.Metadata, markdown []byte, err error) } diff --git a/internal/blog/index.go b/internal/blog/index.go index 861a90e..a09b59e 100644 --- a/internal/blog/index.go +++ b/internal/blog/index.go @@ -1,10 +1,17 @@ package blog -import "time" +import ( + "encoding/json" + "fmt" + "time" + + "github.com/SayaAndy/saya-today-web/internal/frontmatter" +) const IndexFileName = "index.json" +const MedleysIndexFileName = "medleys.json" -const IndexSchemaVersion = 1 +const IndexSchemaVersion = 2 type IndexEntry struct { Link string `json:"link"` @@ -20,13 +27,77 @@ type IndexEntry struct { MedleyPart int `json:"medleyPart,omitempty"` } -type IndexCategory struct { +func (e IndexEntry) Metadata() *frontmatter.Metadata { + return &frontmatter.Metadata{ + Title: e.Title, + ShortDescription: e.ShortDescription, + ActionDate: e.ActionDate, + PublishedTime: e.PublishedTime, + Thumbnail: e.Thumbnail, + Tags: e.Tags, + Geolocation: e.Geolocation, + Medley: e.Medley, + MedleyPart: e.MedleyPart, + } +} + +type IndexV2Category struct { + GeneratedAt time.Time `json:"generatedAt"` + Pages map[string]IndexEntry `json:"pages"` +} + +type IndexV1Category struct { GeneratedAt time.Time `json:"generatedAt"` Pages []IndexEntry `json:"pages"` } type Index struct { - SchemaVersion int `json:"schemaVersion"` - GeneratedAt time.Time `json:"generatedAt"` - Categories map[string]IndexCategory `json:"categories"` + SchemaVersion int `json:"schemaVersion"` + GeneratedAt time.Time `json:"generatedAt"` + Categories any `json:"categories"` +} + +func (idx *Index) UnmarshalJSON(data []byte) error { + var tmp struct { + SchemaVersion int `json:"schemaVersion"` + GeneratedAt time.Time `json:"generatedAt"` + Categories json.RawMessage `json:"categories"` + } + + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + + idx.SchemaVersion = tmp.SchemaVersion + idx.GeneratedAt = tmp.GeneratedAt + + switch tmp.SchemaVersion { + case 1: + var categories map[string]*IndexV1Category + if err := json.Unmarshal(tmp.Categories, &categories); err != nil { + return fmt.Errorf("unmarshal map[string]*IndexV1Category: %w", err) + } + idx.Categories = &categories + case 2: + var categories map[string]*IndexV2Category + if err := json.Unmarshal(tmp.Categories, &categories); err != nil { + return fmt.Errorf("unmarshal map[string]*IndexV2Category: %w", err) + } + idx.Categories = &categories + default: + return fmt.Errorf("unsupported index version: %d", tmp.SchemaVersion) + } + + return nil +} + +type MedleyEntry struct { + Codename string `json:"codename"` + Localnames map[string]string `json:"localnames"` + Content []string `json:"content"` +} + +type MedleyPageEntry struct { + Codename string `json:"codename"` + Position int `json:"position"` } diff --git a/internal/blog/s3.go b/internal/blog/s3.go index bbd5238..3c1a932 100644 --- a/internal/blog/s3.go +++ b/internal/blog/s3.go @@ -1,29 +1,22 @@ package blog import ( + "bytes" "context" "encoding/json" - "errors" "fmt" "io" - "log/slog" - "net/url" - "slices" "strings" - "sync" - "time" "github.com/SayaAndy/saya-today-web/config" "github.com/SayaAndy/saya-today-web/internal/frontmatter" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/aws/aws-sdk-go-v2/aws" awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" - s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" ) -const s3ScanConcurrency = 32 - type S3Client struct { prefix string bucketName string @@ -66,39 +59,38 @@ func NewS3Client(cfg *config.StorageConfig) (Client, error) { return &S3Client{s3cfg.Prefix, s3cfg.BucketName, s3cl}, nil } -func (c *S3Client) Scan(prefix string) ([]*Page, error) { - pages, err := c.scanFromIndex(prefix) - if err == nil { - return pages, nil +func (c *S3Client) GetMedleys() ([]MedleyEntry, error) { + idxRaw, err := c.readAll(MedleysIndexFileName) + if err != nil { + return nil, fmt.Errorf("read %s: %w", MedleysIndexFileName, err) } - var nsk *s3types.NoSuchKey - if !errors.As(err, &nsk) { - return nil, err + var idx []MedleyEntry + if err := json.Unmarshal(idxRaw, &idx); err != nil { + return nil, fmt.Errorf("unmarshal %s: %w", MedleysIndexFileName, err) } - slog.Warn("index.json missing, falling back to listing", slog.String("prefix", c.prefix)) - return c.scanByListing(prefix) + return idx, nil } -func (c *S3Client) scanFromIndex(prefix string) ([]*Page, error) { +func (c *S3Client) Scan(prefix string) ([]*Page, error) { out, err := c.s3cl.GetObject(context.Background(), &s3.GetObjectInput{ Bucket: aws.String(c.bucketName), Key: aws.String(IndexFileName), }) if err != nil { - return nil, fmt.Errorf("get index.json: %w", err) + return nil, fmt.Errorf("get %s: %w", IndexFileName, err) } defer out.Body.Close() raw, err := io.ReadAll(out.Body) if err != nil { - return nil, fmt.Errorf("read index.json: %w", err) + return nil, fmt.Errorf("read %s: %w", IndexFileName, err) } var idx Index if err := json.Unmarshal(raw, &idx); err != nil { - return nil, fmt.Errorf("unmarshal index.json: %w", err) + return nil, fmt.Errorf("unmarshal %s: %w", IndexFileName, err) } wantLang := "" @@ -108,160 +100,93 @@ func (c *S3Client) scanFromIndex(prefix string) ([]*Page, error) { fullPrefix := c.prefix + prefix pages := make([]*Page, 0) - for catKey, cat := range idx.Categories { - lang, ok := strings.CutPrefix(catKey, c.prefix) - if !ok { - continue - } - if wantLang != "" && wantLang != lang { - continue - } - for _, e := range cat.Pages { - if !strings.HasPrefix(e.Link, fullPrefix) { + + switch idx.SchemaVersion { + case 1: + for catKey, cat := range *idx.Categories.(*map[string]*IndexV1Category) { + lang, ok := strings.CutPrefix(catKey, c.prefix) + if !ok { continue } - linkParts := strings.Split(e.Link, "/") - nameParts := strings.Split(linkParts[len(linkParts)-1], ".") - fileName := strings.Join(nameParts[:len(nameParts)-1], ".") - pages = append(pages, &Page{ - Link: e.Link, - FileName: fileName, - Lang: lang, - ModifiedTime: e.ModifiedTime, - Metadata: &frontmatter.Metadata{ - Title: e.Title, - ShortDescription: e.ShortDescription, - ActionDate: e.ActionDate, - PublishedTime: e.PublishedTime, - Thumbnail: e.Thumbnail, - Tags: e.Tags, - Geolocation: e.Geolocation, - Medley: e.Medley, - MedleyPart: e.MedleyPart, - }, - }) - } - } - return pages, nil -} - -func (c *S3Client) scanByListing(prefix string) ([]*Page, error) { - fullPrefix := c.prefix + prefix - - type candidate struct { - key string - lastModified time.Time - } - var candidates []candidate - - paginator := s3.NewListObjectsV2Paginator(c.s3cl, &s3.ListObjectsV2Input{ - Bucket: aws.String(c.bucketName), - Prefix: aws.String(fullPrefix), - }) - for paginator.HasMorePages() { - output, err := paginator.NextPage(context.Background()) - if err != nil { - return nil, fmt.Errorf("list S3 objects: %w", err) - } - for _, obj := range output.Contents { - key := aws.ToString(obj.Key) - if !strings.HasSuffix(key, ".md") { + if wantLang != "" && wantLang != lang { continue } - candidates = append(candidates, candidate{key, aws.ToTime(obj.LastModified)}) - } - } - - pages := make([]*Page, len(candidates)) - sem := make(chan struct{}, s3ScanConcurrency) - var wg sync.WaitGroup - var firstErr error - var errMu sync.Mutex - - for i, cand := range candidates { - sem <- struct{}{} - wg.Go(func() { - defer func() { <-sem }() - - head, err := c.s3cl.HeadObject(context.Background(), &s3.HeadObjectInput{ - Bucket: aws.String(c.bucketName), - Key: aws.String(cand.key), - }) - if err != nil { - errMu.Lock() - if firstErr == nil { - firstErr = fmt.Errorf("head S3 object %s: %w", cand.key, err) + for _, e := range cat.Pages { + if !strings.HasPrefix(e.Link, fullPrefix) { + continue } - errMu.Unlock() - return + fileName := e.Link[strings.LastIndex(e.Link, "/")+1 : strings.LastIndex(e.Link, ".")] + pages = append(pages, &Page{ + Link: e.Link, + FileName: fileName, + Lang: lang, + ModifiedTime: e.ModifiedTime, + Metadata: &frontmatter.Metadata{ + Title: e.Title, + ShortDescription: e.ShortDescription, + ActionDate: e.ActionDate, + PublishedTime: e.PublishedTime, + Thumbnail: e.Thumbnail, + Tags: e.Tags, + Geolocation: e.Geolocation, + Medley: e.Medley, + MedleyPart: e.MedleyPart, + }, + }) } - - if head.ContentType == nil || !strings.Contains(*head.ContentType, "text/markdown") { - return + } + case 2: + for catKey, cat := range *idx.Categories.(*map[string]*IndexV2Category) { + lang, ok := strings.CutPrefix(catKey, c.prefix) + if !ok { + continue } - meta := head.Metadata - if meta["title"] == "" { - return + if wantLang != "" && wantLang != lang { + continue } - - publishedTime, err := time.Parse(time.RFC3339, meta["published-time"]) - if err != nil { - errMu.Lock() - if firstErr == nil { - firstErr = fmt.Errorf("failed to parse published time metadata field: %w", err) + for codename, e := range cat.Pages { + if !strings.HasPrefix(e.Link, fullPrefix) { + continue } - errMu.Unlock() - return + pages = append(pages, &Page{ + Link: e.Link, + FileName: codename, + Lang: lang, + ModifiedTime: e.ModifiedTime, + Metadata: &frontmatter.Metadata{ + Title: e.Title, + ShortDescription: e.ShortDescription, + ActionDate: e.ActionDate, + PublishedTime: e.PublishedTime, + Thumbnail: e.Thumbnail, + Tags: e.Tags, + Geolocation: e.Geolocation, + Medley: e.Medley, + MedleyPart: e.MedleyPart, + }, + }) } - - linkParts := strings.Split(cand.key, "/") - nameParts := strings.Split(linkParts[len(linkParts)-1], ".") - fileName := strings.Join(nameParts[:len(nameParts)-1], ".") - lang, _ := strings.CutPrefix(linkParts[0], c.prefix) - - tags := strings.Split(meta["tags"], ",") - slices.Sort(tags) - - title, _ := url.QueryUnescape(meta["title"]) - shortDescription, _ := url.QueryUnescape(meta["short-description"]) - thumbnail, _ := url.QueryUnescape(meta["thumbnail"]) - - pages[i] = &Page{ - Link: cand.key, - FileName: fileName, - Lang: lang, - ModifiedTime: cand.lastModified, - Metadata: &frontmatter.Metadata{ - Title: title, - ShortDescription: shortDescription, - ActionDate: meta["action-date"], - PublishedTime: publishedTime, - Thumbnail: thumbnail, - Tags: tags, - Geolocation: meta["geolocation"], - }, - } - }) - } - wg.Wait() - - if firstErr != nil { - return nil, firstErr + } } - out := pages[:0] - for _, p := range pages { - if p != nil { - out = append(out, p) + medleys, _ := c.GetMedleys() + for _, medley := range medleys { + for locale, localname := range medley.Localnames { + l10n.T.SetPath(localname, true, locale, "Medleys", medley.Codename) } } - return out, nil + + return pages, nil } func (c *S3Client) ReadAll(path string) ([]byte, error) { + return c.readAll(c.prefix + path) +} + +func (c *S3Client) readAll(path string) ([]byte, error) { output, err := c.s3cl.GetObject(context.Background(), &s3.GetObjectInput{ Bucket: aws.String(c.bucketName), - Key: aws.String(c.prefix + path), + Key: aws.String(path), }) if err != nil { return nil, fmt.Errorf("get S3 object: %w", err) @@ -277,10 +202,40 @@ func (c *S3Client) ReadAll(path string) ([]byte, error) { } func (c *S3Client) ReadFrontmatter(path string) (metadata *frontmatter.Metadata, markdown []byte, err error) { + idxRaw, err := c.readAll(IndexFileName) + if err != nil { + return nil, nil, fmt.Errorf("read %s: %w", IndexFileName, err) + } + + var idx Index + if err := json.Unmarshal(idxRaw, &idx); err != nil { + return nil, nil, fmt.Errorf("unmarshal %s: %w", IndexFileName, err) + } + contentBytes, err := c.ReadAll(path) if err != nil { return nil, nil, fmt.Errorf("failed to read file for frontmatter parsing: %w", err) } + switch idx.SchemaVersion { + case 1: + return frontmatter.ParseFrontmatter(contentBytes) + case 2: + fullPath := c.prefix + path + page := (*idx.Categories.(*map[string]*IndexV2Category))[fullPath[:strings.LastIndex(fullPath, "/")]].Pages[fullPath[strings.LastIndex(fullPath, "/")+1:strings.LastIndex(fullPath, ".")]] + metadata = page.Metadata() + + if !bytes.HasPrefix(contentBytes, []byte("---\n")) { + return metadata, contentBytes, nil + } + + end := bytes.Index(contentBytes[4:], []byte("\n---\n")) + if end == -1 { + return metadata, contentBytes, nil + } + + return metadata, contentBytes[end+9:], nil + } + return frontmatter.ParseFrontmatter(contentBytes) } diff --git a/internal/mailer/mailer.go b/internal/mailer/mailer.go index ffa7b30..33db4f7 100644 --- a/internal/mailer/mailer.go +++ b/internal/mailer/mailer.go @@ -16,7 +16,7 @@ import ( "github.com/SayaAndy/saya-today-web/internal/blog" "github.com/SayaAndy/saya-today-web/internal/templatemanager" - "github.com/SayaAndy/saya-today-web/locale" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/dgraph-io/ristretto/v2" "github.com/gofiber/fiber/v2" "github.com/wneessen/go-mail" @@ -43,8 +43,6 @@ type Mailer struct { hashMap map[string][]byte hashMapMutex sync.RWMutex - - l map[string]*locale.LocaleConfig } type SubscriptionType int @@ -55,7 +53,7 @@ const ( Specific ) -func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string, mailAddress string, username string, password string, salt []byte, localization map[string]*locale.LocaleConfig) (*Mailer, error) { +func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string, mailAddress string, username string, password string, salt []byte) (*Mailer, error) { verificationCodes, err := ristretto.NewCache(&ristretto.Config[uint64, string]{ NumCounters: 10000, MaxCost: 1 << 20, // 1 MB @@ -111,7 +109,7 @@ func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string End time.Time CodeExpiry time.Time }, 0), - l: localization}, nil + }, nil } func (m *Mailer) GetHash(id string) []byte { @@ -261,10 +259,9 @@ func (m *Mailer) SendVerificationCode(userId string, address string, lang string verificationInfo := fmt.Sprintf("%s.%s", base64.RawStdEncoding.EncodeToString([]byte(userId)), base64.RawStdEncoding.EncodeToString([]byte(address))) m.verificationCodes.Set(verificationCode, verificationInfo, int64(len(verificationInfo)+8)) - message.Subject(m.l[lang].Mail.VerifyEmail.Subject) + message.Subject(l10n.T.GetPath(lang, "Mail", "VerifyEmail", "Subject").(string)) msg, err := m.tm.Render("verify-email", fiber.Map{ - "L": m.l[lang], "Lang": lang, "VerificationCode": fmt.Sprintf("%X", verificationCode), "ClientHost": m.clientHost, @@ -499,11 +496,10 @@ rowLoop: rand.Read(unsubscribeCodeBytes) unsubscribeCode := binary.LittleEndian.Uint64(unsubscribeCodeBytes) - unsubscribeFooter := strings.Replace(m.l[post.Lang].Mail.UnsubscribeFooter, "{}", fmt.Sprintf(`<a style="color: #273de1 !important;" href="https://%s/%s/user/unsubscribe?code=%X">`, m.clientHost, post.Lang, unsubscribeCode), 1) + unsubscribeFooter := strings.Replace(l10n.T.GetPath(post.Lang, "Mail", "UnsubscribeFooter").(string), "{}", fmt.Sprintf(`<a style="color: #273de1 !important;" href="https://%s/%s/user/unsubscribe?code=%X">`, m.clientHost, post.Lang, unsubscribeCode), 1) unsubscribeFooter = strings.Replace(unsubscribeFooter, "{/}", "</a>", 1) msgBody, err := m.tm.Render("new-post", fiber.Map{ - "L": m.l[post.Lang], "Lang": post.Lang, "Post": post, "ClientHost": m.clientHost, @@ -529,7 +525,7 @@ rowLoop: message.SetMessageID() message.SetDate() message.SetBulk() - message.Subject(m.l[post.Lang].Mail.NewPost.Subject) + message.Subject(l10n.T.GetPath(post.Lang, "Mail", "NewPost", "Subject").(string)) message.SetBodyString(mail.TypeTextHTML, string(msgBody)) m.unsubscribeCodes.Set(unsubscribeCode, user.userId, 40) diff --git a/internal/router/handlers/api-v1-blog-search.go b/internal/router/handlers/api-v1-blog-search.go index 38004a1..b59f09a 100644 --- a/internal/router/handlers/api-v1-blog-search.go +++ b/internal/router/handlers/api-v1-blog-search.go @@ -1,13 +1,13 @@ package handlers import ( + "cmp" "encoding/json" "fmt" "log/slog" "net/url" "regexp" "slices" - "strings" "time" "github.com/SayaAndy/saya-today-web/internal/blog" @@ -51,6 +51,10 @@ func (r *BlogSearchHandler) RateLimiter() *fiber.Handler { func (r *BlogSearchHandler) Render(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { sort := c.Query("sort") tz := c.Query("tz") + medley := c.Query("medley") + highlight := c.Query("highlight") + hideTags := c.QueryBool("hideTags", false) + hidePublishedTime := c.QueryBool("hidePublishedTime", false) loc, err := time.LoadLocation(tz) if err != nil { @@ -85,21 +89,26 @@ func (r *BlogSearchHandler) Render(c *fiber.Ctx, supplements *router.Supplements for _, page := range pages { for _, tag := range page.Metadata.Tags { if len(tags) == 0 || slices.Contains(tags, tag) { - pageMeta = append(pageMeta, fiber.Map{ - "Link": page.Link, - "ArticleLink": "/" + lang + "/blog/" + page.FileName, - "Title": page.Metadata.Title, - "PublishedTime": page.Metadata.PublishedTime.In(loc).Format("2006-01-02 15:04:05 -07:00"), - "ActionDate": page.Metadata.ActionDate, - "ShortDescription": page.Metadata.ShortDescription, - "Thumbnail": page.Metadata.Thumbnail, - "Tags": page.Metadata.Tags, - "LikeCount": supplements.ClientCache.GetLikeCount(page.FileName), - "Liked": supplements.ClientCache.GetLikeStatus(c.IP(), page.FileName), - "ViewCount": supplements.ClientCache.GetViewCount(page.FileName), - "Viewed": supplements.ClientCache.GetViewStatus(c.IP(), page.FileName), - }) - break + if medley == "" || medley == page.Metadata.Medley { + pageMeta = append(pageMeta, fiber.Map{ + "Link": page.Link, + "ArticleLink": "/" + lang + "/blog/" + page.FileName, + "Title": page.Metadata.Title, + "PublishedTime": page.Metadata.PublishedTime.In(loc).Format("2006-01-02 15:04:05 -07:00"), + "ActionDate": page.Metadata.ActionDate, + "ShortDescription": page.Metadata.ShortDescription, + "Thumbnail": page.Metadata.Thumbnail, + "Tags": page.Metadata.Tags, + "LikeCount": supplements.ClientCache.GetLikeCount(page.FileName), + "Liked": supplements.ClientCache.GetLikeStatus(c.IP(), page.FileName), + "ViewCount": supplements.ClientCache.GetViewCount(page.FileName), + "Viewed": supplements.ClientCache.GetViewStatus(c.IP(), page.FileName), + "Medley": page.Metadata.Medley, + "MedleyPart": page.Metadata.MedleyPart, + "ToHighlight": page.FileName == highlight, + }) + break + } } } } @@ -107,13 +116,13 @@ func (r *BlogSearchHandler) Render(c *fiber.Ctx, supplements *router.Supplements slices.SortFunc(pageMeta, func(a, b fiber.Map) int { switch sort { case "titleAsc": - return strings.Compare(a["Title"].(string), b["Title"].(string)) + return cmp.Compare(a["Title"].(string), b["Title"].(string)) case "titleDesc": - return strings.Compare(b["Title"].(string), a["Title"].(string)) + return cmp.Compare(b["Title"].(string), a["Title"].(string)) case "actionDateAsc": - return strings.Compare(a["ActionDate"].(string), b["ActionDate"].(string)) + return cmp.Compare(a["ActionDate"].(string), b["ActionDate"].(string)) case "actionDateDesc": - return strings.Compare(b["ActionDate"].(string), a["ActionDate"].(string)) + return cmp.Compare(b["ActionDate"].(string), a["ActionDate"].(string)) case "publicationDateAsc": publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -07:00", a["PublishedTime"].(string)) publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", b["PublishedTime"].(string)) @@ -122,11 +131,15 @@ func (r *BlogSearchHandler) Render(c *fiber.Ctx, supplements *router.Supplements publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -07:00", a["PublishedTime"].(string)) publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", b["PublishedTime"].(string)) return publishedTimeB.Compare(publishedTimeA) + case "medley": + return cmp.Compare(a["MedleyPart"].(int), b["MedleyPart"].(int)) } return 0 }) templateMap["BlogPages"] = pageMeta + templateMap["HideTags"] = hideTags + templateMap["HidePublishedTime"] = hidePublishedTime return fiber.StatusOK, nil } diff --git a/internal/router/handlers/api-v1-email-is-in-verification.go b/internal/router/handlers/api-v1-email-is-in-verification.go index 4f05f48..b8ae162 100644 --- a/internal/router/handlers/api-v1-email-is-in-verification.go +++ b/internal/router/handlers/api-v1-email-is-in-verification.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -49,7 +50,7 @@ func (r *OngoingVerificationHandler) Render(c *fiber.Ctx, supplements *router.Su if !isAllowed { sterileDataset["striked-end-time"] = template.HTMLAttr(fmt.Sprintf("data-striked-end-time=\"%d\"", whenAllowed.UnixMilli())) templateMap["Status"] = "Neutral" - templateMap["Message"] = strings.ReplaceAll(supplements.Localization[lang].UserProfile.DelayTilVerification, "{}", whenAllowed.Format("2006-01-02 15:04:05 MST")) + templateMap["Message"] = strings.ReplaceAll(l10n.T.GetPath(lang, "UserProfile", "DelayTilVerification").(string), "{}", whenAllowed.Format("2006-01-02 15:04:05 MST")) } else { templateMap["Status"] = "OK" templateMap["Message"] = "" diff --git a/internal/router/handlers/api-v1-email-send-verification-code.go b/internal/router/handlers/api-v1-email-send-verification-code.go index 0b53efa..ea8df4d 100644 --- a/internal/router/handlers/api-v1-email-send-verification-code.go +++ b/internal/router/handlers/api-v1-email-send-verification-code.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -49,27 +50,27 @@ func (r *SendVerificationCodeHandler) Render(c *fiber.Ctx, supplements *router.S email := c.FormValue("email") if email == "" { templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.EmailEmpty + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "EmailEmpty").(string) return fiber.StatusUnprocessableEntity, nil } isTaken, err := supplements.Mailer.MailIsTaken(email) if err != nil { templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationCodeSendingError + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationCodeSendingError").(string) slog.Error("failed to check if address is already taken", slog.String("error", err.Error())) return fiber.StatusUnprocessableEntity, nil } if isTaken { templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.EmailTaken + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "EmailTaken").(string) return fiber.StatusUnprocessableEntity, nil } if isAllowed, whenAllowed, _ := supplements.Mailer.IsAllowedToRetryVerification(id); !isAllowed { templateMap["Status"] = "Failed" - templateMap["Message"] = strings.ReplaceAll(supplements.Localization[lang].UserProfile.DelayTilVerification, "{}", whenAllowed.Format("2006-01-02 15:04:05 MST")) + templateMap["Message"] = strings.ReplaceAll(l10n.T.GetPath(lang, "UserProfile", "DelayTilVerification").(string), "{}", whenAllowed.Format("2006-01-02 15:04:05 MST")) templateMap["DataAttributes"] = map[string]any{ "striked-end-time": template.HTMLAttr(fmt.Sprintf("data-striked-end-time=\"%d\"", whenAllowed.UnixMilli())), } @@ -78,20 +79,20 @@ func (r *SendVerificationCodeHandler) Render(c *fiber.Ctx, supplements *router.S if previousEmail, _, _ := supplements.Mailer.GetInfo(supplements.Mailer.GetHash(id)); previousEmail == email { templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.EmailAlreadyValidated + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "EmailAlreadyValidated").(string) return fiber.StatusUnprocessableEntity, nil } if err = supplements.Mailer.SendVerificationCode(id, email, lang); err != nil { templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationCodeSendingError + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationCodeSendingError").(string) slog.Error("failed to send a verification code", slog.String("error", err.Error())) return fiber.StatusUnprocessableEntity, nil } _, endTime, codeExpiry := supplements.Mailer.IsAllowedToRetryVerification(id) templateMap["Status"] = "OK" - templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationCodeSent + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationCodeSent").(string) templateMap["DataAttributes"] = map[string]any{ "striked-end-time": template.HTMLAttr(fmt.Sprintf("data-striked-end-time=\"%d\"", endTime.UnixMilli())), "code-expiry-time": template.HTMLAttr(fmt.Sprintf("data-code-expiry-time=\"%d\"", codeExpiry.UnixMilli())), diff --git a/internal/router/handlers/api-v1-email-verify.go b/internal/router/handlers/api-v1-email-verify.go index ace1870..65ad6d9 100644 --- a/internal/router/handlers/api-v1-email-verify.go +++ b/internal/router/handlers/api-v1-email-verify.go @@ -5,6 +5,7 @@ import ( "log/slog" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -46,19 +47,19 @@ func (r *VerifyCodeHandler) Render(c *fiber.Ctx, supplements *router.Supplements if verificationCode == "" { templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationEmpty + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationEmpty").(string) return fiber.StatusUnprocessableEntity, nil } if err = supplements.Mailer.Verify(verificationCode, lang); err != nil { slog.Warn("verification code is invalid", slog.String("verification_code", verificationCode), slog.String("error", err.Error())) templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationFailed + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationFailed").(string) return fiber.StatusUnprocessableEntity, nil } templateMap["Status"] = "OK" - templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationSuccess + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationSuccess").(string) templateMap["DataAttributes"] = map[string]any{ "hide-verification-panel": template.HTMLAttr("data-code-expiry-time=\"true\""), } diff --git a/internal/router/handlers/api-v1-map-get.go b/internal/router/handlers/api-v1-map-get.go new file mode 100644 index 0000000..e19c54a --- /dev/null +++ b/internal/router/handlers/api-v1-map-get.go @@ -0,0 +1,116 @@ +package handlers + +import ( + "fmt" + "log/slog" + "slices" + "strconv" + "strings" + + "github.com/SayaAndy/saya-today-web/internal/blog" + "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/gofiber/fiber/v2" +) + +type GetMapHandler struct { + router.BasicHandler +} + +func init() { + router.Routes = append(router.Routes, &GetMapHandler{}) +} + +func (r *GetMapHandler) Filter() (method string, path string) { + return "GET", "/api/v1/map" +} + +func (r *GetMapHandler) IsTemplated() bool { + return false +} + +func (r *GetMapHandler) TemplatesToInject() []string { + return []string{"views/partials/global-map-widget.html"} +} + +func (r *GetMapHandler) ToCache() router.CacheSetting { + return router.ByUrlAndQuery +} + +func (r *GetMapHandler) ToValidateLang() router.LangSetting { + return router.InReferer +} + +func (r *GetMapHandler) Render(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { + codename := c.Query("codename") + zoom := c.QueryInt("zoom", 4) + zoomPosition := c.Query("zoomPosition") + + pages, err := supplements.BlogClient.Scan(lang + "/") + status := fiber.StatusOK + if err != nil { + slog.Error("received an error while scanning blog pages", + slog.String("error", err.Error()), + slog.String("lang", lang), + ) + status = fiber.StatusPartialContent + pages = []*blog.Page{} + } + slices.SortFunc(pages, func(a *blog.Page, b *blog.Page) int { + return a.Metadata.PublishedTime.Compare(b.Metadata.PublishedTime) + }) + + type MapMarker struct { + Index int `json:"Index"` + Title string `json:"Title"` + PageLink string `json:"PageLink"` + Lat float64 `json:"Lat"` + Long float64 `json:"Long"` + AccuracyMeters int64 `json:"AccuracyMeters"` + Thumbnail string `json:"Thumbnail"` + ToHighlight bool `json:"ToHighlight"` + } + + templateMap["MapLocationLat"] = 45.4507 + templateMap["MapLocationLong"] = 68.8319 + templateMap["MapLocationZoom"] = zoom + templateMap["ZoomPosition"] = zoomPosition + + mapMarkers := make([]*MapMarker, 0, len(pages)) + for i, page := range pages { + geolocationParts := strings.Split(page.Metadata.Geolocation, " ") + if len(geolocationParts) < 2 { + continue + } + + var x, y float64 + var areaError int64 + if len(geolocationParts) >= 2 { + x, _ = strconv.ParseFloat(geolocationParts[0], 64) + y, _ = strconv.ParseFloat(geolocationParts[1], 64) + } + if len(geolocationParts) >= 3 { + areaError, _ = strconv.ParseInt(geolocationParts[2], 10, 64) + } + + toHighlight := page.FileName == codename + if toHighlight { + templateMap["MapLocationLat"] = x + templateMap["MapLocationLong"] = y + } + + mapMarkers = append(mapMarkers, &MapMarker{ + Index: i, + Title: page.Metadata.Title, + PageLink: fmt.Sprintf("/%s/blog/%s", lang, page.FileName), + Lat: x, + Long: y, + AccuracyMeters: areaError, + Thumbnail: page.Metadata.Thumbnail, + ToHighlight: toHighlight, + }) + } + + templateMap["MapMarkers"] = mapMarkers + + return status, nil +} diff --git a/internal/router/handlers/api-v1-subs-put.go b/internal/router/handlers/api-v1-subs-put.go index 32fba7e..12dc5cf 100644 --- a/internal/router/handlers/api-v1-subs-put.go +++ b/internal/router/handlers/api-v1-subs-put.go @@ -3,6 +3,7 @@ package handlers import ( "github.com/SayaAndy/saya-today-web/internal/mailer" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -52,18 +53,18 @@ func (r *PutSubsHandler) Render(c *fiber.Ctx, supplements *router.Supplements, l subscriptionTypeEnum = mailer.Specific default: templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.SubscribeInvalidType + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "SubscribeInvalidType").(string) return fiber.StatusUnprocessableEntity, nil } specificTags := c.FormValue("tags_picked") if err = supplements.Mailer.Subscribe(supplements.Mailer.GetHash(c.IP()), subscriptionTypeEnum, specificTags); err != nil { templateMap["Status"] = "Failed" - templateMap["Message"] = supplements.Localization[lang].UserProfile.FailedToSubscribe + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "FailedToSubscribe").(string) return fiber.StatusUnprocessableEntity, nil } templateMap["Status"] = "OK" - templateMap["Message"] = supplements.Localization[lang].UserProfile.SubscribedSuccessfully + templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "SubscribedSuccessfully").(string) return fiber.StatusOK, nil } diff --git a/internal/router/handlers/lang-blog-title.go b/internal/router/handlers/lang-blog-title.go index 49744c1..5a1155d 100644 --- a/internal/router/handlers/lang-blog-title.go +++ b/internal/router/handlers/lang-blog-title.go @@ -10,6 +10,7 @@ import ( "github.com/SayaAndy/saya-today-web/internal/blog" "github.com/SayaAndy/saya-today-web/internal/frontmatter" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" "github.com/yuin/goldmark" ) @@ -70,11 +71,15 @@ func (r *BlogPageHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, if err != nil { return nil, fmt.Errorf("failed to read frontmatter of the desired blog post: %w", err) } + title := metadata.Title + if metadata.Medley != "" { + title += " // " + l10n.T.GetPath(lang, "Medleys", metadata.Medley).(string) + } return []router.MetaField{ - {Property: "og:title", Content: metadata.Title}, + {Property: "og:title", Content: title}, {Property: "og:description", Content: fmt.Sprintf("%s [%s]", metadata.ShortDescription, metadata.ActionDate)}, - {Property: "og:image", Content: fmt.Sprintf(supplements.PhotoStorage.Thumbnail320p.BaseUrl, metadata.Thumbnail)}, + {Property: "og:image", Content: fmt.Sprintf(supplements.PhotoStorage.Thumbnail560p.BaseUrl, metadata.Thumbnail)}, {Property: "og:url", Content: fmt.Sprintf("%s/%s/blog/%s", templateMap["CanonicalEndpoint"], lang, c.Params("title"))}, {Property: "og:type", Content: "website"}, {Name: "twitter:card", Content: "summary_large_image"}, @@ -86,11 +91,15 @@ func (r *BlogPageHandler) AddLinkedData(c *fiber.Ctx, supplements *router.Supple if err != nil { return nil, fmt.Errorf("failed to read frontmatter of the desired blog post: %w", err) } + title := metadata.Title + if metadata.Medley != "" { + title += " // " + l10n.T.GetPath(lang, "Medleys", metadata.Medley).(string) + } return map[string]any{ "@context": "https://schema.org", "@type": "Article", - "headline": metadata.Title, + "headline": title, "description": fmt.Sprintf("%s [%s]", metadata.ShortDescription, metadata.ActionDate), "author": map[string]string{"@type": "Person", "name": "Saya Andy"}, "datePublished": metadata.PublishedTime.UTC().Format(time.RFC3339), @@ -102,10 +111,11 @@ func (r *BlogPageHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplemen if err != nil { return fiber.StatusBadRequest, fmt.Errorf("failed to get path from referer: %w", err) } + title := pathParts[2] - metadata, parsedMarkdown, err := readBlogPost(supplements.MarkdownRenderer, supplements.BlogClient, lang+"/"+pathParts[2]) + metadata, parsedMarkdown, err := readBlogPost(supplements.MarkdownRenderer, supplements.BlogClient, lang+"/"+title) if err != nil { - return fiber.StatusNotFound, fmt.Errorf("failed to find '%s' post: %w", pathParts[2], err) + return fiber.StatusNotFound, fmt.Errorf("failed to find '%s' post: %w", title, err) } geolocationParts := strings.Split(metadata.Geolocation, " ") @@ -122,12 +132,15 @@ func (r *BlogPageHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplemen templateMap["MapLocationY"] = y templateMap["MapLocationAreaMeters"] = areaError templateMap["Title"] = metadata.Title + templateMap["Codename"] = title templateMap["ParsedMarkdown"] = template.HTML(parsedMarkdown) templateMap["PublishedDate"] = metadata.PublishedTime.Format("2006-01-02 15:04:05 -07:00") templateMap["ActionDate"] = metadata.ActionDate templateMap["ShortDescription"] = metadata.ShortDescription + templateMap["Thumbnail"] = metadata.Thumbnail + templateMap["Medley"] = metadata.Medley - go supplements.ClientCache.View(c.IP(), pathParts[2]) + go supplements.ClientCache.View(c.IP(), title) return fiber.StatusOK, nil } diff --git a/internal/router/handlers/lang-blog.go b/internal/router/handlers/lang-blog.go index 79ec9f3..c31c2eb 100644 --- a/internal/router/handlers/lang-blog.go +++ b/internal/router/handlers/lang-blog.go @@ -11,6 +11,7 @@ import ( "github.com/SayaAndy/saya-today-web/internal/blog" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -57,18 +58,16 @@ func (r *CatalogueHandler) SitemapInfo(supplements *router.Supplements) []router func (r *CatalogueHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta []router.MetaField, err error) { return []router.MetaField{ - {Property: "og:title", Content: supplements.Localization[lang].BlogSearch.Header}, - {Property: "og:description", Content: supplements.Localization[lang].BlogSearch.Description}, + {Property: "og:title", Content: l10n.T.GetPath(lang, "BlogSearch", "Header").(string)}, + {Property: "og:description", Content: l10n.T.GetPath(lang, "BlogSearch", "Description").(string)}, {Property: "og:url", Content: fmt.Sprintf("%s/%s/blog", templateMap["CanonicalEndpoint"], lang)}, {Property: "og:type", Content: "website"}, }, nil } func (r *CatalogueHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { - querySort := c.Query("sort") - if querySort == "" { - querySort = "publicationDateDesc" - } + querySort := c.Query("sort", "publicationDateDesc") + previousBlogPage := c.Query("codename") encodedQuery := c.Request().URI().QueryString() decodedQuery, _ := url.QueryUnescape(string(encodedQuery)) @@ -87,13 +86,14 @@ func (r *CatalogueHandler) RenderBody(c *fiber.Ctx, supplements *router.Suppleme templateMap["Tags"] = tagsArray templateMap["QuerySort"] = querySort templateMap["QueryTags"] = strings.Join(queryTags, ",") - templateMap["Title"] = supplements.Localization[lang].BlogSearch.Header + templateMap["Title"] = l10n.T.GetPath(lang, "BlogSearch", "Header").(string) + templateMap["PreviousBlogPage"] = previousBlogPage return fiber.StatusOK, nil } func (r *CatalogueHandler) RenderHeader(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { - templateMap["Title"] = supplements.Localization[lang].BlogSearch.Header + templateMap["Title"] = l10n.T.GetPath(lang, "BlogSearch", "Header").(string) return fiber.StatusOK, nil } diff --git a/internal/router/handlers/lang-map.go b/internal/router/handlers/lang-map.go index d452bc7..32ab35e 100644 --- a/internal/router/handlers/lang-map.go +++ b/internal/router/handlers/lang-map.go @@ -1,13 +1,6 @@ package handlers import ( - "fmt" - "log/slog" - "slices" - "strconv" - "strings" - - "github.com/SayaAndy/saya-today-web/internal/blog" "github.com/SayaAndy/saya-today-web/internal/router" "github.com/gofiber/fiber/v2" ) @@ -49,61 +42,5 @@ func (r *MapHandler) SitemapInfo(supplements *router.Supplements) []router.Sitem } func (r *MapHandler) Render(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { - pages, err := supplements.BlogClient.Scan(lang + "/") - slices.SortFunc(pages, func(a *blog.Page, b *blog.Page) int { - return a.Metadata.PublishedTime.Compare(b.Metadata.PublishedTime) - }) - status := fiber.StatusOK - if err != nil { - slog.Error("received an error while scanning b2 pages", - slog.String("error", err.Error()), - slog.String("lang", lang), - ) - status = fiber.StatusPartialContent - pages = []*blog.Page{} - } - - type MapMarker struct { - Index int `json:"Index"` - Title string `json:"Title"` - PageLink string `json:"PageLink"` - Lat float64 `json:"Lat"` - Long float64 `json:"Long"` - AccuracyMeters int64 `json:"AccuracyMeters"` - Thumbnail string `json:"Thumbnail"` - } - - mapMarkers := make([]*MapMarker, 0, len(pages)) - for i, page := range pages { - geolocationParts := strings.Split(page.Metadata.Geolocation, " ") - if len(geolocationParts) < 2 { - continue - } - - var x, y float64 - var areaError int64 - if len(geolocationParts) >= 2 { - x, _ = strconv.ParseFloat(geolocationParts[0], 64) - y, _ = strconv.ParseFloat(geolocationParts[1], 64) - } - if len(geolocationParts) >= 3 { - areaError, _ = strconv.ParseInt(geolocationParts[2], 10, 64) - } - - mapMarkers = append(mapMarkers, &MapMarker{ - Index: i, - Title: page.Metadata.Title, - PageLink: fmt.Sprintf("/%s/blog/%s", lang, page.FileName), - Lat: x, - Long: y, - AccuracyMeters: areaError, - Thumbnail: page.Metadata.Thumbnail, - }) - } - - templateMap["MapMarkers"] = mapMarkers - templateMap["MapLocationLat"] = 45.4507 - templateMap["MapLocationLong"] = 68.8319 - - return status, nil + return fiber.StatusOK, nil } diff --git a/internal/router/handlers/lang-user-unsubscribe.go b/internal/router/handlers/lang-user-unsubscribe.go index 4fbd244..e22d854 100644 --- a/internal/router/handlers/lang-user-unsubscribe.go +++ b/internal/router/handlers/lang-user-unsubscribe.go @@ -4,6 +4,7 @@ import ( "log/slog" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -39,24 +40,24 @@ func (r *UnsubscribeHandler) Render(c *fiber.Ctx, supplements *router.Supplement if unsubscribeCode == "" { statusColor = "0, 0, 255" statusEmoji = "(╭ರ_•́)" - statusText = supplements.Localization[lang].UnsubscribePage.UnsetCode + statusText = l10n.T.GetPath(lang, "UnsubscribePage", "UnsetCode").(string) status = fiber.ErrBadRequest.Code } else if clientError, serverError := supplements.Mailer.Unsubscribe(unsubscribeCode); clientError != nil { slog.Info("got a client error when unsubscribing", slog.String("error", clientError.Error())) statusColor = "255, 0, 0" statusEmoji = "(͠≖~≖ ͡ )" - statusText = supplements.Localization[lang].UnsubscribePage.InvalidCode + statusText = l10n.T.GetPath(lang, "UnsubscribePage", "InvalidCode").(string) status = fiber.ErrBadRequest.Code } else if serverError != nil { slog.Error("got a server error when unsubscribing", slog.String("error", serverError.Error())) statusColor = "255, 128, 0" statusEmoji = "( ˶°ㅁ°) !!" - statusText = supplements.Localization[lang].UnsubscribePage.OnServerError + statusText = l10n.T.GetPath(lang, "UnsubscribePage", "OnServerError").(string) status = fiber.ErrInternalServerError.Code } else { statusColor = "0, 255, 0" statusEmoji = "♡⸜(˶˃ ᵕ ˂˶)⸝♡" - statusText = supplements.Localization[lang].UnsubscribePage.Success + statusText = l10n.T.GetPath(lang, "UnsubscribePage", "Success").(string) status = fiber.StatusOK } diff --git a/internal/router/handlers/lang-user.go b/internal/router/handlers/lang-user.go index 0dda529..085a9aa 100644 --- a/internal/router/handlers/lang-user.go +++ b/internal/router/handlers/lang-user.go @@ -6,6 +6,7 @@ import ( "github.com/SayaAndy/saya-today-web/internal/mailer" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -40,15 +41,15 @@ func (r *UserHandler) ToValidateLang() router.LangSetting { func (r *UserHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta []router.MetaField, err error) { return []router.MetaField{ {Name: "robots", Content: "noindex,nofollow"}, - {Property: "og:title", Content: supplements.Localization[lang].UserProfile.Header}, - {Property: "og:description", Content: supplements.Localization[lang].UserProfile.Description}, + {Property: "og:title", Content: l10n.T.GetPath(lang, "UserProfile", "Header").(string)}, + {Property: "og:description", Content: l10n.T.GetPath(lang, "UserProfile", "Description").(string)}, {Property: "og:url", Content: fmt.Sprintf("%s/%s/user", templateMap["CanonicalEndpoint"], lang)}, {Property: "og:type", Content: "website"}, }, nil } func (r *UserHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { - templateMap["Title"] = supplements.Localization[lang].UserProfile.Header + templateMap["Title"] = l10n.T.GetPath(lang, "UserProfile", "Header").(string) email, _, err := supplements.Mailer.GetInfo(supplements.Mailer.GetHash(c.IP())) if err != nil { @@ -83,6 +84,6 @@ func (r *UserHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, } func (r *UserHandler) RenderHeader(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { - templateMap["Title"] = supplements.Localization[lang].UserProfile.Header + templateMap["Title"] = l10n.T.GetPath(lang, "UserProfile", "Header").(string) return fiber.StatusOK, nil } diff --git a/internal/router/handlers/lang.go b/internal/router/handlers/lang.go index 25b5036..cc0b33b 100644 --- a/internal/router/handlers/lang.go +++ b/internal/router/handlers/lang.go @@ -6,6 +6,7 @@ import ( "time" "github.com/SayaAndy/saya-today-web/internal/router" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/gofiber/fiber/v2" ) @@ -51,8 +52,8 @@ func (r *HomeHandler) SitemapInfo(supplements *router.Supplements) []router.Site func (r *HomeHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta []router.MetaField, err error) { return []router.MetaField{ - {Property: "og:title", Content: supplements.Localization[lang].HomePage.Header}, - {Property: "og:description", Content: supplements.Localization[lang].HomePage.HomePageDescription}, + {Property: "og:title", Content: l10n.T.GetPath(lang, "HomePage", "Header").(string)}, + {Property: "og:description", Content: l10n.T.GetPath(lang, "HomePage", "HomePageDescription").(string)}, {Property: "og:image", Content: fmt.Sprintf( supplements.PhotoStorage.HomePageGifs.BaseUrl, supplements.PhotoStorage.HomePageGifs.Indexes[rand.Int()%len(supplements.PhotoStorage.HomePageGifs.Indexes)], @@ -63,7 +64,7 @@ func (r *HomeHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lan } func (r *HomeHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { - templateMap["Title"] = supplements.Localization[lang].HomePage.Header + templateMap["Title"] = l10n.T.GetPath(lang, "HomePage", "Header").(string) templateMap["FilledHeartCount"] = uint(40) templateMap["OutlineHeartCount"] = uint(40) templateMap["GifUrl"] = fmt.Sprintf( @@ -75,6 +76,6 @@ func (r *HomeHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, } func (r *HomeHandler) RenderHeader(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) { - templateMap["Title"] = supplements.Localization[lang].HomePage.Header + templateMap["Title"] = l10n.T.GetPath(lang, "HomePage", "Header").(string) return fiber.StatusOK, nil } diff --git a/internal/router/router.go b/internal/router/router.go index 7f3511a..c8b5686 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -23,7 +23,6 @@ import ( "github.com/SayaAndy/saya-today-web/internal/mailer" "github.com/SayaAndy/saya-today-web/internal/tailwind" "github.com/SayaAndy/saya-today-web/internal/templatemanager" - "github.com/SayaAndy/saya-today-web/locale" "github.com/dgraph-io/ristretto/v2" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/compress" @@ -95,7 +94,6 @@ type Route interface { type Supplements struct { DB *sql.DB BlogClient blog.Client - Localization map[string]*locale.LocaleConfig AvailableLanguages []config.AvailableLanguageConfig ClientCache *ClientCache PageCache *ristretto.Cache[string, []byte] @@ -106,7 +104,7 @@ type Supplements struct { MarkdownRenderer goldmark.Markdown Meta config.MetaConfig PhotoStorage config.PhotoStorageConfig - StaticStorage config.PhotoTypeConfig + StaticStorage config.StaticStorageConfig } type Router struct { @@ -149,16 +147,7 @@ func NewRouter(cfg *config.Config) (*Router, error) { supplements.BlogClient, err = blog.NewClientMap[cfg.BlogPages.Storage.Type](&cfg.BlogPages.Storage) if err != nil { - return nil, fmt.Errorf("fail to initialize b2 client: %w", err) - } - - supplements.Localization = make(map[string]*locale.LocaleConfig, len(cfg.AvailableLanguages)) - for _, lang := range cfg.AvailableLanguages { - localeCfg, err := locale.InitConfig(cfg.LocalePath + lang.LocFile) - if err != nil { - return nil, fmt.Errorf("fail to initialize a locale: %w", err) - } - supplements.Localization[lang.Name] = localeCfg + return nil, fmt.Errorf("fail to initialize blog client: type %s: %w", cfg.BlogPages.Storage.Type, err) } supplements.MarkdownRenderer = goldmark.New( @@ -200,7 +189,7 @@ func NewRouter(cfg *config.Config) (*Router, error) { } supplements.Mailer, err = mailer.NewMailer(supplements.DB, cfg.Mail.ClientHost, cfg.Mail.MailHost, - cfg.Mail.PublicName, cfg.Mail.MailAddress, cfg.Mail.Username, cfg.Mail.Password, []byte(cfg.Mail.Salt), supplements.Localization) + cfg.Mail.PublicName, cfg.Mail.MailAddress, cfg.Mail.Username, cfg.Mail.Password, []byte(cfg.Mail.Salt)) if err != nil { return nil, fmt.Errorf("fail to initialize mailer: %w", err) } @@ -314,13 +303,12 @@ func (r *Router) InitRoutes() (err error) { } defaultMap := fiber.Map{ - "L": r.supplements.Localization[lang], - "Lang": lang, - "Path": trimmedPath, - "QueryString": queryString, - "CanonicalEndpoint": r.canonicalEndpoint, - "StaticStorageBaseUrl": r.supplements.StaticStorage.BaseUrl, - "ThumbnailBaseUrl": r.supplements.PhotoStorage.Thumbnail320p.BaseUrl, + "Lang": lang, + "Path": trimmedPath, + "QueryString": queryString, + "CanonicalEndpoint": r.canonicalEndpoint, + "StaticStorage": r.supplements.StaticStorage, + "PhotoStorage": r.supplements.PhotoStorage, } statusCode, err := currentRoute.Render(c, r.supplements, lang, defaultMap) @@ -467,13 +455,12 @@ func (r *Router) generalPage(c *fiber.Ctx, route Route, lang string) error { } valueMap := fiber.Map{ - "L": r.supplements.Localization[lang], - "Lang": lang, - "Path": trimmedPath, - "QueryString": queryString, - "CanonicalEndpoint": r.canonicalEndpoint, - "StaticStorageBaseUrl": r.supplements.StaticStorage.BaseUrl, - "ThumbnailBaseUrl": r.supplements.PhotoStorage.Thumbnail320p.BaseUrl, + "Lang": lang, + "Path": trimmedPath, + "QueryString": queryString, + "CanonicalEndpoint": r.canonicalEndpoint, + "StaticStorage": r.supplements.StaticStorage, + "PhotoStorage": r.supplements.PhotoStorage, } var err error @@ -576,11 +563,12 @@ func (r *Router) generalPageSegment(c *fiber.Ctx, part string) error { cacheKey := "" trimmedPath := strings.Trim(path, "/") + requestQuery := string(c.Request().URI().QueryString()) switch route.ToCache() { case ByUrlOnly: cacheKey = fmt.Sprintf("%s.%s.%s", method, part, trimmedPath) case ByUrlAndQuery: - cacheKey = fmt.Sprintf("%s.%s.%s.%s", method, part, trimmedPath, queryString) + cacheKey = fmt.Sprintf("%s.%s.%s.%s.%s", method, part, trimmedPath, queryString, requestQuery) } if route.ToCache() != Disabled { @@ -592,13 +580,12 @@ func (r *Router) generalPageSegment(c *fiber.Ctx, part string) error { var statusCode int defaultMap := fiber.Map{ - "L": r.supplements.Localization[lang], - "Lang": lang, - "Path": strings.Trim(path, "/"), - "QueryString": queryString, - "CanonicalEndpoint": r.canonicalEndpoint, - "StaticStorageBaseUrl": r.supplements.StaticStorage.BaseUrl, - "ThumbnailBaseUrl": r.supplements.PhotoStorage.Thumbnail320p.BaseUrl, + "Lang": lang, + "Path": strings.Trim(path, "/"), + "QueryString": queryString, + "CanonicalEndpoint": r.canonicalEndpoint, + "StaticStorage": r.supplements.StaticStorage, + "PhotoStorage": r.supplements.PhotoStorage, } switch part { diff --git a/internal/templatemanager/templatemanager.go b/internal/templatemanager/templatemanager.go index 9d14e15..ff1214a 100644 --- a/internal/templatemanager/templatemanager.go +++ b/internal/templatemanager/templatemanager.go @@ -8,6 +8,8 @@ import ( "path/filepath" "strings" "time" + + "github.com/SayaAndy/saya-today-web/l10n" ) type TemplateManager struct { @@ -38,6 +40,10 @@ var templateFuncMap = template.FuncMap{ "fdiv": func(a, b int) float64 { return float64(a) / float64(b) }, + "l": func(path ...any) any { + return l10n.T.GetPath(path...) + }, + "join": strings.Join, } func NewTemplateManager(templates ...TemplateManagerTemplates) (*TemplateManager, error) { diff --git a/locale/localization.en.yaml b/l10n/en.yaml index 9571aca..9571aca 100644 --- a/locale/localization.en.yaml +++ b/l10n/en.yaml diff --git a/locale/localization.ru.yaml b/l10n/ru.yaml index 1818998..1818998 100644 --- a/locale/localization.ru.yaml +++ b/l10n/ru.yaml diff --git a/l10n/translator-hook.go b/l10n/translator-hook.go new file mode 100644 index 0000000..ff5837c --- /dev/null +++ b/l10n/translator-hook.go @@ -0,0 +1,39 @@ +package l10n + +var hooks map[string]*TranslatorHook + +type TranslatorHook struct { + t *Translator + prefix string +} + +func NewTranslatorHook(prefix string) *TranslatorHook { + if _, ok := hooks[prefix]; !ok { + hooks[prefix] = &TranslatorHook{T, prefix} + } + return hooks[prefix] +} + +func (h *TranslatorHook) Set(key string, val any, toOverwrite bool) (alreadySet bool, err error) { + return h.t.Set(h.prefix+key, val, toOverwrite) +} + +func (h *TranslatorHook) SetPath(val any, toOverwrite bool, path ...any) (alreadySet bool, err error) { + return h.t.SetPath(val, toOverwrite, append([]any{h.prefix}, path...)...) +} + +func (h *TranslatorHook) Get(key string) any { + return h.t.Get(h.prefix + "." + key) +} + +func (h *TranslatorHook) GetPath(path ...any) any { + return h.t.GetPath(append([]any{h.prefix}, path...)...) +} + +func (h *TranslatorHook) GetDefault(key string, def any) any { + return h.t.GetDefault(h.prefix+"."+key, def) +} + +func (h *TranslatorHook) GetDefaultPath(def any, path ...any) any { + return h.t.GetDefaultPath(def, append([]any{h.prefix}, path...)...) +} diff --git a/l10n/translator.go b/l10n/translator.go new file mode 100644 index 0000000..4d4b4eb --- /dev/null +++ b/l10n/translator.go @@ -0,0 +1,167 @@ +package l10n + +import ( + "errors" + "fmt" + "log/slog" + "os" + "strings" + "sync" + + "gopkg.in/yaml.v3" +) + +var T *Translator + +func init() { + var err error + if T, err = NewTranslator("ru", "en"); err != nil { + slog.Error("fail to load localization", slog.String("error", err.Error())) + os.Exit(1) + } +} + +type Translator struct { + l map[string]any + mu sync.RWMutex +} + +func NewTranslator(locales ...string) (*Translator, error) { + t := &Translator{l: make(map[string]any)} + errs := make([]error, 0) + + for _, locale := range locales { + contentBytes, err := os.ReadFile(fmt.Sprintf("l10n/%s.yaml", locale)) + if err != nil { + errs = append(errs, fmt.Errorf("opening one of files: l10n/%s.yaml: %w", locale, err)) + continue + } + + var content map[string]any + if err := yaml.Unmarshal(contentBytes, &content); err != nil { + errs = append(errs, fmt.Errorf("parsing one of files: l10n/%s.yaml: %w", locale, err)) + continue + } + + errs = append(errs, t.initMap("."+locale, content)...) + } + + return t, errors.Join(errs...) +} + +func (t *Translator) Set(key string, val any, toOverwrite bool) (alreadySet bool, err error) { + t.mu.RLock() + if _, alreadySet = t.l[key]; !alreadySet || (alreadySet && toOverwrite) { + t.mu.RUnlock() + return alreadySet, errors.Join(t.setSafe(key, val)...) + } + t.mu.RUnlock() + return alreadySet, nil +} + +func (t *Translator) SetPath(val any, toOverwrite bool, path ...any) (alreadySet bool, err error) { + key, err := formatPath(path...) + if err != nil { + return false, err + } + return t.Set(key, val, toOverwrite) +} + +func (t *Translator) Get(key string) any { + return t.GetDefault(key, key[strings.LastIndex(key, ".")+1:]) +} + +func (t *Translator) GetPath(path ...any) any { + return t.GetDefaultPath(path[len(path)-1], path...) +} + +func (t *Translator) GetDefault(key string, def any) any { + t.mu.RLock() + defer t.mu.RUnlock() + if v, ok := t.l[key]; ok { + return v + } + slog.Warn("failed to find localization for a key", slog.String("key", key)) + return def +} + +func (t *Translator) GetDefaultPath(def any, path ...any) any { + t.mu.RLock() + defer t.mu.RUnlock() + key, err := formatPath(path...) + if err != nil { + slog.Error("get localized value by path: path is invalid", slog.String("error", err.Error())) + return def + } + return t.GetDefault(key, def) +} + +func (t *Translator) initSlice(parent string, level []any) []error { + errs := make([]error, 0) + for i, v := range level { + newParent := parent + fmt.Sprintf("[%d]", i) + t.setSafe(newParent, v) + } + return errs +} + +func (t *Translator) initMap(parent string, level map[string]any) []error { + errs := make([]error, 0) + for k, v := range level { + key, err := formatPath(k) + if err != nil { + errs = append(errs, err) + continue + } + + errs = append(errs, t.setSafe(parent+key, v)...) + } + return errs +} + +func (t *Translator) setSafe(k string, v any) []error { + errs := make([]error, 0) + t.mu.Lock() + switch vv := v.(type) { + case int: + t.l[k] = vv + t.mu.Unlock() + case float64: + t.l[k] = vv + t.mu.Unlock() + case string: + t.l[k] = vv + t.mu.Unlock() + case []any: + t.mu.Unlock() + errs = append(errs, t.initSlice(k, vv)...) + case map[string]any: + t.mu.Unlock() + errs = append(errs, t.initMap(k, vv)...) + default: + t.mu.Unlock() + errs = append(errs, fmt.Errorf("invalid value type: %s, %v", k, vv)) + } + return errs +} + +func formatPath(path ...any) (string, error) { + var key string + for _, part := range path { + switch k := part.(type) { + case int: + key += fmt.Sprintf("[%d]", k) + case string: + key += "." + strings.ReplaceAll(k, ".", "\\.") + case []any: + add, err := formatPath(k...) + if err != nil { + return "", fmt.Errorf("while formatting nested slice: %w", err) + } + key += add + default: + return "", fmt.Errorf("invalid key part: after %s follows %v", key, k) + } + } + return key, nil +} diff --git a/locale/localization.go b/locale/localization.go deleted file mode 100644 index 87b8ea1..0000000 --- a/locale/localization.go +++ /dev/null @@ -1,144 +0,0 @@ -package locale - -import ( - "os" - - "github.com/go-playground/validator/v10" - "gopkg.in/yaml.v3" -) - -type LocaleConfig struct { - TagsLabel string `yaml:"TagsLabel" json:"TagsLabel"` - BlogSearch BlogSearchConfig `yaml:"BlogSearch" json:"BlogSearch"` - GlobalMap GlobalMapConfig `yaml:"GlobalMap" json:"GlobalMap"` - HomePage HomePageConfig `yaml:"HomePage" json:"HomePage"` - UnsubscribePage UnsubscribePageConfig `yaml:"UnsubscribePage" json:"UnsubscribePage"` - Metadata MetadataConfig `yaml:"Metadata" json:"Metadata"` - Mail MailConfig `yaml:"Mail" json:"Mail"` - UserProfile UserProfileConfig `yaml:"UserProfile" json:"UserProfile"` -} - -type BlogSearchConfig struct { - Header string `yaml:"Header" json:"Header"` - Description string `yaml:"Description" json:"Description"` - TagsHeader string `yaml:"TagsHeader" json:"TagsHeader"` - OrderByHeader string `yaml:"OrderByHeader" json:"OrderByHeader"` - TitleOrdered string `yaml:"TitleOrdered" json:"TitleOrdered"` - ActionDateOrdered string `yaml:"ActionDateOrdered" json:"ActionDateOrdered"` - PublicationDateOrdered string `yaml:"PublicationDateOrdered" json:"PublicationDateOrdered"` - ChooseAllTags string `yaml:"ChooseAllTags" json:"ChooseAllTags"` -} - -type GlobalMapConfig struct { - Header string `yaml:"Header" json:"Header"` - Description string `yaml:"Description" json:"Description"` -} - -type HomePageConfig struct { - Header string `yaml:"Header" json:"Header"` - Contacts string `yaml:"Contacts" json:"Contacts"` - DidYouKnowThat string `yaml:"DidYouKnowThat" json:"DidYouKnowThat"` - SidebarDescription string `yaml:"SidebarDescription" json:"SidebarDescription"` - BackDescription string `yaml:"BackDescription" json:"BackDescription"` - HomePageDescription string `yaml:"HomePageDescription" json:"HomePageDescription"` - BlogSearchDescription string `yaml:"BlogSearchDescription" json:"BlogSearchDescription"` - MarkerMapDescription string `yaml:"MarkerMapDescription" json:"MarkerMapDescription"` - ThemeSwitchDescription string `yaml:"ThemeSwitchDescription" json:"ThemeSwitchDescription"` - AccountDescription string `yaml:"AccountDescription" json:"AccountDescription"` - Hymn1 string `yaml:"Hymn1" json:"Hymn1"` - Hymn2 string `yaml:"Hymn2" json:"Hymn2"` - Hymn3 string `yaml:"Hymn3" json:"Hymn3"` - Hymn4 string `yaml:"Hymn4" json:"Hymn4"` -} - -type UnsubscribePageConfig struct { - Header string `yaml:"Header" json:"Header"` - UnsetCode string `yaml:"UnsetCode" json:"UnsetCode"` - InvalidCode string `yaml:"InvalidCode" json:"InvalidCode"` - OnServerError string `yaml:"OnServerError" json:"OnServerError"` - Success string `yaml:"Success" json:"Success"` -} - -type MetadataConfig struct { - Action string `yaml:"Action" json:"Action"` - Published string `yaml:"Published" json:"Published"` -} - -type MailConfig struct { - UnsubscribeFooter string `yaml:"UnsubscribeFooter" json:"UnsubscribeFooter"` - VerifyEmail VerifyEmailConfig `yaml:"VerifyEmail" json:"VerifyEmail"` - NewPost NewPostConfig `yaml:"NewPost" json:"NewPost"` -} - -type VerifyEmailConfig struct { - Subject string `yaml:"Subject" json:"Subject"` - Welcome string `yaml:"Welcome" json:"Welcome"` - Intro string `yaml:"Intro" json:"Intro"` - GotoLink string `yaml:"GotoLink" json:"GotoLink"` - InputCode string `yaml:"InputCode" json:"InputCode"` - IfRandom string `yaml:"IfRandom" json:"IfRandom"` -} - -type NewPostConfig struct { - Subject string `yaml:"Subject" json:"Subject"` - CapturedOn string `yaml:"CapturedOn" json:"CapturedOn"` - Intro string `yaml:"Intro" json:"Intro"` -} - -type UserProfileConfig struct { - Header string `yaml:"Header" json:"Header"` - Description string `yaml:"Description" json:"Description"` - EmailHeader string `yaml:"EmailHeader" json:"EmailHeader"` - VerificationCodeHeader string `yaml:"VerificationCodeHeader" json:"VerificationCodeHeader"` - TypeEmail string `yaml:"TypeEmail" json:"TypeEmail"` - SendCodeButton string `yaml:"SendCodeButton" json:"SendCodeButton"` - VerifyButton string `yaml:"VerifyButton" json:"VerifyButton"` - VerificationCodeSent string `yaml:"VerificationCodeSent" json:"VerificationCodeSent"` - DelayTilVerification string `yaml:"DelayTilVerification" json:"DelayTilVerification"` - FailedEmailRender string `yaml:"FailedEmailRender" json:"FailedEmailRender"` - VerificationCodeSendingError string `yaml:"VerificationCodeSendingError" json:"VerificationCodeSendingError"` - EmailAlreadyValidated string `yaml:"EmailAlreadyValidated" json:"EmailAlreadyValidated"` - EmailTaken string `yaml:"EmailTaken" json:"EmailTaken"` - EmailEmpty string `yaml:"EmailEmpty" json:"EmailEmpty"` - VerificationSuccess string `yaml:"VerificationSuccess" json:"VerificationSuccess"` - VerificationFailed string `yaml:"VerificationFailed" json:"VerificationFailed"` - VerificationEmpty string `yaml:"VerificationEmpty" json:"VerificationEmpty"` - SubscriptionHeader string `yaml:"SubscriptionHeader" json:"SubscriptionHeader"` - TagsNone string `yaml:"TagsNone" json:"TagsNone"` - TagsAll string `yaml:"TagsAll" json:"TagsAll"` - TagsSpecific string `yaml:"TagsSpecific" json:"TagsSpecific"` - TagDoesNotExist string `yaml:"TagDoesNotExist" json:"TagDoesNotExist"` - TagAlreadyAdded string `yaml:"TagAlreadyAdded" json:"TagAlreadyAdded"` - SaveButton string `yaml:"SaveButton" json:"SaveButton"` - SubscribeInvalidType string `yaml:"SubscribeInvalidType" json:"SubscribeInvalidType"` - FailedToSubscribe string `yaml:"FailedToSubscribe" json:"FailedToSubscribe"` - SubscribedSuccessfully string `yaml:"SubscribedSuccessfully" json:"SubscribedSuccessfully"` - RefreshPage string `yaml:"RefreshPage" json:"RefreshPage"` -} - -func LoadConfig(path string, config *LocaleConfig) error { - fileBytes, err := os.ReadFile(path) - if err != nil { - return err - } - - if err = yaml.Unmarshal(fileBytes, config); err != nil { - return err - } - - return nil -} - -func InitConfig(path string) (*LocaleConfig, error) { - config := &LocaleConfig{} - if err := LoadConfig(path, config); err != nil { - return nil, err - } - - validate := validator.New(validator.WithRequiredStructEnabled()) - if err := validate.Struct(config); err != nil { - return nil, err - } - - return config, nil -} diff --git a/static/input.css b/static/input.css index e1ba37d..98caaad 100644 --- a/static/input.css +++ b/static/input.css @@ -42,10 +42,6 @@ html { font-size: 12px; --crossed-out: url("data:image/svg+xml,<svg id='patternId' width='100%' height='100%' xmlns='http://www.w3.org/2000/svg'><defs><pattern id='a' patternUnits='userSpaceOnUse' width='30' height='30' patternTransform='scale(1) rotate(65)'><rect x='0' y='0' width='100%' height='100%' fill='%232b2b3100' /><path d='M30 15L22.5 30 15 15 22.5 0zm-15 0L7.5 30 0 15 7.5 0z' stroke-width='0.6' stroke='%23ffffffff' fill='none' /></pattern></defs><rect width='800%' height='800%' transform='translate(0,0)' fill='url(%23a)' /></svg>"); --bg-paper: url(data:image/webp;base64,UklGRi4BAABXRUJQVlA4ICIBAABQCQCdASqAAIAAPm02lUmkIqIkIImIgA2JaW3rMFmGDAOKLAoJtP591ByFR74Tun2DP+Lygpfsz5FH7+cKgxi2Islfd03i5AJdXUfU0SDc8SXYAAD++hlzeYAJWQEF8ZdkJdlcA+5btZg0FMGaso8SHoBph0qAOQysElJ5C5SJFNoKIV1J550UyL7Y9Gh0/c/uzLnBmr8lvz7k1WeA81H4S5ViU/wBqa7aM1m8D9rDtrk8cSTqx+xWF6MS8UfZGSsks6CT/igtgv6syFN9uNA3LjnjiCJKYOvFcMkX+jB2578GWVo3nyLaM1QNPYCxn3TiUbnf7U0MdD2NsBpg1+Jd1wu6yW/U+goN+IQS6Nt4RMWltVcZHlxrneKJi0SobO0AAA==); - --split-left-background: unset; - --split-left-repeat: no-repeat; - --split-left-size: cover; - --split-left-position: left; --split-right-background: unset; --split-right-repeat: no-repeat; --split-right-size: cover; @@ -104,12 +100,12 @@ body { [data-theme="olive"] { --color-accent-light: var(--color-lime-300); --color-accent-deep: var(--color-lime-600); - --color-main-dark-hard: #4b513a; - --color-main-dark-medium: #8a9d8a; - --color-main-dark-soft: #a5b8a5; - --color-main-light-soft: #ffffdd; + --color-main-dark-hard: oklch(42.269% 0.03716 119.926); + --color-main-dark-medium: oklch(67.552% 0.03493 145.278); + --color-main-dark-soft: oklch(76.256% 0.03392 145.352); + --color-main-light-soft: oklch(99.13% 0.04397 107.223); --color-main-light-medium: var(--color-main-light-soft); - --color-main-light-hard: #fffff0; + --color-main-light-hard: oklch(99.598% 0.01951 106.845); --color-main: var(--color-main-dark-hard); --color-main-hard: var(--color-main-dark-hard); --color-main-medium: var(--color-main-dark-medium); @@ -133,15 +129,31 @@ body { --squares-and-triangles-background-height-coef: 10; } +@media (prefers-color-scheme: dark) { + [data-theme="olive"] { + --color-accent-light: var(--color-lime-600); + --color-accent-deep: var(--color-lime-300); + --color-main: var(--color-main-light-hard); + --color-main-hard: var(--color-main-light-hard); + --color-main-medium: var(--color-main-light-medium); + --color-main-soft: var(--color-main-light-soft); + --color-background: var(--color-main-dark-hard); + --color-background-dark: var(--color-main-dark-hard); + --color-background-medium: var(--color-main-dark-medium); + --color-background-light: var(--color-main-dark-soft); + --color-sidebar-stroke: var(--color-main-light-hard); + } +} + [data-theme="lettuce"] { --color-accent-light: var(--color-lime-300); --color-accent-deep: var(--color-lime-600); - --color-main-dark-hard: #384a0c; - --color-main-dark-medium: #617c1f; - --color-main-dark-soft: #9bb665; - --color-main-light-soft: #f5eee2; + --color-main-dark-hard: oklch(38.049% 0.087 124.935); + --color-main-dark-medium: oklch(54.648% 0.12144 124.626); + --color-main-dark-soft: oklch(73.722% 0.11138 124.213); + --color-main-light-soft: oklch(95.132% 0.01754 81.278); --color-main-light-medium: var(--color-main-light-soft); - --color-main-light-hard: #fff9ef; + --color-main-light-hard: oklch(98.403% 0.01457 80.636); --color-main: var(--color-main-dark-hard); --color-main-hard: var(--color-main-dark-hard); --color-main-medium: var(--color-main-dark-medium); @@ -157,8 +169,8 @@ body { --color-leaflet-link: var(--color-main-medium); --color-leaflet-link-hover: var(--color-main-soft); --interlocked-hexagons-background: - url("https://storage.yandexcloud.kz/sayauz-static/themes/lettuce/linen-fabric.webp"), - url("https://storage.yandexcloud.kz/sayauz-static/themes/lettuce/lettuce-tiles.svg"); + url("https://cdn.saya.uz/static/themes/lettuce/linen-fabric.webp"), + url("https://cdn.saya.uz/static/themes/lettuce/lettuce-tiles.svg"); --interlocked-hexagons-background-repeat: repeat, repeat; --interlocked-hexagons-background-size: 11dvh, 20dvh; --squares-and-triangles-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23bbd095%22%2F%3E%3Cpath%20fill%3D%22%23f5ede1%22%20d%3D%22M0%200v6.77L6.72%200zm8.91%200L0%208.96v3.86L12.83%200zm6.11%200L0%2015.06v3.87L18.9%200zm6.11%200L0%2021.12v3.92L25%200zM25%200l25%2024.99v-3.87L28.87%200zm25%2024.99%2025%2024.98L100%2025%2075%200zm-50%20.05v3.87l21.13%2021.12H25l-25-25zm25%2024.99h3.87L50%2028.9v-3.87zm0%200L0%2075l25%2025%2025-24.99zM50%2075l25-24.98h-3.87L50%2071.14v3.87zm0%200v3.87L71.13%20100H75L50%2075.01zm25%2025h3.87L100%2078.88v-3.87zm25-24.99v-3.87L78.87%2050.03H75L100%2075zM31.1%200%2050%2018.93v-3.87L34.98%200h-3.87zm6.07%200L50%2012.82V8.96L41.04%200zm6.1%200L50%206.77V0h-6.72zM25%202.19%202.14%2025.04H25zm0%2022.85v22.8l22.81-22.8zM75%203.92l21.13%2021.12L75%2046.16%2053.87%2025.04zm0%202.19L56.06%2025.04l18.89%2018.88%2018.94-18.88L75%206.1zm0%203.92%2015.07%2015L75%2040.06%2059.98%2025.04zm0%202.18L62.17%2025.04%2075%2037.86l12.83-12.82L75%2012.2zm0%203.82%208.96%209L75%2033.95%2066.04%2025zm0%202.24-6.72%206.77L75%2031.76l6.77-6.72h-.05zM0%2031.09v3.87l15.02%2015.07h3.87zm50%200L31.1%2050.03h3.88L50%2034.96V31.1zM0%2037.2v3.87l8.91%208.96h3.92zm50%200L37.17%2050.03h3.87L50%2041.07zM0%2043.3v6.73h6.72zm50%200-6.72%206.73H50zm0%206.73v6.71l6.72-6.71zm8.96%200L50%2058.93v3.92l12.83-12.82zm6.06%200L50%2065.03v3.88l18.94-18.88zm16.09%200L100%2068.9v-3.87l-15.02-15zm6.11%200L100%2062.85v-3.92l-8.91-8.9zm6.06%200%206.72%206.71v-6.71zM75%2052.2%2052.19%2075H75zM75%2075v22.8L97.81%2075zM25%2053.9l21.13%2021.12L25%2096.13%203.87%2075.01zm0%202.2L6.1%2074.95l18.95%2018.88%2018.9-18.88L25%2056.08zm0%203.9%2015.02%2015.01L25%2090.03%209.98%2075zm0%202.19L12.17%2075%2025%2087.84%2037.83%2075%2025%2062.2zm0%203.87L33.96%2075%2025%2083.97%2016.04%2075zm0%202.23-6.72%206.72L25%2081.73l6.72-6.72L25%2068.3zm25%2012.83v3.87L65.02%20100h3.87zm50%200L81.1%20100h3.88L100%2084.99zm-50%206.06v3.86l8.96%208.96h3.87zm50%200L87.22%20100h3.87l8.91-8.96zm-50%206.1V100h6.72zm50%200L93.28%20100H100z%22%2F%3E%3C%2Fsvg%3E"); @@ -167,66 +179,76 @@ body { --squares-and-triangles-background-height-coef: 10; } +@media (prefers-color-scheme: dark) { + [data-theme="lettuce"] { + --color-accent-light: var(--color-lime-600); + --color-accent-deep: var(--color-lime-300); + --color-main: var(--color-main-light-hard); + --color-main-hard: var(--color-main-light-hard); + --color-main-medium: var(--color-main-light-medium); + --color-main-soft: var(--color-main-light-soft); + --color-background: var(--color-main-dark-hard); + --color-background-dark: var(--color-main-dark-hard); + --color-background-medium: var(--color-main-dark-medium); + --color-background-light: var(--color-main-dark-soft); + --color-sidebar-stroke: var(--color-main-light-hard); + } +} + [data-theme="night"] { - --color-accent-light: var(--color-indigo-900); - --color-accent-deep: var(--color-indigo-500); - --color-main-light-hard: var(--color-sky-100); - --color-main-light-medium: var(--color-sky-200); - --color-main-light-soft: var(--color-sky-300); - --color-main-dark-hard: #13131b; + --color-accent-light: var(--color-indigo-500); + --color-accent-deep: var(--color-indigo-800); + --color-main-light-hard: var(--color-indigo-300); + --color-main-light-medium: var(--color-indigo-200); + --color-main-light-soft: var(--color-indigo-200); + --color-main-dark-hard: oklch(19.064% 0.01614 284.757); --color-main-dark-medium: var(--color-main-dark-hard); - --color-main-dark-soft: #222230; - --color-main: var(--color-main-light-hard); - --color-main-hard: var(--color-main-light-hard); - --color-main-medium: var(--color-main-light-medium); - --color-main-soft: var(--color-main-light-soft); - --color-background: var(--color-main-dark-soft); - --color-background-dark: var(--color-main-dark-hard); - --color-background-medium: var(--color-main-dark-medium); - --color-background-light: var(--color-main-dark-soft); + --color-main-dark-soft: oklch(25.846% 0.02619 284.414); + --color-main: var(--color-main-dark-hard); + --color-main-hard: var(--color-main-dark-hard); + --color-main-medium: var(--color-main-dark-medium); + --color-main-soft: var(--color-main-dark-soft); + --color-background: var(--color-main-light-soft); + --color-background-dark: var(--color-main-light-hard); + --color-background-medium: var(--color-main-light-medium); + --color-background-light: var(--color-main-light-soft); --color-sidebar-text: var(--color-main-hard); - --color-sidebar-stroke: var(--color-main-light-soft); + --color-sidebar-stroke: var(--color-main-dark-soft); --color-secondary: var(--color-main-soft); --color-leaflet-text: var(--color-background-dark); --color-leaflet-link: var(--color-background-dark); --color-leaflet-link-hover: var(--color-background-light); --interlocked-hexagons-background: - radial-gradient( - 33.5% 25% at 100% 82%, - rgba(190, 227, 248, 0.2) 0%, - rgba(197, 179, 246, 0.13) 33.33%, - rgba(255, 0, 0, 0) 100% - ), - radial-gradient( - 54% 25% at 55% 100%, - rgba(190, 227, 248, 0.2) 0%, - rgba(197, 179, 246, 0.13) 33.33%, - rgba(255, 0, 0, 0) 100% - ), - radial-gradient( - 37% 19.5% at 20% 100%, - rgba(176, 220, 248, 0.3) 0%, - rgba(197, 172, 247, 0.2) 33.33%, - rgba(255, 0, 0, 0) 100% + linear-gradient( + 180deg, + #4c51bf 0%, + rgba(89, 108, 214, 0.75) 12%, + rgba(107, 136, 231, 0.5) 24%, + rgba(163, 191, 250, 0) 48% ), radial-gradient( - 63% 34.5% at 66% 77%, - rgba(190, 227, 248, 0.15) 0%, - rgba(197, 179, 246, 0.1) 33.33%, - rgba(255, 0, 0, 0) 100% + 100% 37.5% at 50% 100%, + #f687b3 0%, + rgba(245, 151, 227, 0.75) 25%, + rgba(217, 182, 245, 0.5) 50%, + rgba(190, 227, 248, 0) 100% + ), + linear-gradient( + 360deg, + #f6ad55 0%, + rgba(247, 183, 98, 0.75) 14%, + rgba(249, 192, 112, 0.5) 28%, + rgba(251, 211, 141, 0) 56% ), linear-gradient( - 0deg, - #2f5883 0%, - #2f4e7a 4.75%, - #2e4472 9.5%, - #2a3065 19%, - #272e57 26.5%, - #242b4b 34%, - #1f2437 49%, - #1c2030 57.75%, - #1a1b29 66.5%, - #13131b 84% + 90deg, + #bee3f8 0%, + #b7e0f7 14.29%, + #b1ddf7 28.57%, + #aadaf6 42.86%, + #a4d7f6 57.14%, + #9dd3f5 71.43%, + #90cdf4 100% ); --interlocked-hexagons-background-repeat: no-repeat; --interlocked-hexagons-background-size: 100dvw 100dvh; @@ -236,47 +258,117 @@ body { --squares-and-triangles-background-height-coef: 5; } +@media (prefers-color-scheme: dark) { + [data-theme="night"] { + --color-accent-light: var(--color-indigo-900); + --color-accent-deep: var(--color-indigo-500); + --color-main: var(--color-main-light-hard); + --color-main-hard: var(--color-main-light-hard); + --color-main-medium: var(--color-main-light-medium); + --color-main-soft: var(--color-main-light-soft); + --color-background: var(--color-main-dark-soft); + --color-background-dark: var(--color-main-dark-hard); + --color-background-medium: var(--color-main-dark-medium); + --color-background-light: var(--color-main-dark-soft); + --color-sidebar-stroke: var(--color-main-light-soft); + --interlocked-hexagons-background: + radial-gradient( + 33.5% 25% at 100% 82%, + rgba(190, 227, 248, 0.2) 0%, + rgba(197, 179, 246, 0.13) 33.33%, + rgba(255, 0, 0, 0) 100% + ), + radial-gradient( + 54% 25% at 55% 100%, + rgba(190, 227, 248, 0.2) 0%, + rgba(197, 179, 246, 0.13) 33.33%, + rgba(255, 0, 0, 0) 100% + ), + radial-gradient( + 37% 19.5% at 20% 100%, + rgba(176, 220, 248, 0.3) 0%, + rgba(197, 172, 247, 0.2) 33.33%, + rgba(255, 0, 0, 0) 100% + ), + radial-gradient( + 63% 34.5% at 66% 77%, + rgba(190, 227, 248, 0.15) 0%, + rgba(197, 179, 246, 0.1) 33.33%, + rgba(255, 0, 0, 0) 100% + ), + linear-gradient( + 0deg, + #2f5883 0%, + #2f4e7a 4.75%, + #2e4472 9.5%, + #2a3065 19%, + #272e57 26.5%, + #242b4b 34%, + #1f2437 49%, + #1c2030 57.75%, + #1a1b29 66.5%, + #13131b 84% + ); + } +} + [data-theme="ram"] { - --color-accent-light: var(--color-orange-800); - --color-accent-deep: var(--color-orange-300); - --color-main-light-hard: #fefad8; - --color-main-light-medium: #f6e4c5; - --color-main-light-soft: #e8d4b8; - --color-main-dark-hard: #413b34; - --color-main-dark-medium: #62584f; - --color-main-dark-soft: #6d6258; - --color-main: var(--color-main-light-hard); - --color-main-hard: var(--color-main-light-hard); - --color-main-medium: var(--color-main-light-medium); - --color-main-soft: var(--color-main-light-soft); - --color-background: var(--color-main-dark-soft); - --color-background-dark: var(--color-main-dark-hard); - --color-background-medium: var(--color-main-dark-medium); - --color-background-light: var(--color-main-dark-soft); + --color-accent-light: var(--color-orange-300); + --color-accent-deep: var(--color-orange-800); + --color-main-light-hard: oklch(97.95% 0.0443 101.66); + --color-main-light-medium: oklch(92.541% 0.04519 81.305); + --color-main-light-soft: oklch(87.909% 0.04338 76.287); + --color-main-dark-hard: oklch(35.615% 0.01442 71.558); + --color-main-dark-medium: oklch(46.724% 0.01927 64.275); + --color-main-dark-soft: oklch(50.352% 0.02092 64.558); + --color-main: var(--color-main-dark-hard); + --color-main-hard: var(--color-main-dark-hard); + --color-main-medium: var(--color-main-dark-medium); + --color-main-soft: var(--color-main-dark-soft); + --color-background: var(--color-main-light-hard); + --color-background-dark: var(--color-main-light-hard); + --color-background-medium: var(--color-main-light-medium); + --color-background-light: var(--color-main-light-soft); --color-sidebar-text: var(--color-main-hard); - --color-sidebar-stroke: var(--color-main-light-soft); + --color-sidebar-stroke: var(--color-main-dark-soft); --color-secondary: var(--color-main-soft); --color-leaflet-text: var(--color-background-dark); --color-leaflet-link: var(--color-background-dark); --color-leaflet-link-hover: var(--color-background-light); - --interlocked-hexagons-background: url("https://storage.yandexcloud.kz/sayauz-static/themes/ram/ram-background.webp"); + --interlocked-hexagons-background: url("https://cdn.saya.uz/static/themes/ram/ram-background.webp"); --interlocked-hexagons-background-repeat: repeat; --interlocked-hexagons-background-size: auto; - --squares-and-triangles-background: url("https://storage.yandexcloud.kz/sayauz-static/themes/ram/ram-sidebar.webp"); + --squares-and-triangles-background: url("https://cdn.saya.uz/static/themes/ram/ram-sidebar.webp"); --squares-and-triangles-background-repeat: repeat; --squares-and-triangles-background-width-coef: 5; --squares-and-triangles-background-height-coef: 15; } +@media (prefers-color-scheme: dark) { + [data-theme="ram"] { + --color-accent-light: var(--color-orange-800); + --color-accent-deep: var(--color-orange-300); + --color-main: var(--color-main-light-hard); + --color-main-hard: var(--color-main-light-hard); + --color-main-medium: var(--color-main-light-medium); + --color-main-soft: var(--color-main-light-soft); + --color-background: var(--color-main-dark-soft); + --color-background-dark: var(--color-main-dark-hard); + --color-background-medium: var(--color-main-dark-medium); + --color-background-light: var(--color-main-dark-soft); + --color-sidebar-stroke: var(--color-main-light-soft); + } +} + [data-theme="lilac"] { --color-accent-light: var(--color-indigo-300); --color-accent-deep: var(--color-indigo-800); - --color-main-dark-hard: #343b58; - --color-main-dark-medium: #414868; - --color-main-dark-soft: #6c6e75; - --color-main-light-hard: #e3e6ff; - --color-main-light-medium: #d1d6ff; - --color-main-light-soft: #c3caff; + --color-main-dark-hard: oklch(35.926% 0.05132 273.187); + --color-main-dark-medium: oklch(40.944% 0.05461 274.279); + --color-main-dark-soft: oklch(53.885% 0.01118 273.226); + --color-main-light-hard: oklch(93.022% 0.03408 280.254); + --color-main-light-medium: oklch(88.545% 0.05677 279.69); + --color-main-light-soft: oklch(85.154% 0.07433 278.817); --color-main: var(--color-main-dark-hard); --color-main-hard: var(--color-main-dark-hard); --color-main-medium: var(--color-main-dark-medium); @@ -298,8 +390,24 @@ body { --squares-and-triangles-background-repeat: repeat; --squares-and-triangles-background-width-coef: 5; --squares-and-triangles-background-height-coef: 5; - --split-left-background: url("https://storage.yandexcloud.kz/sayauz-static/themes/lilac/split-left.webp"); - --split-right-background: url("https://storage.yandexcloud.kz/sayauz-static/themes/lilac/split-right.webp"); + --split-right-background: url("https://cdn.saya.uz/static/themes/lilac/split-right-light.webp"); +} + +@media (prefers-color-scheme: dark) { + [data-theme="lilac"] { + --color-accent-light: var(--color-indigo-800); + --color-accent-deep: var(--color-indigo-300); + --color-main: var(--color-main-light-hard); + --color-main-hard: var(--color-main-light-hard); + --color-main-medium: var(--color-main-light-medium); + --color-main-soft: var(--color-main-light-soft); + --color-background: var(--color-main-dark-hard); + --color-background-dark: var(--color-main-dark-hard); + --color-background-medium: var(--color-main-dark-medium); + --color-background-light: var(--color-main-dark-soft); + --color-sidebar-stroke: var(--color-main-dark-hard); + --split-right-background: url("https://cdn.saya.uz/static/themes/lilac/split-right-dark.webp"); + } } a, @@ -314,16 +422,16 @@ a:hover, } #map-container { - color: var(--color-leaflet-text) !important; + color: var(--color-main-hard) !important; } #map-container a { - color: var(--color-leaflet-link) !important; + color: var(--color-main-medium) !important; text-decoration: underline; } #map-container a:hover { - color: var(--color-leaflet-link-hover) !important; + color: var(--color-main-soft) !important; text-decoration: underline; } @@ -348,6 +456,26 @@ form.crossable.htmx-request input { filter: grayscale(1); } +.multitone { + position: relative; + z-index: 1; +} + +.multitone::before { + filter: url(#multitone); + background-image: var(--multitone-bg); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; +} + .bg-interlocked-hexagons { background-image: var(--interlocked-hexagons-background); background-repeat: var(--interlocked-hexagons-background-repeat); @@ -359,13 +487,6 @@ form.crossable.htmx-request input { background-repeat: var(--squares-and-triangles-background-repeat); } -.bg-split-left { - background-image: var(--split-left-background); - background-repeat: var(--split-left-repeat); - background-size: var(--split-left-size); - background-position: var(--split-left-position); -} - .bg-split-right { background-image: var(--split-right-background); background-repeat: var(--split-right-repeat); @@ -718,9 +839,29 @@ form.crossable.htmx-request input { height: 100% !important; } -body[data-theme~="night"] .leaflet-tile, -body[data-theme~="ram"] .leaflet-tile { - filter: invert(1) hue-rotate(190deg) contrast(1.1) brightness(1.2) !important; +.leaflet-container .leaflet-control-attribution { + background: color-mix(in oklch, var(--color-background-medium) 80%, transparent) !important; +} + +.leaflet-popup-content-wrapper, .leaflet-popup-tip { + background: var(--color-background-medium) !important; +} + +.leaflet-bar a { + background-color: var(--color-background-medium) !important; +} + +.leaflet-popup-content-wrapper, .leaflet-popup-tip { + color: var(--color-main-medium) !important; +} + +.leaflet-marker-icon.marker-cluster { + color: var(--color-main-dark-hard) !important; +} + +.leaflet-attribution-flag { + visibility: hidden; + width: 0; } .home-page-heart { diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html index 4230a57..9220b7f 100644 --- a/views/layouts/general-page.html +++ b/views/layouts/general-page.html @@ -14,7 +14,7 @@ {{end}} /> {{- if eq .Property "og:title" }} - <title>{{.Content}} // SAYA.UZ</title> + <title>{{.Content}}</title> {{- end }} {{- if eq .Property "og:description" }} <meta name="description" content="{{.Content}}" /> {{- end }} {{- if eq .Property "og:url" }} @@ -22,15 +22,23 @@ {{- end }} {{- end }} <link rel="stylesheet" - href="{{ .StaticStorageBaseUrl }}/fonts/fonts.css" + href="{{ .StaticStorage.BaseUrl }}/fonts/fonts.css" /> <link rel="stylesheet" - href="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.css" + href="{{ .StaticStorage.BaseUrl }}/libs/leaflet/1.9.4/leaflet.css" /> <link rel="stylesheet" - href="{{ .StaticStorageBaseUrl }}/libs/glightbox/3.3.0/css/glightbox.min.css" + href="{{ .StaticStorage.BaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.css" + /> + <link + rel="stylesheet" + href="{{ .StaticStorage.BaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css" + /> + <link + rel="stylesheet" + href="{{ .StaticStorage.BaseUrl }}/libs/glightbox/3.3.0/css/glightbox.min.css" /> <link rel="stylesheet" href="/output.css" /> <link @@ -55,6 +63,11 @@ {{ . }} </script> {{- end }} + <script src="{{ .StaticStorage.BaseUrl }}/libs/htmx/2.0.6/htmx.min.js"></script> + <script src="{{ .StaticStorage.BaseUrl }}/libs/leaflet/1.9.4/leaflet.js"></script> + <script src="{{ .StaticStorage.BaseUrl }}/libs/protomaps-leaflet/5.0.0/dist/protomaps-leaflet.js"></script> + <script src="{{ .StaticStorage.BaseUrl }}/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js"></script> + <script src="{{ .StaticStorage.BaseUrl }}/libs/glightbox/3.3.0/js/glightbox.min.js"></script> </head> <body @@ -109,6 +122,17 @@ <path d="M17.71 1c0.79 0.05 1.58 0 2.37 0a7.47 7.47 0 0 1 0.94 0.09 9 9 0 0 1 1.26 0.16 0.47 0.47 0 0 1 0.29 0.28 4.56 4.56 0 0 1 0.25 1.14 32.83 32.83 0 0 1 0.28 3.72c0 0.09 0.51 0.32 0.6 -0.14l0.06 -0.47a21.7 21.7 0 0 0 0.16 -3.22 4.7 4.7 0 0 0 -0.28 -1.5 1.53 1.53 0 0 0 -1 -0.93 8.59 8.59 0 0 0 -1.49 -0.1 9.73 9.73 0 0 0 -1.08 0c-0.79 0.06 -1.56 0.2 -2.35 0.25a0.34 0.34 0 0 0 -0.34 0.34 0.34 0.34 0 0 0 0.33 0.38Z" fill="var(--color-accent-deep)" fill-rule="evenodd" stroke-width="1"></path> <path d="M23.82 18.17c0 -0.42 -0.05 -0.84 -0.4 -0.78s-0.26 0.35 -0.27 0.81a28.84 28.84 0 0 1 -0.27 3.08 4.63 4.63 0 0 1 -0.24 1.14 0.46 0.46 0 0 1 -0.3 0.28 7.6 7.6 0 0 1 -1.25 0.16 7.63 7.63 0 0 1 -0.95 0.09l-2.36 0a0.35 0.35 0 1 0 0 0.69c0.79 0.05 1.56 0.19 2.35 0.25a9.53 9.53 0 0 0 1.07 0 8.52 8.52 0 0 0 1.49 -0.1 1.55 1.55 0 0 0 1 -0.93 4.7 4.7 0 0 0 0.28 -1.5 23 23 0 0 0 -0.15 -3.19Z" fill="var(--color-accent-deep)" fill-rule="evenodd" stroke-width="1"></path> </g></symbol> + <symbol id="icon-like" viewBox="-2 -2 28 28" fill="none"> + <path fill="currentColor" d="M22.155 6.29294c-0.8113 -1.34837 -1.8229 -2.56561 -3 -3.61 -1.6282 -1.47261 -3.6782 -2.396134 -5.86 -2.6399952C11.1388 -0.137008 8.97321 0.252665 7.015 1.17294c-1.6708 0.75913 -3.14018 1.8997 -4.29 3.33C1.59785 5.92036 0.798705 7.56988 0.385003 9.33294c-0.010341 0.04271 -0.012051 0.08704 -0.005029 0.13041 0.007022 0.04337 0.022633 0.08491 0.045919 0.12216 0.023286 0.03726 0.053777 0.06949 0.089685 0.09481 0.035908 0.02532 0.07651 0.04321 0.119425 0.05262 0.083317 0.01503 0.169224 -0.00254 0.239953 -0.04907 0.070728 -0.04653 0.120867 -0.11846 0.140044 -0.20093 0.42729 -1.65655 1.22384 -3.19495 2.33 -4.5 1.0777 -1.30186 2.44846 -2.32993 4 -3 1.83266 -0.80027 3.84 -1.117035 5.83 -0.92 1.9654 0.25006 3.804 1.10643 5.26 2.45 1.0733 0.9746 1.9996 2.09963 2.75 3.34 0.77 1.20382 1.2366 2.57635 1.36 3.99996 0.1304 1.5877 -0.0565 3.1854 -0.55 4.7 -0.5032 1.5017 -1.4221 2.8298 -2.65 3.83 -0.7981 0.673 -1.7331 1.1644 -2.74 1.44 -1.5427 0.4432 -3.1354 0.6885 -4.74 0.73 -0.0778 0.0052 -0.1505 0.0404 -0.2028 0.0983 -0.0523 0.0579 -0.08 0.1338 -0.0772 0.2117 0 0.039 0.0078 0.0775 0.023 0.1134 0.0152 0.0358 0.0374 0.0683 0.0654 0.0953 0.028 0.0271 0.0612 0.0482 0.0976 0.0622 0.0363 0.014 0.0751 0.0205 0.114 0.0191 1.6629 -0.0085 3.3184 -0.2201 4.93 -0.63 1.0928 -0.2824 2.1146 -0.7899 3 -1.49 1.3927 -1.0653 2.454 -2.5046 3.06 -4.15 0.5754 -1.6287 0.8235 -3.3551 0.73 -5.08 -0.1076 -1.59917 -0.6055 -3.14765 -1.45 -4.50996Z" stroke-width="1"></path> + <path fill="currentColor" d="M15.935 15.903c0.6791 -0.2938 1.2468 -0.7964 1.6208 -1.4347 0.3741 -0.6384 0.5349 -1.3794 0.4592 -2.1153 -0.0284 -0.1682 -0.0994 -0.3264 -0.2061 -0.4594 -0.1068 -0.133 -0.2458 -0.2365 -0.4039 -0.3007 -0.4529 -0.1228 -0.927 -0.1466 -1.39 -0.0699 -1.22 0.07 -3.05 0.38 -4.54 0.54 -0.4 0.05 -0.78 0.08 -1.11 0.1 -0.0689 0.0127 -0.1312 0.0492 -0.1761 0.1031 -0.0448 0.0539 -0.0693 0.1217 -0.0693 0.1919 0 0.0701 0.0245 0.1379 0.0693 0.1918 0.0449 0.0539 0.1072 0.0904 0.1761 0.1032l5.66 -0.18h0.83c0.0328 0.5028 -0.0896 1.0037 -0.3508 1.4347 -0.2611 0.431 -0.6483 0.7715 -1.1092 0.9753 -0.4499 0.3065 -0.9606 0.5125 -1.4972 0.6039 -0.5367 0.0915 -1.0868 0.0663 -1.6128 -0.074 -0.4102 -0.1727 -0.7888 -0.4126 -1.12 -0.71 -0.3351 -0.2774 -0.6136 -0.6169 -0.82 -1 -0.0184 -0.0407 -0.0446 -0.0774 -0.0771 -0.108 -0.0326 -0.0305 -0.0709 -0.0544 -0.1127 -0.0702 -0.0418 -0.0158 -0.0862 -0.0232 -0.1309 -0.0218 -0.04465 0.0014 -0.08858 0.0116 -0.12929 0.03 -0.04037 0.0174 -0.07686 0.0426 -0.10734 0.0743 -0.03049 0.0316 -0.05435 0.069 -0.07018 0.11 -0.01584 0.041 -0.02333 0.0847 -0.02203 0.1286 0.00129 0.044 0.01133 0.0872 0.02955 0.1272 0.21114 0.4969 0.52069 0.9459 0.90999 1.32 0.3786 0.3811 0.8137 0.7015 1.29 0.95 0.6592 0.2289 1.361 0.3082 2.0546 0.2321 0.6936 -0.0762 1.3616 -0.3057 1.9554 -0.6721Z" stroke-width="1"></path> + <path fill="currentColor" d="M8.33499 10.9829c0.22252 0.0613 0.45748 0.0613 0.68 0 0.17653 -0.0617 0.33498 -0.1663 0.46112 -0.3044 0.12615 -0.1381 0.21605 -0.3053 0.26164 -0.4867 0.04559 -0.1814 0.04545 -0.37124 -0.00041 -0.55256 -0.04586 -0.18131 -0.136 -0.34841 -0.26235 -0.4863 -1 -1.24 -3 0.82 -1.74 1.71996 0.1869 0.0887 0.39382 0.1266 0.6 0.11Z" stroke-width="1"></path> + <path fill="currentColor" d="M19.1653 10.343c0.2247 0.0401 0.456 0.0194 0.67 -0.06 0.1784 -0.0611 0.3386 -0.1658 0.4661 -0.30464 0.1275 -0.13885 0.2183 -0.3074 0.264 -0.4903 0.0457 -0.1829 0.045 -0.37432 -0.0022 -0.55685 -0.0472 -0.18253 -0.1393 -0.35035 -0.2679 -0.48818 -1 -1.23 -3 0.83 -1.74 1.72997 0.1838 0.1117 0.3949 0.1705 0.61 0.17Z" stroke-width="1"></path> + <path fill="currentColor" d="M9.63505 20.0929c-0.10233 -0.204 -0.25778 -0.3768 -0.45 -0.5 0.19629 -0.3235 0.27075 -0.7064 0.21 -1.08 -0.04172 -0.3535 -0.15757 -0.6943 -0.34 -1 -0.13496 -0.1997 -0.31256 -0.3671 -0.52 -0.49 0.04212 -0.0952 0.06912 -0.1964 0.08 -0.3 0.05325 -0.3277 0.02651 -0.6634 -0.07796 -0.9786 -0.10447 -0.3151 -0.28357 -0.6003 -0.52204 -0.8314 -0.51428 -0.3306 -1.1194 -0.4915 -1.73 -0.46 -0.74972 -0.022 -1.49478 0.125 -2.18 0.43 0.05982 -0.8997 -0.19079 -1.7927 -0.71 -2.53 -0.9 -1 -2.54 -0.13 -2.71 1.36 -0.017326 0.6116 0.060262 1.2222 0.23 1.81 0.28 1 0.23 0.95 0.48 1.33 -0.22002 0.2395 -0.405175 0.5088 -0.55 0.8 -0.1 0.34 -0.28 0.74 -0.12 1 0.16 0.26 0.3 0.23 0.47 0.06 0.17 -0.17 0.09 -0.44 0.14 -0.6 0.07428 -0.2866 0.22256 -0.5487 0.43 -0.76 0.16 -0.21 0.28 -0.22 0.47 -0.4 1.85 -1.77 4.62 -1.97 5.12 -1.28 0.85 1.16 -1.16 1.29 -2.9 2.37 -0.03825 0.011 -0.07372 0.03 -0.10405 0.0557 -0.03034 0.0258 -0.05483 0.0577 -0.07187 0.0936 -0.01703 0.036 -0.0262 0.0752 -0.02691 0.1149 -0.0007 0.0398 0.00708 0.0793 0.02283 0.1158 0.01228 0.0374 0.03203 0.0719 0.05805 0.1014 0.02602 0.0295 0.05777 0.0534 0.09332 0.0702 0.03554 0.0168 0.07414 0.0263 0.11345 0.0277 0.0393 0.0015 0.07849 -0.0051 0.11518 -0.0193 0.41 -0.06 0.77 -0.06 1.12 -0.11 1 -0.17 1.25 -0.66 2.16 -0.51 0.15573 0.127 0.26274 0.3039 0.30284 0.5008 0.0401 0.1968 0.01083 0.4015 -0.08284 0.5792 -0.45993 0.315 -0.97588 0.539 -1.52 0.66 -0.36935 0.1765 -0.72678 0.377 -1.07 0.6 -0.03798 0.0103 -0.07348 0.0282 -0.10433 0.0526 -0.03085 0.0244 -0.05641 0.0549 -0.07512 0.0895 -0.01871 0.0346 -0.03018 0.0727 -0.03371 0.1119 -0.00353 0.0391 0.00094 0.0786 0.01316 0.116 0.01027 0.038 0.02815 0.0735 0.05258 0.1044 0.02442 0.0308 0.05486 0.0564 0.08948 0.0751 0.03461 0.0187 0.07267 0.0302 0.11185 0.0337 0.03919 0.0035 0.07869 -0.0009 0.11609 -0.0132 0.36103 -0.003 0.7216 -0.0264 1.08 -0.07 0.15662 -0.0212 0.31072 -0.0581 0.46 -0.11 0.36406 -0.2158 0.79585 -0.2872 1.21 -0.2 0.03286 0.0062 0.06407 0.0192 0.09162 0.0381 0.02755 0.019 0.05084 0.0435 0.06838 0.0719 0.03012 0.0772 0.03012 0.1629 0 0.24 -0.05277 0.2543 -0.17376 0.4893 -0.35 0.68 -0.20205 0.2292 -0.43028 0.434 -0.68 0.61 -0.66869 0.4607 -1.44234 0.7461 -2.25 0.83 -0.80071 0.1179 -1.61838 -0.0039 -2.35 -0.35 -0.53395 -0.2824 -0.95681 -0.737 -1.2 -1.29 -0.27045 -0.5855 -0.42982 -1.2162 -0.47 -1.86 -0.00126 -0.0442 -0.0114 -0.0877 -0.02981 -0.1279 -0.01841 -0.0403 -0.04473 -0.0764 -0.07738 -0.1062 -0.03265 -0.0299 -0.07097 -0.0529 -0.11269 -0.0676 -0.04171 -0.0148 -0.08596 -0.021 -0.13012 -0.0183 -0.044675 0.0026 -0.088408 0.0139 -0.12869 0.0334s-0.076321 0.0468 -0.106051 0.0802c-0.029729 0.0334 -0.052563 0.0724 -0.067194 0.1147 -0.014631 0.0423 -0.02077 0.0871 -0.018065 0.1317 0.012623 0.8862 0.235395 1.7567 0.65 2.54 0.30359 0.5453 0.74942 0.9981 1.29 1.31 0.8694 0.479 1.8607 0.6912 2.85 0.61 0.9936 -0.0912 1.94725 -0.4355 2.77 -1 0.4107 -0.274 0.77525 -0.6115 1.08 -1 0.2272 -0.2941 0.3845 -0.6361 0.46 -1 0.04902 -0.304 -0.00001 -0.6157 -0.14 -0.89Zm-7.81 -3.72 -0.09 -1c0 -0.75 -0.13 -1.73 0.37 -2.19 0.1 0 0.45 -0.59 0.88 0.22 0.2375 0.4432 0.38371 0.9294 0.43 1.43 -0.00039 0.0506 0.01118 0.1005 0.03377 0.1457 0.0226 0.0451 0.05557 0.0843 0.09623 0.1143 -0.63566 0.3358 -1.2158 0.7676 -1.72 1.28Z" stroke-width="1"></path> + </symbol> + <symbol id="icon-view" viewBox="-2 -2 28 28" fill="none"> + <path fill="currentColor" fill-rule="evenodd" d="M23.6534 10.5263c-1.3628 -1.56103 -3.0387 -2.81795 -4.9189 -3.68916 -1.8052 -0.8978 -3.7485 -1.48585 -5.7486 -1.7396 -0.998 -0.11306 -2.0067 -0.09289 -2.99934 0.05999 -2.63062 0.3739 -5.14267 1.33771 -7.34828 2.81934 -0.84337 0.55535 -1.63325 1.18793 -2.359454 1.88956 -0.08425 0.0868 -0.161162 0.18047 -0.2299462 0.27997C0.0169406 10.1973 0 10.2562 0 10.3163s0.0169406 0.1191 0.0488798 0.17c0.0179573 0.0358 0.0429176 0.0677 0.0734052 0.0937 0.030488 0.0261 0.065885 0.0457 0.104099 0.0578 0.038214 0.0122 0.078469 0.0165 0.118385 0.0128 0.039917 -0.0037 0.078686 -0.0154 0.114015 -0.0343C1.5971 9.42178 2.95486 8.45777 4.45785 7.77692c0.959 -0.48729 1.9636 -0.87909 2.9993 -1.16973 0.88979 -0.25558 1.79943 -0.43617 2.71935 -0.53987 0.8961 -0.11518 1.8033 -0.11518 2.6994 0 1.8879 0.28843 3.7154 0.88632 5.4087 1.76959 1.6883 0.80557 3.2029 1.93308 4.459 3.31919 0.0877 0.152 0.1339 0.3244 0.1339 0.4999s-0.0462 0.3479 -0.1339 0.4999c-0.2004 0.4189 -0.4747 0.7983 -0.8098 1.1197 -0.9132 0.9438 -1.9179 1.7945 -2.9993 2.5394 -1.0941 0.7277 -2.2767 1.3123 -3.5192 1.7396 -0.786 0.2536 -1.6039 0.395 -2.4294 0.4199 -1.2488 0.0246 -2.4978 -0.0321 -3.73917 -0.1699 -1.55221 -0.2376 -3.01813 -0.8673 -4.259 -1.8296 -1.29902 -0.9356 -2.40844 -2.1095 -3.26924 -3.4592 -0.02265 -0.0404 -0.05333 -0.0757 -0.09014 -0.1039 -0.03681 -0.0281 -0.07898 -0.0484 -0.12392 -0.0596 -0.04494 -0.0113 -0.0917 -0.0132 -0.13741 -0.0057s-0.08941 0.0243 -0.12842 0.0492c-0.07659 0.0529 -0.13044 0.1327 -0.15082 0.2235 -0.02039 0.0908 -0.0058 0.1859 0.04085 0.2664 0.8813 1.4449 2.02784 2.7102 3.37921 3.7292 1.34805 1.0808 2.9532 1.7938 4.65891 2.0695 1.26545 0.179 2.54115 0.2758 3.81915 0.2899 0.8996 0.0048 1.7952 -0.1198 2.6593 -0.3699 1.3434 -0.4251 2.6244 -1.0269 3.8092 -1.7896 1.1699 -0.7705 2.2556 -1.6619 3.2392 -2.6594 0.5478 -0.5108 0.9601 -1.1498 1.1997 -1.8595 0.0999 -0.2934 0.1302 -0.606 0.0886 -0.9131 -0.0417 -0.3071 -0.1541 -0.6003 -0.3285 -0.8565Z" clip-rule="evenodd" stroke-width="1"></path> + <path fill="currentColor" fill-rule="evenodd" d="M10.6964 9.1166c0.7827 -0.2444 1.6136 -0.29247 2.4194 -0.13997 0.3938 0.09551 0.7634 0.27195 1.0852 0.51809 0.3219 0.24613 0.589 0.55658 0.7844 0.91158 0.0189 0.0362 0.0448 0.0683 0.0762 0.0944 0.0314 0.0262 0.0677 0.0458 0.1067 0.0579 0.039 0.0121 0.08 0.0163 0.1207 0.0125 0.0407 -0.0038 0.0802 -0.0157 0.1163 -0.0348 0.0713 -0.04 0.124 -0.1066 0.1465 -0.1853 0.0224 -0.0786 0.0129 -0.163 -0.0266 -0.2346 -0.213 -0.44629 -0.5169 -0.84312 -0.8922 -1.16507 -0.3753 -0.32195 -0.8138 -0.56192 -1.2873 -0.70452 -0.9344 -0.26583 -1.92 -0.29674 -2.8693 -0.08998 -0.53345 0.13369 -1.02665 0.39446 -1.4375 0.76005 -0.41085 0.36559 -0.72716 0.82514 -0.92194 1.33942 -0.41881 1.0115 -0.41881 2.1478 0 3.1593 0.4668 0.9845 1.29879 1.7483 2.31944 2.1295 0.9793 0.3819 2.0556 0.438 3.0693 0.16 0.8982 -0.2612 1.6632 -0.8546 2.1395 -1.6596 0.4789 -0.7747 0.6995 -1.6814 0.6299 -2.5894 -0.0061 -0.0449 -0.0211 -0.0881 -0.0441 -0.127 -0.0231 -0.039 -0.0536 -0.073 -0.0899 -0.1 -0.0363 -0.027 -0.0777 -0.0465 -0.1216 -0.0573 -0.044 -0.0108 -0.0897 -0.0127 -0.1344 -0.0057 -0.0457 0.0049 -0.0899 0.019 -0.1301 0.0415 -0.0401 0.0224 -0.0752 0.0528 -0.1033 0.0893 -0.028 0.0364 -0.0484 0.0782 -0.0598 0.1227 -0.0114 0.0446 -0.0137 0.091 -0.0067 0.1364 -0.0016 0.7166 -0.2252 1.4151 -0.6398 1.9996 -0.3854 0.5945 -0.9884 1.0144 -1.6796 1.1697 -0.8061 0.1738 -1.6466 0.0863 -2.3995 -0.2499 -0.7468 -0.2609 -1.36555 -0.7974 -1.72958 -1.4997 -0.29453 -0.7244 -0.29453 -1.5351 0 -2.2595 0.1115 -0.3727 0.31161 -0.7129 0.58321 -0.99148 0.27161 -0.27856 0.60667 -0.48722 0.97647 -0.60812Z" clip-rule="evenodd" stroke-width="1"></path> + </symbol> </svg> <script> @@ -149,8 +173,6 @@ } </script> - <script src="{{ .StaticStorageBaseUrl }}/libs/glightbox/3.3.0/js/glightbox.min.js"></script> - <script> function sfc32(a, b, c, d) { return function () { @@ -205,6 +227,89 @@ return vminValue; } + const _probe = document.createElement('div'); + _probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none'; + document.body.appendChild(_probe); + const _ctx = document.createElement('canvas').getContext('2d'); + + function resolveColor(cssValue) { + // Step 1: let the DOM resolve color-mix/oklch/var to its computed form + _probe.style.backgroundColor = ''; + _probe.style.backgroundColor = cssValue; + const computed = getComputedStyle(_probe).backgroundColor; + if (!computed) return [0,0,0]; + + // Step 2: funnel through canvas to force sRGB + _ctx.fillStyle = '#000'; + _ctx.fillStyle = computed; + const s = _ctx.fillStyle; // "#rrggbb" or "rgba(...)" + + if (s.startsWith('#')) { + return [ + parseInt(s.slice(1,3),16)/255, + parseInt(s.slice(3,5),16)/255, + parseInt(s.slice(5,7),16)/255, + ]; + } + const m = s.match(/[\d.]+/g); + return [+m[0]/255, +m[1]/255, +m[2]/255]; + } + + function oklchMix(c1, c2, t) { + // Use the browser's color-mix() to interpolate in oklch + return `color-mix(in oklch, ${c1} ${(1-t)*100}%, ${c2})`; + } + + function makeMultitoneOklch(id, stops, ...vars) { + if (vars.length < 2) { + console.error('Less than 2 colors set in input'); + return; + } + + const root = getComputedStyle(document.body); + const rs = [], gs = [], bs = []; + + for (let i = 0; i < vars.length - 1; i++) { + const c1 = root.getPropertyValue(vars[i]).trim(); + const c2 = root.getPropertyValue(vars[i+1]).trim(); + + if (!c1) { + console.error(`CSS variable ${vars[i]} not found`); + return; + } + if (!c2) { + console.error(`CSS variable ${vars[i+1]} not found`); + return; + } + + for (let i = 0; i < stops; i++) { + const t = i / (stops - 1); + const [r, g, b] = resolveColor(oklchMix(c1, c2, t)); + rs.push(r.toFixed(4)); + gs.push(g.toFixed(4)); + bs.push(b.toFixed(4)); + } + } + + let svg = document.getElementById(id + '-svg'); + if (svg) svg.remove(); + document.body.insertAdjacentHTML('beforeend', ` + <svg id="${id}-svg" width="0" height="0" style="position:absolute"> + <filter id="${id}"> + <feColorMatrix type="matrix" values=" + 0.299 0.587 0.114 0 0 + 0.299 0.587 0.114 0 0 + 0.299 0.587 0.114 0 0 + 0 0 0 1 0"/> + <feComponentTransfer color-interpolation-filters="sRGB"> + <feFuncR type="table" tableValues="${rs.join(' ')}"/> + <feFuncG type="table" tableValues="${gs.join(' ')}"/> + <feFuncB type="table" tableValues="${bs.join(' ')}"/> + </feComponentTransfer> + </filter> + </svg>`); + } + function switchThemeDecor(theme) { document.querySelectorAll(".theme-decor").forEach((e) => { e.remove(); @@ -248,7 +353,7 @@ const frameCenters = new Array(4); for (i = 1; i <= 4; i++) { let frame = document.createElement("img"); - frame.src = `{{ .StaticStorageBaseUrl }}/themes/ram/ram-frame${i}.webp`; + frame.src = `{{ .StaticStorage.BaseUrl }}/themes/ram/ram-frame${i}.webp`; frame.classList.add( "ram-frame", "theme-decor", @@ -311,12 +416,23 @@ const savedTheme = localStorage.getItem("theme") || "lilac"; switchTheme(savedTheme); + + function switchColorScheme(isNightMode) { + if (isNightMode) { + makeMultitoneOklch('multitone', 4, '--color-background-dark', '--color-accent-light', '--color-background-light'); + } else { + makeMultitoneOklch('multitone', 4, '--color-background-light', '--color-accent-light', '--color-background-dark'); + } + } + + const mq = matchMedia('(prefers-color-scheme: dark)'); + switchColorScheme(mq.matches); + mq.addEventListener('change', (e) => switchColorScheme(e.matches)); </script> - <script src="{{ .StaticStorageBaseUrl }}/libs/htmx/2.0.6/htmx.min.js"></script> - <script src="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.js"></script> - <script src="{{ .StaticStorageBaseUrl }}/libs/imagesloaded/5.0.0/imagesloaded.pkgd.min.js"></script> - <script src="{{ .StaticStorageBaseUrl }}/libs/packery/3.0.0/dist/packery.pkgd.min.js"></script> + <script src="{{ .StaticStorage.BaseUrl }}/libs/htmx/2.0.6/htmx.min.js"></script> + <script src="{{ .StaticStorage.BaseUrl }}/libs/imagesloaded/5.0.0/imagesloaded.pkgd.min.js"></script> + <script src="{{ .StaticStorage.BaseUrl }}/libs/packery/3.0.0/dist/packery.pkgd.min.js"></script> <script> // Enable fitWidth-like attribute for Packery. Source: https://codepen.io/desandro/pen/kVmLYz @@ -525,7 +641,7 @@ ></div> <div - class="max-h-full max-w-full flex flex-col grow bg-paper bg-background-dark relative md:pt-4 md:pr-4" + class="max-h-full max-w-full flex flex-col grow bg-paper bg-background-dark relative pt-4 md:pr-4" > <header id="general-page-header" @@ -545,7 +661,7 @@ <main id="general-page-body" - class="relative flex-1 overflow-y-auto" + class="relative flex-1 overflow-y-auto @container-size" >{{ .RenderedBody }}</main> <div id="general-page-body-trigger" @@ -553,6 +669,7 @@ hx-trigger="popstate from:window" hx-target="#general-page-body" hx-swap="innerHTML" + hx-include="#general-page-body .htmx-exit-trigger" ></div> <footer diff --git a/views/messages/new-post.html b/views/messages/new-post.html index 98f0b52..4e586e2 100644 --- a/views/messages/new-post.html +++ b/views/messages/new-post.html @@ -1,15 +1,15 @@ {{ define "body" }} -<p>{{ .L.Mail.NewPost.Intro }}</p> +<p>{{ l $.Lang "Mail" "NewPost" "Intro" }}</p> <table> <tr> - <td rowspan="3"><a href="https://{{ .ClientHost }}/{{ .Lang }}/blog/{{ .Post.FileName }}"><img src="{{ printf .ThumbnailBaseUrl .Post.Metadata.Thumbnail }}"></a></td> + <td rowspan="3"><a href="https://{{ .ClientHost }}/{{ .Lang }}/blog/{{ .Post.FileName }}"><img src="{{ printf .PhotoStorage.Thumbnail320p.BaseUrl .Post.Metadata.Thumbnail }}"></a></td> <td class="darkened" style="font-size: 24px"><a style="color: #273de1 !important;" href="https://{{ .ClientHost }}/{{ .Lang }}/blog/{{ .Post.FileName }}">{{ .Post.Metadata.Title }}</a></td> </tr> <tr> <td class="darkened" style="font-size: 16px">{{ .Post.Metadata.ShortDescription }}</td> </tr> <tr> - <td class="darkened" style="font-size: 16px">{{ .L.Mail.NewPost.CapturedOn }} {{ .Post.Metadata.ActionDate }}</td> + <td class="darkened" style="font-size: 16px">{{ l $.Lang "Mail" "NewPost" "CapturedOn" }} {{ .Post.Metadata.ActionDate }}</td> </tr> </table> {{ end }} diff --git a/views/messages/verify-email.html b/views/messages/verify-email.html index dd6ef33..e6fff76 100644 --- a/views/messages/verify-email.html +++ b/views/messages/verify-email.html @@ -1,10 +1,10 @@ {{ define "body" }} -<p>{{ .L.Mail.VerifyEmail.Welcome }}</p> -<p>{{ .L.Mail.VerifyEmail.Intro }}</p> +<p>{{ l $.Lang "Mail" "VerifyEmail" "Welcome" }}</p> +<p>{{ l $.Lang "Mail" "VerifyEmail" "Intro" }}</p> <ol> - <li>{{ .L.Mail.VerifyEmail.GotoLink }} <a href="https://{{ .ClientHost }}/{{ .Lang }}/user?email_code={{ .VerificationCode }}" class="outlier">>>></a></li> - <li>{{ .L.Mail.VerifyEmail.InputCode }} <span class="outlier">{{ .VerificationCode }}</span><span></li> + <li>{{ l $.Lang "Mail" "VerifyEmail" "GotoLink" }} <a href="https://{{ .ClientHost }}/{{ .Lang }}/user?email_code={{ .VerificationCode }}" class="outlier">>>></a></li> + <li>{{ l $.Lang "Mail" "VerifyEmail" "InputCode" }} <span class="outlier">{{ .VerificationCode }}</span><span></li> </ol> -<p>{{ .L.Mail.VerifyEmail.IfRandom }}</p> +<p>{{ l $.Lang "Mail" "VerifyEmail" "IfRandom" }}</p> <a href="https://{{ .ClientHost }}" class="outlier">{{ .ClientHost }}</a> {{ end }}
\ No newline at end of file diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html index 9c0f7fd..b2bb0a3 100644 --- a/views/pages/blog-catalogue.html +++ b/views/pages/blog-catalogue.html @@ -1,17 +1,6 @@ {{ define "body" }} <!-- Streamline Icons: https://streamlinehq.com --> <svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true"> - <symbol id="icon-like" viewBox="-2 -2 28 28" fill="none"> - <path fill="currentColor" d="M22.155 6.29294c-0.8113 -1.34837 -1.8229 -2.56561 -3 -3.61 -1.6282 -1.47261 -3.6782 -2.396134 -5.86 -2.6399952C11.1388 -0.137008 8.97321 0.252665 7.015 1.17294c-1.6708 0.75913 -3.14018 1.8997 -4.29 3.33C1.59785 5.92036 0.798705 7.56988 0.385003 9.33294c-0.010341 0.04271 -0.012051 0.08704 -0.005029 0.13041 0.007022 0.04337 0.022633 0.08491 0.045919 0.12216 0.023286 0.03726 0.053777 0.06949 0.089685 0.09481 0.035908 0.02532 0.07651 0.04321 0.119425 0.05262 0.083317 0.01503 0.169224 -0.00254 0.239953 -0.04907 0.070728 -0.04653 0.120867 -0.11846 0.140044 -0.20093 0.42729 -1.65655 1.22384 -3.19495 2.33 -4.5 1.0777 -1.30186 2.44846 -2.32993 4 -3 1.83266 -0.80027 3.84 -1.117035 5.83 -0.92 1.9654 0.25006 3.804 1.10643 5.26 2.45 1.0733 0.9746 1.9996 2.09963 2.75 3.34 0.77 1.20382 1.2366 2.57635 1.36 3.99996 0.1304 1.5877 -0.0565 3.1854 -0.55 4.7 -0.5032 1.5017 -1.4221 2.8298 -2.65 3.83 -0.7981 0.673 -1.7331 1.1644 -2.74 1.44 -1.5427 0.4432 -3.1354 0.6885 -4.74 0.73 -0.0778 0.0052 -0.1505 0.0404 -0.2028 0.0983 -0.0523 0.0579 -0.08 0.1338 -0.0772 0.2117 0 0.039 0.0078 0.0775 0.023 0.1134 0.0152 0.0358 0.0374 0.0683 0.0654 0.0953 0.028 0.0271 0.0612 0.0482 0.0976 0.0622 0.0363 0.014 0.0751 0.0205 0.114 0.0191 1.6629 -0.0085 3.3184 -0.2201 4.93 -0.63 1.0928 -0.2824 2.1146 -0.7899 3 -1.49 1.3927 -1.0653 2.454 -2.5046 3.06 -4.15 0.5754 -1.6287 0.8235 -3.3551 0.73 -5.08 -0.1076 -1.59917 -0.6055 -3.14765 -1.45 -4.50996Z" stroke-width="1"></path> - <path fill="currentColor" d="M15.935 15.903c0.6791 -0.2938 1.2468 -0.7964 1.6208 -1.4347 0.3741 -0.6384 0.5349 -1.3794 0.4592 -2.1153 -0.0284 -0.1682 -0.0994 -0.3264 -0.2061 -0.4594 -0.1068 -0.133 -0.2458 -0.2365 -0.4039 -0.3007 -0.4529 -0.1228 -0.927 -0.1466 -1.39 -0.0699 -1.22 0.07 -3.05 0.38 -4.54 0.54 -0.4 0.05 -0.78 0.08 -1.11 0.1 -0.0689 0.0127 -0.1312 0.0492 -0.1761 0.1031 -0.0448 0.0539 -0.0693 0.1217 -0.0693 0.1919 0 0.0701 0.0245 0.1379 0.0693 0.1918 0.0449 0.0539 0.1072 0.0904 0.1761 0.1032l5.66 -0.18h0.83c0.0328 0.5028 -0.0896 1.0037 -0.3508 1.4347 -0.2611 0.431 -0.6483 0.7715 -1.1092 0.9753 -0.4499 0.3065 -0.9606 0.5125 -1.4972 0.6039 -0.5367 0.0915 -1.0868 0.0663 -1.6128 -0.074 -0.4102 -0.1727 -0.7888 -0.4126 -1.12 -0.71 -0.3351 -0.2774 -0.6136 -0.6169 -0.82 -1 -0.0184 -0.0407 -0.0446 -0.0774 -0.0771 -0.108 -0.0326 -0.0305 -0.0709 -0.0544 -0.1127 -0.0702 -0.0418 -0.0158 -0.0862 -0.0232 -0.1309 -0.0218 -0.04465 0.0014 -0.08858 0.0116 -0.12929 0.03 -0.04037 0.0174 -0.07686 0.0426 -0.10734 0.0743 -0.03049 0.0316 -0.05435 0.069 -0.07018 0.11 -0.01584 0.041 -0.02333 0.0847 -0.02203 0.1286 0.00129 0.044 0.01133 0.0872 0.02955 0.1272 0.21114 0.4969 0.52069 0.9459 0.90999 1.32 0.3786 0.3811 0.8137 0.7015 1.29 0.95 0.6592 0.2289 1.361 0.3082 2.0546 0.2321 0.6936 -0.0762 1.3616 -0.3057 1.9554 -0.6721Z" stroke-width="1"></path> - <path fill="currentColor" d="M8.33499 10.9829c0.22252 0.0613 0.45748 0.0613 0.68 0 0.17653 -0.0617 0.33498 -0.1663 0.46112 -0.3044 0.12615 -0.1381 0.21605 -0.3053 0.26164 -0.4867 0.04559 -0.1814 0.04545 -0.37124 -0.00041 -0.55256 -0.04586 -0.18131 -0.136 -0.34841 -0.26235 -0.4863 -1 -1.24 -3 0.82 -1.74 1.71996 0.1869 0.0887 0.39382 0.1266 0.6 0.11Z" stroke-width="1"></path> - <path fill="currentColor" d="M19.1653 10.343c0.2247 0.0401 0.456 0.0194 0.67 -0.06 0.1784 -0.0611 0.3386 -0.1658 0.4661 -0.30464 0.1275 -0.13885 0.2183 -0.3074 0.264 -0.4903 0.0457 -0.1829 0.045 -0.37432 -0.0022 -0.55685 -0.0472 -0.18253 -0.1393 -0.35035 -0.2679 -0.48818 -1 -1.23 -3 0.83 -1.74 1.72997 0.1838 0.1117 0.3949 0.1705 0.61 0.17Z" stroke-width="1"></path> - <path fill="currentColor" d="M9.63505 20.0929c-0.10233 -0.204 -0.25778 -0.3768 -0.45 -0.5 0.19629 -0.3235 0.27075 -0.7064 0.21 -1.08 -0.04172 -0.3535 -0.15757 -0.6943 -0.34 -1 -0.13496 -0.1997 -0.31256 -0.3671 -0.52 -0.49 0.04212 -0.0952 0.06912 -0.1964 0.08 -0.3 0.05325 -0.3277 0.02651 -0.6634 -0.07796 -0.9786 -0.10447 -0.3151 -0.28357 -0.6003 -0.52204 -0.8314 -0.51428 -0.3306 -1.1194 -0.4915 -1.73 -0.46 -0.74972 -0.022 -1.49478 0.125 -2.18 0.43 0.05982 -0.8997 -0.19079 -1.7927 -0.71 -2.53 -0.9 -1 -2.54 -0.13 -2.71 1.36 -0.017326 0.6116 0.060262 1.2222 0.23 1.81 0.28 1 0.23 0.95 0.48 1.33 -0.22002 0.2395 -0.405175 0.5088 -0.55 0.8 -0.1 0.34 -0.28 0.74 -0.12 1 0.16 0.26 0.3 0.23 0.47 0.06 0.17 -0.17 0.09 -0.44 0.14 -0.6 0.07428 -0.2866 0.22256 -0.5487 0.43 -0.76 0.16 -0.21 0.28 -0.22 0.47 -0.4 1.85 -1.77 4.62 -1.97 5.12 -1.28 0.85 1.16 -1.16 1.29 -2.9 2.37 -0.03825 0.011 -0.07372 0.03 -0.10405 0.0557 -0.03034 0.0258 -0.05483 0.0577 -0.07187 0.0936 -0.01703 0.036 -0.0262 0.0752 -0.02691 0.1149 -0.0007 0.0398 0.00708 0.0793 0.02283 0.1158 0.01228 0.0374 0.03203 0.0719 0.05805 0.1014 0.02602 0.0295 0.05777 0.0534 0.09332 0.0702 0.03554 0.0168 0.07414 0.0263 0.11345 0.0277 0.0393 0.0015 0.07849 -0.0051 0.11518 -0.0193 0.41 -0.06 0.77 -0.06 1.12 -0.11 1 -0.17 1.25 -0.66 2.16 -0.51 0.15573 0.127 0.26274 0.3039 0.30284 0.5008 0.0401 0.1968 0.01083 0.4015 -0.08284 0.5792 -0.45993 0.315 -0.97588 0.539 -1.52 0.66 -0.36935 0.1765 -0.72678 0.377 -1.07 0.6 -0.03798 0.0103 -0.07348 0.0282 -0.10433 0.0526 -0.03085 0.0244 -0.05641 0.0549 -0.07512 0.0895 -0.01871 0.0346 -0.03018 0.0727 -0.03371 0.1119 -0.00353 0.0391 0.00094 0.0786 0.01316 0.116 0.01027 0.038 0.02815 0.0735 0.05258 0.1044 0.02442 0.0308 0.05486 0.0564 0.08948 0.0751 0.03461 0.0187 0.07267 0.0302 0.11185 0.0337 0.03919 0.0035 0.07869 -0.0009 0.11609 -0.0132 0.36103 -0.003 0.7216 -0.0264 1.08 -0.07 0.15662 -0.0212 0.31072 -0.0581 0.46 -0.11 0.36406 -0.2158 0.79585 -0.2872 1.21 -0.2 0.03286 0.0062 0.06407 0.0192 0.09162 0.0381 0.02755 0.019 0.05084 0.0435 0.06838 0.0719 0.03012 0.0772 0.03012 0.1629 0 0.24 -0.05277 0.2543 -0.17376 0.4893 -0.35 0.68 -0.20205 0.2292 -0.43028 0.434 -0.68 0.61 -0.66869 0.4607 -1.44234 0.7461 -2.25 0.83 -0.80071 0.1179 -1.61838 -0.0039 -2.35 -0.35 -0.53395 -0.2824 -0.95681 -0.737 -1.2 -1.29 -0.27045 -0.5855 -0.42982 -1.2162 -0.47 -1.86 -0.00126 -0.0442 -0.0114 -0.0877 -0.02981 -0.1279 -0.01841 -0.0403 -0.04473 -0.0764 -0.07738 -0.1062 -0.03265 -0.0299 -0.07097 -0.0529 -0.11269 -0.0676 -0.04171 -0.0148 -0.08596 -0.021 -0.13012 -0.0183 -0.044675 0.0026 -0.088408 0.0139 -0.12869 0.0334s-0.076321 0.0468 -0.106051 0.0802c-0.029729 0.0334 -0.052563 0.0724 -0.067194 0.1147 -0.014631 0.0423 -0.02077 0.0871 -0.018065 0.1317 0.012623 0.8862 0.235395 1.7567 0.65 2.54 0.30359 0.5453 0.74942 0.9981 1.29 1.31 0.8694 0.479 1.8607 0.6912 2.85 0.61 0.9936 -0.0912 1.94725 -0.4355 2.77 -1 0.4107 -0.274 0.77525 -0.6115 1.08 -1 0.2272 -0.2941 0.3845 -0.6361 0.46 -1 0.04902 -0.304 -0.00001 -0.6157 -0.14 -0.89Zm-7.81 -3.72 -0.09 -1c0 -0.75 -0.13 -1.73 0.37 -2.19 0.1 0 0.45 -0.59 0.88 0.22 0.2375 0.4432 0.38371 0.9294 0.43 1.43 -0.00039 0.0506 0.01118 0.1005 0.03377 0.1457 0.0226 0.0451 0.05557 0.0843 0.09623 0.1143 -0.63566 0.3358 -1.2158 0.7676 -1.72 1.28Z" stroke-width="1"></path> - </symbol> - <symbol id="icon-view" viewBox="-2 -2 28 28" fill="none"> - <path fill="currentColor" fill-rule="evenodd" d="M23.6534 10.5263c-1.3628 -1.56103 -3.0387 -2.81795 -4.9189 -3.68916 -1.8052 -0.8978 -3.7485 -1.48585 -5.7486 -1.7396 -0.998 -0.11306 -2.0067 -0.09289 -2.99934 0.05999 -2.63062 0.3739 -5.14267 1.33771 -7.34828 2.81934 -0.84337 0.55535 -1.63325 1.18793 -2.359454 1.88956 -0.08425 0.0868 -0.161162 0.18047 -0.2299462 0.27997C0.0169406 10.1973 0 10.2562 0 10.3163s0.0169406 0.1191 0.0488798 0.17c0.0179573 0.0358 0.0429176 0.0677 0.0734052 0.0937 0.030488 0.0261 0.065885 0.0457 0.104099 0.0578 0.038214 0.0122 0.078469 0.0165 0.118385 0.0128 0.039917 -0.0037 0.078686 -0.0154 0.114015 -0.0343C1.5971 9.42178 2.95486 8.45777 4.45785 7.77692c0.959 -0.48729 1.9636 -0.87909 2.9993 -1.16973 0.88979 -0.25558 1.79943 -0.43617 2.71935 -0.53987 0.8961 -0.11518 1.8033 -0.11518 2.6994 0 1.8879 0.28843 3.7154 0.88632 5.4087 1.76959 1.6883 0.80557 3.2029 1.93308 4.459 3.31919 0.0877 0.152 0.1339 0.3244 0.1339 0.4999s-0.0462 0.3479 -0.1339 0.4999c-0.2004 0.4189 -0.4747 0.7983 -0.8098 1.1197 -0.9132 0.9438 -1.9179 1.7945 -2.9993 2.5394 -1.0941 0.7277 -2.2767 1.3123 -3.5192 1.7396 -0.786 0.2536 -1.6039 0.395 -2.4294 0.4199 -1.2488 0.0246 -2.4978 -0.0321 -3.73917 -0.1699 -1.55221 -0.2376 -3.01813 -0.8673 -4.259 -1.8296 -1.29902 -0.9356 -2.40844 -2.1095 -3.26924 -3.4592 -0.02265 -0.0404 -0.05333 -0.0757 -0.09014 -0.1039 -0.03681 -0.0281 -0.07898 -0.0484 -0.12392 -0.0596 -0.04494 -0.0113 -0.0917 -0.0132 -0.13741 -0.0057s-0.08941 0.0243 -0.12842 0.0492c-0.07659 0.0529 -0.13044 0.1327 -0.15082 0.2235 -0.02039 0.0908 -0.0058 0.1859 0.04085 0.2664 0.8813 1.4449 2.02784 2.7102 3.37921 3.7292 1.34805 1.0808 2.9532 1.7938 4.65891 2.0695 1.26545 0.179 2.54115 0.2758 3.81915 0.2899 0.8996 0.0048 1.7952 -0.1198 2.6593 -0.3699 1.3434 -0.4251 2.6244 -1.0269 3.8092 -1.7896 1.1699 -0.7705 2.2556 -1.6619 3.2392 -2.6594 0.5478 -0.5108 0.9601 -1.1498 1.1997 -1.8595 0.0999 -0.2934 0.1302 -0.606 0.0886 -0.9131 -0.0417 -0.3071 -0.1541 -0.6003 -0.3285 -0.8565Z" clip-rule="evenodd" stroke-width="1"></path> - <path fill="currentColor" fill-rule="evenodd" d="M10.6964 9.1166c0.7827 -0.2444 1.6136 -0.29247 2.4194 -0.13997 0.3938 0.09551 0.7634 0.27195 1.0852 0.51809 0.3219 0.24613 0.589 0.55658 0.7844 0.91158 0.0189 0.0362 0.0448 0.0683 0.0762 0.0944 0.0314 0.0262 0.0677 0.0458 0.1067 0.0579 0.039 0.0121 0.08 0.0163 0.1207 0.0125 0.0407 -0.0038 0.0802 -0.0157 0.1163 -0.0348 0.0713 -0.04 0.124 -0.1066 0.1465 -0.1853 0.0224 -0.0786 0.0129 -0.163 -0.0266 -0.2346 -0.213 -0.44629 -0.5169 -0.84312 -0.8922 -1.16507 -0.3753 -0.32195 -0.8138 -0.56192 -1.2873 -0.70452 -0.9344 -0.26583 -1.92 -0.29674 -2.8693 -0.08998 -0.53345 0.13369 -1.02665 0.39446 -1.4375 0.76005 -0.41085 0.36559 -0.72716 0.82514 -0.92194 1.33942 -0.41881 1.0115 -0.41881 2.1478 0 3.1593 0.4668 0.9845 1.29879 1.7483 2.31944 2.1295 0.9793 0.3819 2.0556 0.438 3.0693 0.16 0.8982 -0.2612 1.6632 -0.8546 2.1395 -1.6596 0.4789 -0.7747 0.6995 -1.6814 0.6299 -2.5894 -0.0061 -0.0449 -0.0211 -0.0881 -0.0441 -0.127 -0.0231 -0.039 -0.0536 -0.073 -0.0899 -0.1 -0.0363 -0.027 -0.0777 -0.0465 -0.1216 -0.0573 -0.044 -0.0108 -0.0897 -0.0127 -0.1344 -0.0057 -0.0457 0.0049 -0.0899 0.019 -0.1301 0.0415 -0.0401 0.0224 -0.0752 0.0528 -0.1033 0.0893 -0.028 0.0364 -0.0484 0.0782 -0.0598 0.1227 -0.0114 0.0446 -0.0137 0.091 -0.0067 0.1364 -0.0016 0.7166 -0.2252 1.4151 -0.6398 1.9996 -0.3854 0.5945 -0.9884 1.0144 -1.6796 1.1697 -0.8061 0.1738 -1.6466 0.0863 -2.3995 -0.2499 -0.7468 -0.2609 -1.36555 -0.7974 -1.72958 -1.4997 -0.29453 -0.7244 -0.29453 -1.5351 0 -2.2595 0.1115 -0.3727 0.31161 -0.7129 0.58321 -0.99148 0.27161 -0.27856 0.60667 -0.48722 0.97647 -0.60812Z" clip-rule="evenodd" stroke-width="1"></path> - </symbol> <symbol id="icon-arrange-letter-asc" viewBox="-2 -2 28 28" fill="none"> <path fill="var(--color-accent-light)" d="M11.9999 20.9053c5.1771 0 9.3739 -4.1969 9.3739 -9.374 0 -5.17702 -4.1968 -9.37389 -9.3739 -9.37389 -5.17706 0 -9.37392 4.19687 -9.37392 9.37389 0 5.1771 4.19686 9.374 9.37392 9.374Z" stroke-width="1"></path> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M5.90674 4.49902V18.5599" stroke-width="1"></path> @@ -62,57 +51,57 @@ class="tags-list relative flex flex-col w-full bg-paper bg-background-light"> <div class="flex flex-col overflow-y-auto"> <fieldset> - <legend class="font-bold w-full text-center">{{ .L.BlogSearch.OrderByHeader }}</legend> + <legend class="font-bold w-full text-center">{{ l $.Lang "BlogSearch" "OrderByHeader" }}</legend> <div class="md:flex flex-col grid grid-cols-3 grid-rows-2 grid-flow-col"> <div> <label class="flex justify-start gap-1 items-center"> <input type="radio" id="sortTitleAsc" name="sort" value="titleAsc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "titleAsc" }}checked{{ end }}> - {{ .L.BlogSearch.TitleOrdered }} + {{ l $.Lang "BlogSearch" "TitleOrdered" }} <svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-letter-asc"/></svg> </label> </div> <div> <label class="flex justify-start gap-1 items-center"> <input type="radio" id="sortTitleDesc" name="sort" value="titleDesc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "titleDesc" }}checked{{ end }}> - {{ .L.BlogSearch.TitleOrdered }} + {{ l $.Lang "BlogSearch" "TitleOrdered" }} <svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-letter-desc"/></svg> </label> </div> <div> <label class="flex justify-start gap-1 items-center"> <input type="radio" id="sortActionDateAsc" name="sort" value="actionDateAsc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "actionDateAsc" }}checked{{ end }}> - {{ .L.BlogSearch.ActionDateOrdered }} + {{ l $.Lang "BlogSearch" "ActionDateOrdered" }} <svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-number-asc"/></svg> </label> </div> <div> <label class="flex justify-start gap-1 items-center"> <input type="radio" id="sortActionDateDesc" name="sort" value="actionDateDesc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "actionDateDesc" }}checked{{ end }}> - {{ .L.BlogSearch.ActionDateOrdered }} + {{ l $.Lang "BlogSearch" "ActionDateOrdered" }} <svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-number-desc"/></svg> </label> </div> <div> <label class="flex justify-start gap-1 items-center"> <input type="radio" id="sortPublicationDateAsc" name="sort" value="publicationDateAsc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "publicationDateAsc" }}checked{{ end }}> - {{ .L.BlogSearch.PublicationDateOrdered }} + {{ l $.Lang "BlogSearch" "PublicationDateOrdered" }} <svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-number-asc"/></svg> </label> </div> <div> <label class="flex justify-start gap-1 items-center"> <input type="radio" id="sortPublicationDateDesc" name="sort" value="publicationDateDesc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "publicationDateDesc" }}checked{{ end }}> - {{ .L.BlogSearch.PublicationDateOrdered }} + {{ l $.Lang "BlogSearch" "PublicationDateOrdered" }} <svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-number-desc"/></svg> </label> </div> </div> </fieldset> <fieldset class="mt-1"> - <legend class="font-bold w-full text-center">{{ .L.BlogSearch.TagsHeader }}</legend> + <legend class="font-bold w-full text-center">{{ l $.Lang "BlogSearch" "TagsHeader" }}</legend> <div class="md:flex flex-col grid grid-cols-3 xs:grid-cols-4 sm:grid-cols-5 grid-flow-row-dense"> <div class="m-1"> - <label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();" class="bg-accent-light rounded border-transparent text-accent-deep focus:border-transparent focus:bg-background-dark focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep"> {{ .L.BlogSearch.ChooseAllTags }}</label> + <label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();" class="bg-accent-light rounded border-transparent text-accent-deep focus:border-transparent focus:bg-background-dark focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep"> {{ l $.Lang "BlogSearch" "ChooseAllTags" }}</label> </div> {{- range .Tags }} <div class="m-1"> @@ -139,7 +128,7 @@ <div class="relative flex grow h-full"> <div class="absolute w-full h-full inset-0 inset-shadow-[0_0_0.4rem_black] pointer-events-none z-20"></div> - <div hx-get="/api/v1/blog-search" hx-vals='js:{lang: "{{ .Lang }}", tz: clientTimeZone}' hx-target="this" hx-swap="innerHTML" hx-trigger="load" hx-include=".tags-list" + <div hx-get="/api/v1/blog-search" hx-vals='js:{lang: "{{ .Lang }}", tz: clientTimeZone, highlight: "{{ .PreviousBlogPage }}"}' hx-target="this" hx-swap="innerHTML" hx-trigger="load" hx-include=".tags-list" class="blog-cards relative grow flex flex-col overflow-y-auto bg-paper bg-background-light pr-2"> Loading... </div> diff --git a/views/pages/blog-page.html b/views/pages/blog-page.html index d8cfbff..c02c3f5 100644 --- a/views/pages/blog-page.html +++ b/views/pages/blog-page.html @@ -1,62 +1,50 @@ {{ define "body" }} -<div class="absolute w-full h-full inset-0 inset-shadow-[0_0_0.4rem_black] pointer-events-none z-20"></div> +<input class="htmx-exit-trigger" type="hidden" name="codename" value="{{ .Codename }}"> +<div class="sticky top-0 left-0 w-full h-[100cqb] -mb-[100cqb] inset-shadow-[0_0_0.4rem_black] pointer-events-none z-20"></div> <article class="relative bg-paper bg-background-light flex flex-col px-8 py-4 overflow-y-auto"> - <h1 class="max-xs:flex flex-row content-center [@media(max-height:32rem)]:block hidden font-gentium text-2xl font-bold italic text-main-hard tracking-[.0125rem] mb-1"> - <div hx-get="/api/v1/like" hx-target="this" hx-swap="outerHTML" hx-trigger="load"></div> - {{ .Title }} - </h1> - <p class="block grow text-lg font-gentium text-secondary"> - <b>{{ .L.Metadata.Published }}</b>: - <time datetime="{{ .PublishedDate }}" hx-get="/api/v1/tz" hx-vals='js:{timestamp: "{{ .PublishedDate }}", tz: clientTimeZone}' hx-target="this" hx-swap="innerHTML" hx-trigger="load"> - {{ .PublishedDate }} - </time> - </p> - <p class="block grow text-lg font-gentium text-secondary"><b>{{ .L.Metadata.Action }}</b>: {{ .ActionDate }}</p> - <p class="block grow text-md font-gentium font-thin text-main-hard">{{ .ShortDescription }}</p> - <hr class="block border-t-2 border-dotted border-main-hard mt-1 mb-2 w-full ml-auto mr-auto"> + <div class="flex flex-col sm:flex-row h-[30cqb] sm:h-[15cqb]"> + <div class="grow shrink-2 flex-1 overflow-y-auto -ml-8 -mt-4 {{if .Medley}}max-sm:-mr-8{{else}}-mr-8{{end}} z-1 shadow-elevation-2"> + <div class="multitone min-h-full" style="--multitone-bg: url({{ printf $.PhotoStorage.Thumbnail560p.BaseUrl .Thumbnail }});"> + <div class="ml-8 py-4"> + <h1 class="max-xs:flex flex-row content-center [@media(max-height:32rem)]:block hidden font-gentium text-2xl font-bold italic text-main-hard tracking-[.0125rem] mb-1"> + <div hx-get="/api/v1/like" hx-target="this" hx-swap="outerHTML" hx-trigger="load"></div> + {{ .Title }} + </h1> + <p class="block grow text-lg font-gentium text-secondary"> + <b>{{ l $.Lang "Metadata" "Published" }}</b>: + <time datetime="{{ .PublishedDate }}" hx-get="/api/v1/tz" hx-vals='js:{timestamp: "{{ .PublishedDate }}", tz: clientTimeZone}' hx-target="this" hx-swap="innerHTML" hx-trigger="load"> + {{ .PublishedDate }} + </time> + </p> + <p class="block grow text-lg font-gentium text-secondary"><b>{{ l $.Lang "Metadata" "Action" }}</b>: {{ .ActionDate }}</p> + <p class="block grow text-md font-gentium font-thin text-main-hard">{{ .ShortDescription }}</p> + </div> + </div> + </div> + <hr class="block sm:hidden border-t-3 bg-paper bg-background-dark border-dotted border-main-hard w-full"> + {{- if .Medley }} + <div class="overflow-y-auto grow-2 shrink-0 flex-1 bg-paper bg-background-dark shadow-elevation-2 -mr-8 sm:-mt-4 max-sm:-ml-8 z-2"> + <h2 class="text-2xl font-gentium font-extrabold text-main-hard tracking-[.0125rem] ml-2"> + {{ l .Lang "Medleys" .Medley }} + </h2> + <hr class="block border-t-2 my-1 border-dotted border-main-hard w-full"> + <div hx-get="/api/v1/blog-search" hx-vals='js:{lang: "{{ .Lang }}", tz: clientTimeZone, medley: "{{ .Medley }}", sort: "medley", hideTags: true, hidePublishedTime: true, highlight: "{{ .Codename }}"}' hx-target="this" hx-swap="innerHTML" hx-trigger="load" + class="grow flex flex-col pr-2"> + Loading... + </div> + </div> + {{- end }} + </div> + <hr class="block border-t-3 mb-2 border-dotted border-main-hard w-full ml-auto mr-auto"> {{ .ParsedMarkdown }} {{- if .MapLocationX }} <hr class="border-t-3 border-dotted border-main-hard mt-1 mb-2 w-[80%] ml-auto mr-auto"> - <div id="map-container" class="relative p-1 flex-none ml-auto mr-auto w-[80%] lg:w-[60%] h-[30dvh] md:h-[40dvh]"></div> + <div id="map-outer-container" class="relative p-1 flex-none ml-auto mr-auto w-[80%] lg:w-[60%] h-[30dvh] md:h-[40dvh]" + hx-get="/api/v1/map" hx-vals='{"lang": "{{ .Lang }}", "codename": "{{ .Codename }}", "zoom": 8}' hx-target="this" hx-swap="innerHTML" hx-trigger="load"> + </div> <hr class="border-t-3 border-dotted border-main-hard mt-1 mb-2 w-[80%] ml-auto mr-auto"> <script> - function initLocationMap(id, x, y, relativeErrorMeters = 0) { - map = L.map(id).setView([x, y], 8); - - L.tileLayer('https://tiles.stadiamaps.com/tiles/outdoors/{z}/{x}/{y}{r}.png', { - maxZoom: 20, - attribution: '© <a href="https://stadiamaps.com/" target="_blank">Stadia Maps</a>, © <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> © <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>', - }).addTo(map); - - if (relativeErrorMeters != 0) { - var circle = L.circle([x, y], { - color: '#8a9d8a', - fillColor: '#8a9d8a', - fillOpacity: 0.15, - radius: relativeErrorMeters - }).addTo(map); - } - - var marker = L.marker([x, y], { - title: '{{ .Title }}', - }).addTo(map) - .bindPopup('<span><b>{{ .Title }}</b><br><a href="https://www.openstreetmap.org/#map=13/{{ .MapLocationX }}/{{ .MapLocationY }}">{{ .MapLocationX }}, {{ .MapLocationY }}</a></span>') - .openPopup(); - } - - if (map) { - map.remove(); - map = null; - } - - setTimeout(() => initLocationMap( - 'map-container', - {{ .MapLocationX }}, - {{ .MapLocationY }}, - {{ .MapLocationAreaMeters }} - ), 1000); - var lightbox = GLightbox({ selector: '.glightbox', moreLength: 0 diff --git a/views/pages/global-map.html b/views/pages/global-map.html index a6dc006..4d1853f 100644 --- a/views/pages/global-map.html +++ b/views/pages/global-map.html @@ -3,23 +3,23 @@ <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <meta name="description" content="{{ .L.GlobalMap.Description }}" /> - <title>{{ .L.GlobalMap.Header }} // SAYA.UZ</title> - <meta property="og:title" content="{{ .L.GlobalMap.Header }}" /> + <meta name="description" content="{{ l $.Lang "GlobalMap" "Description" }}" /> + <title>{{ l $.Lang "GlobalMap" "Header" }} // SAYA.UZ</title> + <meta property="og:title" content="{{ l $.Lang "GlobalMap" "Header" }}" /> <meta property="og:description" - content="{{ .L.GlobalMap.Description }}" + content="{{ l $.Lang "GlobalMap" "Description" }}" /> <meta property="og:url" content="{{ .CanonicalEndpoint }}/{{ .Lang }}/map" /> <meta property="og:type" content="site" /> - <title>{{ .L.GlobalMap.Header }} // SAYA.UZ</title> + <title>{{ l $.Lang "GlobalMap" "Header" }} // SAYA.UZ</title> <link rel="canonical" href="{{ .CanonicalEndpoint }}/{{ .Lang }}/map" /> <link rel="stylesheet" - href="{{ .StaticStorageBaseUrl }}/fonts/fonts.css" + href="{{ .StaticStorage.BaseUrl }}/fonts/fonts.css" /> {{ block "header" . }}{{ end }} <link rel="stylesheet" href="/output.css" /> @@ -42,16 +42,20 @@ /> <link rel="stylesheet" - href="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.css" + href="{{ .StaticStorage.BaseUrl }}/libs/leaflet/1.9.4/leaflet.css" /> <link rel="stylesheet" - href="{{ .StaticStorageBaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.css" + href="{{ .StaticStorage.BaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.css" /> <link rel="stylesheet" - href="{{ .StaticStorageBaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css" + href="{{ .StaticStorage.BaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css" /> + <script src="{{ .StaticStorage.BaseUrl }}/libs/htmx/2.0.6/htmx.min.js"></script> + <script src="{{ .StaticStorage.BaseUrl }}/libs/leaflet/1.9.4/leaflet.js"></script> + <script src="{{ .StaticStorage.BaseUrl }}/libs/protomaps-leaflet/5.0.0/dist/protomaps-leaflet.js"></script> + <script src="{{ .StaticStorage.BaseUrl }}/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js"></script> </head> <body @@ -216,21 +220,14 @@ </div> </nav> - <div - id="map-container" - class="bg-paper bg-background-dark flex z-10 w-full h-full" - ></div> - - <script src="{{ .StaticStorageBaseUrl }}/libs/htmx/2.0.6/htmx.min.js"></script> - <script src="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.js"></script> - <script src="{{ .StaticStorageBaseUrl }}/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js"></script> + <div id="map-outer-container" class="bg-paper bg-background-dark flex z-10 w-full h-full" + hx-get="/api/v1/map" hx-vals='{"lang": "{{ .Lang }}", "codename": "{{ .Codename }}", "zoom": 4, "zoomPosition": "topright"}' hx-target="this" hx-swap="innerHTML" hx-trigger="load"> + </div> <script> const themeWheel = document.getElementById("theme-wheel"); const themeButton = document.getElementById("sidebar-theme-button"); const themeIcons = document.querySelectorAll(".theme-icon"); - const themeWheelRadius = convertRemToPixels(6); - const THUMB_BASE = "{{ .ThumbnailBaseUrl }}"; themeIcons.forEach((icon) => { if (icon.getAttribute("data-theme") == savedTheme) { @@ -251,6 +248,7 @@ function positionThemeIcons() { const sliceAngle = 360 / themeIcons.length; + const themeWheelRadius = convertRemToPixels(6); themeIcons.forEach((icon, index) => { const startAngle = index * sliceAngle; @@ -294,6 +292,7 @@ const buttonRect = themeButton.getBoundingClientRect(); const buttonCenterX = buttonRect.left + buttonRect.width / 2; const buttonCenterY = buttonRect.top + buttonRect.height / 2; + const themeWheelRadius = convertRemToPixels(6); const margin = 0; @@ -367,95 +366,5 @@ } }); </script> - - <script> - var map; - var markers = L.markerClusterGroup(); - - function createCustomIcon(color, borderColor = "var(--color-main-soft)") { - const svgIcon = ` - <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> - <path d="M12 1C7.5 1 4 4.5 4 9c0 6.5 8 14 8 14s8-7.5 8-14c0-4.5-3.5-8-8-8zm0 11c-1.5 0-3-1.5-3-3s1.5-3 3-3 3 1.5 3 3-1.5 3-3 3z" - fill="${color}" - stroke="${borderColor}" - stroke-width="1" - stroke-linejoin="round"/> - </svg> - `; - - return L.divIcon({ - html: svgIcon, - className: 'custom-svg-marker', - iconSize: [32, 32], - iconAnchor: [16, 32], - popupAnchor: [0, -32] - }); - } - - function initLocationMap() { - map = L.map('map-container').setView([{{ .MapLocationLat }}, {{ .MapLocationLong }}], 4); - - L.tileLayer('https://tiles.stadiamaps.com/tiles/outdoors/{z}/{x}/{y}{r}.png', { - maxZoom: 20, - attribution: '© <a href="https://stadiamaps.com/" target="_blank">Stadia Maps</a>, © <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> © <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>', - zoomControl: false - }).addTo(map); - - L.control.zoom({ - position: 'topright' - }).addTo(map); - - map.addLayer(markers); - } - - function markerToHsl(str, newCoef) { - let hash = 0; - for (let i = 0; i < str.length / 2; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - hash = str.charCodeAt(str.length - i - 1) + ((hash << 5) - hash); - hash %= 360; - } - - return [hash, (20 + newCoef * 80).toFixed(0), (20 + newCoef * 75).toFixed(0)]; - } - - function mapAddMarker(x, y, relativeErrorMeters = 0, title = "", link = "", thumbnail = "", newCoef = 1) { - const [h, s, l] = markerToHsl(title, newCoef); - const color = `hsl(${h} ${s}% ${l}%)`; - - if (relativeErrorMeters != 0) { - var circle = L.circle([x, y], { - color: color, - fillColor: color, - fillOpacity: 0.15, - radius: relativeErrorMeters - }).addTo(map); - } - - var marker = L.marker([x, y], { - icon: createCustomIcon(color), - title: title, - }).bindPopup(` - <div class="flex flex-row justify-center justify-items-center"> - <img onclick="location.href='${link}';" class="cursor-pointer object-cover rounded-[10%] select-none w-12 h-12 mr-1" src="${THUMB_BASE.replace('%s', thumbnail)}"> - <span><b><a href="${link}">${title}</a></b><br><a href="https://www.openstreetmap.org/#map=13/${x}/${y}">${x}, ${y}</a></span> - </div>`); - - markers.addLayer(marker); - } - - document.addEventListener('DOMContentLoaded', () => { - initLocationMap(); - {{- range .MapMarkers }} - mapAddMarker({{ .Lat }}, {{ .Long }}, {{ .AccuracyMeters }}, {{ .Title }}, {{ .PageLink }}, {{ .Thumbnail }}, {{ fdiv .Index (len $.MapMarkers) }}); - {{- end }} - }); - - window.addEventListener('resize', function() { - setTimeout(() => { - map.invalidateSize(); - }, 100); - }); - </script> </body> </html> diff --git a/views/pages/home-page.html b/views/pages/home-page.html index 6e026aa..e89be39 100644 --- a/views/pages/home-page.html +++ b/views/pages/home-page.html @@ -23,17 +23,17 @@ <div class="flex flex-col justify-start items-start bg-paper bg-background-dark mb-8 shadow-[0.4rem_0.4rem_0.4rem_black]"> <p class="text-3xl/loose sm:text-4xl/loose lg:text-5xl/loose tracking-widest uppercase w-full font-m-plus font-thin italic text-left border-l-4 border-solid border-background-dark - bg-linear-to-r from-background-dark to-background-light">{{ .L.HomePage.Hymn1 }}</p> + bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "Hymn1" }}</p> <hr class="w-full border-t-2 border-dotted border-main-hard mb-1"> - <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ .L.HomePage.Hymn2 }}</p> - <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ .L.HomePage.Hymn3 }}</p> - <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ .L.HomePage.Hymn4 }}</p> + <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ l $.Lang "HomePage" "Hymn2" }}</p> + <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ l $.Lang "HomePage" "Hymn3" }}</p> + <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ l $.Lang "HomePage" "Hymn4" }}</p> </div> <div class="flex flex-col justify-center items-center"> <p class="text-xl sm:text-2xl lg:text-3xl p-1 w-full font-m-plus font-thin italic text-left border-l-1.5 border-solid border-background-dark - bg-linear-to-r from-background-dark to-background-light">{{ .L.HomePage.DidYouKnowThat }}</p> + bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "DidYouKnowThat" }}</p> <hr class="w-full border-t-2 border-dotted border-main-hard mb-1"> <ul class="w-full text-left list-[circle]"> <li class="ml-4 mb-1">{{ index .FunFacts 0 }}</li> @@ -47,7 +47,7 @@ <div class="flex flex-col justify-center items-start mb-4"> <p class="text-xl sm:text-2xl lg:text-3xl p-1 w-full font-m-plus font-thin italic text-left border-l-1.5 border-solid border-background-dark - bg-linear-to-r from-background-dark to-background-light">{{ .L.HomePage.Contacts }}</p> + bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "Contacts" }}</p> <hr class="w-full border-t-2 border-dotted border-main-hard mb-1"> <a href="https://t.me/EarlInisMona"> <svg class="w-4 h-4 mr-1 inline" viewBox="0 0 24 24"><use href="#icon-telegram"/></svg> @@ -122,22 +122,22 @@ <div class="flex flex-col justify-center items-center"> <p class="text-xl sm:text-2xl lg:text-3xl p-1 w-full font-m-plus font-thin italic text-left border-l-1.5 border-solid border-color-background-dark - bg-linear-to-r from-background-dark to-background-light">{{ .L.HomePage.SidebarDescription }}</p> + bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "SidebarDescription" }}</p> <hr class="w-full border-t-2 border-dotted border-main-hard mb-1"> <div class="w-full grid grid-cols-[1fr_4fr] content-center items-center [&>*:nth-child(4n+2)]:bg-background-medium/50 [&>*:nth-child(4n+3)]:bg-background-medium/50" style="container-type: inline-size;"> <svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-back"/></svg> - <p class="p-1 w-[85cqi]">{{ .L.HomePage.BackDescription }}</p> + <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "BackDescription" }}</p> <svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-logo"/></svg> - <p class="p-1 w-[85cqi]">{{ .L.HomePage.HomePageDescription }}</p> + <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "HomePageDescription" }}</p> <svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-newspaper"/></svg> - <p class="p-1 w-[85cqi]">{{ .L.HomePage.BlogSearchDescription }}</p> + <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "BlogSearchDescription" }}</p> <svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-map"/></svg> - <p class="p-1 w-[85cqi]">{{ .L.HomePage.MarkerMapDescription }}</p> + <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "MarkerMapDescription" }}</p> <svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-palette"/></svg> - <p class="p-1 w-[85cqi]">{{ .L.HomePage.ThemeSwitchDescription }}</p> + <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "ThemeSwitchDescription" }}</p> <svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-account"/></svg> - <p class="p-1 w-[85cqi]">{{ .L.HomePage.AccountDescription }}</p> + <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "AccountDescription" }}</p> </div> </div> </div> diff --git a/views/pages/unsubscribe-page.html b/views/pages/unsubscribe-page.html index 9a78488..12ff1f0 100644 --- a/views/pages/unsubscribe-page.html +++ b/views/pages/unsubscribe-page.html @@ -21,7 +21,7 @@ href="/favicon.ico" type="image/x-icon" /> - <title>{{ .L.UnsubscribePage.Header }} // SAYA.UZ</title> + <title>{{ l $.Lang "UnsubscribePage" "Header" }} // SAYA.UZ</title> <style> .bg-paper { background-image: url(data:image/webp;base64,UklGRi4BAABXRUJQVlA4ICIBAABQCQCdASqAAIAAPm02lUmkIqIkIImIgA2JaW3rMFmGDAOKLAoJtP591ByFR74Tun2DP+Lygpfsz5FH7+cKgxi2Islfd03i5AJdXUfU0SDc8SXYAAD++hlzeYAJWQEF8ZdkJdlcA+5btZg0FMGaso8SHoBph0qAOQysElJ5C5SJFNoKIV1J550UyL7Y9Gh0/c/uzLnBmr8lvz7k1WeA81H4S5ViU/wBqa7aM1m8D9rDtrk8cSTqx+xWF6MS8UfZGSsks6CT/igtgv6syFN9uNA3LjnjiCJKYOvFcMkX+jB2578GWVo3nyLaM1QNPYCxn3TiUbnf7U0MdD2NsBpg1+Jd1wu6yW/U+goN+IQS6Nt4RMWltVcZHlxrneKJi0SobO0AAA==); diff --git a/views/pages/user-page.html b/views/pages/user-page.html index 0dd006a..dc4966b 100644 --- a/views/pages/user-page.html +++ b/views/pages/user-page.html @@ -6,16 +6,16 @@ <div class="flex flex-row w-full mb-4 z-21"> <div class="flex flex-col w-full sm:flex-3/5 grow-0"> <label class="block text-main-medium font-bold mb-1 z-22"> - {{ .L.UserProfile.EmailHeader }} + {{ l $.Lang "UserProfile" "EmailHeader" }} </label> <input class="bg-background-medium appearance-none border-background-medium rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-[0_0_0.4rem_black] focus:outline-none focus:bg-background-light focus:text-main-hard z-22" - id="email-input" name="email" type="text" value="{{ .Email }}" placeholder="{{ .L.UserProfile.TypeEmail }}" maxlength="64" + id="email-input" name="email" type="text" value="{{ .Email }}" placeholder="{{ l $.Lang "UserProfile" "TypeEmail" }}" maxlength="64" data-validated-email="{{ .Email }}" onchange="checkIfValidated(this);"> </div> </div> <button class="w-max mx-auto bg-main-hard hover:bg-main-medium active:bg-main-soft active:inset-shadow-[0.2em_0.2em_0.2rem_black] transition-colors transition-300 text-background-light font-bold py-2 px-4 rounded z-22" hx-post="/api/v1/email/send-verification-code" hx-target="#email-message" hx-swap="outerHTML" hx-indicator="#email-form"> - {{ .L.UserProfile.SendCodeButton }} + {{ l $.Lang "UserProfile" "SendCodeButton" }} </button> </form> <div id="email-message" class="bg-paper bg-background-dark text-main-soft z-26 w-full inset-shadow-[0_0_0.4rem_black] py-2 px-4 text-center"></div> @@ -27,7 +27,7 @@ <div class="flex flex-row w-full mb-4 z-21"> <div class="flex flex-col w-full sm:flex-2/5 grow-0"> <label class="block text-main-medium font-bold ml-1 z-22"> - {{ .L.UserProfile.VerificationCodeHeader }} + {{ l $.Lang "UserProfile" "VerificationCodeHeader" }} </label> <input class="bg-background-medium appearance-none border-background-medium rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-[0_0_0.4rem_black] focus:outline-none focus:bg-background-light focus:text-main-hard z-22" name="email_code" type="text" value="{{ .EmailCode }}" placeholder="0123456789ABCDEF" maxlength="16" autocapitalize="characters"> @@ -35,7 +35,7 @@ </div> <button class="w-max mx-auto bg-main-hard hover:bg-main-medium active:bg-main-soft active:inset-shadow-[0.2em_0.2em_0.2rem_black] transition-colors transition-300 text-background-light font-bold py-2 px-4 rounded z-22" hx-post="/api/v1/email/verify" hx-target="#verification-message" hx-swap="outerHTML" hx-indicator="#email-verification-form"> - {{ .L.UserProfile.VerifyButton }} + {{ l $.Lang "UserProfile" "VerifyButton" }} </button> </form> <div id="verification-message" class="bg-paper bg-background-dark text-main-soft z-26 w-full inset-shadow-[0_0_0.4rem_black] py-2 px-4 text-center"></div> @@ -47,17 +47,17 @@ <form id="subs-form" class="w-full mb-2 flex flex-col relative crossable"> <div class="flex flex-col w-full mb-4 z-21"> <label class="block text-main-medium font-bold mb-1 z-22"> - {{ .L.UserProfile.SubscriptionHeader }} + {{ l $.Lang "UserProfile" "SubscriptionHeader" }} </label> <div class="flex flex-col sm:flex-row w-full"> <div class="m-1 w-1/3"> - <label><input class="mr-1" type="radio" id="tagsNone" name="tags" value="none" {{ if eq .TagsPicked "none" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ .L.UserProfile.TagsNone }}</label> + <label><input class="mr-1" type="radio" id="tagsNone" name="tags" value="none" {{ if eq .TagsPicked "none" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ l $.Lang "UserProfile" "TagsNone" }}</label> </div> <div class="m-1 w-1/3"> - <label><input class="mr-1" type="radio" id="tagsAll" name="tags" value="all" {{ if eq .TagsPicked "all" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ .L.UserProfile.TagsAll }}</label> + <label><input class="mr-1" type="radio" id="tagsAll" name="tags" value="all" {{ if eq .TagsPicked "all" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ l $.Lang "UserProfile" "TagsAll" }}</label> </div> <div class="m-1 w-1/3"> - <label><input class="mr-1" type="radio" id="tagsSpecific" name="tags" value="specific" {{ if eq .TagsPicked "specific" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ .L.UserProfile.TagsSpecific }}</label> + <label><input class="mr-1" type="radio" id="tagsSpecific" name="tags" value="specific" {{ if eq .TagsPicked "specific" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ l $.Lang "UserProfile" "TagsSpecific" }}</label> </div> </div> <div id="pick-tags-form" class="flex flex-col w-full relative crossable"> @@ -80,7 +80,7 @@ </div> <button class="w-max mx-auto bg-color-main-hard hover:bg-main-medium active:bg-color-main-soft active:inset-shadow-[0.2em_0.2em_0.2rem_black] transition-colors transition-300 text-background-light font-bold py-2 px-4 rounded z-22" hx-put="/api/v1/subs" hx-target="#subs-message" hx-swap="outerHTML" hx-indicator="#subs-form"> - {{ .L.UserProfile.SaveButton }} + {{ l $.Lang "UserProfile" "SaveButton" }} </button> </div> </form> @@ -168,7 +168,7 @@ function addTag(name) { found = [...existingTags].find(opt => opt.value===name); if (!found) { subsMessage.classList.remove("hidden"); - subsMessage.innerHTML = "{{ .L.UserProfile.TagDoesNotExist }}".replace("{}", name); + subsMessage.innerHTML = "{{ l $.Lang "UserProfile" "TagDoesNotExist" }}".replace("{}", name); return; } @@ -177,7 +177,7 @@ function addTag(name) { found = [...subscribedTags].find(opt => opt.value===name); if (found) { subsMessage.classList.remove("hidden"); - subsMessage.innerHTML = "{{ .L.UserProfile.TagAlreadyAdded }}".replace("{}", name); + subsMessage.innerHTML = "{{ l $.Lang "UserProfile" "TagAlreadyAdded" }}".replace("{}", name); return; } diff --git a/views/partials/blog-page-like-button.html b/views/partials/blog-page-like-button.html index 54b2914..af668e6 100644 --- a/views/partials/blog-page-like-button.html +++ b/views/partials/blog-page-like-button.html @@ -12,7 +12,7 @@ <path d="M13.94 4.33a2.57 2.57 0 0 1 1.43 0.88 4.52 4.52 0 0 1 1 1.54 0.57 0.57 0 0 0 0.56 0.37 0.57 0.57 0 0 0 0.53 -0.41 2.81 2.81 0 0 1 1 -1.41 6.9 6.9 0 0 1 1.69 -1 0.49 0.49 0 0 0 0.31 -0.43 0.5 0.5 0 0 0 -0.24 -0.47 8.18 8.18 0 0 1 -2.26 -1.47 2.39 2.39 0 0 1 -0.71 -1.46 0.34 0.34 0 0 0 -0.34 -0.34 0.35 0.35 0 0 0 -0.34 0.35 0.29 0.29 0 0 0 -0.27 0.23 4.41 4.41 0 0 1 -1 1.63 3.53 3.53 0 0 1 -1.48 1 0.47 0.47 0 0 0 -0.32 0.5 0.48 0.48 0 0 0 0.44 0.49Zm2 -1.47a5.53 5.53 0 0 0 0.82 -1.44 3.34 3.34 0 0 0 0.59 1 6.91 6.91 0 0 0 1.53 1.3 5.93 5.93 0 0 0 -1.48 1 6.91 6.91 0 0 0 -0.51 0.57 4.8 4.8 0 0 0 -0.7 -0.85 4 4 0 0 0 -1 -0.74 5.76 5.76 0 0 0 0.71 -0.84Z" fill="var(--color-accent-deep)" fill-rule="evenodd" stroke-width="1"></path> </g> </svg> - <div class="ml-auto bg-accent-light rounded-full w-6 h-6 text-main">{{ .LikedCount }}</div> + <div class="ml-auto bg-accent-light rounded-full w-6 h-6 text-main! font-m-plus! font-normal! tracking-normal! not-italic! text-lg!">{{ .LikedCount }}</div> {{ else }} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="Like-Hand--Streamline-Freehand" class="h-10"> <desc> @@ -23,6 +23,6 @@ <path d="M3.53 9.34a2.22 2.22 0 0 0 -1.24 -0.88 5.37 5.37 0 0 0 -2 -0.26 0.3 0.3 0 0 0 -0.29 0.31 0.3 0.3 0 0 0 0.32 0.29 4.36 4.36 0 0 1 1.44 0.25 1.74 1.74 0 0 1 1 0.69A9.16 9.16 0 0 1 3 11.8c0 0.64 0.06 1.38 0.09 2.14 0 1.07 0.08 2.2 0.11 3.22s0.06 2.19 0 2.84c0 0.23 -0.12 0.45 -0.07 0.51A2.7 2.7 0 0 1 2 21a3.34 3.34 0 0 1 -1.49 0 0.34 0.34 0 0 0 -0.22 0.65 4.22 4.22 0 0 0 1.82 0.13 3.37 3.37 0 0 0 1.79 -0.69 1.82 1.82 0 0 0 0.26 -0.82 37.13 37.13 0 0 0 0.14 -5.13c0 -1 -0.1 -2 -0.19 -2.83a12.48 12.48 0 0 0 -0.58 -2.97Z" fill="var(--color-accent-deep)" fill-rule="evenodd" stroke-width="1"></path> </g> </svg> - <div class="ml-auto bg-accent-deep rounded-full w-6 h-6 text-background">{{ .LikedCount }}</div> + <div class="ml-auto bg-accent-deep rounded-full w-6 h-6 text-background! font-m-plus! font-normal! tracking-normal! not-italic! text-lg!">{{ .LikedCount }}</div> {{ end }} </button> diff --git a/views/partials/catalogue-blog-cards.html b/views/partials/catalogue-blog-cards.html index 0a93797..b066a9e 100644 --- a/views/partials/catalogue-blog-cards.html +++ b/views/partials/catalogue-blog-cards.html @@ -1,8 +1,8 @@ {{- range .BlogPages }} <hr class="first-of-type:hidden border-t-2 border-dotted border-main-hard"> -<div class="w-full flex flex-row items-stretch"> +<div {{ if .ToHighlight }}id="blog-page-highlighted"{{ end }} class="w-full flex flex-row items-stretch {{ if .ToHighlight }}bg-accent-light bg-paper{{ end }}"> <div class="relative min-h-24 flex flex-col w-28 overflow-hidden shrink-0 mask-r-from-70% mask-r-to-100%"> - <img onclick="return changeUrl('{{ .ArticleLink }}');" class="h-full min-h-24 w-full cursor-pointer select-none object-cover block absolute" src="{{ printf $.ThumbnailBaseUrl .Thumbnail }}"> + <img onclick="return changeUrl('{{ .ArticleLink }}');" class="h-full min-h-24 w-full cursor-pointer select-none object-cover block absolute" src="{{ printf $.PhotoStorage.Thumbnail320p.BaseUrl .Thumbnail }}"> <div class="w-full h-6 flex flex-row mt-auto relative"> <div class="flex-1/2 flex flex-row justify-center select-none bg-linear-180 {{ if .Liked }}from-accent-deep/40 to-accent-deep/80 text-background-dark{{ else }}from-accent-light/40 to-accent-light/80 text-main-hard{{ end }}"> <svg viewBox="0 0 24 24"><use href="#icon-like"/></svg> @@ -20,10 +20,20 @@ <span class="font-extrabold">{{ .Title }}</span> <span class="italic">[{{ .ActionDate }}]</span> </a> + {{- if not $.HidePublishedTime }} <p class="basis-56 grow text-base italic text-secondary text-right">{{ .PublishedTime }}</p> + {{- end }} </div> - <p class="font-gentium">{{ .ShortDescription }}</p> + <p class="font-m-plus">{{ .ShortDescription }}</p> + {{- if not $.HideTags }} <p class="font-m-plus text-sm">{{ $.L.TagsLabel }} {{ template "catalogue-blog-card-tags.html" . }}</p> + {{- end }} </div> </div> -{{- end }}
\ No newline at end of file +{{- if .ToHighlight }} +<script> + element = document.getElementById("blog-page-highlighted"); + element.scrollIntoView({ behavior: "smooth", block: "center" }); +</script> +{{- end }} +{{- end }} diff --git a/views/partials/general-page-body.html b/views/partials/general-page-body.html index d697c14..b71a0d1 100644 --- a/views/partials/general-page-body.html +++ b/views/partials/general-page-body.html @@ -1 +1,2 @@ +<input class="htmx-exit-trigger" type="hidden" name="dummy" value=""> {{ block "body" . }}{{ end }}
\ No newline at end of file diff --git a/views/partials/global-map-widget.html b/views/partials/global-map-widget.html new file mode 100644 index 0000000..f50020f --- /dev/null +++ b/views/partials/global-map-widget.html @@ -0,0 +1,109 @@ +<div + id="map-container" + class="bg-paper bg-background-dark flex z-10 w-full h-full" +></div> + +<script> +(function() { + var map; + var markers = L.markerClusterGroup(); + const THUMB_BASE = "{{ .PhotoStorage.Thumbnail320p.BaseUrl }}"; + + function createCustomIcon(color, borderColor = "var(--color-main-soft)") { + const svgIcon = ` + <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> + <path d="M12 1C7.5 1 4 4.5 4 9c0 6.5 8 14 8 14s8-7.5 8-14c0-4.5-3.5-8-8-8zm0 11c-1.5 0-3-1.5-3-3s1.5-3 3-3 3 1.5 3 3-1.5 3-3 3z" + fill="${color}" + stroke="${borderColor}" + stroke-width="1" + stroke-linejoin="round"/> + </svg> + `; + + return L.divIcon({ + html: svgIcon, + className: 'custom-svg-marker', + iconSize: [32, 32], + iconAnchor: [16, 32], + popupAnchor: [0, -32] + }); + } + + function initLocationMap() { + map = L.map('map-container').setView([{{ .MapLocationLat }}, {{ .MapLocationLong }}], {{ .MapLocationZoom }}); + + protomapsL.leafletLayer({ + url: '{{ .StaticStorage.Map.BaseUrl }}/{{ .StaticStorage.Map.PMTiles }}', + maxZoom: 15, + flavor: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light', + lang: '{{ .Lang }}' + }).addTo(map); + + {{- with .ZoomPosition }} + L.control.zoom({ + position: '{{ . }}' + }).addTo(map); + {{- end }} + + map.addLayer(markers); + } + + function markerToHsl(str, newCoef) { + let hash = 0; + for (let i = 0; i < str.length / 2; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + hash = str.charCodeAt(str.length - i - 1) + ((hash << 5) - hash); + hash %= 360; + } + + return [hash, (20 + newCoef * 80).toFixed(0), (20 + newCoef * 75).toFixed(0)]; + } + + function mapAddMarker(x, y, relativeErrorMeters = 0, title = "", link = "", thumbnail = "", newCoef = 1, toHighlight = false) { + const [h, s, l] = markerToHsl(title, newCoef); + const color = `hsl(${h} ${s}% ${l}%)`; + + if (relativeErrorMeters != 0) { + var circle = L.circle([x, y], { + color: color, + fillColor: color, + fillOpacity: 0.15, + radius: relativeErrorMeters + }).addTo(map); + } + + var marker = L.marker([x, y], { + icon: createCustomIcon(color), + title: title, + }).bindPopup(` + <div class="flex flex-row justify-center justify-items-center"> + <img onclick="location.href='${link}';" class="cursor-pointer object-cover rounded-[10%] select-none w-12 h-12 mr-1" src="${THUMB_BASE.replace('%s', thumbnail)}"> + <span><b><a href="${link}">${title}</a></b><br><a href="https://www.openstreetmap.org/#map=13/${x}/${y}">${x}, ${y}</a></span> + </div>`); + + markers.addLayer(marker); + + if (toHighlight) { + markers.zoomToShowLayer(marker, () => marker.openPopup()); + } + } + + initLocationMap(); + {{- range .MapMarkers }} + mapAddMarker({{ .Lat }}, {{ .Long }}, {{ .AccuracyMeters }}, {{ .Title }}, {{ .PageLink }}, {{ .Thumbnail }}, {{ fdiv .Index (len $.MapMarkers) }}, {{ .ToHighlight }}); + {{- end }} + + function onResize() { + setTimeout(() => { + if (map) map.invalidateSize(); + }, 300); + } + window.addEventListener('resize', onResize); + new MutationObserver((muts, obs) => { + if (!document.getElementById('map-container')) { + window.removeEventListener('resize', onResize); + obs.disconnect(); + } + }).observe(document.body, { childList: true, subtree: true }); +})(); +</script> |