51 files changed, 1263 insertions, 965 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..fefe2e7 100644 --- a/config/config.local.yaml +++ b/config/config.local.yaml @@ -30,11 +30,11 @@ localePath: ./locale/ availableLanguages: - name: ru alt: Русский - flag: https://storage.yandexcloud.kz/sayauz-static/flags/ru.svg + flag: https://cdn.saya.uz/stats/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/stats/flags/gb.svg locFile: localization.en.yaml auth: db: @@ -56,23 +56,26 @@ mail: canonicalEndpoint: "http://127.0.0.1:3000" photoStorage: full: - baseUrl: https://storage.yandexcloud.kz/sayauz-photos/%s + baseUrl: https://cdn.saya.uz/photos/jpeg-full/%s webp: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-full/%s.webp thumbnail1600p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp1600/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-1600p/%s.webp thumbnail1200p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp1200/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-1200p/%s.webp thumbnail800p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp800/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-800p/%s.webp thumbnail560p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp560/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-560p/%s.webp thumbnail320p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp320/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-320p/%s.webp homePageGifs: - baseUrl: https://storage.yandexcloud.kz/sayauz-static/home-page-gifs/otter-%s.gif + baseUrl: https://cdn.saya.uz/stats/home-page-gifs/otter-%s.gif indexes: ["1", "2", "3"] staticStorage: - baseUrl: https://storage.yandexcloud.kz/sayauz-static + baseUrl: https://cdn.saya.uz/stats + 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..ce0941d 100644 --- a/config/config.prod.yaml +++ b/config/config.prod.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/stats/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/stats/flags/gb.svg locFile: localization.en.yaml auth: db: @@ -59,23 +59,26 @@ meta: yandexVerification: "${YANDEX_VERIFICATION}" photoStorage: full: - baseUrl: https://storage.yandexcloud.kz/sayauz-photos/%s + baseUrl: https://cdn.saya.uz/photos/jpeg-full/%s webp: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-full/%s.webp thumbnail1600p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp1600/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-1600p/%s.webp thumbnail1200p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp1200/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-1200p/%s.webp thumbnail800p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp800/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-800p/%s.webp thumbnail560p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp560/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-560p/%s.webp thumbnail320p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp320/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-320p/%s.webp homePageGifs: - baseUrl: https://storage.yandexcloud.kz/sayauz-static/home-page-gifs/otter-%s.gif + baseUrl: https://cdn.saya.uz/stats/home-page-gifs/otter-%s.gif indexes: ["1", "2", "3"] staticStorage: - baseUrl: https://storage.yandexcloud.kz/sayauz-static + baseUrl: https://cdn.saya.uz/stats + 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..d8819ca 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/stats/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/stats/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://cdn.saya.uz/photos/jpeg-full/%s webp: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-full/%s.webp thumbnail1600p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp1600/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-1600p/%s.webp thumbnail1200p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp1200/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-1200p/%s.webp thumbnail800p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp800/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-800p/%s.webp thumbnail560p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp560/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-560p/%s.webp thumbnail320p: - baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp320/%s.webp + baseUrl: https://cdn.saya.uz/photos/webp-320p/%s.webp homePageGifs: - baseUrl: https://storage.yandexcloud.kz/sayauz-static/home-page-gifs/otter-%s.gif + baseUrl: https://cdn.saya.uz/stats/home-page-gifs/otter-%s.gif indexes: ["1", "2", "3"] staticStorage: - baseUrl: https://storage.yandexcloud.kz/sayauz-static + baseUrl: https://cdn.saya.uz/stats + 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/glightbox/html_renderer.go b/internal/glightbox/html_renderer.go index b2cd855..1bb606b 100644 --- a/internal/glightbox/html_renderer.go +++ b/internal/glightbox/html_renderer.go @@ -151,12 +151,12 @@ func (r *GLightboxHTMLRenderer) renderGLightbox(w util.BufWriter, source []byte, w.WriteString(strings.ReplaceAll(` <div class="items-center flex flex-col"> - <hr class="border-t-3 border-dotted border-main-hard mt-1 mb-2 w-[80%] ml-auto mr-auto"> + <hr class="border-t-[0.375rem] border-dotted border-main-hard my-2 w-24 ml-auto mr-auto"> <div class="grid masonry-grid-{id}"> <div class="grid-sizer grid-sizer-{id}"></div> `+strings.Join(elements, "\n")+` </div> - <hr class="border-t-3 border-dotted border-main-hard mt-1 mb-2 w-[80%] ml-auto mr-auto"> + <hr class="border-t-[0.375rem] border-dotted border-main-hard my-2 w-24 ml-auto mr-auto"> </div> <script> 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 c48b40a..b59f09a 100644 --- a/internal/router/handlers/api-v1-blog-search.go +++ b/internal/router/handlers/api-v1-blog-search.go @@ -8,7 +8,6 @@ import ( "net/url" "regexp" "slices" - "strings" "time" "github.com/SayaAndy/saya-today-web/internal/blog" @@ -53,12 +52,7 @@ func (r *BlogSearchHandler) Render(c *fiber.Ctx, supplements *router.Supplements sort := c.Query("sort") tz := c.Query("tz") medley := c.Query("medley") - referer := c.Get("Referer") - refererParts := strings.Split(referer, "/") - refererPage := "" - if len(refererParts) >= 1 { - refererPage = refererParts[len(refererParts)-1] - } + highlight := c.Query("highlight") hideTags := c.QueryBool("hideTags", false) hidePublishedTime := c.QueryBool("hidePublishedTime", false) @@ -111,7 +105,7 @@ func (r *BlogSearchHandler) Render(c *fiber.Ctx, supplements *router.Supplements "Viewed": supplements.ClientCache.GetViewStatus(c.IP(), page.FileName), "Medley": page.Metadata.Medley, "MedleyPart": page.Metadata.MedleyPart, - "ToHighlight": page.FileName == refererPage, + "ToHighlight": page.FileName == highlight, }) break } 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-like-get.go b/internal/router/handlers/api-v1-like-get.go index 0eaf5f6..9e2c856 100644 --- a/internal/router/handlers/api-v1-like-get.go +++ b/internal/router/handlers/api-v1-like-get.go @@ -42,7 +42,7 @@ func (r *GetLikeHandler) Render(c *fiber.Ctx, supplements *router.Supplements, l return fiber.StatusBadRequest, fmt.Errorf("error getting path from referer: %w", err) } - if len(pathParts) != 3 && pathParts[1] != "blog" { + if len(pathParts) != 3 || pathParts[1] != "blog" { return fiber.StatusBadRequest, fmt.Errorf("invalid path format: expected '/:lang/blog/:page', got '%s'", path) } 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 f7cd019..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,6 +132,7 @@ 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 @@ -129,7 +140,7 @@ func (r *BlogPageHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplemen 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/tailwind/transformer.go b/internal/tailwind/transformer.go index c9da9df..f90489f 100644 --- a/internal/tailwind/transformer.go +++ b/internal/tailwind/transformer.go @@ -45,7 +45,7 @@ func (t *TailwindTransformer) Transform(node *ast.Document, reader text.Reader, node.SetAttribute([]byte("class"), []byte("text-base/[2] font-gentium tracking-[.0125rem]")) case *ast.Blockquote: - node.SetAttribute([]byte("class"), []byte("border-l-2 border-main-medium bg-paper bg-background-dark p-1 mb-2 italic font-thin")) + node.SetAttribute([]byte("class"), []byte("border-l-[0.125rem] border-main-medium bg-paper bg-background-dark p-1 mb-2 italic font-thin")) case *ast.CodeSpan: node.SetAttribute([]byte("class"), []byte("bg-paper bg-background-dark")) 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 8b71614..e0c5c81 100644 --- a/static/input.css +++ b/static/input.css @@ -22,30 +22,40 @@ --color-main-dark-hard: var(--color-main-dark-hard); --color-main-dark-medium: var(--color-main-dark-medium); --color-main-dark-soft: var(--color-main-dark-soft); - --shadow-elevation-flat: 0px 1px 1px 0px rgba(0, 0, 0, 0.2); + --shadow-elevation-flat: 0px 0.0625rem 0.0625rem 0px rgba(0, 0, 0, 0.2); + --inset-shadow-elevation-flat: inset 0px 0.0625rem 0.125rem 0px rgba(0, 0, 0, 0.15); --shadow-elevation-0: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12); - --shadow-elevation-1: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12); - --shadow-elevation-2: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); - --shadow-elevation-3: 0px 3px 3px -2px rgba(0, 0, 0, 0.2), 0px 3px 4px 0px rgba(0, 0, 0, 0.14), 0px 1px 8px 0px rgba(0, 0, 0, 0.12); - --shadow-elevation-4: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); - --shadow-elevation-5: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 5px 8px 0px rgba(0, 0, 0, 0.14), 0px 1px 14px 0px rgba(0, 0, 0, 0.12); - --shadow-elevation-6: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12); - --shadow-elevation-7: 0px 4px 5px -2px rgba(0, 0, 0, 0.2), 0px 7px 10px 1px rgba(0, 0, 0, 0.14), 0px 2px 16px 1px rgba(0, 0, 0, 0.12); - --shadow-elevation-8: 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12); - --shadow-elevation-9: 0px 5px 6px -3px rgba(0, 0, 0, 0.2), 0px 9px 12px 1px rgba(0, 0, 0, 0.14), 0px 3px 16px 2px rgba(0, 0, 0, 0.12); - --shadow-elevation-10: 0px 6px 6px -3px rgba(0, 0, 0, 0.2), 0px 10px 14px 1px rgba(0, 0, 0, 0.14), 0px 4px 18px 3px rgba(0, 0, 0, 0.12); - --shadow-elevation-11: 0px 6px 7px -4px rgba(0, 0, 0, 0.2), 0px 11px 15px 1px rgba(0, 0, 0, 0.14), 0px 4px 20px 3px rgba(0, 0, 0, 0.12); - --shadow-elevation-12: 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 12px 17px 2px rgba(0, 0, 0, 0.14), 0px 5px 22px 4px rgba(0, 0, 0, 0.12); + --inset-shadow-elevation-0: inset 0px 0px 0px 0px rgba(0, 0, 0, 0); + --shadow-elevation-1: 0px 0.125rem 0.0625rem -0.0625rem rgba(0, 0, 0, 0.2), 0px 0.0625rem 0.0625rem 0px rgba(0, 0, 0, 0.14), 0px 0.0625rem 0.1875rem 0px rgba(0, 0, 0, 0.12); + --inset-shadow-elevation-1: inset 0px 0.125rem 0.25rem 0px rgba(0, 0, 0, 0.2); + --shadow-elevation-2: 0px 0.1875rem 0.0625rem -0.125rem rgba(0, 0, 0, 0.2), 0px 0.125rem 0.125rem 0px rgba(0, 0, 0, 0.14), 0px 0.0625rem 0.3125rem 0px rgba(0, 0, 0, 0.12); + --inset-shadow-elevation-2: inset 0px 0.1875rem 0.375rem 0px rgba(0, 0, 0, 0.22); + --shadow-elevation-3: 0px 0.1875rem 0.1875rem -0.125rem rgba(0, 0, 0, 0.2), 0px 0.1875rem 0.25rem 0px rgba(0, 0, 0, 0.14), 0px 0.0625rem 0.5rem 0px rgba(0, 0, 0, 0.12); + --inset-shadow-elevation-3: inset 0px 0.25rem 0.5rem 0px rgba(0, 0, 0, 0.25); + --shadow-elevation-4: 0px 0.125rem 0.25rem -0.0625rem rgba(0, 0, 0, 0.2), 0px 0.25rem 0.3125rem 0px rgba(0, 0, 0, 0.14), 0px 0.0625rem 0.625rem 0px rgba(0, 0, 0, 0.12); + --inset-shadow-elevation-4: inset 0px 0.3125rem 0.625rem 0px rgba(0, 0, 0, 0.27); + --shadow-elevation-5: 0px 0.1875rem 0.3125rem -0.0625rem rgba(0, 0, 0, 0.2), 0px 0.3125rem 0.5rem 0px rgba(0, 0, 0, 0.14), 0px 0.0625rem 0.875rem 0px rgba(0, 0, 0, 0.12); + --inset-shadow-elevation-5: inset 0px 0.375rem 0.75rem 0px rgba(0, 0, 0, 0.3); + --shadow-elevation-6: 0px 0.1875rem 0.3125rem -0.0625rem rgba(0, 0, 0, 0.2), 0px 0.375rem 0.625rem 0px rgba(0, 0, 0, 0.14), 0px 0.0625rem 1.125rem 0px rgba(0, 0, 0, 0.12); + --inset-shadow-elevation-6: inset 0px 0.4375rem 0.875rem 0px rgba(0, 0, 0, 0.32); + --shadow-elevation-7: 0px 0.25rem 0.3125rem -0.125rem rgba(0, 0, 0, 0.2), 0px 0.4375rem 0.625rem 0.0625rem rgba(0, 0, 0, 0.14), 0px 0.125rem 1rem 0.0625rem rgba(0, 0, 0, 0.12); + --inset-shadow-elevation-7: inset 0px 0.5rem 1rem 0px rgba(0, 0, 0, 0.35); + --shadow-elevation-8: 0px 0.3125rem 0.3125rem -0.1875rem rgba(0, 0, 0, 0.2), 0px 0.5rem 0.625rem 0.0625rem rgba(0, 0, 0, 0.14), 0px 0.1875rem 0.875rem 0.125rem rgba(0, 0, 0, 0.12); + --inset-shadow-elevation-8: inset 0px 0.5625rem 1.125rem 0px rgba(0, 0, 0, 0.37); + --shadow-elevation-9: 0px 0.3125rem 0.375rem -0.1875rem rgba(0, 0, 0, 0.2), 0px 0.5625rem 0.75rem 0.0625rem rgba(0, 0, 0, 0.14), 0px 0.1875rem 1rem 0.125rem rgba(0, 0, 0, 0.12); + --inset-shadow-elevation-9: inset 0px 0.625rem 1.25rem 0px rgba(0, 0, 0, 0.4); + --shadow-elevation-10: 0px 0.375rem 0.375rem -0.1875rem rgba(0, 0, 0, 0.2), 0px 0.625rem 0.875rem 0.0625rem rgba(0, 0, 0, 0.14), 0px 0.25rem 1.125rem 0.1875rem rgba(0, 0, 0, 0.12); + --inset-shadow-elevation-10: inset 0px 0.6875rem 1.375rem 0px rgba(0, 0, 0, 0.42); + --shadow-elevation-11: 0px 0.375rem 0.4375rem -0.25rem rgba(0, 0, 0, 0.2), 0px 0.6875rem 0.9375rem 0.0625rem rgba(0, 0, 0, 0.14), 0px 0.25rem 1.25rem 0.1875rem rgba(0, 0, 0, 0.12); + --inset-shadow-elevation-11: inset 0px 0.75rem 1.5rem 0px rgba(0, 0, 0, 0.45); + --shadow-elevation-12: 0px 0.4375rem 0.5rem -0.25rem rgba(0, 0, 0, 0.2), 0px 0.75rem 1.0625rem 0.125rem rgba(0, 0, 0, 0.14), 0px 0.3125rem 1.375rem 0.25rem rgba(0, 0, 0, 0.12); + --inset-shadow-elevation-12: inset 0px 0.8125rem 1.625rem 0px rgba(0, 0, 0, 0.47); } html { - font-size: 12px; + font-size: clamp(11px, min(1dvw, 1dvh) + 7.6px, 16px); --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; @@ -77,27 +87,25 @@ body { scrollbar-color: var(--color-background-light) var(--color-main-soft); } -@media screen and (min-width: 640px) and (min-height: 640px) { - html { - font-size: 14px; - } +body.glightbox-open { + height: 100dvh; } @media screen and (min-width: 900px) and (min-height: 900px) { html { - font-size: 16px; + font-size: clamp(16px, min(1dvw, 1dvh) + 7px, 20px); } } @media screen and (min-width: 1400px) and (min-height: 1400px) { html { - font-size: 20px; + font-size: clamp(20px, min(1dvw, 1dvh) + 6px, 24px); } } @media screen and (min-width: 1920px) and (min-height: 1920px) { html { - font-size: 24px; + font-size: calc(min(1dvw, 1dvh) + 4.8px); } } @@ -133,6 +141,22 @@ 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); @@ -157,8 +181,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/stats/themes/lettuce/linen-fabric.webp"), + url("https://cdn.saya.uz/stats/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 +191,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-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: oklch(25.846% 0.02619 284.414); - --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: 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,38 +270,108 @@ 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-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-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: 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/stats/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/stats/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); @@ -298,8 +402,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/stats/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/stats/themes/lilac/split-right-dark.webp"); + } } a, @@ -314,16 +434,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; } @@ -379,13 +499,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); @@ -426,14 +539,6 @@ form.crossable.htmx-request input { calc(var(--squares-and-triangles-background-height-coef) * 1rem); } -.inset-shadow { - box-shadow: - inset 0dvh 1dvh 1dvh -1dvh rgba(75, 81, 58, 0.5), - inset 1dvh 0dvh 1dvh -1dvh rgba(75, 81, 58, 0.5), - inset -1dvh 0dvh 1dvh -1dvh rgba(75, 81, 58, 0.5), - inset 0dvh -0.5dvh rgba(75, 81, 58, 0.5); -} - .text-stroke-\(--color-sidebar-stroke\) { -webkit-text-stroke-color: var(--color-sidebar-stroke); } @@ -446,7 +551,7 @@ form.crossable.htmx-request input { position: fixed; width: 12rem; height: 12rem; - box-shadow: 0.25rem 0.25rem 0.25rem var(--color-main-soft); + box-shadow: 0px 0.3125rem 0.3125rem -0.1875rem rgba(0, 0, 0, 0.2), 0px 0.5rem 0.625rem 0.0625rem rgba(0, 0, 0, 0.14), 0px 0.1875rem 0.875rem 0.125rem rgba(0, 0, 0, 0.12); border-radius: 50%; opacity: 0; visibility: hidden; @@ -481,7 +586,7 @@ form.crossable.htmx-request input { .theme-icon i { position: absolute; - filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3)); + filter: drop-shadow(0 0.125rem 0.25rem rgba(0, 0, 0, 0.3)); z-index: 1; } @@ -507,7 +612,7 @@ form.crossable.htmx-request input { box-shadow: 0rem 0rem 1rem var(--color-main-hard); border: 0.5rem solid color-mix(in srgb, var(--color-main-soft) 90%, transparent); - backdrop-filter: blur(2px); + backdrop-filter: blur(0.125rem); top: 35%; left: 35%; width: 30%; @@ -532,6 +637,10 @@ form.crossable.htmx-request input { transition-timing-function: var(--ease-in-out); } +.accent-button { + box-shadow: 0px 0.1875rem 0.1875rem -0.125rem rgba(0, 0, 0, 0.2), 0px 0.1875rem 0.25rem 0px rgba(0, 0, 0, 0.14), 0px 0.0625rem 0.5rem 0px rgba(0, 0, 0, 0.12); +} + .accent-button:hover, .accent-button.active:active:hover { cursor: pointer; @@ -545,6 +654,7 @@ form.crossable.htmx-request input { --tw-gradient-position: from 45deg in oklab; --tw-gradient-to: color-mix(in oklab, var(--color-accent-dark) 30%, transparent); --tw-gradient-via: color-mix(in oklab, var(--color-accent-light) 50%, transparent); + box-shadow: inset 0px 0.25rem 0.5rem 0px rgba(0, 0, 0, 0.25); } .accent-button.active:hover, @@ -560,7 +670,7 @@ form.crossable.htmx-request input { } .custom-svg-marker svg { - filter: drop-shadow(2px 2px 2px rgba(0, 0, 0, 0.3)); + filter: drop-shadow(0.125rem 0.125rem 0.125rem rgba(0, 0, 0, 0.3)); transition: transform 0.2s; } @@ -616,6 +726,15 @@ form.crossable.htmx-request input { width: 10rem; } +.grid-item picture { + mask-image: + linear-gradient(to right, #FFFFFF calc(100% - 0.5rem), transparent 100%), + linear-gradient(to left, #FFFFFF calc(100% - 0.5rem), transparent 100%), + linear-gradient(to top, #FFFFFF calc(100% - 0.5rem), transparent 100%), + linear-gradient(to bottom, #FFFFFF calc(100% - 0.5rem), transparent 100%); + mask-composite: intersect; +} + .grid-sizer { width: 10.5rem; } @@ -639,6 +758,10 @@ form.crossable.htmx-request input { opacity: 0; } +.grid-item:hover picture { + mask-image: none; +} + .grid-item-2x { width: 20rem; } @@ -738,9 +861,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 { @@ -761,8 +904,8 @@ body[data-theme~="ram"] .leaflet-tile { } .ram-frame { - width: 20vmin; - height: 20vmin; + width: 8rem; + height: 8rem; user-select: none; -webkit-user-select: none; } diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html index 2d125ae..06ef625 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 @@ -160,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 () { @@ -202,18 +213,8 @@ var seed = cyrb128("{{ .Title }}"); var rand = sfc32(seed[0], seed[1], seed[2], seed[3]); - function calculateVmin(percent) { - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - const smallerDimension = Math.min( - viewportWidth, - viewportHeight, - ); - - const vminValue = (smallerDimension / 100) * percent; - - return vminValue; + function calculateRem(remNum) { + return remNum * parseFloat(getComputedStyle(document.documentElement).fontSize); } const _probe = document.createElement('div'); @@ -308,7 +309,7 @@ theme = document.body.getAttribute("data-theme"); } - vmin = calculateVmin(1); + rem = calculateRem(1); var clWidth = document.documentElement.clientWidth; var clHeight = document.documentElement.clientHeight; @@ -327,11 +328,11 @@ "z-0", "absolute", ); - const x = Math.floor(rand() * clWidth); - const y = Math.floor(rand() * clHeight * 0.8); const rot = rand() * 90; const animDuration = rand() * 10 + 3; const sz = Math.floor(rand() * 8) + 12; + const x = Math.floor(rand() * clWidth - 1.42 * sz); // 1.42 ~ sqrt(2) + const y = Math.floor(rand() * clHeight * 0.8); star.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rot}deg)`; star.style.animationDuration = `${animDuration}s`; star.style.fontSize = `${sz}px`; @@ -342,7 +343,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", @@ -351,14 +352,14 @@ ); let x, y; outerLoop: while (true) { - x = Math.floor(rand() * clWidth - 20 * vmin); - y = Math.floor(rand() * clHeight - 20 * vmin); + x = Math.floor(rand() * clWidth - 22.64 * rem) + 11.32 * rem; // 11.32 ~ 8 * sqrt(2) + y = Math.floor(rand() * clHeight - 22.64 * rem) + 11.32 * rem; for (j = 0; j < i - 1; j++) { const xd = frameCenters[j].x - x; const yd = frameCenters[j].y - y; if ( Math.sqrt(xd * xd + yd * yd) < - 30 * vmin + 23 * rem ) { continue outerLoop; } @@ -401,21 +402,28 @@ localStorage.setItem("theme", theme); document.body.setAttribute("data-theme", theme); switchThemeDecor(theme); - if (theme == "ram" || theme == "night") { + switchColorScheme(matchMedia('(prefers-color-scheme: dark)').matches); + } + + 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 savedTheme = localStorage.getItem("theme") || "lilac"; - switchTheme(savedTheme); + 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 @@ -620,15 +628,15 @@ class="flex flex-1 min-h-0 z-1 bg-paper bg-background shadow-elevation-3 @container/body" > <div - class="bg-paper bg-background-dark border-r-2 border-dashed border-main-hard" + class="bg-paper bg-background-dark border-r-[0.25rem] border-dashed border-main-hard" ></div> <div - class="max-h-full max-w-full flex flex-col grow bg-paper bg-background-dark relative pt-4 md:pr-4" + class="max-h-full max-w-full flex flex-col grow bg-paper bg-background-dark relative md:pr-4" > <header id="general-page-header" - class="max-xs:hidden [@media(max-height:32rem)]:hidden flex flex-col ml-8 basis-8" + class="max-xs:hidden [@media(max-height:32rem)]:hidden pt-4 flex flex-col ml-8 basis-8" >{{ .RenderedHeader }}</header> <div id="general-page-header-trigger" @@ -639,12 +647,12 @@ ></div> <hr - class="max-xs:hidden [@media(max-height:32rem)]:hidden border-t-4 border-dotted border-main-hard mb-2" + class="max-xs:hidden [@media(max-height:32rem)]:hidden border-t-[0.5rem] border-dotted border-main-hard mb-2" /> <main id="general-page-body" - class="relative flex-1 overflow-y-auto @container-size" + class="relative flex-1 overflow-y-auto @container-size inset-shadow-elevation-8" >{{ .RenderedBody }}</main> <div id="general-page-body-trigger" @@ -652,6 +660,7 @@ hx-trigger="popstate from:window" hx-target="#general-page-body" hx-swap="innerHTML" + hx-include="#general-page-body .htmx-exit-trigger" ></div> <footer @@ -665,14 +674,10 @@ hx-target="#general-page-footer" hx-swap="innerHTML" ></div> - - <hr - class="border-t-4 border-dotted border-main-hard mb-2" - /> </div> <div - class="bg-paper bg-background-dark shadow-2xl border-l-2 border-dotted border-main-hard" + class="bg-paper bg-background-dark shadow-2xl border-l-[0.25rem] border-dotted border-main-hard" ></div> </div> </div> @@ -699,17 +704,6 @@ } }); - function calculateVmax(percent) { - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - const largerDimension = Math.max(viewportWidth, viewportHeight); - - const vmaxValue = (largerDimension / 100) * percent; - - return vmaxValue; - } - function positionThemeIcons() { const sliceAngle = 360 / themeIcons.length; 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 4a7d9bf..ca8f578 100644 --- a/views/pages/blog-catalogue.html +++ b/views/pages/blog-catalogue.html @@ -45,63 +45,63 @@ </symbol> </svg> <div class="flex md:flex-row flex-col text-base h-full"> - <div class="relative flex shrink-0 w-full max-h-[30%] md:min-w-1/10 md:max-w-1/4 md:max-h-full md:h-full md:w-fit"> - <div class="absolute w-full h-full inset-shadow-[0_0_0.4rem_black] pointer-events-none z-20"></div> + <div class="relative flex shrink-0 w-full max-h-[30%] md:min-w-1/10 md:max-w-1/4 md:max-h-full md:h-full md:w-fit shadow-elevation-6"> + <div class="absolute w-full h-full pointer-events-none z-20"></div> <form hx-get="/api/v1/blog-search" hx-vals='{"lang": "{{ .Lang }}"}' hx-target=".blog-cards" hx-swap="innerHTML" - class="tags-list relative flex flex-col w-full bg-paper bg-background-light"> - <div class="flex flex-col overflow-y-auto"> + class="tags-list relative flex flex-col w-full"> + <div class="flex flex-col bg-paper bg-background-light overflow-y-auto inset-shadow-elevation-6"> <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> + <div class="m-1"> <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> + <div class="m-1"> <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> + <div class="m-1"> <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> + <div class="m-1"> <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> + <div class="m-1"> <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> + <div class="m-1"> <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"> @@ -111,24 +111,21 @@ </div> </fieldset> </div> - <hr class="border-t-2 border-dotted border-main-hard my-0.5"> - <div class="flex flex-row bg-paper bg-background-dark p-0.5 rounded-1 h-12"> - <button type="button" onclick="defaultSearchParams(); cleanHrefParams();" class="flex flex-1 justify-center text-main-hard bg-paper bg-accent-light cursor-pointer hover:bg-accent-deep active:text-background-dark transition-colors duration-500"> - <svg viewBox="0 0 24 24"><use href="#icon-camera-settings-flip"/></svg> + <hr class="border-t-[0.375rem] border-dotted border-main-hard my-2 mx-auto w-24"> + <div class="flex flex-row p-0.5 rounded-1 h-12"> + <button type="button" onclick="defaultSearchParams(); cleanHrefParams();" class="accent-button grow h-full"> + <svg class="h-full mx-auto" viewBox="0 0 24 24"><use href="#icon-camera-settings-flip"/></svg> </button> - <div class="w-1 bg-paper bg-background-light grow-0"></div> - <button type="submit" onclick="setHrefParams();" class="flex flex-1 justify-center text-main-hard bg-paper bg-accent-light cursor-pointer hover:bg-accent-deep active:text-background-dark transition-colors duration-500"> - <svg viewBox="0 0 24 24"><use href="#icon-search-magnifier"/></svg> + <button type="submit" onclick="setHrefParams();" class="accent-button grow h-full"> + <svg class="h-full mx-auto" viewBox="0 0 24 24"><use href="#icon-search-magnifier"/></svg> </button> </div> </form> </div> - <div class="w-[0.5dvh] border-r-2 border-dotted border-main-hard bg-paper bg-background-dark"></div> - <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 class="absolute w-full h-full inset-0 inset-shadow-elevation-6 pointer-events-none z-20"></div> + <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 dd716fb..3d3c623 100644 --- a/views/pages/blog-page.html +++ b/views/pages/blog-page.html @@ -1,83 +1,50 @@ {{ define "body" }} -<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"> - <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 $.ThumbnailBaseUrl .Thumbnail }});"> +<input class="htmx-exit-trigger" type="hidden" name="codename" value="{{ .Codename }}"> +<div class="relative bg-paper bg-background-light flex flex-col py-4 overflow-y-auto"> + <div class="flex flex-col sm:flex-row h-[30cqb] sm:h-[15cqb] shadow-elevation-6"> + <div class="grow shrink-2 flex-1 overflow-y-auto -mt-4 z-1 shadow-elevation-3"> + <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.Metadata.Published }}</b>: + <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.Metadata.Action }}</b>: {{ .ActionDate }}</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"> + <hr class="block sm:hidden border-t-[0.1875rem] 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]"> - {{ .Medley }} + <div class="overflow-y-auto grow-2 shrink-0 flex-1 bg-paper bg-background-dark shadow-elevation-3 sm:-mt-4 z-2"> + <h2 class="text-2xl font-gentium font-extrabold text-main-hard tracking-[.0125rem] ml-2 mb-2 border-b-[0.25rem] border-dotted w-fit"> + {{ 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}' hx-target="this" hx-swap="innerHTML" hx-trigger="load" + <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 }} + <hr class="block border-t-[0.375rem] w-24 mx-auto my-4 border-dotted border-main-hard"> + <article class="px-8 flex flex-col"> + {{ .ParsedMarkdown }} + </article> {{- 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> - <hr class="border-t-3 border-dotted border-main-hard mt-1 mb-2 w-[80%] ml-auto mr-auto"> + <hr class="border-t-[0.375rem] border-dotted border-main-hard my-2 w-24 mx-auto"> + <div id="map-outer-container" class="relative p-1 flex-none mx-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-[0.375rem] border-dotted border-main-hard my-2 w-24 mx-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..d494a08 100644 --- a/views/pages/home-page.html +++ b/views/pages/home-page.html @@ -17,24 +17,23 @@ <path d="M4.59 15.35a39.94 39.94 0 0 0 -2.94 3.25 0.35 0.35 0 1 0 0.53 0.46c0.59 -0.6 1.69 -1.51 2.89 -2.58 0.63 -0.57 1.29 -1.17 1.9 -1.79s1.42 -1.52 2 -2.21 -0.14 -1 -0.82 -0.39c-0.52 0.46 -1.08 0.94 -1.67 1.47s-1.28 1.19 -1.89 1.79Z" fill="var(--color-accent-deep)" stroke-width="1"></path> </g></symbol> </svg> -<div class="absolute w-full h-full inset-0 inset-shadow-[0_0_0.4rem_black] pointer-events-none z-20"></div> <div class="relative bg-paper bg-background-light font-gentium flex flex-col sm:flex-row w-full h-full overflow-y-auto text-base/loose tracking-wide z-10"> <div class="sm:flex-3/5 justify-center content-start p-8"> <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> - <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> + border-l-[0.25rem] border-solid border-background-dark + bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "Hymn1" }}</p> + <hr class="w-full border-t-[0.125rem] 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 $.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> - <hr class="w-full border-t-2 border-dotted border-main-hard mb-1"> + border-l-[0.125rem] border-solid border-background-dark + bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "DidYouKnowThat" }}</p> + <hr class="w-full border-t-[0.125rem] 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> <li class="ml-4 mb-1">{{ index .FunFacts 1 }}</li> @@ -46,9 +45,9 @@ <div class="sm:flex-2/5 justify-center content-start p-8"> <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> - <hr class="w-full border-t-2 border-dotted border-main-hard mb-1"> + border-l-[0.125rem] border-solid border-background-dark + bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "Contacts" }}</p> + <hr class="w-full border-t-[0.125rem] 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> <span class="font-bold">Telegram:</span> @@ -74,7 +73,7 @@ <div class="home-page-heart z-50">♡</div> {{- end }} <img src="{{ .GifUrl }}" - class="home-page-gif z-49 -rotate-12 max-w-[70%] max-h-[70%] border-4 border-background-dark border-inset"> + class="home-page-gif z-49 -rotate-12 max-w-[70%] max-h-[70%] border-[0.25rem] border-background-dark border-inset"> </div> <script> @@ -121,23 +120,23 @@ <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> - <hr class="w-full border-t-2 border-dotted border-main-hard mb-1"> + border-l-[0.125rem] border-solid border-color-background-dark + bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "SidebarDescription" }}</p> + <hr class="w-full border-t-[0.125rem] 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/language-pick.html b/views/pages/language-pick.html index f4119e4..9576a65 100644 --- a/views/pages/language-pick.html +++ b/views/pages/language-pick.html @@ -1,7 +1,7 @@ {{ define "body" }} <div class="flex flex-col justify-center text-3xl h-full"> {{- range .AvailableLanguages }} - <hr class="border-t-2 border-dotted border-main-hard"> + <hr class="border-t-[0.125rem] border-dotted border-main-hard"> <div class="nth-[2n+1_of_div]:bg-background-medium/50 nth-[2n_of_div]:bg-background-light/50 p-2.5 flex flex-row justify-center justify-items-center"> <a href="/{{ .Name }}" class="w-12 h-12 basis-12 shrink-0 mr-4"> <img onclick="return changeUrl('{{ .Name }}');" class="w-full h-full aspect-square cursor-pointer object-cover rounded-md select-none" src="{{ .Flag }}"> @@ -11,6 +11,6 @@ </a> </div> {{- end }} - <hr class="border-t-2 border-dotted border-main-hard"> + <hr class="border-t-[0.125rem] border-dotted border-main-hard"> </div> {{ end }} 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..a94dcb3 100644 --- a/views/pages/user-page.html +++ b/views/pages/user-page.html @@ -1,74 +1,74 @@ {{ define "body" }} -<div class="absolute w-full h-full inset-0 inset-shadow-[0_0_0.4rem_black] pointer-events-none z-20"></div> +<div class="absolute w-full h-full inset-0 inset-shadow-elevation-6 pointer-events-none z-20"></div> <div class="relative bg-paper bg-background-light flex flex-col px-8 py-4"> <form id="email-form" class="w-full mb-2 flex flex-col relative crossable" hx-get="/api/v1/email/is-in-verification" hx-target="#email-message" hx-swap="outerHTML" hx-trigger="load" hx-indicator="this"> <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" + <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-elevation-6 focus:outline-none focus:bg-background-light focus:text-main-hard z-22" + 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> + <div id="email-message" class="bg-paper bg-background-dark text-main-soft z-26 w-full inset-shadow-elevation-6 py-2 px-4 text-center"></div> - <hr class="my-2 border-t-4 border-dotted border-main-hard"> + <hr class="my-2 border-t-[0.25rem] border-dotted border-main-hard"> <form id="email-verification-form" class="w-full mb-2 flex flex-col relative hidden crossable" {{ if .EmailCode }}hx-post="/api/v1/email/verify" hx-target="#verification-message" hx-swap="outerHTML" hx-trigger="load" hx-indicator="this"{{ end }}> <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" + <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-elevation-6 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"> </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/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> + <div id="verification-message" class="bg-paper bg-background-dark text-main-soft z-26 w-full inset-shadow-elevation-6 py-2 px-4 text-center"></div> </div> -<hr class="my-4 border-t-4 border-dotted border-main-hard"> +<hr class="my-4 border-t-[0.25rem] border-dotted border-main-hard"> -<div class="bg-paper bg-background-light flex flex-col inset-shadow-[0_0_0.4rem_black] px-8 py-4"> +<div class="bg-paper bg-background-light flex flex-col inset-shadow-elevation-6 px-8 py-4"> <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"> - <input type="text" 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" + <input type="text" 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-elevation-6 focus:outline-none focus:bg-background-light focus:text-main-hard z-22" list="existing-tags" onchange="addTag(this.value);" onkeydown="if (event.keyCode === 13) {addTag(this.value); this.value=''; return false;}"> <datalist id="existing-tags"> {{- range .ExistingTags }} <option value="{{ .Name }}"> {{- end }} </datalist> - <div id="tags-picked-list" class="bg-paper bg-background-dark flex flex-wrap inset-shadow-[0_0_0.4rem_black] p-2 m-4 w-full"> + <div id="tags-picked-list" class="bg-paper bg-background-dark flex flex-wrap inset-shadow-elevation-6 p-2 m-4 w-full"> {{- range .TagsPickedList }} <div id="tagPicked{{ . }}" class="bg-paper bg-main-hard text-background-light m-2 flex flex-row"> <p class="py-1 px-2">{{ . }}</p> @@ -80,11 +80,11 @@ </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> - <div id="subs-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> + <div id="subs-message" class="bg-paper bg-background-dark text-main-soft z-26 w-full inset-shadow-elevation-6 py-2 px-4 text-center"></div> </div> <script> @@ -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..50ceae9 100644 --- a/views/partials/blog-page-like-button.html +++ b/views/partials/blog-page-like-button.html @@ -1,5 +1,5 @@ <button hx-put="/api/v1/like" hx-vals='{"like": {{ not .Liked }}}' hx-target="this" hx-swap="outerHTML" hx-trigger="click" - class="accent-button rounded-xl flex flex-row shrink-0 w-fit h-full mx-2 my-auto{{ if .Liked }} active{{ end }}"> + class="accent-button rounded-xl relative shrink-0 w-fit aspect-square mr-4 p-2 my-auto{{ if .Liked }} active{{ end }}"> {{ if .Liked }} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="Like-Hand-1--Streamline-Freehand" class="h-10"> <desc> @@ -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="absolute left-10 -top-2 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="absolute left-10 -top-2 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 3eebd58..6466262 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 {{ 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 }}"> +<hr class="first-of-type:hidden border-t-[0.375rem] my-1 w-24 mx-auto border-dotted border-main-hard"> +<div {{ if .ToHighlight }}id="blog-page-highlighted"{{ end }} class="w-full flex flex-row items-stretch mask-t-from-[calc(100%-0.5rem)] mask-t-to-100% mask-b-from-[calc(100%-0.5rem)] mask-b-to-100% {{ if .ToHighlight }}bg-accent-light bg-paper{{ end }}"> + <div class="relative min-h-24 flex flex-col w-32 overflow-hidden shrink-0 mask-r-from-[calc(100%-1rem)] 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 $.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> @@ -14,7 +14,7 @@ </div> </div> </div> - <div class="grow w-full h-fit flex flex-col justify-center my-auto py-1 px-1"> + <div class="grow w-full h-fit flex flex-col justify-center my-auto py-1 px-2"> <div class="flex flex-row gap-2 max-w-full"> <a href="{{ .ArticleLink }}" onclick="return changeUrl('{{ .ArticleLink }}');" class="grow-2 text-base cursor-pointer font-m-plus"> <span class="font-extrabold">{{ .Title }}</span> @@ -24,7 +24,7 @@ <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 }} 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/general-page-header.html b/views/partials/general-page-header.html index 3b8a6c8..ce77b5c 100644 --- a/views/partials/general-page-header.html +++ b/views/partials/general-page-header.html @@ -2,7 +2,7 @@ <div class="flex flex-row mb-2"> {{ block "header" . }}{{ end }} <h1 - class="text-4xl font-m-plus font-bold italic text-shadow-[0_2px_2px_var(--color-main-soft)] text-left my-auto" + class="text-4xl font-m-plus font-bold italic text-shadow-[0_0.125rem_0.125rem_var(--color-main-soft)] text-left my-auto" > {{ .Title }} </h1> 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> diff --git a/views/partials/personal-page-status.html b/views/partials/personal-page-status.html index 078fb24..41033a9 100644 --- a/views/partials/personal-page-status.html +++ b/views/partials/personal-page-status.html @@ -1,4 +1,4 @@ -<div id="{{ .StatusId }}" class='z-26 w-full inset-shadow-[0_0_0.4rem_black] +<div id="{{ .StatusId }}" class='z-26 w-full inset-shadow-elevation-6 {{if eq .Status "OK"}}bg-green-700 text-green-100 {{else if eq .Status "Failed"}}bg-red-700 text-red-100 {{else}}bg-paper bg-background-dark text-main-soft{{end}} |