44 files changed, 548 insertions, 2695 deletions
diff --git a/.github/workflows/build-and-deploy-stage.yml b/.github/workflows/build-and-deploy-stage.yml deleted file mode 100644 index 0aea0a4..0000000 --- a/.github/workflows/build-and-deploy-stage.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Docker image building and publishing onto Stage -on: - push: - branches: [stage] -jobs: - build-and-push: - if: github.repository == 'SayaAndy/saya-today-web' - runs-on: ubuntu-latest - environment: Stage - permissions: - packages: write - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Generate output.css with tailwindcss - uses: ZoeyVid/tailwindcss-update@main - with: - input: static/input.css - output: static/output.css - params: "--minify" - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GHCR_TOKEN }} - - - name: Set github short sha - id: ghss_vars - run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile - push: true - tags: | - ghcr.io/sayaandy/saya-today-web:latest - ghcr.io/sayaandy/saya-today-web:commit-${{ steps.ghss_vars.outputs.sha_short }} - - deploy: - if: github.repository == 'SayaAndy/saya-today-web' - runs-on: ubuntu-latest - environment: Stage - needs: [build-and-push] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up SSH connection for pl.saya.today - run: | - mkdir -p ~/.ssh - (cat <<EOF - ${{ secrets.SVC_GITHUB_PK }} - EOF - ) > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - sed -i 's/\r$//' ~/.ssh/id_ed25519 - ssh-keyscan -H pl.saya.today >> ~/.ssh/known_hosts - - - name: Test SSH connection - run: | - ssh -o ConnectTimeout=10 -i ~/.ssh/id_ed25519 svc_github@pl.saya.today "echo 'SSH connection successful'" - - - name: Install Ansible - shell: bash - run: | - sudo apt update - sudo apt install -y ansible - - - name: Set github short sha - id: ghss_vars - run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - - - name: Run Ansible playbook - env: - ANSIBLE_HOST_KEY_CHECKING: False - working-directory: ./deploy - run: | - ansible-playbook -i inventory.yml -l stage playbook.yml --private-key ~/.ssh/id_ed25519 -u svc_github -e saya_today_web_auth_salt=${{ secrets.AUTH_SALT }} -e saya_today_web_b2_key_id=${{ secrets.B2_KEY_ID }} -e saya_today_web_b2_application_key=${{ secrets.B2_APPLICATION_KEY }} -e saya_today_web_environment=stage -e saya_today_web_tag=commit-${{ steps.ghss_vars.outputs.sha_short }} diff --git a/.github/workflows/build-and-deploy-prod.yml b/.github/workflows/build-and-deploy.yml index a180d39..57cfc96 100644 --- a/.github/workflows/build-and-deploy-prod.yml +++ b/.github/workflows/build-and-deploy.yml @@ -1,4 +1,4 @@ -name: Docker image building and publishing onto Production +name: Publish Docker image on: push: tags: @@ -8,8 +8,6 @@ jobs: if: github.repository == 'SayaAndy/saya-today-web' runs-on: ubuntu-latest environment: Production - permissions: - packages: write steps: - name: Checkout uses: actions/checkout@v4 @@ -20,24 +18,23 @@ jobs: output: static/output.css params: "--minify" - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v1 - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v1 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GHCR_TOKEN }} - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v2 with: context: . file: ./Dockerfile push: true tags: | ghcr.io/sayaandy/saya-today-web:latest - ghcr.io/sayaandy/saya-today-web:stable ghcr.io/sayaandy/saya-today-web:${{ github.ref_name }} deploy: @@ -48,31 +45,22 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - - name: Set up SSH connection for pl.saya.today - run: | - mkdir -p ~/.ssh - (cat <<EOF - ${{ secrets.SVC_GITHUB_PK }} - EOF - ) > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - sed -i 's/\r$//' ~/.ssh/id_ed25519 - ssh-keyscan -H pl.saya.today >> ~/.ssh/known_hosts - - - name: Test SSH connection + - name: Set up SSH private key run: | - ssh -o ConnectTimeout=10 -i ~/.ssh/id_ed25519 svc_github@pl.saya.today "echo 'SSH connection successful'" - + echo "${{ secrets.SVC_GITHUB_PK }}" > deploy/private_key.pem + chmod 600 deploy/private_key.pem - name: Install Ansible shell: bash run: | sudo apt update sudo apt install -y ansible - - name: Run Ansible playbook env: ANSIBLE_HOST_KEY_CHECKING: False working-directory: ./deploy - run: | - ansible-playbook -i inventory.yml -l prod playbook.yml --private-key ~/.ssh/id_ed25519 -u svc_github -e saya_today_web_auth_salt=${{ secrets.AUTH_SALT }} -e saya_today_web_b2_key_id=${{ secrets.B2_KEY_ID }} -e saya_today_web_b2_application_key=${{ secrets.B2_APPLICATION_KEY }} -e saya_today_web_environment=prod -e saya_today_web_tag=${{ github.ref_name }} + run: > + ansible-playbook -i inventory.yml -l stage playbook.yml + --private-key private_key.pem + -e saya_today_web.b2.key_id=${{ secrets.B2_KEY_ID }} + -e saya_today_web.b2.application_key=${{ secrets.B2_APPLICATION_KEY }} + -e saya_today_web.tag=${{ github.ref_name }} @@ -36,9 +36,7 @@ config*.json /main /sayana-demo -/static/output.css +/static/input.css /.vscode .envrc - -/.ansible @@ -1,26 +1,18 @@ -FROM golang:1.24.6-alpine3.22 AS build-stage - -RUN apk add --no-cache sqlite-dev musl-dev gcc +FROM golang:1.24.5-alpine3.22 AS build-stage WORKDIR /builddir COPY . . -RUN CGO_ENABLED=1 go build -o sayana-web . +RUN go build -o sayana-demo . FROM alpine:3.22.1 AS runtime-stage -ENV ENVIRONMENT="" -ENV AUTH_SALT="" ENV B2_KEY_ID="" ENV B2_APPLICATION_KEY="" WORKDIR /app -COPY --from=build-stage /builddir/sayana-web /app/sayana-web -COPY --from=build-stage /builddir/migrations /app/migrations +COPY --from=build-stage /builddir/sayana-demo /app/sayana-demo 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/ - -RUN apk add --no-cache tzdata sqlite +COPY --from=build-stage /builddir/config/config.yaml /app/config.yaml -ENTRYPOINT /app/sayana-web -c /app/config/config.${ENVIRONMENT}.yaml +ENTRYPOINT [ "/app/sayana-demo", "-c", "/app/config.yaml" ] diff --git a/config/config.go b/config/config.go index 7438fe1..67c0c4d 100644 --- a/config/config.go +++ b/config/config.go @@ -9,15 +9,13 @@ import ( ) type Config struct { - LogLevel slog.Level `json:"LogLevel" yaml:"logLevel" validate:"required"` - BlogPages BlogPagesConfig `json:"BlogPages" yaml:"blogPages" validate:"required"` - LocalePath string `json:"LocalePath" yaml:"localePath" validate:"required,filepath"` - AvailableLanguages []AvailableLanguageConfig `json:"AvailableLanguages" yaml:"availableLanguages" validate:"required"` - Auth AuthConfig `json:"Auth" yaml:"auth" validate:"required"` + LogLevel slog.Level `json:"LogLevel" yaml:"logLevel" validate:"required"` + BlogPages BlogPagesConfig `json:"BlogPages" yaml:"blogPages" validate:"required"` } type BlogPagesConfig struct { - Storage StorageConfig `json:"Storage" yaml:"storage" validate:"required"` + Storage StorageConfig `json:"Storage" yaml:"storage" validate:"required"` + AvailableLanguages []AvailableLanguageConfig `json:"AvailableLanguages" yaml:"availableLanguages" validate:"required"` } type StorageConfig struct { @@ -34,24 +32,9 @@ type B2Config struct { } type AvailableLanguageConfig struct { - Name string `json:"Name" yaml:"name" validate:"required"` - Alt string `json:"Alt" yaml:"alt"` - Flag string `json:"Flag" yaml:"flag" validate:"url"` - LocFile string `json:"LocFile" yaml:"locFile" validate:"required,filepath"` -} - -type AuthConfig struct { - Salt string `json:"Salt" yaml:"salt" validate:"required"` - Db DbConfig `json:"Db" yaml:"db" validate:"required"` -} - -type DbConfig struct { - Type string `json:"Type" yaml:"type" validate:"required,oneof=sqlite3"` - Cfg Sqlite3Config `json:"Config" yaml:"config"` -} - -type Sqlite3Config struct { - DSN string `json:"DSN" yaml:"dsn" validate:"required"` + Name string `json:"Name" yaml:"name" validate:"required"` + Alt string `json:"Alt" yaml:"alt"` + Flag string `json:"Flag" yaml:"flag" validate:"url"` } func LoadConfig(path string, config *Config) error { diff --git a/config/config.local.yaml b/config/config.local.yaml deleted file mode 100644 index 0b48e47..0000000 --- a/config/config.local.yaml +++ /dev/null @@ -1,26 +0,0 @@ -logLevel: debug -blogPages: - storage: - type: b2 - config: - bucketName: sayana-pages - region: eu-central-003 - prefix: 'stage-' - keyID: '${B2_KEY_ID}' - applicationKey: '${B2_APPLICATION_KEY}' -localePath: ./locale/ -availableLanguages: - - name: ru - alt: Русский - flag: https://f003.backblazeb2.com/file/sayana-static/flags/ru.svg - locFile: localization.ru.yaml - - name: en - alt: English - flag: https://f003.backblazeb2.com/file/sayana-static/flags/gb.svg - locFile: localization.en.yaml -auth: - db: - type: sqlite3 - config: - dsn: 'file:/tmp/auth.db?cache=shared&mode=rwc' - salt: '123' diff --git a/config/config.prod.yaml b/config/config.prod.yaml deleted file mode 100644 index e1294c0..0000000 --- a/config/config.prod.yaml +++ /dev/null @@ -1,26 +0,0 @@ -logLevel: info -blogPages: - storage: - type: b2 - config: - bucketName: sayana-pages - region: eu-central-003 - prefix: 'prod-' - keyID: '${B2_KEY_ID}' - applicationKey: '${B2_APPLICATION_KEY}' -localePath: ./locale/ -availableLanguages: - - name: ru - alt: Русский - flag: https://f003.backblazeb2.com/file/sayana-static/flags/ru.svg - locFile: localization.ru.yaml - - name: en - alt: English - flag: https://f003.backblazeb2.com/file/sayana-static/flags/gb.svg - locFile: localization.en.yaml -auth: - db: - type: sqlite3 - config: - dsn: 'file:/data/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2' - salt: '${AUTH_SALT}' diff --git a/config/config.stage.yaml b/config/config.stage.yaml deleted file mode 100644 index 30233c1..0000000 --- a/config/config.stage.yaml +++ /dev/null @@ -1,26 +0,0 @@ -logLevel: debug -blogPages: - storage: - type: b2 - config: - bucketName: sayana-pages - region: eu-central-003 - prefix: 'stage-' - keyID: '${B2_KEY_ID}' - applicationKey: '${B2_APPLICATION_KEY}' -localePath: ./locale/ -availableLanguages: - - name: ru - alt: Русский - flag: https://f003.backblazeb2.com/file/sayana-static/flags/ru.svg - locFile: localization.ru.yaml - - name: en - alt: English - flag: https://f003.backblazeb2.com/file/sayana-static/flags/gb.svg - locFile: localization.en.yaml -auth: - db: - type: sqlite3 - config: - dsn: 'file:/data/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2' - salt: '${AUTH_SALT}' diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..908bfe2 --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,17 @@ +logLevel: debug +blogPages: + storage: + type: b2 + config: + bucketName: sayana-pages + region: eu-central-003 + prefix: '' + keyID: '${B2_KEY_ID}' + applicationKey: '${B2_APPLICATION_KEY}' + availableLanguages: + - name: ru + alt: Русский + flag: https://f003.backblazeb2.com/file/sayana-static/flags/ru.svg + - name: en + alt: English + flag: https://f003.backblazeb2.com/file/sayana-static/flags/gb.svg diff --git a/deploy/inventory.yml b/deploy/inventory.yml index dde952e..d3c325f 100644 --- a/deploy/inventory.yml +++ b/deploy/inventory.yml @@ -1,17 +1,15 @@ -all: - children: - prod: - hosts: - node_prod: - ansible_host: pl.saya.today - saya_today_web_name: sayana-web - saya_today_web_hostname: sayana-web - saya_today_web_ipv4_address: 172.16.0.18 +prod: + hosts: + pl.saya.today: + saya_today_web: + name: sayana-web + hostname: sayana-web + ipv4_address: 172.16.0.18 - stage: - hosts: - node_stage: - ansible_host: pl.saya.today - saya_today_web_name: sayana-demo - saya_today_web_hostname: sayana-demo - saya_today_web_ipv4_address: 172.16.0.17 +stage: + hosts: + pl.saya.today: + saya_today_web: + name: sayana-demo + hostname: sayana-demo + ipv4_address: 172.16.0.17 diff --git a/deploy/playbook.yml b/deploy/playbook.yml index dfceaf7..571ed2b 100644 --- a/deploy/playbook.yml +++ b/deploy/playbook.yml @@ -1,29 +1,20 @@ --- - name: Deploy saya-today-web Docker Container - hosts: all remote_user: svc_github tasks: - - name: Create a volume - community.docker.docker_volume: - name: "{{ saya_today_web_name }}-volume" - - - name: Deploy saya-today-web Docker Container - community.docker.docker_container: - name: "{{ saya_today_web_name }}" - hostname: "{{ saya_today_web_hostname }}" - image: "ghcr.io/sayaandy/saya-today-web:{{ saya_today_web_tag }}" - env: - B2_KEY_ID: "{{ saya_today_web_b2_key_id }}" - B2_APPLICATION_KEY: "{{ saya_today_web_b2_application_key }}" - ENVIRONMENT: "{{ saya_today_web_environment }}" - AUTH_SALT: "{{ saya_today_web_auth_salt }}" - network_mode: caddy - networks: - - name: caddy - ipv4_address: "{{ saya_today_web_ipv4_address }}" - volumes: - - "{{ saya_today_web_name }}-volume:/data:rw" - restart_policy: unless-stopped - no_log: true + - name: Deploy saya-today-web Docker Container + community.docker.docker_container: + name: "{{ saya_today_web.name }}" + hostname: "{{ saya_today_web.hostname }}" + image: "ghcr.io/sayaandy/saya-today-web:{{ saya_today_web.tag }}" + env: + B2_KEY_ID: "{{ saya_today_web.b2.key_id }}" + B2_APPLICATION_KEY: "{{ saya_today_web.b2.application_key }}" + network_mode: caddy + networks: + - name: caddy + ipv4_address: "{{ saya_today_web.ipv4_address }}" + restart_policy: unless-stopped + no_log: true ... @@ -1,36 +1,33 @@ module github.com/SayaAndy/saya-today-web -go 1.24.6 +go 1.24.5 -require ( - github.com/Backblaze/blazer v0.7.2 - github.com/go-playground/validator/v10 v10.27.0 - github.com/gofiber/fiber/v2 v2.52.9 - github.com/golang-migrate/migrate/v4 v4.18.3 - github.com/mattn/go-sqlite3 v1.14.32 - github.com/yuin/goldmark v1.7.13 - golang.org/x/crypto v0.41.0 - gopkg.in/yaml.v3 v3.0.1 -) +require github.com/gofiber/fiber/v2 v2.52.9 require ( - github.com/andybalholm/brotli v1.2.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/Backblaze/blazer v0.7.2 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/gofiber/template v1.8.3 // indirect + github.com/gofiber/template/html/v2 v2.1.3 // indirect + github.com/gofiber/utils v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/stretchr/testify v1.10.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.65.0 // indirect - go.uber.org/atomic v1.7.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/yuin/goldmark v1.7.13 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) @@ -1,14 +1,9 @@ github.com/Backblaze/blazer v0.7.2 h1:UWNHMLB+Nf+UmbO2qkVvgriODLEMz4kIyr2Hm+DVXQM= github.com/Backblaze/blazer v0.7.2/go.mod h1:T4y3EYa9IQ5J0PKc/C/J8/CEnSd3qa/lgNw938wZg10= -github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= -github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -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/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -17,56 +12,47 @@ github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHO github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= -github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= -github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= +github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc= +github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8= +github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o= +github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE= +github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= +github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= -github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8= -github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4= -github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= -github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/b2/client.go b/internal/b2/client.go index 7b745a8..f2b6cad 100644 --- a/internal/b2/client.go +++ b/internal/b2/client.go @@ -33,14 +33,13 @@ func NewB2Client(cfg *config.B2Config) (*B2Client, error) { type BlogPage struct { Link string - FileName string Metadata *frontmatter.Metadata } -func (c *B2Client) Scan(prefix string) ([]*BlogPage, error) { +func (c *B2Client) Scan() ([]*BlogPage, error) { filePaths := []*BlogPage{} - iter := c.bucket.List(context.Background(), b2.ListPrefix(c.prefix+prefix)) + iter := c.bucket.List(context.Background(), b2.ListPrefix(c.prefix)) for iter.Next() { obj := iter.Object() @@ -70,13 +69,10 @@ func (c *B2Client) Scan(prefix string) ([]*BlogPage, 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], ".") + linkParts := strings.Split(obj.Name(), ".") filePaths = append(filePaths, &BlogPage{ - Link: obj.Name(), - FileName: fileName, + Link: strings.Join(linkParts[:len(linkParts)-1], "."), Metadata: &frontmatter.Metadata{ Title: attrs.Info["title"], ShortDescription: attrs.Info["short-description"], @@ -84,7 +80,6 @@ func (c *B2Client) Scan(prefix string) ([]*BlogPage, error) { PublishedTime: publishedTime, Thumbnail: attrs.Info["thumbnail"], Tags: strings.Split(attrs.Info["tags"], ","), - Geolocation: attrs.Info["geolocation"], }, }) } diff --git a/internal/lightgallery/block.go b/internal/lightgallery/block.go index 8eb7c9e..dade2ea 100644 --- a/internal/lightgallery/block.go +++ b/internal/lightgallery/block.go @@ -1,21 +1,16 @@ package lightgallery -import ( - "time" - - "github.com/yuin/goldmark/ast" -) +import "github.com/yuin/goldmark/ast" // LightGalleryBlock represents a light gallery block in the AST type LightGalleryBlock struct { ast.BaseBlock - Images []LightGalleryImage - Location *time.Location + Images []LightGalleryImage } type LightGalleryImage struct { URL string - Caption []byte + Caption string } var KindLightGalleryBlock = ast.NewNodeKind("LightGalleryBlock") diff --git a/internal/lightgallery/html_renderer.go b/internal/lightgallery/html_renderer.go index 17a6f2b..e611f0b 100644 --- a/internal/lightgallery/html_renderer.go +++ b/internal/lightgallery/html_renderer.go @@ -1,13 +1,11 @@ package lightgallery import ( - "bytes" "fmt" "math/rand" "strings" "time" - "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/renderer/html" @@ -16,18 +14,11 @@ import ( type LightGalleryHTMLRenderer struct { html.Config - md goldmark.Markdown } func NewLightGalleryHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { r := &LightGalleryHTMLRenderer{ Config: html.NewConfig(), - md: goldmark.New( - goldmark.WithRendererOptions( - html.WithHardWraps(), - html.WithXHTML(), - ), - ), } for _, opt := range opts { opt.SetHTMLOption(&r.Config) @@ -62,70 +53,79 @@ func (r *LightGalleryHTMLRenderer) renderLightGallery(w util.BufWriter, source [ imageUrlWithoutExt := strings.Join(imageUrlSegments[:len(imageUrlSegments)-1], ".") imageNameParts := strings.Split(imageUrlWithoutExt, "-") - var captionBuf bytes.Buffer - captionHTML := img.Caption - if err := r.md.Convert(img.Caption, &captionBuf); err == nil { - captionHTML = captionBuf.Bytes() - captionHTML = bytes.TrimPrefix(captionHTML, []byte("<p>")) - captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>\n")) - captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>")) - } - dayDate, _ := time.Parse("20060102 150405", imageNameParts[len(imageNameParts)-2]+" "+imageNameParts[len(imageNameParts)-1]) - dayDate = dayDate.In(gallery.Location) dynamicElements = append(dynamicElements, fmt.Sprintf(`{ src: "https://f003.backblazeb2.com/file/sayana-photos/full/%s", downloadUrl: "https://f003.backblazeb2.com/file/sayana-photos/full/%s", alt: "%s", - responsive: "https://f003.backblazeb2.com/file/sayana-photos/webp-320p/%s.webp 384, https://f003.backblazeb2.com/file/sayana-photos/webp-560p/%s.webp 672, https://f003.backblazeb2.com/file/sayana-photos/webp-800p/%s.webp 960, https://f003.backblazeb2.com/file/sayana-photos/webp-1200p/%s.webp 1440, https://f003.backblazeb2.com/file/sayana-photos/webp-1600p/%s.webp 1920", + sources: [{ + srcset: "https://f003.backblazeb2.com/file/sayana-photos/thumbnails/%s.webp", + media: "(max-width: 800px)" + }], thumb: - "https://f003.backblazeb2.com/file/sayana-photos/webp-320p/%s.webp", + "https://f003.backblazeb2.com/file/sayana-photos/thumbnails/%s.webp", subHtml: `+"`"+`<div class="flex flex-row light-gallery-captions"> - <p class="grow !text-[1vmax]/[1] text-left font-spectral text-main-dark">%s</p> - <p class="!text-[1vmax]/[1] text-right font-spectral text-secondary">%s</p> + <p class="grow !text-[1vmax]/[0.9] text-left font-spectral text-main-dark">%s</p> + <p class="!text-[1vmax]/[0.9] text-right font-spectral text-secondary">%s</p> </div>`+"`"+` - }`, img.URL, img.URL, util.EscapeHTML(captionHTML), imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, captionHTML, dayDate.Format("2006-01-02 15:04:05 -07:00"))) + }`, img.URL, img.URL, img.Caption, imageUrlWithoutExt, imageUrlWithoutExt, img.Caption, dayDate.Format("2006-01-02 15:04:05 -07:00"))) } w.WriteString(fmt.Sprintf(` <script> -function createLightGallery%s() { - const $lgContainer = document.getElementById('lg-%s'); - const config = { - container: $lgContainer, - dynamic: true, - dynamicEl: [%s], - width: "100%%", - height: "50vmin", - hash: false, - closable: false, - showMaximizeIcon: true, - appendSubHtmlTo: ".lg-sub-html", - isMobile: () => false, - slideDelay: 0, - plugins: [lgZoom, lgThumbnail], - thumbWidth: calculateVmin(10), - thumbHeight: "10vmin", - thumbMargin: 4 - }; - const inlineGallery = lightGallery($lgContainer, config); - - setTimeout(() => { - inlineGallery.openGallery(); - }, 200); +function createLightLibrary%s() { + const $lgContainer = document.getElementById('lg-%s'); + const inlineGallery = lightGallery($lgContainer, { + container: $lgContainer, + dynamic: true, + dynamicEl: [%s], + width: "100%%", + height: "50vmin", + hash: false, + closable: false, + showMaximizeIcon: true, + appendSubHtmlTo: ".lg-sub-html", + isMobile: () => false, + slideDelay: 0, + plugins: [lgZoom, lgThumbnail], + thumbWidth: 160, + thumbHeight: "10vmin", + thumbMargin: 4 + }); - galleryMap.set(inlineGallery, createLightGallery%s); + setTimeout(() => { + inlineGallery.openGallery(); + }, 200); } -document.addEventListener('DOMContentLoaded', createLightGallery%s); -</script>`, galleryID, galleryID, strings.Join(dynamicElements, ","), galleryID, galleryID)) +document.addEventListener('DOMContentLoaded', createLightLibrary%s); + +let resizeTimer%s; +window.addEventListener("resize", () => { + clearTimeout(resizeTimer%s); + resizeTimer%s = setTimeout(() => { + const $lgContainer = document.getElementById('lg-%s'); + $lgContainer.innerHTML = ""; + createLightLibrary%s(); + }, 1000); +}); +</script>`, galleryID, galleryID, strings.Join(dynamicElements, ","), galleryID, galleryID, galleryID, galleryID, galleryID, galleryID)) } return ast.WalkContinue, nil } +func escapeHTML(s string) string { + s = strings.ReplaceAll(s, "&", "&") + s = strings.ReplaceAll(s, "<", "<") + s = strings.ReplaceAll(s, ">", ">") + s = strings.ReplaceAll(s, "\"", """) + s = strings.ReplaceAll(s, "'", "'") + return s +} + func generateDivId(length int) string { const charset = "abcdefghijklmnopqrstuvwxyz" seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) // Seed with current time diff --git a/internal/lightgallery/parser.go b/internal/lightgallery/parser.go index daec797..0ce209c 100644 --- a/internal/lightgallery/parser.go +++ b/internal/lightgallery/parser.go @@ -2,9 +2,7 @@ package lightgallery import ( "bytes" - "log/slog" - "regexp" - "time" + "strings" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" @@ -24,52 +22,43 @@ func (p *LightGalleryParser) Trigger() []byte { func (p *LightGalleryParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { line, _ := reader.PeekLine() - if !bytes.HasPrefix(line, []byte("{Gallery:")) { + if !bytes.HasPrefix(line, []byte("{Gallery}")) { return nil, parser.NoChildren } - r := regexp.MustCompile(`^\{Gallery:([A-Za-z0-9\+\-/]+)\}$`) - trimmed := bytes.TrimSpace(line) - parts := r.FindSubmatch(trimmed) - if len(parts) < 2 { - slog.Warn("invalid gallery header format", slog.String("line", string(trimmed)), slog.Int("submatch_count", len(parts))) + if !bytes.Equal(trimmed, []byte("{Gallery}")) { return nil, parser.NoChildren } - loc, err := time.LoadLocation(string(parts[1])) - if err != nil { - slog.Warn("invalid location specified for a gallery", slog.String("error", err.Error()), slog.String("line", string(trimmed)), slog.String("location", string(parts[1]))) - return nil, parser.NoChildren - } - - return &LightGalleryBlock{Location: loc}, parser.NoChildren + return &LightGalleryBlock{}, parser.NoChildren } func (p *LightGalleryParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { line, segment := reader.PeekLine() if len(line) == 0 || segment.Len() == 0 { - return parser.Continue | parser.NoChildren + return parser.Close } trimmed := bytes.TrimSpace(line) - if bytes.Equal(trimmed, []byte("{Gallery}")) || bytes.Equal(trimmed, []byte("{/Gallery}")) { + if bytes.Equal(trimmed, []byte("{Gallery}")) { reader.AdvanceLine() return parser.Close } gallery := node.(*LightGalleryBlock) + lineStr := string(trimmed) - parts := bytes.SplitN(trimmed, []byte{'|'}, 2) - url := bytes.TrimSpace(parts[0]) + parts := strings.SplitN(lineStr, "|", 2) + url := strings.TrimSpace(parts[0]) + caption := "" - caption := make([]byte, 0) if len(parts) > 1 { - caption = bytes.TrimSpace(parts[1]) + caption = strings.TrimSpace(parts[1]) } gallery.Images = append(gallery.Images, LightGalleryImage{ - URL: string(url), + URL: url, Caption: caption, }) diff --git a/internal/router/api-v1-blog-search.go b/internal/router/api-v1-blog-search.go deleted file mode 100644 index e22fabe..0000000 --- a/internal/router/api-v1-blog-search.go +++ /dev/null @@ -1,113 +0,0 @@ -package router - -import ( - "fmt" - "log/slog" - "net/url" - "regexp" - "slices" - "strings" - "time" - - "github.com/SayaAndy/saya-today-web/internal/b2" - "github.com/SayaAndy/saya-today-web/locale" - "github.com/gofiber/fiber/v2" -) - -func init() { - tm.Add("catalogue-blog-cards", "views/partials/catalogue-blog-cards.html", "views/partials/catalogue-blog-card-tags.html") -} - -func Api_V1_BlogSearch(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - sort := c.Query("sort") - lang := c.Query("lang") - tz := c.Query("tz") - - if !slices.Contains(langs, lang) { - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language", lang)) - } - - loc, err := time.LoadLocation(tz) - if err != nil { - loc = time.UTC - slog.Warn("unable to parse a client timezone, defaulting to UTC", slog.String("error", err.Error()), slog.String("tz", tz)) - } - - pages, err := b2Client.Scan(lang + "/") - if err != nil { - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - return c.Status(fiber.ErrInternalServerError.Code).SendString(fmt.Sprintf("failed to scan pages for '%s' lang: %v", lang, err)) - } - - encodedQuery := c.Request().URI().QueryString() - re, err := regexp.Compile(`tags\[\]=([\w]+)`) - if err != nil { - slog.Warn("failed to generate regex for tags gathering", slog.String("error", err.Error())) - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate regex for tags gathering") - } - decodedQuery, _ := url.QueryUnescape(string(encodedQuery)) - matches := re.FindAllStringSubmatch(decodedQuery, -1) - - tags := make([]string, 0, len(matches)) - for _, match := range matches { - tags = append(tags, string(match[1])) - } - - pageMeta := make([]fiber.Map, 0, len(pages)) - for _, page := range pages { - for _, tag := range page.Metadata.Tags { - if len(tags) == 0 || slices.Contains(tags, tag) { - pageMeta = append(pageMeta, fiber.Map{ - "Link": page.Link, - "ArticleLink": "/" + lang + "/blog/" + page.FileName, - "Title": page.Metadata.Title, - "PublishedTime": page.Metadata.PublishedTime.In(loc).Format("2006-01-02 15:04:05 -07:00"), - "ActionDate": page.Metadata.ActionDate, - "ShortDescription": page.Metadata.ShortDescription, - "Thumbnail": page.Metadata.Thumbnail, - "Tags": page.Metadata.Tags, - "LikeCount": CCache.GetLikeCount(page.FileName), - }) - break - } - } - } - - slices.SortFunc(pageMeta, func(a, b fiber.Map) int { - switch sort { - case "titleAsc": - return strings.Compare(a["Title"].(string), b["Title"].(string)) - case "titleDesc": - return strings.Compare(b["Title"].(string), a["Title"].(string)) - case "actionDateAsc": - return strings.Compare(a["ActionDate"].(string), b["ActionDate"].(string)) - case "actionDateDesc": - return strings.Compare(b["ActionDate"].(string), a["ActionDate"].(string)) - case "publicationDateAsc": - publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -07:00", a["PublishedTime"].(string)) - publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", b["PublishedTime"].(string)) - return publishedTimeA.Compare(publishedTimeB) - case "publicationDateDesc": - publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -07:00", a["PublishedTime"].(string)) - publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", b["PublishedTime"].(string)) - return publishedTimeB.Compare(publishedTimeA) - } - return 0 - }) - - content, err := tm.Render("catalogue-blog-cards", fiber.Map{ - "BlogPages": pageMeta, - "L": l[lang], - }) - if err != nil { - slog.Warn("failed to generate div", slog.String("page", "/"+lang+"/blog"), slog.String("error", err.Error())) - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") - } - - return c.Type("html").Send(content) - } -} diff --git a/internal/router/api-v1-like.go b/internal/router/api-v1-like.go deleted file mode 100644 index 327dafa..0000000 --- a/internal/router/api-v1-like.go +++ /dev/null @@ -1,122 +0,0 @@ -package router - -import ( - "fmt" - "log/slog" - "net/url" - "strconv" - "strings" - - "github.com/SayaAndy/saya-today-web/internal/b2" - "github.com/SayaAndy/saya-today-web/locale" - "github.com/gofiber/fiber/v2" -) - -func init() { - tm.Add("blog-page-like-button", "views/partials/blog-page-like-button.html") -} - -func Api_V1_Like_Put(l map[string]*locale.LocaleConfig, b2 *b2.B2Client) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - - referer := c.Get("Referer", "") - if referer == "" { - return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is empty") - } - urlStruct, err := url.ParseRequestURI(referer) - if err != nil { - return c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("'Referer' header is invalid: %s", err.Error())) - } - - path := urlStruct.EscapedPath() - pathParts := strings.Split(strings.Trim(path, "/"), "/") - if len(pathParts) != 3 { - return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/blog/{page}'") - } - - lang, page := pathParts[0], pathParts[2] - - pageLink := lang + "/" + page + ".md" - if pages, _ := b2.Scan(pageLink); len(pages) == 0 { - return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server did not find '%s' article", pageLink)) - } - - newLikeStatus, err := strconv.ParseBool(c.FormValue("like", "true")) - if err != nil { - return c.Status(fiber.ErrBadRequest.Code).SendString("invalid 'like' value") - } - - ip := c.IP() - if newLikeStatus { - CCache.LikeOn(ip, page) - } else { - CCache.LikeOff(ip, page) - } - - slog.Debug("someone pressed the like button!", slog.String("ip", ip), slog.String("page", page), slog.String("new_like_status", fmt.Sprint(newLikeStatus))) - if c.Get("HX-Request", "false") == "true" { - content, err := tm.Render("blog-page-like-button", fiber.Map{ - "L": l[lang], - "Liked": newLikeStatus, - "LikedCount": CCache.GetLikeCount(page), - }) - if err != nil { - slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) - return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") - } - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(fiber.StatusOK).Send(content) - } - - return c.Status(fiber.StatusOK).SendString(fmt.Sprint(newLikeStatus)) - } -} - -func Api_V1_Like_Get(l map[string]*locale.LocaleConfig, b2 *b2.B2Client) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - - referer := c.Get("Referer", "") - if referer == "" { - return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is empty") - } - urlStruct, err := url.ParseRequestURI(referer) - if err != nil { - return c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("'Referer' header is invalid: %s", err.Error())) - } - - path := urlStruct.EscapedPath() - pathParts := strings.Split(strings.Trim(path, "/"), "/") - if len(pathParts) != 3 { - return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/blog/{page}'") - } - - lang, page := pathParts[0], pathParts[2] - - pageLink := lang + "/" + page + ".md" - if pages, _ := b2.Scan(pageLink); len(pages) == 0 { - return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server did not find '%s' article", pageLink)) - } - - ip := c.IP() - likeStatus := CCache.GetLikeStatus(ip, page) - - slog.Debug("someone requested the like status!", slog.String("ip", ip), slog.String("page", page), slog.Bool("like_status", likeStatus)) - if c.Get("HX-Request", "false") == "true" { - content, err := tm.Render("blog-page-like-button", fiber.Map{ - "L": l[lang], - "Liked": likeStatus, - "LikedCount": CCache.GetLikeCount(page), - }) - if err != nil { - slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) - return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") - } - c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) - return c.Status(fiber.StatusOK).Send(content) - } - - return c.Status(fiber.StatusOK).SendString(fmt.Sprint(likeStatus)) - } -} diff --git a/internal/router/api-v1-tz.go b/internal/router/api-v1-tz.go deleted file mode 100644 index 4e4e462..0000000 --- a/internal/router/api-v1-tz.go +++ /dev/null @@ -1,37 +0,0 @@ -package router - -import ( - "log/slog" - "time" - - "github.com/gofiber/fiber/v2" -) - -func Api_V1_TZ() func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - timestampString := c.Query("timestamp") - tz := c.Query("tz") - - loc, err := time.LoadLocation(tz) - if err != nil { - loc = time.UTC - slog.Warn("unable to parse a client timezone, defaulting to UTC", slog.String("error", err.Error()), slog.String("tz", tz)) - } - - var format string - var timestamp time.Time - for _, format = range []string{"2006-01-02 15:04:05 -07:00", time.RFC3339} { - if timestamp, err = time.Parse(format, timestampString); err == nil { - break - } - } - - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - - if err != nil { - return c.Status(fiber.StatusBadRequest).SendString("invalid timestamp format") - } - - return c.Status(fiber.StatusOK).SendString(timestamp.In(loc).Format(format)) - } -} diff --git a/internal/router/client-cache.go b/internal/router/client-cache.go deleted file mode 100644 index 81d7641..0000000 --- a/internal/router/client-cache.go +++ /dev/null @@ -1,229 +0,0 @@ -package router - -import ( - "database/sql" - "encoding/base64" - "fmt" - "log/slog" - "strings" - "sync" - - "golang.org/x/crypto/argon2" -) - -type PageLike struct { - PageRef string - UserId string -} - -type ClientCache struct { - hashMap map[string]string - hashMapMutex sync.RWMutex - - likePageMap map[string]map[string]struct{} - pageMutexMap map[string]*sync.RWMutex - pageMutexMapMutex sync.Mutex - - salt []byte - db *sql.DB -} - -var CCache *ClientCache - -func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) { - tx, err := db.Begin() - if err != nil { - return nil, fmt.Errorf("fail to init transaction with db to fill cache: %w", err) - } - - rows, err := tx.Query("select * from blog_likes;") - if err != nil { - tx.Rollback() - return nil, fmt.Errorf("fail to query db for blog_likes to fill cache: %w", err) - } - - likePageMap := make(map[string]map[string]struct{}) - pageMutexMap := make(map[string]*sync.RWMutex) - - for rows.Next() { - var pageRef string - var userId []byte - if err = rows.Scan(&pageRef, &userId); err != nil { - tx.Rollback() - return nil, fmt.Errorf("fail scanning blog_likes to fill cache: %w", err) - } - userIdString := base64.RawStdEncoding.EncodeToString(userId) - if _, ok := likePageMap[pageRef]; !ok { - likePageMap[pageRef] = make(map[string]struct{}) - pageMutexMap[pageRef] = &sync.RWMutex{} - } - likePageMap[pageRef][userIdString] = struct{}{} - } - - if err = tx.Commit(); err != nil { - return nil, fmt.Errorf("fail to commit transaction in db: %w", err) - } - - return &ClientCache{ - hashMap: make(map[string]string), - likePageMap: likePageMap, - pageMutexMap: pageMutexMap, - salt: salt, - db: db, - }, nil -} - -func (c *ClientCache) Close() error { - tx, err := c.db.Begin() - if err != nil { - return fmt.Errorf("fail to init transaction with db to dump cache: %w", err) - } - - if _, err = tx.Exec("delete from blog_likes;"); err != nil { - tx.Rollback() - return fmt.Errorf("fail to truncate table blog_likes: %w", err) - } - - userIdBytes := make(map[string][]byte) - - sqlStatement := fmt.Sprintf(` - INSERT OR IGNORE INTO blog_likes (page_ref, user_id) - VALUES %s(?, ?); - `, strings.Repeat("(?, ?), ", 99)) - sqlStatementVars := make([]any, 0, 200) - - for pageRef, userSet := range c.likePageMap { - for userId := range userSet { - if _, ok := userIdBytes[userId]; !ok { - userIdBytes[userId], err = base64.RawStdEncoding.DecodeString(userId) - if err != nil { - slog.Warn("couldn't parse one of user hashes into bytes back", slog.String("hash", userId), slog.String("error", err.Error())) - continue - } - } - - sqlStatementVars = append(sqlStatementVars, any(pageRef), any(userIdBytes[userId])) - if len(sqlStatementVars) < 200 { - continue - } - - if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { - slog.Warn("couldn't insert blog like pairs into db", slog.String("error", err.Error())) - } - - sqlStatementVars = make([]any, 0, 200) - } - } - - if len(sqlStatementVars) > 0 { - sqlStatement = fmt.Sprintf(` - INSERT OR IGNORE INTO blog_likes (page_ref, user_id) - VALUES %s(?, ?); - `, strings.Repeat("(?, ?), ", len(sqlStatementVars)/2-1)) - - if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { - slog.Warn("couldn't insert blog like pairs into db", slog.String("error", err.Error())) - } - } - - return tx.Commit() -} - -func (c *ClientCache) GetHash(id string) string { - c.hashMapMutex.RLock() - if val, ok := c.hashMap[id]; ok { - c.hashMapMutex.RUnlock() - slog.Debug("gave an old hash", slog.String("hash", val)) - return val - } - c.hashMapMutex.RUnlock() - - c.hashMapMutex.Lock() - defer c.hashMapMutex.Unlock() - - if val, ok := c.hashMap[id]; ok { - slog.Debug("gave a newly generated hash", slog.String("hash", val)) - return val - } - - c.hashMap[id] = base64.RawStdEncoding.EncodeToString(argon2.IDKey([]byte(id), c.salt, 1, 64*1024, 4, 32)) - slog.Debug("generated hash", slog.String("hash", c.hashMap[id])) - return c.hashMap[id] -} - -func (c *ClientCache) getPageMutex(page string) *sync.RWMutex { - c.pageMutexMapMutex.Lock() - defer c.pageMutexMapMutex.Unlock() - - if mutex, ok := c.pageMutexMap[page]; ok { - return mutex - } - - c.pageMutexMap[page] = &sync.RWMutex{} - return c.pageMutexMap[page] -} - -func (c *ClientCache) GetLikeStatus(id string, page string) bool { - page = strings.Clone(page) - - mutex := c.getPageMutex(page) - mutex.RLock() - defer mutex.RUnlock() - - if _, ok := c.likePageMap[page]; !ok { - return false - } - _, ok := c.likePageMap[page][c.GetHash(id)] - return ok -} - -func (c *ClientCache) GetLikeCount(page string) int { - page = strings.Clone(page) - - mutex := c.getPageMutex(page) - mutex.RLock() - defer mutex.RUnlock() - - if userSet, ok := c.likePageMap[page]; ok { - return len(userSet) - } - return 0 -} - -func (c *ClientCache) LikeOn(id string, page string) (alreadyLiked bool) { - page = strings.Clone(page) - hash := c.GetHash(id) - - mutex := c.getPageMutex(page) - mutex.Lock() - defer mutex.Unlock() - - if userSet, ok := c.likePageMap[page]; ok { - _, alreadyLiked = userSet[hash] - c.likePageMap[page][hash] = struct{}{} - return - } - - c.likePageMap[page] = make(map[string]struct{}) - c.likePageMap[page][hash] = struct{}{} - return -} - -func (c *ClientCache) LikeOff(id string, page string) (alreadyUnliked bool) { - page = strings.Clone(page) - hash := c.GetHash(id) - - mutex := c.getPageMutex(page) - mutex.Lock() - defer mutex.Unlock() - - if _, ok := c.likePageMap[page]; !ok { - return true - } - if _, ok := c.likePageMap[page][hash]; !ok { - return true - } - - delete(c.likePageMap[page], hash) - return false -} diff --git a/internal/router/lang-blog-title.go b/internal/router/lang-blog-title.go deleted file mode 100644 index bafb0d2..0000000 --- a/internal/router/lang-blog-title.go +++ /dev/null @@ -1,82 +0,0 @@ -package router - -import ( - "bytes" - "fmt" - "html/template" - "log/slog" - "slices" - "strconv" - "strings" - - "github.com/SayaAndy/saya-today-web/internal/b2" - "github.com/SayaAndy/saya-today-web/internal/frontmatter" - "github.com/SayaAndy/saya-today-web/locale" - "github.com/gofiber/fiber/v2" - "github.com/yuin/goldmark" -) - -func init() { - tm.Add("blog-page", "views/layouts/general-page.html", "views/pages/blog-page.html") -} - -func Lang_Blog_Title(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client, md goldmark.Markdown) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - ip := c.IP() - slog.Debug("client entering blog page", slog.String("ip", ip), slog.String("page", c.Path())) - - lang := c.Params("lang") - if !slices.Contains(langs, lang) { - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) - } - - metadata, parsedMarkdown, err := readBlogPost(md, b2Client, lang+"/"+c.Params("title")) - if err != nil { - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("failed to find '%s' post", c.Params("title"))) - } - - geolocationParts := strings.Split(metadata.Geolocation, " ") - var x, y, areaError string - if len(geolocationParts) >= 2 { - x = geolocationParts[0] - y = geolocationParts[1] - } - if len(geolocationParts) >= 3 { - areaError = geolocationParts[2] - } - - content, err := tm.Render("blog-page", fiber.Map{ - "Title": metadata.Title, - "PublishedDate": metadata.PublishedTime.Format("2006-01-02 15:04:05 -07:00"), - "PublishedYear": strconv.Itoa(metadata.PublishedTime.Year()), - "ParsedMarkdown": template.HTML(parsedMarkdown), - "MapLocationX": x, - "MapLocationY": y, - "MapLocationAreaMeters": areaError, - "Lang": lang, - }) - if err != nil { - slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/blog/"+c.Params("title")), slog.String("error", err.Error())) - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page") - } - - return c.Type("html").Send(content) - } -} - -func readBlogPost(md goldmark.Markdown, b2Client *b2.B2Client, sourceName string) (metadata *frontmatter.Metadata, html string, err error) { - metadata, markdown, err := b2Client.ReadFrontmatter(sourceName + ".md") - if err != nil { - return nil, "", fmt.Errorf("failed to read a frontmatter file: %w", err) - } - - var buf bytes.Buffer - if err := md.Convert(markdown, &buf); err != nil { - return nil, "", fmt.Errorf("convert source context from md to html: %w", err) - } - - return metadata, buf.String(), nil -} diff --git a/internal/router/lang-blog.go b/internal/router/lang-blog.go deleted file mode 100644 index dd3f079..0000000 --- a/internal/router/lang-blog.go +++ /dev/null @@ -1,93 +0,0 @@ -package router - -import ( - "fmt" - "log/slog" - "net/url" - "regexp" - "slices" - "strings" - - "github.com/SayaAndy/saya-today-web/internal/b2" - "github.com/SayaAndy/saya-today-web/locale" - "github.com/gofiber/fiber/v2" -) - -func init() { - tm.Add("blog-catalogue", "views/layouts/general-page.html", "views/pages/blog-catalogue.html") -} - -func Lang_Blog(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - lang := c.Params("lang") - if !slices.Contains(langs, lang) { - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) - } - - querySort := c.Query("sort") - if querySort == "" { - querySort = "publicationDateDesc" - } - - encodedQuery := c.Request().URI().QueryString() - re, err := regexp.Compile(`tags\[\]=([\w]+)`) - if err != nil { - slog.Warn("failed to generate regex for tags gathering", slog.String("error", err.Error())) - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate regex for tags gathering") - } - decodedQuery, _ := url.QueryUnescape(string(encodedQuery)) - matches := re.FindAllStringSubmatch(decodedQuery, -1) - - queryTags := make([]string, 0, len(matches)) - for _, match := range matches { - queryTags = append(queryTags, string(match[1])) - } - - pages, err := b2Client.Scan(lang + "/") - status := fiber.StatusOK - if err != nil { - status = fiber.StatusPartialContent - pages = []*b2.BlogPage{} - } - - tagsMap := make(map[string]int) - for _, page := range pages { - for _, tag := range page.Metadata.Tags { - tagsMap[tag]++ - } - } - slog.Debug("enlist pages for catalogue", slog.Int("tag_count", len(tagsMap)), slog.Int("page_count", len(pages)), slog.String("path", c.Path())) - - type Tag struct { - Name string `json:"Name" yaml:"name"` - Count int `json:"Count" yaml:"count"` - } - - tagsArray := make([]Tag, 0, len(tagsMap)) - for tag, count := range tagsMap { - tagsArray = append(tagsArray, Tag{tag, count}) - } - slices.SortFunc(tagsArray, func(a Tag, b Tag) int { - return strings.Compare(a.Name, b.Name) - }) - - content, err := tm.Render("blog-catalogue", fiber.Map{ - "QuerySort": querySort, - "QueryTags": strings.Join(queryTags, ","), - "Tags": tagsArray, - "Lang": lang, - "L": l[lang], - "PublishedYear": "2025", - "Title": l[lang].BlogSearch.Header, - }) - if err != nil { - slog.Warn("failed to generate page", slog.String("path", c.Path()), slog.String("error", err.Error())) - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page") - } - - return c.Type("html").Status(status).Send(content) - } -} diff --git a/internal/router/lang-map.go b/internal/router/lang-map.go deleted file mode 100644 index e94d756..0000000 --- a/internal/router/lang-map.go +++ /dev/null @@ -1,85 +0,0 @@ -package router - -import ( - "fmt" - "log/slog" - "slices" - "strconv" - "strings" - - "github.com/SayaAndy/saya-today-web/internal/b2" - "github.com/SayaAndy/saya-today-web/locale" - "github.com/gofiber/fiber/v2" -) - -func init() { - tm.Add("global-map", "views/pages/global-map.html") -} - -func Lang_Map(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - lang := c.Params("lang") - if !slices.Contains(langs, lang) { - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) - } - - pages, err := b2Client.Scan(lang + "/") - status := fiber.StatusOK - if err != nil { - status = fiber.StatusPartialContent - pages = []*b2.BlogPage{} - } - - type MapMarker struct { - 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 _, 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{ - Title: page.Metadata.Title, - PageLink: fmt.Sprintf("/%s/blog/%s", lang, page.FileName), - Lat: x, - Long: y, - AccuracyMeters: areaError, - Thumbnail: page.Metadata.Thumbnail, - }) - } - - content, err := tm.Render("global-map", fiber.Map{ - "Lang": lang, - "L": l[lang], - "MapLocationLat": 45.4507, - "MapLocationLong": 68.8319, - "MapMarkers": mapMarkers, - }) - if err != nil { - slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/map"), slog.String("error", err.Error())) - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page") - } - - return c.Type("html").Status(status).Send(content) - } -} diff --git a/internal/router/root.go b/internal/router/root.go deleted file mode 100644 index f4a0692..0000000 --- a/internal/router/root.go +++ /dev/null @@ -1,27 +0,0 @@ -package router - -import ( - "log/slog" - - "github.com/SayaAndy/saya-today-web/config" - "github.com/gofiber/fiber/v2" -) - -func init() { - tm.Add("index", "views/index.html") -} - -func Root(localeCfg []config.AvailableLanguageConfig) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - content, err := tm.Render("index", fiber.Map{ - "AvailableLanguages": localeCfg, - }) - if err != nil { - slog.Warn("failed to generate page", slog.String("page", "/"), slog.String("error", err.Error())) - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page") - } - - return c.Type("html").Send(content) - } -} diff --git a/internal/router/tm.go b/internal/router/tm.go deleted file mode 100644 index e8095ee..0000000 --- a/internal/router/tm.go +++ /dev/null @@ -1,18 +0,0 @@ -package router - -import ( - "log/slog" - "os" - - "github.com/SayaAndy/saya-today-web/internal/templatemanager" -) - -var tm = assert(templatemanager.NewTemplateManager()) - -func assert[T any](t T, err error) T { - if err != nil { - slog.Error("fail to initialize template manager", slog.String("error", err.Error())) - os.Exit(1) - } - return t -} diff --git a/internal/templatemanager/templatemanager.go b/internal/templatemanager/templatemanager.go deleted file mode 100644 index f89d0c8..0000000 --- a/internal/templatemanager/templatemanager.go +++ /dev/null @@ -1,77 +0,0 @@ -package templatemanager - -import ( - "bytes" - "fmt" - "html/template" - "path/filepath" - "strings" -) - -type TemplateManager struct { - templates map[string]templateManagerRender -} - -type templateManagerRender struct { - Main string - Tmpl *template.Template -} - -type TemplateManagerTemplates struct { - Name string - Files []string -} - -func NewTemplateManager(templates ...TemplateManagerTemplates) (*TemplateManager, error) { - templateMap := make(map[string]templateManagerRender) - - for _, tmplStruct := range templates { - tmpl := template.New("").Funcs(template.FuncMap{ - "contains": strings.Contains, - }) - tmpl, err := tmpl.ParseFiles(tmplStruct.Files...) - if err != nil { - return nil, err - } - templateMap[tmplStruct.Name] = templateManagerRender{ - Main: filepath.Base(tmplStruct.Files[0]), - Tmpl: tmpl, - } - } - - return &TemplateManager{ - templates: templateMap, - }, nil -} - -func (tm *TemplateManager) Render(name string, data interface{}) ([]byte, error) { - tmpl, exists := tm.templates[name] - if !exists { - return nil, fmt.Errorf("template %s is not found", name) - } - - var buf bytes.Buffer - err := tmpl.Tmpl.ExecuteTemplate(&buf, tmpl.Main, data) - return buf.Bytes(), err -} - -func (tm *TemplateManager) Add(name string, files ...string) error { - if len(files) == 0 { - return fmt.Errorf("you can't add template without any files") - } - - tmpl := template.New("").Funcs(template.FuncMap{ - "contains": strings.Contains, - }) - - tmpl, err := tmpl.ParseFiles(files...) - if err != nil { - return fmt.Errorf("failed to add template into manager: %w", err) - } - - tm.templates[name] = templateManagerRender{ - Main: filepath.Base(files[0]), - Tmpl: tmpl, - } - return nil -} diff --git a/locale/localization.en.yaml b/locale/localization.en.yaml deleted file mode 100644 index 48ef9da..0000000 --- a/locale/localization.en.yaml +++ /dev/null @@ -1,12 +0,0 @@ -TagsLabel: 'Tags:' -BlogSearch: - Header: 'Blog Search' - TagsHeader: 'Tags' - OrderByHeader: 'Order by...' - TitleOrdered: 'Title' - ActionDateOrdered: 'Action Date' - PublicationDateOrdered: 'Publication Date' - ChooseAllTags: 'Choose All' -GlobalMap: - Header: 'Global Map' -LikeButton: 'Like!' diff --git a/locale/localization.go b/locale/localization.go deleted file mode 100644 index b557e08..0000000 --- a/locale/localization.go +++ /dev/null @@ -1,56 +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"` - LikeButton string `yaml:"LikeButton" json:"LikeButton"` -} - -type BlogSearchConfig struct { - Header string `yaml:"Header" json:"Header"` - 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"` -} - -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/locale/localization.ru.yaml b/locale/localization.ru.yaml deleted file mode 100644 index d6e9fe4..0000000 --- a/locale/localization.ru.yaml +++ /dev/null @@ -1,12 +0,0 @@ -TagsLabel: 'Тэги:' -BlogSearch: - Header: 'Поиск по блогу' - TagsHeader: 'Тэги' - OrderByHeader: 'Упорядочить по...' - TitleOrdered: 'названию' - ActionDateOrdered: 'дате действия' - PublicationDateOrdered: 'времени публикации' - ChooseAllTags: 'Выбрать все' -GlobalMap: - Header: 'Глобальная карта' -LikeButton: 'Нраица!' @@ -1,30 +1,26 @@ package main import ( - "database/sql" - "errors" + "bytes" "flag" + "fmt" + "html/template" + "log" "log/slog" "os" - "os/signal" - "syscall" + "strconv" + "strings" "github.com/SayaAndy/saya-today-web/config" "github.com/SayaAndy/saya-today-web/internal/b2" + "github.com/SayaAndy/saya-today-web/internal/frontmatter" "github.com/SayaAndy/saya-today-web/internal/lightgallery" - "github.com/SayaAndy/saya-today-web/internal/router" "github.com/SayaAndy/saya-today-web/internal/tailwind" - "github.com/SayaAndy/saya-today-web/locale" "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/redirect" - "github.com/golang-migrate/migrate/v4" - "github.com/golang-migrate/migrate/v4/database/sqlite3" + "github.com/gofiber/template/html/v2" "github.com/yuin/goldmark" "github.com/yuin/goldmark/parser" gmhtml "github.com/yuin/goldmark/renderer/html" - - _ "github.com/golang-migrate/migrate/v4/source/file" - _ "github.com/mattn/go-sqlite3" ) var ( @@ -40,10 +36,8 @@ var ( gmhtml.WithXHTML(), ), ) - b2Client *b2.B2Client - configPath = flag.String("c", "config.yaml", "Path to the configuration file (in YAML format)") - availableLanguages = make([]string, 0) - localization = make(map[string]*locale.LocaleConfig) + b2Client *b2.B2Client + configPath = flag.String("c", "config.yaml", "Path to the configuration file (in YAML format)") ) func main() { @@ -60,97 +54,94 @@ func main() { slog.SetLogLoggerLevel(cfg.LogLevel) slog.Info("starting sayana-web server...") - db, err := sql.Open(cfg.Auth.Db.Type, cfg.Auth.Db.Cfg.DSN) - if err != nil { - slog.Error("fail to initialize db", slog.String("error", err.Error())) - os.Exit(1) - } - - driver, err := sqlite3.WithInstance(db, &sqlite3.Config{}) - if err != nil { - slog.Error("fail to initialize driver for migrating db", slog.String("error", err.Error())) - os.Exit(1) - } - - m, err := migrate.NewWithDatabaseInstance( - "file://migrations", - cfg.Auth.Db.Type, driver) - if err != nil { - slog.Error("fail to initialize migration client", slog.String("error", err.Error())) - os.Exit(1) - } - - if err = m.Up(); err != nil && err == errors.New("no change") { - slog.Error("fail to apply migrations", slog.String("error", err.Error())) - os.Exit(1) - } - slog.Info("successfully applied migrations") - b2Client, err = b2.NewB2Client(&cfg.BlogPages.Storage.Config) if err != nil { slog.Error("fail to initialize b2 client", slog.String("error", err.Error())) os.Exit(1) } - for _, lang := range cfg.AvailableLanguages { - localeCfg, err := locale.InitConfig(cfg.LocalePath + lang.LocFile) - if err != nil { - slog.Warn("fail to initialize a locale", slog.String("locale", lang.Name), slog.String("error", err.Error())) - continue - } - availableLanguages = append(availableLanguages, lang.Name) - localization[lang.Name] = localeCfg - } + engine := html.New("./views", ".html") app := fiber.New(fiber.Config{ - ProxyHeader: "X-Forwarded-For", + Views: engine, }) - app.Use(redirect.New(redirect.Config{ - Rules: map[string]string{ - "/ru": "/", - "/en": "/", - }, - StatusCode: 301, - })) + app.Get("/", func(c *fiber.Ctx) error { + pages, err := b2Client.Scan() + status := fiber.StatusOK + if err != nil { + status = fiber.StatusPartialContent + pages = []*b2.BlogPage{} + } - router.CCache, err = router.NewClientCache(db, []byte(cfg.Auth.Salt)) - if err != nil { - slog.Error("fail to initialize cache", slog.String("error", err.Error())) - os.Exit(1) - } + pageMeta := make([]map[string]string, 0, len(pages)) + for _, page := range pages { + slog.Debug("enlist page for catalogue", slog.Any("page", page), slog.String("endpoint", "/")) + pageMeta = append(pageMeta, map[string]string{ + "Link": page.Link, + "Title": page.Metadata.Title, + "PublishedTime": page.Metadata.PublishedTime.Format("2006-01-02 15:04:05 -0700"), + "ActionDate": page.Metadata.ActionDate, + "ShortDescription": page.Metadata.ShortDescription, + "Thumbnail": page.Metadata.Thumbnail, + "Tags": strings.Join(page.Metadata.Tags, ", "), + }) + } - app.Get("/", router.Root(cfg.AvailableLanguages)) - app.Get("/:lang/map", router.Lang_Map(localization, availableLanguages, b2Client)) - app.Get("/:lang/blog", router.Lang_Blog(localization, availableLanguages, b2Client)) - app.Get("/:lang/blog/:title", router.Lang_Blog_Title(localization, availableLanguages, b2Client, md)) - app.Get("/api/v1/tz", router.Api_V1_TZ()) - app.Get("/api/v1/blog-search", router.Api_V1_BlogSearch(localization, availableLanguages, b2Client)) - app.Get("/api/v1/like", router.Api_V1_Like_Get(localization, b2Client)) - app.Put("/api/v1/like", router.Api_V1_Like_Put(localization, b2Client)) + return c.Status(status).Render("index", fiber.Map{ + "PublishedYear": "2025", + "BlogPages": pageMeta, + }) + }) - app.Static("/", "./static") + app.Get("/blog/:lang/:title", func(c *fiber.Ctx) error { + metadata, parsedMarkdownDesktop, parsedMarkdownMobile, err := readBlogPost(c.Params("lang") + "/" + c.Params("title")) + if err != nil { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("failed to find '%s' post", c.Params("title"))) + } - go func() { - if err := app.Listen(":3000"); err != nil { - slog.Error("error while running fiber server", slog.String("error", err.Error())) - panic(err) + geolocationParts := strings.Split(metadata.Geolocation, " ") + var x, y, areaError string + if len(geolocationParts) >= 2 { + x = geolocationParts[0] + y = geolocationParts[1] + } + if len(geolocationParts) >= 3 { + areaError = geolocationParts[2] } - }() - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + return c.Render("layouts/blog-page", fiber.Map{ + "Title": metadata.Title, + "PublishedDate": metadata.PublishedTime.Format("2006-01-02 15:04:05 -07:00"), + "PublishedYear": strconv.Itoa(metadata.PublishedTime.Year()), + "ParsedMarkdownDesktop": template.HTML(parsedMarkdownDesktop), + "ParsedMarkdownMobile": template.HTML(parsedMarkdownMobile), + "MapLocationX": x, + "MapLocationY": y, + "MapLocationAreaMeters": areaError, + }) + }) - <-sigChan - slog.Info("gracefully shutting down...") - if err = app.Shutdown(); err != nil { - slog.Error("fail to shutdown fiber server", slog.String("error", err.Error())) + app.Static("/", "./static") + + log.Fatal(app.Listen(":3000")) +} + +func readBlogPost(sourceName string) (metadata *frontmatter.Metadata, desktopBody string, mobileBody string, err error) { + metadata, markdown, err := b2Client.ReadFrontmatter(sourceName + ".md") + if err != nil { + return nil, "", "", fmt.Errorf("failed to read a frontmatter file: %w", err) } - if err = router.CCache.Close(); err != nil { - slog.Error("fail to dump cache", slog.String("error", err.Error())) + + var bufDesktop bytes.Buffer + if err := md.Convert(markdown, &bufDesktop); err != nil { + return nil, "", "", fmt.Errorf("convert source context from md to html (desktop version): %w", err) } - if err = db.Close(); err != nil { - slog.Error("fail to close db connection", slog.String("error", err.Error())) + var bufMobile bytes.Buffer + if err := md.Convert(markdown, &bufMobile); err != nil { + return nil, "", "", fmt.Errorf("convert source context from md to html (mobile version): %w", err) } - db.Close() + + return metadata, bufDesktop.String(), bufMobile.String(), nil } diff --git a/migrations/1_create_stats_tables.down.sql b/migrations/1_create_stats_tables.down.sql deleted file mode 100644 index ee4593c..0000000 --- a/migrations/1_create_stats_tables.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP TABLE IF EXISTS blog_views; -DROP TABLE IF EXISTS blog_likes; diff --git a/migrations/1_create_stats_tables.up.sql b/migrations/1_create_stats_tables.up.sql deleted file mode 100644 index 8852268..0000000 --- a/migrations/1_create_stats_tables.up.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE IF NOT EXISTS blog_likes ( - page_ref VARCHAR(32) NOT NULL, - user_id VARCHAR(32) NOT NULL, - PRIMARY KEY (page_ref, user_id) -) WITHOUT ROWID; - -CREATE TABLE IF NOT EXISTS blog_views ( - page_ref VARCHAR(32) NOT NULL, - user_id VARCHAR(32) NOT NULL, - PRIMARY KEY (page_ref, user_id) -) WITHOUT ROWID; diff --git a/static/favicon.ico b/static/favicon.ico Binary files differdeleted file mode 100644 index 33eb226..0000000 --- a/static/favicon.ico +++ /dev/null diff --git a/static/input.css b/static/input.css index 47e7d16..edecc19 100644 --- a/static/input.css +++ b/static/input.css @@ -1,6 +1,17 @@ @import "tailwindcss"; @theme { + --color-olive-dark: #4b513a; + --color-olive-medium: #8a9d8a; + --color-olive-light: #a5b8a5; + --color-cream-dark: #ffffdd; + --color-cream-light: #fffff0; + --color-lettuce-dark: #384a0c; + --color-lettuce-medium: #9bb665; + --color-lettuce-light: #bbd095; + --color-sunny-dark: #f5eee2; + --color-sunny-light: #fff93f; + --font-spectral: Spectral, serif; --font-patua: "Patua One", cursive; } @@ -14,16 +25,12 @@ --sidebar-text-color: var(--background-dark-color); --sidebar-stroke-color: var(--main-dark-color); --secondary-color: var(--main-light-color); - --leaflet-text-color: var(--main-dark-color); - --leaflet-link-color: var(--main-medium-color); - --leaflet-link-hover-color: var(--main-light-color); --interlocked-hexagons-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%23ffffdb%22%2F%3E%3Cpath%20fill%3D%22%23777e63%22%20d%3D%22m33.64%200-.17.25h-33L4.3%206h33.07l4-6zm8.33%200-4.33%206.5h-33l3.83%205.75h33.06L49.7%200zm8.34%200-8.5%2012.75h-33l3.83%205.75H45.7L58.03%200zm8.33%200L45.97%2019h-33l3.83%205.75h33.06L66.36%200zm-8.5%2025.25L33.63%2050l3.86%205.8L54.03%2031h33l-3.83-5.75zm4.16%206.25L37.8%2056.25l3.87%205.8%2016.53-24.8h33l-3.84-5.75zm4.17%206.25L41.97%2062.5l3.86%205.8%2016.54-24.8h33l-3.84-5.75zM62.63%2044l-16.5%2024.75%203.87%205.8%2016.53-24.8h33L95.7%2044zM100%2050.45l-3.87%205.8%203.87%205.81v-11.6zM0%2050.46v11.6L12.5%2080.8l3.86-5.8zm95.83%206.25-3.87%205.8L100%2074.55v-11.6zm-4.17%206.24-3.86%205.8%2012.2%2018.3v-11.6zM0%2062.96v11.6l8.33%2012.49%203.86-5.8zm87.5%206.24L83.63%2075%20100%2099.54V87.96zM50%2075.45%2033.64%20100h7.72l12.5-18.75zm-50%200v11.6l4.16%206.24%203.87-5.8zm54.17%206.25L41.97%20100h7.73l8.33-12.5zm4.17%206.25L50.3%20100h7.73l4.17-6.25zM0%2087.95v11.59l3.86-5.8zm62.5%206.25-3.87%205.8h7.73z%22%2F%3E%3Cpath%20fill%3D%22%23a0a182%22%20d%3D%22M66.8.25%2062.97%206h33l4.03%206.05V.45l-.14-.2zM0%20.45v11.61L12.5%2030.8l3.86-5.8zM62.63%206.5l-3.83%205.75h33l8.2%2012.3v-11.6L95.7%206.5zm-4.16%206.25-3.84%205.75h33L100%2037.06v-11.6l-8.47-12.71zM0%2012.96v11.6l8.33%2012.49%203.86-5.8zM54.3%2019l-3.84%205.75h33L100%2049.55l.01-.01V37.96L87.36%2019zm-4.77%206.25H41.8L25.3%2050l16.66%2025-16.5%2024.75h7.74L49.7%2075%2033.03%2050zm-8.33%200h-7.74L16.96%2050l16.67%2025-16.5%2024.75h7.73L41.36%2075%2024.7%2050Zm-8.34%200h-7.73L8.63%2050%2025.3%2075%208.8%2099.75h7.73L33.03%2075%2016.36%2050zm-16.07%200L.3%2050l16.67%2025L.46%2099.75H8.2L24.7%2075%208.02%2050l16.5-24.75zM0%2025.46v11.6l4.16%206.24%203.87-5.8zm0%2012.5v11.58l3.86-5.79zm66.8%2012.29L50.3%2074.99l3.87%205.81%204.16%206.25%204.17%206.25%204.3%206.45h7.73L58.03%2075l16.5-24.75zm16.06%200h-7.73L58.63%2075l16.5%2024.75h7.73L66.36%2075zm.6%200L66.96%2075l16.5%2024.75h7.74L74.7%2075l16.5-24.75zm16.07%200H91.8L75.3%2075l16.5%2024.75h7.73L83.03%2075z%22%2F%3E%3C%2Fsvg%3E"); --interlocked-hexagons-background-repeat: repeat; --interlocked-hexagons-background-size: 10vh; --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%234a5139%22%2F%3E%3Cpath%20fill%3D%22%23b2b285%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"); --squares-and-triangles-background-repeat: repeat; - --squares-and-triangles-background-width-coef: 10; - --squares-and-triangles-background-height-coef: 10; + --squares-and-triangles-background-size: 10; } [data-theme="lettuce"] { @@ -35,16 +42,12 @@ --sidebar-text-color: var(--main-dark-color); --sidebar-stroke-color: var(--background-dark-color); --secondary-color: var(--main-medium-color); - --leaflet-text-color: var(--main-dark-color); - --leaflet-link-color: var(--main-medium-color); - --leaflet-link-hover-color: var(--main-light-color); --interlocked-hexagons-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%23f5ede1%22%2F%3E%3Cpath%20fill%3D%22%239ab663%22%20d%3D%22m33.64%200-.17.25h-33L4.3%206h33.07l4-6zm8.33%200-4.33%206.5h-33l3.83%205.75h33.06L49.7%200zm8.34%200-8.5%2012.75h-33l3.83%205.75H45.7L58.03%200zm8.33%200L45.97%2019h-33l3.83%205.75h33.06L66.36%200zm-8.5%2025.25L33.63%2050l3.86%205.8L54.03%2031h33l-3.83-5.75zm4.16%206.25L37.8%2056.25l3.87%205.8%2016.53-24.8h33l-3.84-5.75zm4.17%206.25L41.97%2062.5l3.86%205.8%2016.54-24.8h33l-3.84-5.75zM62.63%2044l-16.5%2024.75%203.87%205.8%2016.53-24.8h33L95.7%2044zM100%2050.45l-3.87%205.8%203.87%205.81v-11.6zM0%2050.46v11.6L12.5%2080.8l3.86-5.8zm95.83%206.25-3.87%205.8L100%2074.55v-11.6zm-4.17%206.24-3.86%205.8%2012.2%2018.3v-11.6zM0%2062.96v11.6l8.33%2012.49%203.86-5.8zm87.5%206.24L83.63%2075%20100%2099.54V87.96zM50%2075.45%2033.64%20100h7.72l12.5-18.75zm-50%200v11.6l4.16%206.24%203.87-5.8zm54.17%206.25L41.97%20100h7.73l8.33-12.5zm4.17%206.25L50.3%20100h7.73l4.17-6.25zM0%2087.95v11.59l3.86-5.8zm62.5%206.25-3.87%205.8h7.73z%22%2F%3E%3Cpath%20fill%3D%22%23bbd095%22%20d%3D%22M66.8.25%2062.97%206h33l4.03%206.05V.45l-.14-.2zM0%20.45v11.61L12.5%2030.8l3.86-5.8zM62.63%206.5l-3.83%205.75h33l8.2%2012.3v-11.6L95.7%206.5zm-4.16%206.25-3.84%205.75h33L100%2037.06v-11.6l-8.47-12.71zM0%2012.96v11.6l8.33%2012.49%203.86-5.8zM54.3%2019l-3.84%205.75h33L100%2049.55l.01-.01V37.96L87.36%2019zm-4.77%206.25H41.8L25.3%2050l16.66%2025-16.5%2024.75h7.74L49.7%2075%2033.03%2050zm-8.33%200h-7.74L16.96%2050l16.67%2025-16.5%2024.75h7.73L41.36%2075%2024.7%2050Zm-8.34%200h-7.73L8.63%2050%2025.3%2075%208.8%2099.75h7.73L33.03%2075%2016.36%2050zm-16.07%200L.3%2050l16.67%2025L.46%2099.75H8.2L24.7%2075%208.02%2050l16.5-24.75zM0%2025.46v11.6l4.16%206.24%203.87-5.8zm0%2012.5v11.58l3.86-5.79zm66.8%2012.29L50.3%2074.99l3.87%205.81%204.16%206.25%204.17%206.25%204.3%206.45h7.73L58.03%2075l16.5-24.75zm16.06%200h-7.73L58.63%2075l16.5%2024.75h7.73L66.36%2075zm.6%200L66.96%2075l16.5%2024.75h7.74L74.7%2075l16.5-24.75zm16.07%200H91.8L75.3%2075l16.5%2024.75h7.73L83.03%2075z%22%2F%3E%3C%2Fsvg%3E"); --interlocked-hexagons-background-repeat: repeat; --interlocked-hexagons-background-size: 10vh; --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"); --squares-and-triangles-background-repeat: repeat; - --squares-and-triangles-background-width-coef: 10; - --squares-and-triangles-background-height-coef: 10; + --squares-and-triangles-background-size: 10; } [data-theme="night"] { @@ -56,60 +59,12 @@ --sidebar-text-color: var(--main-dark-color); --sidebar-stroke-color: var(--background-light-color); --secondary-color: var(--main-light-color); - --leaflet-text-color: var(--background-dark-color); - --leaflet-link-color: var(--background-dark-color); - --leaflet-link-hover-color: var(--background-light-color); --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%); --interlocked-hexagons-background-repeat: no-repeat; --interlocked-hexagons-background-size: 100vw 100vh; --squares-and-triangles-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2260%22%20height%3D%2260%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%2313131b%22%2F%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23363645%22%20stroke-width%3D%22.5%22%20d%3D%22M10%2050H0m60-40H50m0%2040v10M10%200v10m21%2030v20m0-60v20m9%2010h20M0%2030h20m0%2010h20V20H20v10zM10%2010h40v40H10zM0%200h60v60H0z%22%2F%3E%3C%2Fsvg%3E"); --squares-and-triangles-background-repeat: repeat; - --squares-and-triangles-background-width-coef: 5; - --squares-and-triangles-background-height-coef: 5; -} - -[data-theme="ram"] { - --main-dark-color: #fefad8; - --main-medium-color: #f6e4c5; - --main-light-color: #e8d4b8; - --background-dark-color: #413b34; - --background-light-color: #6d6258; - --sidebar-text-color: var(--main-dark-color); - --sidebar-stroke-color: var(--background-light-color); - --secondary-color: var(--main-light-color); - --leaflet-text-color: var(--background-dark-color); - --leaflet-link-color: var(--background-dark-color); - --leaflet-link-hover-color: var(--background-light-color); - --interlocked-hexagons-background: url('https://f003.backblazeb2.com/file/sayana-static/themes/ram/ram-background.webp'); - --interlocked-hexagons-background-repeat: repeat; - --interlocked-hexagons-background-size: auto; - --squares-and-triangles-background: url('https://f003.backblazeb2.com/file/sayana-static/themes/ram/ram-sidebar.webp'); - --squares-and-triangles-background-repeat: repeat; - --squares-and-triangles-background-width-coef: 5; - --squares-and-triangles-background-height-coef: 15; -} - -a { - color: var(--main-medium-color) !important; - text-decoration: underline; -} - -a:hover { - color: var(--main-light-color) !important; -} - -#map-container { - color: var(--leaflet-text-color) !important; -} - -#map-container a { - color: var(--leaflet-link-color) !important; - text-decoration: underline; -} - -#map-container a:hover { - color: var(--leaflet-link-hover-color) !important; - text-decoration: underline; + --squares-and-triangles-background-size: 5; } .bg-interlocked-hexagons { @@ -139,18 +94,10 @@ a:hover { color: var(--main-medium-color); } -.text-main-light { - color: var(--main-light-color); -} - .text-background-dark { color: var(--background-dark-color); } -.text-background-light { - color: var(--background-dark-color); -} - .text-secondary { color: var(--secondary-color); } @@ -159,14 +106,6 @@ a:hover { background-color: var(--main-dark-color); } -.bg-main-medium { - background-color: var(--main-medium-color); -} - -.bg-main-light { - background-color: var(--main-light-color); -} - .bg-background-dark { background-color: var(--background-dark-color); } @@ -183,35 +122,13 @@ a:hover { border-color: var(--main-medium-color); } -.border-main-light { - border-color: var(--main-light-color); -} - -.border-background-dark { - border-color: var(--background-dark-color); -} - -.border-background-light { - border-color: var(--background-light-color); -} - -.border-inset { - border-style: inset; -} - -.border-outset { - border-style: outset; -} - .desktop-sidebar-custom { width: 5vw; background-size: calc(var(--squares-and-triangles-background-size) * 1vw); } -.bg-sidebar { - background-image: var(--squares-and-triangles-background); - background-repeat: var(--squares-and-triangles-background-repeat); - background-size: calc(var(--squares-and-triangles-background-width-coef) * 1vw) calc(var(--squares-and-triangles-background-height-coef) * 1vw); +body:has(.lg-container:not([class~="lg-inline"])) .desktop-sidebar-custom { + display: none !important; } .mobile-topbar-custom { @@ -221,7 +138,7 @@ a:hover { margin-left: 5vw; } -body:has(.lg-container:not([class~="lg-inline"])) #sidebar { +body:has(.lg-container:not([class~="lg-inline"])) .mobile-topbar-custom { display: none !important; } @@ -248,6 +165,10 @@ body:has(.lg-container:not([class~="lg-inline"])) #sidebar { font-size: 4vw; } +.content-square { + width: 100vh; +} + .border-left-custom { width: 0.5vw; border-right: 0.3vh dashed var(--main-dark-color); @@ -265,72 +186,12 @@ body:has(.lg-container:not([class~="lg-inline"])) #sidebar { inset 0vh -0.5vh rgba(75, 81, 58, 0.5); } -.text-stroke-\(--sidebar-stroke-color\) { - -webkit-text-stroke-color: var(--sidebar-stroke-color); -} - -.text-stroke-\[0\.1vw\] { - -webkit-text-stroke-width: 0.1vw; -} - -.theme-wheel { - position: fixed; - width: 15vmax; - height: 15vmax; - border-radius: 50%; - background: var(--background-light-color); - opacity: 0.8; - border: 1vmax outset var(--background-dark-color); - box-shadow: 0 0 5vmax var(--main-light-color); - opacity: 0; - visibility: hidden; - transform: scale(0.5); - transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); - z-index: 1001; -} - -.theme-wheel.active { - opacity: 1; - visibility: visible; - transform: scale(1); -} - -.theme-icon { - position: absolute; - width: 4vmax; - height: 4vmax; - border-radius: 50%; - cursor: pointer; +.desktop { display: flex; - align-items: center; - justify-content: center; - transition: all 0.3s ease; - border: 0.1vmax inset var(--background-light-color); - background: var(--interlocked-hexagons-background); - background-size: contain; } -.theme-icon:hover { - transform: scale(1.2); -} - -.theme-icon.active { - transform: scale(1.2); - border: 0.3vmax outset var(--main-light-color); -} - -.custom-svg-marker { - background: transparent; - border: none; -} - -.custom-svg-marker svg { - filter: drop-shadow(2px 2px 2px rgba(0,0,0,0.3)); - transition: transform 0.2s; -} - -.custom-svg-marker:hover svg { - transform: scale(1.1); +.mobile { + display: none; } .lg-backdrop { @@ -373,25 +234,14 @@ body:has(.lg-container:not([class~="lg-inline"])) #sidebar { } .lg-content { - top: 2.5vmax !important; - bottom: calc(10vmin + 5vmax) !important; -} - -.lg-outer[class~="lg-single-item"] .lg-content { - bottom: 5vmax !important; -} - -.lg-thumb-item { - width: 10vmin !important; - height: 10vmin !important; + top: 3vmax !important; } .lg-container:not([class~="lg-inline"]) .lg-content { top: 4vmax !important; } -.lg-toolbar .lg-icon, -.lg-content .lg-icon { +.lg-toolbar .lg-icon { padding: 0 0.7vmax; font-size: 1.5vmax !important; height: 1.5vmax; @@ -399,20 +249,11 @@ body:has(.lg-container:not([class~="lg-inline"])) #sidebar { color: var(--main-dark-color); } -.lg-container:not([class~="lg-inline"]) .lg-toolbar .lg-icon, -.lg-container:not([class~="lg-inline"]) .lg-content .lg-icon { +.lg-container:not([class~="lg-inline"]) .lg-toolbar .lg-icon { font-size: 3vmax !important; height: 3vmax; } -.lg-prev { - left: 0 !important; -} - -.lg-next { - right: 0 !important; -} - .lg-toolbar .lg-icon:hover { color: var(--main-medium-color); } @@ -432,7 +273,7 @@ body:has(.lg-container:not([class~="lg-inline"])) #sidebar { .inline-gallery-container { width: 80%; - height: 70vmin; + height: 60vmin; } .light-gallery-captions { @@ -472,50 +313,27 @@ body:has(.lg-container:not([class~="lg-inline"])) #sidebar { } } -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; -} - -@media (min-aspect-ratio: 0.8/1) { - .ar-gt-0\.8\:mr-\[1\.2vmin\] { - margin-right: 1.2vmin; - } - - .ar-gt-0\.8\:w-fit { - width: fit-content; - } - - .ar-gt-0\.8\:min-w-fit { - min-width: fit-content; - } - - .ar-gt-0\.8\:min-w-\[10vh\] { - min-width: 10vh; - } - - .ar-gt-0\.8\:max-w-\[20vh\] { - max-width: 20vh; - } - - .ar-gt-0\.8\:max-w-\[100vh\] { - max-width: 100vh; - } - - .ar-gt-0\.8\:items-center { - align-items: center; +/* Mobile responsive styles */ +@media (max-aspect-ratio: 1.1/1) { + .desktop { + display: none; } -} -@media (max-aspect-ratio: 0.8/1) { - .bg-sidebar { - background-size: calc(var(--squares-and-triangles-background-width-coef) * 1.6vh) calc(var(--squares-and-triangles-background-height-coef) * 1.6vh); + .mobile { + display: flex; } .inline-gallery-container { width: 100%; } + .desktop-sidebar-custom { + width: 90%; + margin-left: 5vw; + height: 10vh; + background-size: 20vh; + } + .logo-custom { display: none; } @@ -537,86 +355,25 @@ body[data-theme~="ram"] .leaflet-tile { background-size: 10vh; } - .ar-lt-0\.8\:grid { - display: grid; - } - - .ar-lt-0\.8\:flex-col { - flex-direction: column; - } - - .ar-lt-0\.8\:flex-row { - flex-direction: row; - } - - .ar-lt-0\.8\:h-\[8vh\] { - height: 8vh; - } - - .ar-lt-0\.8\:h-\[10vh\] { - height: 10vh; + .border-left-custom { + width: 1%; + border-right: 0.3vw dashed var(--main-dark-color); } - .ar-lt-0\.8\:h-\[30vh\] { - height: 30vh; - } - - .ar-lt-0\.8\:h-\[90vh\] { - height: 90vh; - } - - .ar-lt-0\.8\:w-\[8vh\] { - width: 8vh; - } - - .ar-lt-0\.8\:w-\[80\%\] { - width: 80%; - } - - .ar-lt-0\.8\:w-\[90vw\] { - width: 90vw; - } - - .ar-lt-0\.8\:w-screen { - width: 100vw; - } - - .ar-lt-0\.8\:max-h-\[25\%\] { - max-height: 25%; - } - - .ar-lt-0\.8\:max-h-\[80\%\] { - max-height: 80%; - } - - .ar-lt-0\.8\:max-h-\[calc\(100\%-25vh\)\] { - max-height: calc(100% - 25vh); - } - - .ar-lt-0\.8\:text-\[5vh\] { - font-size: 5vh; - } - - .ar-lt-0\.8\:ml-\[5vw\] { - margin-left: 5vw; - } - - .ar-lt-0\.8\:hidden { - display: none; + .border-right-custom { + width: 1.5%; + border-left: 0.5vw dotted var(--main-dark-color); } } @media (max-aspect-ratio: 0.4/1) { - .ar-lt-0\.4\:w-\[100vw\] { - width: 100vw; - } - - .ar-lt-0\.4\:ml-0 { + .desktop-sidebar-custom { + width: 100%; margin-left: 0; } } -.night-star { +.star { color: var(--color-yellow-100); animation-name: blink; animation-duration: 5s; @@ -625,13 +382,6 @@ body[data-theme~="ram"] .leaflet-tile { -webkit-user-select: none; } -.ram-frame { - width: 20vmin; - height: 20vmin; - user-select: none; - -webkit-user-select: none; -} - @keyframes blink { 0% { opacity: 1; diff --git a/views/index.html b/views/index.html index 2b8c937..731def6 100644 --- a/views/index.html +++ b/views/index.html @@ -12,45 +12,33 @@ <style> .menu-outer-body { border-radius: 10%; - width: 40vh; - height: 30vh; + width: 60vw; + height: 40vh; } .menu-inner-body { width: 85%; } - .flex-adaptive { - flex-direction: row; - } - - .language-icon { - max-width: calc(80% / {{ .AvailableLanguages | len }}); - max-height: 80%; - } - - @media (max-aspect-ratio: 0.8/1) { + @media (max-aspect-ratio: 1.2/1) { .menu-outer-body { border-radius: 5%; } - .menu-outer-body { - width: min(40vh, 100vw); - height: 40vh; - border-radius: 0; + .menu-inner-body { + width: 92.5%; } + } - .language-icon { - max-width: 40%; - max-height: calc(80% / {{ .AvailableLanguages | len }}); + @media (max-aspect-ratio: 0.8/1) { + .menu-outer-body { + width: 100vw; + height: 80vh; + border-radius: 0; } .menu-inner-body { - width: 92.5%; - } - - .flex-adaptive { - flex-direction: column; + width: 100%; } } </style> @@ -103,13 +91,27 @@ </script> <div class="menu-outer-body p-4 z-1 bg-background-dark flex flex-col mx-auto my-auto opacity-70"> - <p class="font-patua mx-auto text-[2.5vmax] mt-[0.8vmin] mb-[0.8vmin]">saya.today</p> - <div class="menu-inner-body grow bg-background-light flex flex-adaptive mx-auto opacity-100"> - {{- range .AvailableLanguages }} - <img onclick="location.href='/{{ .Name }}/blog';" class="object-scale-down cursor-pointer flex-1 rounded-[20%] language-icon mx-auto my-auto relative" src="{{ .Flag }}" alt="{{ .Alt }}"> + <p class="font-patua mx-auto text-[2.5vmax] mt-[0.8vmin] mb-[1.6vmin]">saya.today</p> + <div class="menu-inner-body grow bg-background-light flex flex-col mx-auto mb-[1.6vmin] overflow-y-auto opacity-100"> + {{- range .BlogPages }} + <div class="m-2 flex flex-row justify-center justify-items-center gap-4"> + <img onclick="location.href='/blog/{{ .Link }}';" style="flex-grow: 0;" class="flex-1 cursor-pointer object-cover rounded-4xl select-none w-[4vmax] h-[4vmax]" src="https://f003.backblazeb2.com/file/sayana-photos/thumbnails/{{ .Thumbnail }}.webp"> + <div class="flex-5 justify-self-center flex-col border-r-1 border-dashed border-opacity-50 border-main-medium"> + <a href="/blog/{{ .Link }}" class="block font-extrabold">{{ .Title }}</a> + <a href="/blog/{{ .Link }}" class="block italic text-[1vmax] text-secondary">{{ .ActionDate }}</a> + <p>{{ .ShortDescription }}</p> + </div> + <div class="flex-3 justify-self-center flex flex-col text-right"> + <p class="grow text-[1vmax]"> + Тэги: {{ .Tags }} + </p> + <p class="italic text-secondary text-[1vmax]"> + {{ .PublishedTime }} + </p> + </div> + </div> {{- end }} </div> - <p class="font-spectral mx-auto text-[1.5vmax] text-secondary mt-[0.8vmin] mb-[0.8vmin]">Choose your language</p> </div> <script> diff --git a/views/layouts/blog-page.html b/views/layouts/blog-page.html new file mode 100644 index 0000000..08a6f93 --- /dev/null +++ b/views/layouts/blog-page.html @@ -0,0 +1,194 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>SAYA TODAY // {{ .Title }}</title> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lightgallery.min.css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lg-zoom.min.css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lg-thumbnail.min.css"> +{{- if .MapLocationX }} + <link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.css"> +{{- end }} + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,200;1,300;1,400;1,500;1,600;1,700;1,800&family=Patua+One&display=swap" rel="stylesheet"> + <link href="/output.css" rel="stylesheet"> + + <style> + .map-container .leaflet-container { + width: 100% !important; + height: 100% !important; + } + </style> +</head> + +<body data-theme="night" class="font-spectral text-main-dark overflow-hidden bg-interlocked-hexagons h-screen flex"> + + <script> + function switchTheme(theme) { + if (theme == "" || typeof theme == "undefined") { + const currentTheme = document.body.getAttribute('data-theme'); + switch (currentTheme) { + case "olive": theme = "lettuce"; break; + case "lettuce": theme = "night"; break; + case "night": theme = "olive"; break; + } + } + localStorage.setItem('theme', theme); + document.body.setAttribute('data-theme', theme); + } + + const savedTheme = localStorage.getItem('theme') || 'night'; + switchTheme(savedTheme); + </script> + + <div class="desktop-sidebar-custom sticky bg-squares-and-triangles bg-repeat flex-col items-center shadow-2xl z-20 h-screen desktop"> + <div class="logo-custom text-sidebar font-patua font-extrabold text-center relative z-20"> + saya.today + </div> + <div class="flex flex-col gap-0 relative z-20"> + <i onclick="location.href='/';" class="fas fa-home nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> + <i class="fas fa-search nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> + <i class="fas fa-images nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> + <i onclick="switchTheme('')" class="fas fa-swatchbook nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> + </div> + </div> + + <div class="bg-background-dark z-10 h-screen desktop"> + <div class="border-left-custom bg-background-dark"></div> + + <div class="content-square flex flex-col bg-background-dark relative mt-[1.6vmin] mx-[1.6vmin]"> + <div class="flex flex-row mb-[0.4vmin]"> + <i onclick="location.href='/';" class="fas fa-circle-left hover:bg-main-dark hover:bg-opacity-10 cursor-pointer transition-colors duration-300 rounded-md text-[2vmax] mt-auto mb-auto px-[0.8vmin]"></i> + <p class="text-[2vmax] font-spectral italic font-extrabold select-none mt-auto pl-[0.8vmin] pr-[1.2vmin]">//</p> + <p class="text-[2vmax] font-spectral italic text-left mt-auto">{{ .Title }}</p> + <p class="grow text-[1.5vmax] font-spectral text-secondary text-right mt-auto">{{ .PublishedDate }}</p> + </div> + <hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]"> + + <div class="bg-background-light flex flex-col flex-1 overflow-y-auto inset-shadow p-[2.4vmin]"> + {{ .ParsedMarkdownDesktop }} + {{- if .MapLocationX }} + <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto"> + <div id="location-map-desktop" class="relative p-[0.8vmin] flex-none ml-auto mr-auto w-[60%] h-[40vh]"></div> + <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto"> + {{- end }} + </div> + + <div class="flex flex-row mt-[0.8vmin] mb-[0.4vmin]"> + <p class="grow text-[0.8vmax]/[0.9] font-spectral italic text-right text-secondary mr-2"> + <a href="">{{ .Title }}</a> © {{ .PublishedYear }} by <a href="https://t.me/EarlInisMona">Saya Andy</a> is licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a> + </p> + <img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;"> + <img src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;"> + <img src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;"> + </div> + + <hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]"> + </div> + + <div class="border-right-custom bg-background-dark shadow-2xl"></div> + </div> + + <div class="mobile-topbar-custom fixed bg-squares-and-triangles bg-repeat flex-row items-center shadow-2xl w-[90vw] z-20 mobile"> + <div class="flex flex-row gap-0 relative z-20"> + <i onclick="location.href='/';" class="fas fa-home nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> + <i class="fas fa-search nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> + <i class="fas fa-images nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> + <i onclick="switchTheme('')" class="fas fa-swatchbook nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> + </div> + <div class="logo-custom text-background-dark font-patua font-extrabold text-center relative grow z-20"> + demo.saya.today + </div> + </div> + + <div class="bg-background-dark flex-row z-10 w-[90vw] h-[92vh] mobile ml-[5vw] mt-[8vh]"> + <div class="border-left-custom bg-background-dark"></div> + + <div class="content-square flex flex-col bg-background-dark relative mt-[1.6vmin] mx-[1.6vmin]"> + <div class="flex flex-row mb-[0.4vmin]"> + <i onclick="location.href='/';" class="fas fa-circle-left hover:bg-main-dark hover:bg-opacity-10 cursor-pointer transition-colors duration-300 rounded-md text-[2vmax] mt-auto mb-auto px-[0.8vmin]"></i> + <p class="text-[2vmax] font-spectral italic font-extrabold select-none mt-auto pl-[0.8vmin] pr-[1.2vmin]">//</p> + <p class="text-[2vmax] font-spectral italic text-left mt-auto">{{ .Title }}</p> + <p class="grow text-[1.5vmax] font-spectral text-secondary text-right mt-auto">{{ .PublishedDate }}</p> + </div> + <hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]"> + + <div class="bg-background-light flex flex-col overflow-y-auto inset-shadow p-[2.4vmin] max-h-[80%]"> + {{ .ParsedMarkdownMobile }} + {{- if .MapLocationX }} + <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto"> + <div id="location-map-mobile" class="relative p-[0.8vmin] flex-none ml-auto mr-auto w-[100%] h-[40vh]"></div> + <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto"> + {{- end }} + </div> + + <div class="flex flex-row mt-[0.8vmin] mb-[0.4vmin]"> + <p class="grow text-[0.8vmax]/[0.9] font-spectral italic text-right text-secondary mr-2"> + <a href="">{{ .Title }}</a> © {{ .PublishedYear }} by <a href="https://t.me/EarlInisMona">Saya Andy</a> is licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a> + </p> + <img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;"> + <img src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;"> + <img src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;"> + </div> + + <hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]"> + </div> + + <div class="border-right-custom bg-background-dark shadow-2xl"></div> + </div> + + <script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/lightgallery.min.js"></script> + <script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/plugins/lg-zoom.min.js"></script> + <script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/plugins/lg-thumbnail.min.js"></script> +{{- if .MapLocationX }} + <script src="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.js"></script> + + <script> + function initLocationMap(id, x, y, relativeErrorMeters = 0) { + var map = L.map(id).setView([x, y], 13); + + L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, + attribution: '© <a href="http://www.openstreetmap.org/copyright">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]).addTo(map); + } + + document.addEventListener('DOMContentLoaded', () => {initLocationMap( + 'location-map-desktop', + {{ .MapLocationX }}, + {{ .MapLocationY }}, + {{ .MapLocationAreaMeters }} + )}); + + document.addEventListener('DOMContentLoaded', () => {initLocationMap( + 'location-map-mobile', + {{ .MapLocationX }}, + {{ .MapLocationY }}, + {{ .MapLocationAreaMeters }} + )}); + + window.addEventListener('resize', function() { + setTimeout(() => { + if (window.location-map-desktop) window.location-map-desktop.invalidateSize(); + if (window.location-map-mobile) window.location-map-mobile.invalidateSize(); + }, 100); + }); + </script> +{{- end }} + +</body> +</html> diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html deleted file mode 100644 index a7cf66e..0000000 --- a/views/layouts/general-page.html +++ /dev/null @@ -1,321 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>SAYA TODAY // {{ .Title }}</title> - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"> - <link rel="preconnect" href="https://fonts.googleapis.com"> - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> - <link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,200;1,300;1,400;1,500;1,600;1,700;1,800&family=Patua+One&display=swap" rel="stylesheet"> - {{ block "top-embeds" . }}{{ end }} - <link href="/output.css" rel="stylesheet"> -</head> - -<body data-theme="ram" class="font-spectral text-main-dark overflow-hidden bg-interlocked-hexagons h-screen flex flex-row ar-lt-0.8:flex-col"> - - <script> - function sfc32(a, b, c, d) { - return function() { - a |= 0; b |= 0; c |= 0; d |= 0; - let t = (a + b | 0) + d | 0; - d = d + 1 | 0; - a = b ^ b >>> 9; - b = c + (c << 3) | 0; - c = (c << 21 | c >>> 11); - c = c + t | 0; - return (t >>> 0) / 4294967296; - } - } - - function cyrb128(str) { - let h1 = 1779033703, h2 = 3144134277, - h3 = 1013904242, h4 = 2773480762; - for (let i = 0, k; i < str.length; i++) { - k = str.charCodeAt(i); - h1 = h2 ^ Math.imul(h1 ^ k, 597399067); - h2 = h3 ^ Math.imul(h2 ^ k, 2869860233); - h3 = h4 ^ Math.imul(h3 ^ k, 951274213); - h4 = h1 ^ Math.imul(h4 ^ k, 2716044179); - } - h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067); - h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233); - h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213); - h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179); - h1 ^= (h2 ^ h3 ^ h4), h2 ^= h1, h3 ^= h1, h4 ^= h1; - return [h1>>>0, h2>>>0, h3>>>0, h4>>>0]; - } - - 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 switchThemeDecor(theme) { - document.querySelectorAll('.theme-decor').forEach((e) => { - e.remove(); - }); - - if (theme == "" || typeof theme == "undefined") { - theme = document.body.getAttribute('data-theme'); - } - - vmin = calculateVmin(1); - var clWidth = document.documentElement.clientWidth; - var clHeight = document.documentElement.clientHeight; - - switch (theme) { - case "olive": break; - case "lettuce": break; - case "night": - for (i = 0; i < 25; i++) { - let star = document.createElement("div"); - star.append("*"); - star.classList.add("night-star", "theme-decor", "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; - star.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rot}deg)`; - star.style.animationDuration = `${animDuration}s`; - star.style.fontSize = `${sz}px`; - document.body.append(star); - } - break; - case "ram": - const frameCenters = new Array(4); - for (i = 1; i <= 4; i++) { - let frame = document.createElement("img"); - frame.src = `https://f003.backblazeb2.com/file/sayana-static/themes/ram/ram-frame${i}.webp`; - frame.classList.add("ram-frame", "theme-decor", "z-0", "absolute"); - let x, y; - outerLoop: - while (true) { - x = Math.floor(rand() * clWidth - 20*vmin); - y = Math.floor(rand() * clHeight - 20*vmin); - 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) { - continue outerLoop; - } - } - break; - } - frameCenters[i-1] = {x: x, y: y}; - const rot = rand() * 90 - 45; - frame.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rot}deg)`; - document.body.append(frame); - } - break; - } - } - - function switchTheme(theme) { - if (theme == "" || typeof theme == "undefined") { - const currentTheme = document.body.getAttribute('data-theme'); - switch (currentTheme) { - case "olive": theme = "lettuce"; break; - case "lettuce": theme = "night"; break; - case "night": theme = "ram"; break; - case "ram": theme = "olive"; break; - } - } - localStorage.setItem('theme', theme); - document.body.setAttribute('data-theme', theme); - switchThemeDecor(theme); - } - - const savedTheme = localStorage.getItem('theme') || 'ram'; - switchTheme(savedTheme); - </script> - - <script> - const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; - </script> - - <div id="sidebar" class="bg-sidebar flex sticky flex-col ar-gt-0.8:items-center shadow-2xl z-20 w-[5vw] ar-lt-0.8:w-[90vw] ar-lt-0.4:w-[100vw] h-screen ar-lt-0.8:h-[8vh] ar-lt-0.8:ml-[5vw] ar-lt-0.4:ml-0"> - <div class="logo-custom ar-lt-0.8:hidden text-sidebar font-patua font-extrabold text-center relative z-20"> - saya.today - </div> - <div class="text-[3vw] ar-lt-0.8:text-[5vh] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1vw] flex flex-col ar-lt-0.8:flex-row gap-0 relative z-20"> - <i onclick="location.href='/';" class="fas fa-home w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> - <i onclick="location.href='/{{ .Lang }}/blog';" class="fas fa-search w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> - <i onclick="location.href='/{{ .Lang }}/map';" class="fas fa-map w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> - <i onclick="toggleThemeWheel();" id="theme-button" class="fas fa-swatchbook w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> - </div> - <div class="theme-wheel" id="theme-wheel"> - <div class="theme-icon" data-theme="olive"></div> - <div class="theme-icon" data-theme="lettuce"></div> - <div class="theme-icon" data-theme="night"></div> - <div class="theme-icon" data-theme="ram"></div> - </div> - </div> - - <div class="bg-background-dark flex z-10 ar-lt-0.8:w-[90vw] ar-lt-0.4:w-[100vw] h-screen ar-lt-0.8:ml-[5vw] ar-lt-0.4:ml-0"> - <div class="pl-[0.5vw] bg-background-dark border-r-[0.4vmin] border-dashed border-main-dark"></div> - - <div class="ar-gt-0.8:min-w-fit ar-gt-0.8:max-w-[100vh] flex flex-col bg-background-dark relative pt-[1.6vmin] ml-[1.6vmin] pr-[1.6vmin]"> - <div {{ if .PublishedDate }} hx-get="/api/v1/tz" hx-vals='js:{timestamp: "{{ .PublishedDate }}", tz: clientTimeZone}' hx-target="#published-date" hx-swap="innerHTML" hx-trigger="load" {{ end }} - class="flex flex-row mb-[0.4vmin]"> - <i onclick="location.pathname += location.pathname.endsWith('/') ? '../' : '/../';" class="fas fa-circle-left hover:bg-main-dark hover:bg-opacity-10 cursor-pointer transition-colors duration-300 rounded-md text-[2vmax] mt-auto mb-auto px-[0.8vmin]"></i> - <p class="text-[2vmax] font-spectral italic font-extrabold select-none mt-auto pl-[0.8vmin] pr-[1.2vmin]">//</p> - <p class="text-[2vmax] font-spectral italic text-left mt-auto">{{ .Title }}</p> - <p id="published-date" class="grow text-[1.5vmax] font-spectral text-secondary text-right mt-auto">{{ .PublishedDate }}</p> - </div> - {{ block "header" . }}{{ end }} - <hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]"> - - {{ block "body" . }}{{ end }} - - <div class="flex flex-row mt-[0.8vmin] mb-[0.4vmin]"> - {{ block "footer" . }}{{ end }} - <p class="grow content-center text-[1vmax]/[1] font-spectral italic text-right text-secondary mr-2"> - All the photos on <a href="https://saya.today/">saya.today</a> are licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a> by <a href="https://t.me/EarlInisMona">Saya Andy</a> © {{ .PublishedYear }} - </p> - <img class="max-w-[1vmax] max-h-[1vmax] self-center" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="CC"> - <img class="max-w-[1vmax] max-h-[1vmax] self-center" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="BY"> - <img class="max-w-[1vmax] max-h-[1vmax] self-center" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="SA"> - </div> - - <hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]"> - </div> - - <div class="pr-[0.5vw] bg-background-dark shadow-2xl border-l-[0.4vmin] border-dotted border-main-dark"></div> - </div> - - <script src="https://f003.backblazeb2.com/file/sayana-static/libs/htmx/2.0.6/htmx.min.js"></script> - - <script> - const themeWheel = document.getElementById('theme-wheel'); - const themeButton = document.getElementById('theme-button'); - const themeIcons = document.querySelectorAll('.theme-icon'); - - themeIcons.forEach(icon => { - if (icon.getAttribute('data-theme') == savedTheme) { - icon.classList.add('active'); - } - }); - - 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 icons = themeWheel.querySelectorAll('.theme-icon'); - const iconCount = icons.length; - const vmax = calculateVmax(1); - const wheelRadius = 7.5*vmax; // Radius of the wheel - const iconRadius = 4*vmax; // Distance from center to place icons - const iconSize = 3*vmax; // Half of icon size for centering - - icons.forEach((icon, index) => { - const angle = (index * 360 / iconCount) - 90; - const angleRad = (angle * Math.PI) / 180; - - const x = Math.cos(angleRad) * iconRadius + wheelRadius - iconSize; - const y = Math.sin(angleRad) * iconRadius + wheelRadius - iconSize; - - icon.style.left = x + 'px'; - icon.style.top = y + 'px'; - }); - } - - function positionThemeWheel() { - const buttonRect = themeButton.getBoundingClientRect(); - const buttonCenterX = buttonRect.left + buttonRect.width / 2; - const buttonCenterY = buttonRect.top + buttonRect.height / 2; - - const wheelRadius = calculateVmax(7.5); - const margin = calculateVmax(1); - - let wheelX = buttonCenterX - wheelRadius; - let wheelY = buttonCenterY - wheelRadius; - - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - if (wheelX < margin) { - wheelX = margin; - } else if (wheelX + (wheelRadius * 2) > viewportWidth - margin) { - wheelX = viewportWidth - (wheelRadius * 2) - margin; - } - - if (wheelY < margin) { - wheelY = margin; - } else if (wheelY + (wheelRadius * 2) > viewportHeight - margin) { - wheelY = viewportHeight - (wheelRadius * 2) - margin; - } - - themeWheel.style.left = wheelX + 'px'; - themeWheel.style.top = wheelY + 'px'; - } - - function hideThemeWheel() { - themeWheel.classList.remove('active'); - } - - function showThemeWheel() { - positionThemeWheel(); - positionThemeIcons(); - themeWheel.classList.add('active'); - } - - function toggleThemeWheel() { - if (themeWheel.classList.contains('active')) { - hideThemeWheel(); - } else { - showThemeWheel(); - } - } - - themeIcons.forEach(icon => { - icon.addEventListener('click', (e) => { - e.stopPropagation(); - const theme = icon.getAttribute('data-theme'); - switchTheme(theme); - themeIcons.forEach(icon => { - icon.classList.remove('active'); - }) - icon.classList.add('active'); - }); - }); - - themeWheel.addEventListener('mouseleave', hideThemeWheel); - - document.addEventListener('click', (e) => { - if (!themeWheel.contains(e.target) && e.target !== themeButton) { - hideThemeWheel(); - } - }); - </script> - - <script> - let decorResizeTimeout; - window.addEventListener('resize', function() { - clearTimeout(decorResizeTimeout); - decorResizeTimeout = setTimeout(switchThemeDecor, 300); - }); - </script> - - {{ block "bottom-embeds" . }}{{ end }} - -</body> -</html> diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html deleted file mode 100644 index 1d0a2de..0000000 --- a/views/pages/blog-catalogue.html +++ /dev/null @@ -1,161 +0,0 @@ -{{ define "body" }} -<div class="flex flex-row ar-lt-0.8:flex-col max-h-[calc(100%-7vmax)] ar-lt-0.8:max-h-[calc(100%-25vh)] flex-1 text-[1vmax]"> - <form hx-get="/api/v1/blog-search" hx-vals='{"lang": "{{ .Lang }}"}' hx-target=".blog-cards" hx-swap="innerHTML" - class="tags-list ar-gt-0.8:min-w-[10vh] ar-gt-0.8:max-w-[20vh] ar-gt-0.8:w-fit ar-lt-0.8:w-[100%] ar-lt-0.8:max-h-[25%] flex shrink-0 flex-col ar-gt-0.8:mr-[1.2vmin]"> - <div class="flex flex-col overflow-y-auto"> - <fieldset> - <legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.TagsHeader }}</legend> - <div class="flex flex-col ar-lt-0.8:grid grid-cols-5 grid-flow-row-dense"> - <div class="m-[0.4vmin]"> - <label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();"> {{ .L.BlogSearch.ChooseAllTags }}</label> - </div> - {{- range .Tags }} - <div class="m-[0.4vmin]"> - <label><input type="checkbox" id="tag{{ .Name }}Checkbox" name="tags[]" value="{{ .Name }}" onclick="contextAll();" {{ if contains $.QueryTags .Name }}checked{{ end }}> {{ .Name }} <span class="text-secondary italic">{{ .Count }}</span></label> - </div> - {{- end }} - </div> - </fieldset> - <fieldset class="mt-[0.8vmin]"> - <legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.OrderByHeader }}</legend> - <div class="flex flex-col ar-lt-0.8:grid grid-cols-3 grid-rows-2 grid-flow-col"> - <div class="m-[0.4vmin]"> - <label><input type="radio" id="sortTitleAsc" name="sort" value="titleAsc" {{ if eq .QuerySort "titleAsc" }}checked{{ end }}> {{ .L.BlogSearch.TitleOrdered }} <i class="fas fa-arrow-down-a-z"></i></label> - </div> - <div class="m-[0.4vmin]"> - <label><input type="radio" id="sortTitleDesc" name="sort" value="titleDesc" {{ if eq .QuerySort "titleDesc" }}checked{{ end }}> {{ .L.BlogSearch.TitleOrdered }} <i class="fas fa-arrow-down-z-a"></i></label> - </div> - <div class="m-[0.4vmin]"> - <label><input type="radio" id="sortActionDateAsc" name="sort" value="actionDateAsc" {{ if eq .QuerySort "actionDateAsc" }}checked{{ end }}> {{ .L.BlogSearch.ActionDateOrdered }} <i class="fas fa-arrow-down-1-9"></i></label> - </div> - <div class="m-[0.4vmin]"> - <label><input type="radio" id="sortActionDateDesc" name="sort" value="actionDateDesc" {{ if eq .QuerySort "actionDateDesc" }}checked{{ end }}> {{ .L.BlogSearch.ActionDateOrdered }} <i class="fas fa-arrow-down-9-1"></i></label> - </div> - <div class="m-[0.4vmin]"> - <label><input type="radio" id="sortPublicationDateAsc" name="sort" value="publicationDateAsc" {{ if eq .QuerySort "publicationDateAsc" }}checked{{ end }}> {{ .L.BlogSearch.PublicationDateOrdered }} <i class="fas fa-arrow-down-1-9"></i></label> - </div> - <div class="m-[0.4vmin]"> - <label><input type="radio" id="sortPublicationDateDesc" name="sort" value="publicationDateDesc" {{ if eq .QuerySort "publicationDateDesc" }}checked{{ end }}> {{ .L.BlogSearch.PublicationDateOrdered }} <i class="fas fa-arrow-down-9-1"></i></label> - </div> - </div> - </fieldset> - </div> - <hr class="border-t-[0.2vmax] border-dotted border-main-dark my-[0.4vmin]"> - <div class="flex flex-row grow bg-background-light my-[2vmin] p-[0.4vmin] rounded-[0.4vmin] justify-items-center"> - <i onclick="defaultSearchParams(); cleanHrefParams();" class="fas fa-rotate-left flex-1 text-center text-[1.5vmax] hover:bg-background-dark cursor-pointer transition-colors duration-300"></i> - <div class="w-[0.5vmin] bg-background-dark grow-0"></div> - <button class="flex-1" type="submit"> - <i onclick="setHrefParams();" class="fas fa-magnifying-glass text-center text-[1.5vmax] hover:bg-background-dark cursor-pointer transition-colors duration-300"></i> - </button> - </div> - </form> - - <div class="w-[0.5vh] border-r-[0.3vh] border-dotted border-main-dark bg-background-dark"></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" - class="blog-cards grow flex flex-col overflow-y-auto bg-background-light inset-shadow p-[2.4vmin]"> - Loading... - </div> -</div> -{{ end }} - -{{ define "bottom-embeds" }} -<script> - function selectAll() { - const tagsContainers = document.querySelectorAll('form.tags-list'); - tagsContainers.forEach(tagsContainer => { - const selectAllCheckbox = tagsContainer.querySelector('#tagsAllCheckbox'); - const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]:not([id="tagsAllCheckbox"])'); - checkboxes.forEach(checkbox => { - checkbox.checked = selectAllCheckbox.checked; - }); - }); - } - - function contextAll() { - const tagsContainers = document.querySelectorAll('form.tags-list'); - tagsContainers.forEach(tagsContainer => { - const selectAllCheckbox = tagsContainer.querySelector('#tagsAllCheckbox'); - const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]:not([id="tagsAllCheckbox"])'); - var areAllChecked = true; - checkboxes.forEach(checkbox => { - if (!checkbox.checked) areAllChecked = false; - }); - selectAllCheckbox.checked = areAllChecked; - }); - } - - function defaultSearchParams() { - const tagsContainers = document.querySelectorAll('form.tags-list'); - tagsContainers.forEach(tagsContainer => { - const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]'); - checkboxes.forEach(checkbox => { - checkbox.checked = true; - }); - const sortRadio = tagsContainer.querySelector('#sortPublicationDateDesc'); - sortRadio.checked = true; - }); - } - - function cleanHrefParams() { - const url = new URL(window.location.href); - url.search = ''; - window.history.pushState({}, '', url.toString()); - } - - function setHrefParams() { - const url = new URL(window.location.href); - url.search = ''; - - const tagsContainers = document.querySelectorAll('form.tags-list'); - tagsContainers.forEach(tagsContainer => { - const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]'); - checkboxes.forEach(checkbox => { - if (checkbox.checked) - url.searchParams.append('tags[]', checkbox.value); - }); - const sortRadio = tagsContainer.querySelector('input[type="radio"]:checked'); - url.searchParams.set('sort', sortRadio.value); - }); - - window.history.pushState({}, '', url.toString()); - } - - function pickedTagSetHrefParams(tag) { - const url = new URL(window.location.href); - url.searchParams.delete('tags[]'); - url.searchParams.set('tags[]', tag); - - const tagsContainers = document.querySelectorAll('form.tags-list'); - tagsContainers.forEach(tagsContainer => { - const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]'); - checkboxes.forEach(checkbox => { - checkbox.checked = false; - }); - chosenCheckbox = tagsContainer.querySelector(`#tag${tag}Checkbox`); - chosenCheckbox.checked = true; - }); - - window.history.pushState({}, '', url.toString()); - } - - document.addEventListener('DOMContentLoaded', contextAll); -</script> - -<script> - const tagsContainers = document.querySelectorAll('form.tags-list'); - tagsContainers.forEach(tagsContainer => { - const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]'); - anyChecked = false; - checkboxes.forEach(checkbox => { - anyChecked = checkbox.checked ? true : anyChecked; - }); - if (!anyChecked) { - const selectAllCheckbox = tagsContainer.querySelector('#tagsAllCheckbox'); - selectAllCheckbox.checked = true; - selectAll(); - } - }); -</script> - -{{ end }} diff --git a/views/pages/blog-page.html b/views/pages/blog-page.html deleted file mode 100644 index 65ee31d..0000000 --- a/views/pages/blog-page.html +++ /dev/null @@ -1,92 +0,0 @@ -{{ define "top-embeds" }} -<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lightgallery.min.css"> -<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lg-zoom.min.css"> -<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lg-thumbnail.min.css"> -{{- if .MapLocationX }} -<link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.css"> -{{- end }} - -<style> - .map-container .leaflet-container { - width: 100% !important; - height: 100% !important; - } -</style> - -<script> - const galleryMap = new Map(); -</script> -{{ end }} - -{{ define "body" }} -<div class="bg-background-light flex flex-col grow overflow-y-auto inset-shadow p-[2.4vmin] max-h-[calc(100%-7vmax)] ar-lt-0.8:max-h-[calc(100%-25vh)]"> - {{ .ParsedMarkdown }} -{{- if .MapLocationX }} - <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto"> - <div id="map-container" class="relative p-[0.8vmin] flex-none ml-auto mr-auto w-[60%] ar-lt-0.8:w-[80%] h-[40vh] ar-lt-0.8:h-[30vh]"></div> - <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto"> -{{- end }} -</div> -{{ end }} - -{{ define "footer" }} -<div hx-get="/api/v1/like" hx-target="this" hx-swap="outerHTML" hx-trigger="load"></div> -{{ end }} - -{{ define "bottom-embeds" }} -<script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/lightgallery.min.js"></script> -<script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/plugins/lg-zoom.min.js"></script> -<script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/plugins/lg-thumbnail.min.js"></script> -{{- if .MapLocationX }} -<script src="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.js"></script> - -<script> - var map; - - function initLocationMap(id, x, y, relativeErrorMeters = 0) { - map = L.map(id).setView([x, y], 8); - - L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 18, - attribution: '© <a href="http://www.openstreetmap.org/copyright">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(); - } - - document.addEventListener('DOMContentLoaded', () => {initLocationMap( - 'map-container', - {{ .MapLocationX }}, - {{ .MapLocationY }}, - {{ .MapLocationAreaMeters }} - )}); - - let galleryResizeTimeout; - window.addEventListener('resize', function() { - clearTimeout(galleryResizeTimeout); - galleryResizeTimeout = setTimeout(() => { - map.invalidateSize(); - var oldGalleryMap = new Map(galleryMap); - oldGalleryMap.forEach((createFunc, g, map) => { - galleryMap.delete(g); - g.destroy(); - createFunc(); - }); - }, 300); - }); -</script> -{{- end }} -{{ end }}
\ No newline at end of file diff --git a/views/pages/global-map.html b/views/pages/global-map.html deleted file mode 100644 index 569166f..0000000 --- a/views/pages/global-map.html +++ /dev/null @@ -1,267 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>SAYA TODAY // {{ .L.GlobalMap.Header }}</title> - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"> - <link rel="preconnect" href="https://fonts.googleapis.com"> - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> - <link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,200;1,300;1,400;1,500;1,600;1,700;1,800&family=Patua+One&display=swap" rel="stylesheet"> - {{ block "header" . }}{{ end }} - <link href="/output.css" rel="stylesheet"> - <link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.css"> - <link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet.markercluster/1.4.1/MarkerCluster.css"> - <link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css"> -</head> - -<body data-theme="ram" class="font-spectral text-main-dark overflow-hidden bg-interlocked-hexagons h-screen flex flex-row ar-lt-0.8:flex-col"> - - <script> - function switchTheme(theme) { - if (theme == "" || typeof theme == "undefined") { - const currentTheme = document.body.getAttribute('data-theme'); - switch (currentTheme) { - case "olive": theme = "lettuce"; break; - case "lettuce": theme = "night"; break; - case "night": theme = "ram"; break; - case "ram": theme = "olive"; break; - } - } - localStorage.setItem('theme', theme); - document.body.setAttribute('data-theme', theme); - } - - const savedTheme = localStorage.getItem('theme') || 'ram'; - switchTheme(savedTheme); - </script> - - <script> - const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; - </script> - - <div id="sidebar" class="bg-sidebar flex sticky flex-col ar-gt-0.8:items-center shadow-2xl z-20 w-[5vw] ar-lt-0.8:w-screen h-screen ar-lt-0.8:h-[8vh]"> - <div class="logo-custom ar-lt-0.8:hidden text-sidebar font-patua font-extrabold text-center relative z-20"> - saya.today - </div> - <div class="text-[3vw] ar-lt-0.8:text-[5vh] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1vw] flex flex-col ar-lt-0.8:flex-row gap-0 relative z-20"> - <i onclick="location.href='/';" class="fas fa-home w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> - <i onclick="location.href='/{{ .Lang }}/blog';" class="fas fa-search w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> - <i onclick="location.href='/{{ .Lang }}/map';" class="fas fa-map w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> - <i onclick="toggleThemeWheel();" id="theme-button" class="fas fa-swatchbook w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> - </div> - <div class="theme-wheel" id="theme-wheel"> - <div class="theme-icon" data-theme="olive"></div> - <div class="theme-icon" data-theme="lettuce"></div> - <div class="theme-icon" data-theme="night"></div> - <div class="theme-icon" data-theme="ram"></div> - </div> - </div> - - <div id="map-container" class="bg-background-dark flex z-10 w-[95vw] ar-lt-0.8:w-screen h-screen ar-lt-0.8:h-[92vh]"></div> - - <script src="https://f003.backblazeb2.com/file/sayana-static/libs/htmx/2.0.6/htmx.min.js"></script> - <script src="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.js"></script> - <script src="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js"></script> - - <script> - const themeWheel = document.getElementById('theme-wheel'); - const themeButton = document.getElementById('theme-button'); - const themeIcons = document.querySelectorAll('.theme-icon'); - - themeIcons.forEach(icon => { - if (icon.getAttribute('data-theme') == savedTheme) { - icon.classList.add('active'); - } - }); - - 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 icons = themeWheel.querySelectorAll('.theme-icon'); - const iconCount = icons.length; - const vmax = calculateVmax(1); - const wheelRadius = 7.5*vmax; // Radius of the wheel - const iconRadius = 4*vmax; // Distance from center to place icons - const iconSize = 3*vmax; // Half of icon size for centering - - icons.forEach((icon, index) => { - const angle = (index * 360 / iconCount) - 90; - const angleRad = (angle * Math.PI) / 180; - - const x = Math.cos(angleRad) * iconRadius + wheelRadius - iconSize; - const y = Math.sin(angleRad) * iconRadius + wheelRadius - iconSize; - - icon.style.left = x + 'px'; - icon.style.top = y + 'px'; - }); - } - - function positionThemeWheel() { - const buttonRect = themeButton.getBoundingClientRect(); - const buttonCenterX = buttonRect.left + buttonRect.width / 2; - const buttonCenterY = buttonRect.top + buttonRect.height / 2; - - const wheelRadius = calculateVmax(7.5); - const margin = calculateVmax(1); - - let wheelX = buttonCenterX - wheelRadius; - let wheelY = buttonCenterY - wheelRadius; - - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - if (wheelX < margin) { - wheelX = margin; - } else if (wheelX + (wheelRadius * 2) > viewportWidth - margin) { - wheelX = viewportWidth - (wheelRadius * 2) - margin; - } - - if (wheelY < margin) { - wheelY = margin; - } else if (wheelY + (wheelRadius * 2) > viewportHeight - margin) { - wheelY = viewportHeight - (wheelRadius * 2) - margin; - } - - themeWheel.style.left = wheelX + 'px'; - themeWheel.style.top = wheelY + 'px'; - } - - function hideThemeWheel() { - themeWheel.classList.remove('active'); - } - - function showThemeWheel() { - positionThemeWheel(); - positionThemeIcons(); - themeWheel.classList.add('active'); - } - - function toggleThemeWheel() { - if (themeWheel.classList.contains('active')) { - hideThemeWheel(); - } else { - showThemeWheel(); - } - } - - themeIcons.forEach(icon => { - icon.addEventListener('click', (e) => { - e.stopPropagation(); - const theme = icon.getAttribute('data-theme'); - switchTheme(theme); - themeIcons.forEach(icon => { - icon.classList.remove('active'); - }) - icon.classList.add('active'); - }); - }); - - themeWheel.addEventListener('mouseleave', hideThemeWheel); - - document.addEventListener('click', (e) => { - if (!themeWheel.contains(e.target) && e.target !== themeButton) { - hideThemeWheel(); - } - }); - </script> - - <script> - var map; - var markers = L.markerClusterGroup(); - - function createCustomIcon(color, borderColor = "var(--main-light-color)") { - 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://tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 18, - attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' - }).addTo(map); - - map.addLayer(markers); - } - - function stringToColor(str) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } - - let color = '#'; - for (let i = 0; i < 3; i++) { - const value = (hash >> (i * 8)) & 0xFF; - color += ('00' + value.toString(16)).substr(-2); - } - - return color; - } - - function mapAddMarker(x, y, relativeErrorMeters = 0, title = "", link = "", thumbnail = "") { - const titleColor = stringToColor(title); - - if (relativeErrorMeters != 0) { - var circle = L.circle([x, y], { - color: titleColor, - fillColor: titleColor, - fillOpacity: 0.15, - radius: relativeErrorMeters - }).addTo(map); - } - - var marker = L.marker([x, y], { - icon: createCustomIcon(titleColor), - 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-[3vmax] h-[3vmax] mr-[0.5vmax]" src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/${thumbnail}.webp"> - <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 }}); - {{- end }} - }); - - window.addEventListener('resize', function() { - setTimeout(() => { - map.invalidateSize(); - }, 100); - }); - </script> - -</body> -</html> diff --git a/views/partials/blog-page-like-button.html b/views/partials/blog-page-like-button.html deleted file mode 100644 index afc3027..0000000 --- a/views/partials/blog-page-like-button.html +++ /dev/null @@ -1,7 +0,0 @@ -<button - hx-put="/api/v1/like" hx-vals='{"like": {{ not .Liked }}}' hx-target="this" hx-swap="outerHTML" hx-trigger="click" - class="text-[0.8vmax]/[0.9] font-spectral text-left px-[0.4vmax] py-[0.2vmax] {{ if .Liked }}bg-main-light hover:bg-main-medium text-background-dark border-inset{{ else }}bg-main-dark hover:bg-main-medium text-background-light border-outset{{ end }} cursor-pointer border-[0.2vmax] border-background-dark"> - <i class="fas fa-thumbs-up w-[0.8vmax] h-[0.8vmax] mr-[0.4vmax]"></i> - <span>{{ .L.LikeButton }}</span> - <span class="italic ml-[0.2vmax] text-background-light">({{ .LikedCount }})</span> -</button>
\ No newline at end of file diff --git a/views/partials/catalogue-blog-card-tags.html b/views/partials/catalogue-blog-card-tags.html deleted file mode 100644 index 374314a..0000000 --- a/views/partials/catalogue-blog-card-tags.html +++ /dev/null @@ -1 +0,0 @@ -{{ range $i, $tag := .Tags }}{{ if ne $i 0 }}, {{ end }}<a hx-trigger="click" hx-get="/api/v1/blog-search" hx-target=".blog-cards" hx-swap="innerHTML" hx-trigger="load" hx-include=".tags-list" class="hx-anchor cursor-pointer" onclick="pickedTagSetHrefParams({{ $tag }})">{{ $tag }}</a>{{ end }}
\ No newline at end of file diff --git a/views/partials/catalogue-blog-cards.html b/views/partials/catalogue-blog-cards.html deleted file mode 100644 index 35c7ad7..0000000 --- a/views/partials/catalogue-blog-cards.html +++ /dev/null @@ -1,21 +0,0 @@ -{{- range .BlogPages }} -<hr class="first-of-type:hidden border-t-[0.3vh] my-[0.8vmin] border-dotted border-main-dark w-[80%] mx-auto"> -<div class="m-[0.4vmin] flex flex-row justify-center justify-items-center"> - <img onclick="location.href='{{ .ArticleLink }}';" class="cursor-pointer object-cover rounded-[10%] select-none w-[10vmin] h-[10vmin] aspect-square my-[0.4vmin] mr-[0.8vmin]" src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/{{ .Thumbnail }}.webp"> - <div class="grow my-[0.4vmin] mx-[0.8vmin] flex flex-col justify-center"> - <div class="flex flex-row gap-[5%] max-w-[100%]"> - <a href="{{ .ArticleLink }}" class="flex-9/12 grow-0 block text-[1vmax] cursor-pointer"> - <span class="font-extrabold">{{ .Title }}</span> - <span class="font-extrabold select-none text-secondary">//</span> - <span class="italic text-secondary">{{ .ActionDate }}</span> - <span class="font-extrabold select-none text-secondary">//</span> - <i class="fas fa-thumbs-up w-[1vmax] h-[1vmax]"></i> - <span class="italic text-secondary">{{ .LikeCount }}</span> - </a> - <p class="flex-3/12 grow-0 text-[1vmax] italic text-secondary text-right">{{ .PublishedTime }}</p> - </div> - <p class="text-[1vmax] text-main-dark">{{ .ShortDescription }}</p> - <p class="text-[1vmax] text-secondary">{{ $.L.TagsLabel }} {{ template "catalogue-blog-card-tags.html" . }}</p> - </div> -</div> -{{- end }}
\ No newline at end of file |