summaryrefslogtreecommitdiffci
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build-and-deploy-prod.yml83
-rw-r--r--.github/workflows/build-and-deploy-stage.yml89
-rw-r--r--.gitignore2
-rw-r--r--Dockerfile14
-rw-r--r--Jenkinsfile313
-rw-r--r--config/config.go18
-rw-r--r--config/config.local.yaml27
-rw-r--r--config/config.prod.yaml35
-rw-r--r--config/config.stage.yaml27
-rw-r--r--deploy/playbook.yml9
-rw-r--r--go.mod14
-rw-r--r--go.sum28
-rw-r--r--internal/blog/b2.go31
-rw-r--r--internal/blog/client.go1
-rw-r--r--internal/blog/index.go83
-rw-r--r--internal/blog/s3.go269
-rw-r--r--internal/glightbox/html_renderer.go4
-rw-r--r--internal/mailer/mailer.go16
-rw-r--r--internal/router/handlers/api-v1-blog-search.go53
-rw-r--r--internal/router/handlers/api-v1-email-is-in-verification.go3
-rw-r--r--internal/router/handlers/api-v1-email-send-verification-code.go15
-rw-r--r--internal/router/handlers/api-v1-email-verify.go7
-rw-r--r--internal/router/handlers/api-v1-like-get.go2
-rw-r--r--internal/router/handlers/api-v1-map-get.go116
-rw-r--r--internal/router/handlers/api-v1-subs-put.go7
-rw-r--r--internal/router/handlers/lang-blog-title.go31
-rw-r--r--internal/router/handlers/lang-blog.go16
-rw-r--r--internal/router/handlers/lang-map.go65
-rw-r--r--internal/router/handlers/lang-user-unsubscribe.go9
-rw-r--r--internal/router/handlers/lang-user.go9
-rw-r--r--internal/router/handlers/lang.go9
-rw-r--r--internal/router/handlers/root.go7
-rw-r--r--internal/router/router.go116
-rw-r--r--internal/tailwind/transformer.go2
-rw-r--r--internal/templatemanager/templatemanager.go6
-rw-r--r--l10n/en.yaml (renamed from locale/localization.en.yaml)0
-rw-r--r--l10n/ru.yaml (renamed from locale/localization.ru.yaml)0
-rw-r--r--l10n/translator-hook.go39
-rw-r--r--l10n/translator.go167
-rw-r--r--locale/localization.go144
-rw-r--r--static/favicon.icobin0 -> 7633 bytes
-rw-r--r--static/input.css540
-rw-r--r--views/layouts/general-page.html314
-rw-r--r--views/messages/new-post.html6
-rw-r--r--views/messages/verify-email.html10
-rw-r--r--views/pages/blog-catalogue.html74
-rw-r--r--views/pages/blog-page.html95
-rw-r--r--views/pages/global-map.html131
-rw-r--r--views/pages/home-page.html51
-rw-r--r--views/pages/language-pick.html4
-rw-r--r--views/pages/unsubscribe-page.html7
-rw-r--r--views/pages/user-page.html46
-rw-r--r--views/partials/blog-page-like-button.html6
-rw-r--r--views/partials/catalogue-blog-cards.html24
-rw-r--r--views/partials/general-page-body.html1
-rw-r--r--views/partials/general-page-header.html8
-rw-r--r--views/partials/global-map-widget.html109
-rw-r--r--views/partials/personal-page-status.html2
58 files changed, 1927 insertions, 1387 deletions
diff --git a/.github/workflows/build-and-deploy-prod.yml b/.github/workflows/build-and-deploy-prod.yml
deleted file mode 100644
index 97afdf7..0000000
--- a/.github/workflows/build-and-deploy-prod.yml
+++ /dev/null
@@ -1,83 +0,0 @@
-name: Docker image building and publishing onto Production
-on:
- push:
- tags:
- - "*"
-jobs:
- build-and-push:
- if: github.repository == 'SayaAndy/saya-today-web'
- runs-on: ubuntu-latest
- environment: Production
- permissions:
- packages: write
- steps:
- - name: Checkout
- uses: actions/checkout@v6
-
- - 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@v4
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v4
-
- - name: Login to GitHub Container Registry
- uses: docker/login-action@v4
- with:
- registry: ghcr.io
- username: ${{ github.repository_owner }}
- password: ${{ secrets.GHCR_TOKEN }}
-
- - name: Build and push
- uses: docker/build-push-action@v7
- 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:
- if: github.repository == 'SayaAndy/saya-today-web'
- runs-on: ubuntu-latest
- environment: Production
- needs: [build-and-push]
- steps:
- - name: Checkout
- uses: actions/checkout@v6
-
- - name: Set up SSH connection for uz.saya.casa
- 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 uz.saya.casa >> ~/.ssh/known_hosts
-
- - name: Test SSH connection
- run: |
- ssh -o ConnectTimeout=10 -i ~/.ssh/id_ed25519 svc_github@uz.saya.casa "echo 'SSH connection successful'"
-
- - 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_s3_access_key_id="${{ secrets.S3_ACCESS_KEY_ID }}" -e saya_today_web_s3_secret_access_key="${{ secrets.S3_SECRET_ACCESS_KEY }}" -e saya_today_web_environment=prod -e saya_today_web_tag=${{ github.ref_name }} -e saya_today_web_mail_salt="${{ secrets.MAIL_SALT }}" -e saya_today_web_mail_host="${{ secrets.MAIL_HOST }}" -e saya_today_web_mail_address="${{ secrets.MAIL_ADDRESS }}" -e saya_today_web_mail_username="${{ secrets.MAIL_USERNAME }}" -e saya_today_web_mail_password="${{ secrets.MAIL_PASSWORD }}" -e saya_today_google_site_verification="${{ secrets.GOOGLE_SITE_VERIFICATION }}" -e saya_today_yandex_verification="${{ secrets.YANDEX_VERIFICATION }}"
diff --git a/.github/workflows/build-and-deploy-stage.yml b/.github/workflows/build-and-deploy-stage.yml
deleted file mode 100644
index ffee091..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@v6
-
- - 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@v4
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v4
-
- - name: Login to GitHub Container Registry
- uses: docker/login-action@v4
- 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@v7
- 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@v6
-
- - name: Set up SSH connection for uz.saya.casa
- 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 uz.saya.casa >> ~/.ssh/known_hosts
-
- - name: Test SSH connection
- run: |
- ssh -o ConnectTimeout=10 -i ~/.ssh/id_ed25519 svc_github@uz.saya.casa "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_s3_access_key_id="${{ secrets.S3_ACCESS_KEY_ID }}" -e saya_today_web_s3_secret_access_key="${{ secrets.S3_SECRET_ACCESS_KEY }}" -e saya_today_web_environment=stage -e saya_today_web_tag=commit-${{ steps.ghss_vars.outputs.sha_short }} -e saya_today_web_mail_salt="${{ secrets.MAIL_SALT }}" -e saya_today_web_mail_host="${{ secrets.MAIL_HOST }}" -e saya_today_web_mail_address="${{ secrets.MAIL_ADDRESS }}" -e saya_today_web_mail_username="${{ secrets.MAIL_USERNAME }}" -e saya_today_web_mail_password="${{ secrets.MAIL_PASSWORD }}"
diff --git a/.gitignore b/.gitignore
index b5ae410..a620479 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,3 +42,5 @@ config*.json
.envrc
/.ansible
+
+.DS_Store
diff --git a/Dockerfile b/Dockerfile
index 5c38111..d0b6621 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,12 +1,18 @@
-FROM golang:1.26-alpine3.23 AS build-stage
+FROM golang:1.26.4-alpine3.24 AS build-stage
+
+ARG TARGETOS=linux
+ARG TARGETARCH=amd64
+ENV GOOS=${TARGETOS}
+ENV GOARCH=${TARGETARCH}
RUN apk add --no-cache sqlite-dev musl-dev gcc
WORKDIR /builddir
COPY . .
+RUN go mod download
RUN CGO_ENABLED=1 go build -o sayana-web .
-FROM alpine:3.23 AS runtime-stage
+FROM alpine:3.24 AS runtime-stage
ENV ENVIRONMENT=""
ENV AUTH_SALT=""
@@ -18,8 +24,8 @@ COPY --from=build-stage /builddir/sayana-web /app/sayana-web
COPY --from=build-stage /builddir/migrations /app/migrations
COPY --from=build-stage /builddir/static /app/static
COPY --from=build-stage /builddir/views /app/views
-COPY --from=build-stage /builddir/locale/*.yaml /app/locale/
-COPY --from=build-stage /builddir/config/config*.yaml /app/config/
+COPY --from=build-stage /builddir/l10n/*.yaml /app/l10n/
+COPY --from=build-stage /builddir/config/config.*.yaml /app/config/
RUN apk add --no-cache tzdata sqlite
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..8633145
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,313 @@
+pipeline {
+ agent none
+
+ stages {
+ stage('Compile Tailwind CSS') {
+ agent {
+ docker {
+ image 'node:26.3.1-alpine3.24'
+ args '-v /tmp:/tmp -v /.npm:/.npm'
+ }
+ }
+ steps {
+ checkout scm
+ sh '''
+ npm install tailwindcss @tailwindcss/cli @tailwindcss/forms
+ npx tailwindcss -i static/input.css -o static/output.css --minify
+ '''
+ stash name: 'tailwindcss-output', includes: 'static/output.css'
+ }
+ }
+
+ stage('Matrix Build') {
+ matrix {
+ axes {
+ axis {
+ name 'GOOS'
+ values 'linux'
+ }
+ axis {
+ name 'GOARCH'
+ values 'arm64', 'amd64'
+ }
+ }
+ stages {
+ stage('Build') {
+ agent {
+ kubernetes {
+ defaultContainer 'kaniko'
+ yaml """
+apiVersion: v1
+kind: Pod
+metadata:
+ namespace: jenkins
+spec:
+ nodeSelector:
+ kubernetes.io/arch: ${GOARCH}
+ containers:
+ - name: kaniko
+ image: gcr.io/kaniko-project/executor:v1.24.0-debug
+ imagePullPolicy: Always
+ command: [ /busybox/cat ]
+ tty: true
+ resources:
+ requests:
+ cpu: "1"
+ memory: 2Gi
+ ephemeral-storage: 4Gi
+ limits:
+ memory: 4Gi
+ ephemeral-storage: 8Gi
+"""
+ }
+ }
+ environment {
+ IMAGE_PUSH_DESTINATION="registry.sayag.it/sayauz/web"
+ }
+ steps {
+ checkout scm
+ unstash 'tailwindcss-output'
+ container(name: 'kaniko', shell: '/busybox/sh') {
+ withCredentials([string(credentialsId: 'registry-sayagit-jenkins-password', variable: 'REGISTRY_SAYAGIT_JENKINS_PASSWORD')]) {
+ withEnv(['PATH+EXTRA=/busybox']) {
+ script {
+ env.GIT_COMMIT_SHORT = env.GIT_COMMIT ? env.GIT_COMMIT.take(7) : "unknown"
+ try {
+ sh '''#!/busybox/sh
+ mkdir -p /kaniko/.docker
+ printf '{"auths":{"registry.sayag.it":{"username":"jenkins","password":"%s"}}}' "$REGISTRY_SAYAGIT_JENKINS_PASSWORD" > /kaniko/.docker/config.json
+ /kaniko/executor --context `pwd` \
+ --custom-platform=linux/${GOARCH} \
+ --build-arg TARGETOS=linux \
+ --build-arg TARGETARCH=${GOARCH} \
+ --destination $IMAGE_PUSH_DESTINATION:commit-$GIT_COMMIT_SHORT-$GOARCH
+ '''
+ } catch (Exception e) {
+ echo "Caught exception: ${e.getMessage()}"
+ currentBuild.result = 'FAILURE'
+ throw e
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ stage('Stitch Manifest') {
+ agent {
+ kubernetes {
+ defaultContainer 'manifest-tool'
+ yaml """
+apiVersion: v1
+kind: Pod
+metadata:
+ namespace: jenkins
+spec:
+ containers:
+ - name: manifest-tool
+ image: mplatform/manifest-tool:alpine-v2.2.2
+ imagePullPolicy: Always
+ command: [ 'cat' ]
+ tty: true
+ resources:
+ requests:
+ cpu: "1"
+ memory: 2Gi
+ ephemeral-storage: 4Gi
+"""
+ }
+ }
+ environment {
+ IMAGE_PUSH_DESTINATION="registry.sayag.it/sayauz/web"
+ }
+ steps {
+ script {
+ env.GIT_COMMIT_SHORT = env.GIT_COMMIT ? env.GIT_COMMIT.take(7) : "unknown"
+ }
+ withCredentials([string(credentialsId: 'registry-sayagit-jenkins-password', variable: 'REGISTRY_SAYAGIT_JENKINS_PASSWORD')]) {
+ sh '''mkdir -p /root/.docker
+ printf '{"auths":{"registry.sayag.it":{"username":"jenkins","password":"%s"}}}' "$REGISTRY_SAYAGIT_JENKINS_PASSWORD" > /root/.docker/config.json
+
+ manifest-tool push from-args \
+ --platforms linux/amd64,linux/arm64 \
+ --template $IMAGE_PUSH_DESTINATION:commit-$GIT_COMMIT_SHORT-ARCH \
+ --target $IMAGE_PUSH_DESTINATION:commit-$GIT_COMMIT_SHORT
+ manifest-tool push from-args \
+ --platforms linux/amd64,linux/arm64 \
+ --template $IMAGE_PUSH_DESTINATION:commit-$GIT_COMMIT_SHORT-ARCH \
+ --target $IMAGE_PUSH_DESTINATION:latest
+ if [ "$TAG_NAME" != "" ]; then
+ manifest-tool push from-args \
+ --platforms linux/amd64,linux/arm64 \
+ --template $IMAGE_PUSH_DESTINATION:commit-$GIT_COMMIT_SHORT-ARCH \
+ --target $IMAGE_PUSH_DESTINATION:stable
+ manifest-tool push from-args \
+ --platforms linux/amd64,linux/arm64 \
+ --template $IMAGE_PUSH_DESTINATION:commit-$GIT_COMMIT_SHORT-ARCH \
+ --target $IMAGE_PUSH_DESTINATION:$TAG_NAME
+ fi
+ '''
+ }
+ }
+ }
+ stage('Deploy (Stage)') {
+ when { branch 'stage' }
+ agent {
+ kubernetes {
+ defaultContainer 'ansible'
+ yaml """
+apiVersion: v1
+kind: Pod
+metadata:
+ namespace: jenkins
+spec:
+ containers:
+ - name: ansible
+ image: ghcr.io/ansible/community-ansible-dev-tools:v26.7.2
+ command: [ 'cat' ]
+ tty: true
+ resources:
+ requests:
+ cpu: "500m"
+ memory: 1Gi
+"""
+ }
+ }
+ steps {
+ checkout scm
+ container('ansible') {
+ script {
+ env.GIT_COMMIT_SHORT = env.GIT_COMMIT ? env.GIT_COMMIT.take(7) : "unknown"
+
+ def secretVars = [
+ 'sayauz-web-stage-auth-salt' : 'AUTH_SALT',
+ 'sayauz-web-stage-s3-access-key-id' : 'S3_ACCESS_KEY_ID',
+ 'sayauz-web-stage-s3-secret-access-key' : 'S3_SECRET_ACCESS_KEY',
+ 'sayauz-web-stage-mail-salt' : 'MAIL_SALT',
+ 'sayauz-web-stage-mail-host' : 'MAIL_HOST',
+ 'sayauz-web-stage-mail-address' : 'MAIL_ADDRESS',
+ 'sayauz-web-stage-mail-username' : 'MAIL_USERNAME',
+ 'sayauz-web-stage-mail-password' : 'MAIL_PASSWORD'
+ ]
+ def bindings = secretVars.collect { id, varName -> string(credentialsId: id, variable: varName) }
+ bindings << sshUserPrivateKey(credentialsId: 'sayauz-svc-github', keyFileVariable: 'SSH_KEY_FILE', usernameVariable: 'SSH_USER')
+
+ withCredentials(bindings) {
+ sh '''
+ ansible-galaxy collection install community.docker
+
+ mkdir -p ~/.ssh
+ cp "$SSH_KEY_FILE" ~/.ssh/id_ed25519
+ chmod 600 ~/.ssh/id_ed25519
+ ssh-keyscan -H uz.saya.casa >> ~/.ssh/known_hosts
+
+ ssh -o ConnectTimeout=10 -i ~/.ssh/id_ed25519 "$SSH_USER@uz.saya.casa" "echo 'SSH connection successful'"
+
+ cd deploy
+ ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook \
+ playbook.yml \
+ -i inventory.yml \
+ -l stage \
+ --private-key ~/.ssh/id_ed25519 \
+ -u "$SSH_USER" \
+ -e saya_today_web_auth_salt="$AUTH_SALT" \
+ -e saya_today_web_s3_access_key_id="$S3_ACCESS_KEY_ID" \
+ -e saya_today_web_s3_secret_access_key="$S3_SECRET_ACCESS_KEY" \
+ -e saya_today_web_environment=stage \
+ -e saya_today_web_tag=commit-$GIT_COMMIT_SHORT \
+ -e saya_today_web_mail_salt="$MAIL_SALT" \
+ -e saya_today_web_mail_host="$MAIL_HOST" \
+ -e saya_today_web_mail_address="$MAIL_ADDRESS" \
+ -e saya_today_web_mail_username="$MAIL_USERNAME" \
+ -e saya_today_web_mail_password="$MAIL_PASSWORD"
+ '''
+ }
+ }
+ }
+ }
+ }
+ stage('Deploy (Prod)') {
+ when { buildingTag() }
+ agent {
+ kubernetes {
+ defaultContainer 'ansible'
+ yaml """
+apiVersion: v1
+kind: Pod
+metadata:
+ namespace: jenkins
+spec:
+ containers:
+ - name: ansible
+ image: ghcr.io/ansible/community-ansible-dev-tools:v26.7.2
+ command: [ 'cat' ]
+ tty: true
+ resources:
+ requests:
+ cpu: "500m"
+ memory: 1Gi
+"""
+ }
+ }
+ steps {
+ checkout scm
+ container('ansible') {
+ script {
+ def secretVars = [
+ 'sayauz-web-prod-auth-salt' : 'AUTH_SALT',
+ 'sayauz-web-prod-s3-access-key-id' : 'S3_ACCESS_KEY_ID',
+ 'sayauz-web-prod-s3-secret-access-key' : 'S3_SECRET_ACCESS_KEY',
+ 'sayauz-web-prod-mail-salt' : 'MAIL_SALT',
+ 'sayauz-web-prod-mail-host' : 'MAIL_HOST',
+ 'sayauz-web-prod-mail-address' : 'MAIL_ADDRESS',
+ 'sayauz-web-prod-mail-username' : 'MAIL_USERNAME',
+ 'sayauz-web-prod-mail-password' : 'MAIL_PASSWORD',
+ 'sayauz-web-prod-verification-google' : 'GOOGLE_VERIFICATION',
+ 'sayauz-web-prod-verification-yandex' : 'YANDEX_VERIFICATION',
+ 'sayauz-web-prod-verification-bing' : 'BING_VERIFICATION'
+ ]
+ def bindings = secretVars.collect { id, varName -> string(credentialsId: id, variable: varName) }
+ bindings << sshUserPrivateKey(credentialsId: 'sayauz-svc-github', keyFileVariable: 'SSH_KEY_FILE', usernameVariable: 'SSH_USER')
+
+ withCredentials(bindings) {
+ sh '''
+ ansible-galaxy collection install community.docker
+
+ mkdir -p ~/.ssh
+ cp "$SSH_KEY_FILE" ~/.ssh/id_ed25519
+ chmod 600 ~/.ssh/id_ed25519
+ ssh-keyscan -H uz.saya.casa >> ~/.ssh/known_hosts
+
+ ssh -o ConnectTimeout=10 -i ~/.ssh/id_ed25519 "$SSH_USER@uz.saya.casa" "echo 'SSH connection successful'"
+
+ cd deploy
+ ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook \
+ playbook.yml \
+ -i inventory.yml \
+ -l prod \
+ --private-key ~/.ssh/id_ed25519 \
+ -u "$SSH_USER" \
+ -e saya_today_web_auth_salt="$AUTH_SALT" \
+ -e saya_today_web_s3_access_key_id="$S3_ACCESS_KEY_ID" \
+ -e saya_today_web_s3_secret_access_key="$S3_SECRET_ACCESS_KEY" \
+ -e saya_today_web_environment=prod \
+ -e saya_today_web_tag=$TAG_NAME \
+ -e saya_today_web_mail_salt="$MAIL_SALT" \
+ -e saya_today_web_mail_host="$MAIL_HOST" \
+ -e saya_today_web_mail_address="$MAIL_ADDRESS" \
+ -e saya_today_web_mail_username="$MAIL_USERNAME" \
+ -e saya_today_web_mail_password="$MAIL_PASSWORD" \
+ -e saya_today_google_verification="$GOOGLE_VERIFICATION" \
+ -e saya_today_yandex_verification="$YANDEX_VERIFICATION" \
+ -e saya_today_bing_verification="$BING_VERIFICATION"
+ '''
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/config/config.go b/config/config.go
index 0b1570a..9185e6a 100644
--- a/config/config.go
+++ b/config/config.go
@@ -20,9 +20,9 @@ type Config struct {
Auth AuthConfig `json:"Auth" yaml:"auth" validate:"required"`
Mail MailConfig `json:"Mail" yaml:"mail" validate:"required"`
CanonicalEndpoint string `json:"CanonicalEndpoint" yaml:"canonicalEndpoint" validate:"required"`
- Meta MetaConfig `json:"Meta" yaml:"meta"`
+ Meta []MetaConfig `json:"Meta" yaml:"meta"`
PhotoStorage PhotoStorageConfig `json:"PhotoStorage" yaml:"photoStorage"`
- StaticStorage PhotoTypeConfig `json:"StaticStorage" yaml:"staticStorage"`
+ StaticStorage StaticStorageConfig `json:"StaticStorage" yaml:"staticStorage" validate:"required"`
AllowOrigins []string `json:"AllowOrigins" yaml:"allowOrigins"`
}
@@ -237,8 +237,8 @@ type TriggerConfig struct {
}
type MetaConfig struct {
- GoogleSiteVerification string `json:"GoogleSiteVerification" yaml:"googleSiteVerification"`
- YandexVerification string `json:"YandexVerification" yaml:"yandexVerification"`
+ Name string `json:"Name" yaml:"name"`
+ Value string `json:"Value" yaml:"value"`
}
type PhotoStorageConfig struct {
@@ -261,6 +261,16 @@ type HomePageGifsConfig struct {
Indexes []string `json:"Indexes" yaml:"indexes"`
}
+type StaticStorageConfig struct {
+ BaseUrl string `json:"BaseUrl" yaml:"baseUrl" validate:"url,required"`
+ Map MapStorageConfig `json:"Map" yaml:"map" validate:"required"`
+}
+
+type MapStorageConfig struct {
+ BaseUrl string `json:"BaseUrl" yaml:"baseUrl" validate:"url,required"`
+ PMTiles string `json:"PMTiles" yaml:"pmTiles" validate:"required"`
+}
+
func LoadConfig(path string, config *Config) error {
fileBytes, err := os.ReadFile(path)
if err != nil {
diff --git a/config/config.local.yaml b/config/config.local.yaml
index d3e2e33..fefe2e7 100644
--- a/config/config.local.yaml
+++ b/config/config.local.yaml
@@ -30,11 +30,11 @@ localePath: ./locale/
availableLanguages:
- name: ru
alt: Русский
- flag: https://storage.yandexcloud.kz/sayauz-static/flags/ru.svg
+ flag: https://cdn.saya.uz/stats/flags/ru.svg
locFile: localization.ru.yaml
- name: en
alt: English
- flag: https://storage.yandexcloud.kz/sayauz-static/flags/gb.svg
+ flag: https://cdn.saya.uz/stats/flags/gb.svg
locFile: localization.en.yaml
auth:
db:
@@ -56,23 +56,26 @@ mail:
canonicalEndpoint: "http://127.0.0.1:3000"
photoStorage:
full:
- baseUrl: https://storage.yandexcloud.kz/sayauz-photos/%s
+ baseUrl: https://cdn.saya.uz/photos/jpeg-full/%s
webp:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-full/%s.webp
thumbnail1600p:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp1600/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-1600p/%s.webp
thumbnail1200p:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp1200/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-1200p/%s.webp
thumbnail800p:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp800/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-800p/%s.webp
thumbnail560p:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp560/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-560p/%s.webp
thumbnail320p:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp320/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-320p/%s.webp
homePageGifs:
- baseUrl: https://storage.yandexcloud.kz/sayauz-static/home-page-gifs/otter-%s.gif
+ baseUrl: https://cdn.saya.uz/stats/home-page-gifs/otter-%s.gif
indexes: ["1", "2", "3"]
staticStorage:
- baseUrl: https://storage.yandexcloud.kz/sayauz-static
+ baseUrl: https://cdn.saya.uz/stats
+ map:
+ baseUrl: https://cdn.saya.uz/map
+ pmTiles: global.pmtiles
allowOrigins:
- - https://storage.yandexcloud.kz
+ - https://cdn.saya.uz
diff --git a/config/config.prod.yaml b/config/config.prod.yaml
index e5785f8..6ec00d8 100644
--- a/config/config.prod.yaml
+++ b/config/config.prod.yaml
@@ -31,11 +31,11 @@ localePath: ./locale/
availableLanguages:
- name: ru
alt: Русский
- flag: https://storage.yandexcloud.kz/sayauz-static/flags/ru.svg
+ flag: https://cdn.saya.uz/stats/flags/ru.svg
locFile: localization.ru.yaml
- name: en
alt: English
- flag: https://storage.yandexcloud.kz/sayauz-static/flags/gb.svg
+ flag: https://cdn.saya.uz/stats/flags/gb.svg
locFile: localization.en.yaml
auth:
db:
@@ -55,27 +55,34 @@ mail:
onNewPost: "0/5 * * * *"
canonicalEndpoint: "https://${FQDN}"
meta:
- googleSiteVerification: "${GOOGLE_SITE_VERIFICATION}"
- yandexVerification: "${YANDEX_VERIFICATION}"
+ - name: google-site-verification
+ value: "${GOOGLE_SITE_VERIFICATION}"
+ - name: yandex-verification
+ value: "${YANDEX_VERIFICATION}"
+ - name: msvalidate.01
+ value: "${BING_VERIFICATION}"
photoStorage:
full:
- baseUrl: https://storage.yandexcloud.kz/sayauz-photos/%s
+ baseUrl: https://cdn.saya.uz/photos/jpeg-full/%s
webp:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-full/%s.webp
thumbnail1600p:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp1600/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-1600p/%s.webp
thumbnail1200p:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp1200/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-1200p/%s.webp
thumbnail800p:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp800/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-800p/%s.webp
thumbnail560p:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp560/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-560p/%s.webp
thumbnail320p:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp320/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-320p/%s.webp
homePageGifs:
- baseUrl: https://storage.yandexcloud.kz/sayauz-static/home-page-gifs/otter-%s.gif
+ baseUrl: https://cdn.saya.uz/stats/home-page-gifs/otter-%s.gif
indexes: ["1", "2", "3"]
staticStorage:
- baseUrl: https://storage.yandexcloud.kz/sayauz-static
+ baseUrl: https://cdn.saya.uz/stats
+ map:
+ baseUrl: https://cdn.saya.uz/map
+ pmTiles: global.pmtiles
allowOrigins:
- - https://storage.yandexcloud.kz
+ - https://cdn.saya.uz
diff --git a/config/config.stage.yaml b/config/config.stage.yaml
index c2e337b..d8819ca 100644
--- a/config/config.stage.yaml
+++ b/config/config.stage.yaml
@@ -31,11 +31,11 @@ localePath: ./locale/
availableLanguages:
- name: ru
alt: Русский
- flag: https://storage.yandexcloud.kz/sayauz-static/flags/ru.svg
+ flag: https://cdn.saya.uz/stats/flags/ru.svg
locFile: localization.ru.yaml
- name: en
alt: English
- flag: https://storage.yandexcloud.kz/sayauz-static/flags/gb.svg
+ flag: https://cdn.saya.uz/stats/flags/gb.svg
locFile: localization.en.yaml
auth:
db:
@@ -56,23 +56,26 @@ mail:
canonicalEndpoint: "https://${FQDN}"
photoStorage:
full:
- baseUrl: https://storage.yandexcloud.kz/sayauz-photos/%s
+ baseUrl: https://cdn.saya.uz/photos/jpeg-full/%s
webp:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-full/%s.webp
thumbnail1600p:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp1600/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-1600p/%s.webp
thumbnail1200p:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp1200/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-1200p/%s.webp
thumbnail800p:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp800/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-800p/%s.webp
thumbnail560p:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp560/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-560p/%s.webp
thumbnail320p:
- baseUrl: https://storage.yandexcloud.kz/sayauz-thumbnails-webp320/%s.webp
+ baseUrl: https://cdn.saya.uz/photos/webp-320p/%s.webp
homePageGifs:
- baseUrl: https://storage.yandexcloud.kz/sayauz-static/home-page-gifs/otter-%s.gif
+ baseUrl: https://cdn.saya.uz/stats/home-page-gifs/otter-%s.gif
indexes: ["1", "2", "3"]
staticStorage:
- baseUrl: https://storage.yandexcloud.kz/sayauz-static
+ baseUrl: https://cdn.saya.uz/stats
+ map:
+ baseUrl: https://cdn.saya.uz/map
+ pmTiles: global.pmtiles
allowOrigins:
- - https://storage.yandexcloud.kz
+ - https://cdn.saya.uz
diff --git a/deploy/playbook.yml b/deploy/playbook.yml
index ae3e936..057cc19 100644
--- a/deploy/playbook.yml
+++ b/deploy/playbook.yml
@@ -6,7 +6,7 @@
vars:
docker_volumes:
- "{{ saya_today_web_name }}-volume:/data:rw"
-
+
tasks:
- name: Initialize Unix socket volume
when: >-
@@ -26,11 +26,11 @@
community.docker.docker_volume:
name: "{{ saya_today_web_name }}-volume"
- - name: Deploy saya-today-web Docker Container
+ - name: Deploy sayauz/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 }}"
+ image: "registry.sayag.it/sayauz/web:{{ saya_today_web_tag }}"
env:
S3_ACCESS_KEY_ID: "{{ saya_today_web_s3_access_key_id }}"
S3_SECRET_ACCESS_KEY: "{{ saya_today_web_s3_secret_access_key }}"
@@ -42,8 +42,9 @@
MAIL_PASSWORD: "{{ saya_today_web_mail_password }}"
MAIL_SALT: "{{ saya_today_web_mail_salt }}"
FQDN: "{{ saya_today_web_listen_address }}"
- GOOGLE_SITE_VERIFICATION: "{{ saya_today_google_site_verification | default('') }}"
+ GOOGLE_VERIFICATION: "{{ saya_today_google_verification | default('') }}"
YANDEX_VERIFICATION: "{{ saya_today_yandex_verification | default('') }}"
+ BING_VERIFICATION: "{{ saya_today_bing_verification | default('') }}"
network_mode: caddy
networks:
- name: caddy
diff --git a/go.mod b/go.mod
index 779e641..4511ddb 100644
--- a/go.mod
+++ b/go.mod
@@ -5,15 +5,15 @@ go 1.26
require (
github.com/Backblaze/blazer v0.7.2
github.com/dgraph-io/ristretto/v2 v2.4.0
- github.com/go-co-op/gocron/v2 v2.21.1
+ github.com/go-co-op/gocron/v2 v2.21.2
github.com/go-playground/validator/v10 v10.30.2
github.com/gofiber/fiber/v2 v2.52.13
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/mattn/go-sqlite3 v1.14.44
- github.com/valyala/fasthttp v1.70.0
- github.com/wneessen/go-mail v0.7.2
+ github.com/valyala/fasthttp v1.71.0
+ github.com/wneessen/go-mail v0.7.3
github.com/yuin/goldmark v1.8.2
- golang.org/x/crypto v0.50.0
+ golang.org/x/crypto v0.51.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -40,7 +40,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect
- github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1
+ github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0
github.com/aws/smithy-go v1.25.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
@@ -58,6 +58,6 @@ require (
github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
- golang.org/x/sys v0.43.0 // indirect
- golang.org/x/text v0.36.0 // indirect
+ golang.org/x/sys v0.44.0 // indirect
+ golang.org/x/text v0.37.0 // indirect
)
diff --git a/go.sum b/go.sum
index d277091..e6c5284 100644
--- a/go.sum
+++ b/go.sum
@@ -26,8 +26,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/ku
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1 h1:mxuT1xE+dI54NW3RkNjP8DUT5HXqbkiAFvfdyDFwE5c=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
@@ -53,8 +53,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
-github.com/go-co-op/gocron/v2 v2.21.1 h1:QYOK6iOQVCut+jDcs4zRdWRTBHRxRCEeeFi1TnAmgbU=
-github.com/go-co-op/gocron/v2 v2.21.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
+github.com/go-co-op/gocron/v2 v2.21.2 h1:bD8/YwkojYHgXFr3iEulL148KBdTbKVxUZzFKpXcdbY=
+github.com/go-co-op/gocron/v2 v2.21.2/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -104,22 +104,22 @@ github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=
-github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=
-github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
-github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
+github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
+github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
+github.com/wneessen/go-mail v0.7.3 h1:g3DravXC5SMlVdboFrQA8Jx95A8sOzoBeS5F+vzNRK0=
+github.com/wneessen/go-mail v0.7.3/go.mod h1:QGhBX0yNbc1J+Mkjcu7z2rpj4B4l+BmDY8gYznPC9sk=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
-golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
-golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
-golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
-golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
-golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
+golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
+golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
+golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
+golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
+golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/internal/blog/b2.go b/internal/blog/b2.go
index 0caa0ba..f2ac890 100644
--- a/internal/blog/b2.go
+++ b/internal/blog/b2.go
@@ -2,6 +2,7 @@ package blog
import (
"context"
+ "encoding/json"
"fmt"
"slices"
"strings"
@@ -37,6 +38,20 @@ func NewB2Client(cfg *config.StorageConfig) (Client, error) {
return &B2Client{b2cl: b2cl, bucket: bucket, prefix: b2cfg.Prefix}, nil
}
+func (c *B2Client) GetMedleys() ([]MedleyEntry, error) {
+ idxRaw, err := c.readAll(MedleysIndexFileName)
+ if err != nil {
+ return nil, fmt.Errorf("read %s: %w", MedleysIndexFileName, err)
+ }
+
+ var idx []MedleyEntry
+ if err := json.Unmarshal(idxRaw, &idx); err != nil {
+ return nil, fmt.Errorf("unmarshal %s: %w", MedleysIndexFileName, err)
+ }
+
+ return idx, nil
+}
+
func (c *B2Client) Scan(prefix string) ([]*Page, error) {
filePaths := []*Page{}
@@ -70,17 +85,15 @@ func (c *B2Client) Scan(prefix string) ([]*Page, error) {
return nil, fmt.Errorf("failed to parse published time metadata field: %w", err)
}
- linkParts := strings.Split(obj.Name(), "/")
- nameParts := strings.Split(linkParts[len(linkParts)-1], ".")
- fileName := strings.Join(nameParts[:len(linkParts)-1], ".")
-
+ link := obj.Name()
+ fileName := link[strings.LastIndex(link, "/")+1 : strings.LastIndex(link, ".")]
tags := strings.Split(attrs.Info["tags"], ",")
slices.Sort(tags)
- lang, _ := strings.CutPrefix(linkParts[0], c.prefix)
+ lang, _ := strings.CutPrefix(link[0:strings.Index(link, "/")], c.prefix)
filePaths = append(filePaths, &Page{
- Link: obj.Name(),
+ Link: link,
FileName: fileName,
Lang: lang,
ModifiedTime: attrs.LastModified,
@@ -104,7 +117,11 @@ func (c *B2Client) Scan(prefix string) ([]*Page, error) {
}
func (c *B2Client) ReadAll(path string) ([]byte, error) {
- obj := c.bucket.Object(c.prefix + path)
+ return c.readAll(c.prefix + path)
+}
+
+func (c *B2Client) readAll(path string) ([]byte, error) {
+ obj := c.bucket.Object(path)
if obj == nil {
return nil, fmt.Errorf("failed to reference object in B2 bucket")
}
diff --git a/internal/blog/client.go b/internal/blog/client.go
index 56bbee4..53eb0fd 100644
--- a/internal/blog/client.go
+++ b/internal/blog/client.go
@@ -17,6 +17,7 @@ type Page struct {
type Client interface {
Scan(prefix string) ([]*Page, error)
+ GetMedleys() ([]MedleyEntry, error)
ReadAll(path string) ([]byte, error)
ReadFrontmatter(path string) (metadata *frontmatter.Metadata, markdown []byte, err error)
}
diff --git a/internal/blog/index.go b/internal/blog/index.go
index 861a90e..a09b59e 100644
--- a/internal/blog/index.go
+++ b/internal/blog/index.go
@@ -1,10 +1,17 @@
package blog
-import "time"
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/SayaAndy/saya-today-web/internal/frontmatter"
+)
const IndexFileName = "index.json"
+const MedleysIndexFileName = "medleys.json"
-const IndexSchemaVersion = 1
+const IndexSchemaVersion = 2
type IndexEntry struct {
Link string `json:"link"`
@@ -20,13 +27,77 @@ type IndexEntry struct {
MedleyPart int `json:"medleyPart,omitempty"`
}
-type IndexCategory struct {
+func (e IndexEntry) Metadata() *frontmatter.Metadata {
+ return &frontmatter.Metadata{
+ Title: e.Title,
+ ShortDescription: e.ShortDescription,
+ ActionDate: e.ActionDate,
+ PublishedTime: e.PublishedTime,
+ Thumbnail: e.Thumbnail,
+ Tags: e.Tags,
+ Geolocation: e.Geolocation,
+ Medley: e.Medley,
+ MedleyPart: e.MedleyPart,
+ }
+}
+
+type IndexV2Category struct {
+ GeneratedAt time.Time `json:"generatedAt"`
+ Pages map[string]IndexEntry `json:"pages"`
+}
+
+type IndexV1Category struct {
GeneratedAt time.Time `json:"generatedAt"`
Pages []IndexEntry `json:"pages"`
}
type Index struct {
- SchemaVersion int `json:"schemaVersion"`
- GeneratedAt time.Time `json:"generatedAt"`
- Categories map[string]IndexCategory `json:"categories"`
+ SchemaVersion int `json:"schemaVersion"`
+ GeneratedAt time.Time `json:"generatedAt"`
+ Categories any `json:"categories"`
+}
+
+func (idx *Index) UnmarshalJSON(data []byte) error {
+ var tmp struct {
+ SchemaVersion int `json:"schemaVersion"`
+ GeneratedAt time.Time `json:"generatedAt"`
+ Categories json.RawMessage `json:"categories"`
+ }
+
+ if err := json.Unmarshal(data, &tmp); err != nil {
+ return err
+ }
+
+ idx.SchemaVersion = tmp.SchemaVersion
+ idx.GeneratedAt = tmp.GeneratedAt
+
+ switch tmp.SchemaVersion {
+ case 1:
+ var categories map[string]*IndexV1Category
+ if err := json.Unmarshal(tmp.Categories, &categories); err != nil {
+ return fmt.Errorf("unmarshal map[string]*IndexV1Category: %w", err)
+ }
+ idx.Categories = &categories
+ case 2:
+ var categories map[string]*IndexV2Category
+ if err := json.Unmarshal(tmp.Categories, &categories); err != nil {
+ return fmt.Errorf("unmarshal map[string]*IndexV2Category: %w", err)
+ }
+ idx.Categories = &categories
+ default:
+ return fmt.Errorf("unsupported index version: %d", tmp.SchemaVersion)
+ }
+
+ return nil
+}
+
+type MedleyEntry struct {
+ Codename string `json:"codename"`
+ Localnames map[string]string `json:"localnames"`
+ Content []string `json:"content"`
+}
+
+type MedleyPageEntry struct {
+ Codename string `json:"codename"`
+ Position int `json:"position"`
}
diff --git a/internal/blog/s3.go b/internal/blog/s3.go
index bbd5238..3c1a932 100644
--- a/internal/blog/s3.go
+++ b/internal/blog/s3.go
@@ -1,29 +1,22 @@
package blog
import (
+ "bytes"
"context"
"encoding/json"
- "errors"
"fmt"
"io"
- "log/slog"
- "net/url"
- "slices"
"strings"
- "sync"
- "time"
"github.com/SayaAndy/saya-today-web/config"
"github.com/SayaAndy/saya-today-web/internal/frontmatter"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
- s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
)
-const s3ScanConcurrency = 32
-
type S3Client struct {
prefix string
bucketName string
@@ -66,39 +59,38 @@ func NewS3Client(cfg *config.StorageConfig) (Client, error) {
return &S3Client{s3cfg.Prefix, s3cfg.BucketName, s3cl}, nil
}
-func (c *S3Client) Scan(prefix string) ([]*Page, error) {
- pages, err := c.scanFromIndex(prefix)
- if err == nil {
- return pages, nil
+func (c *S3Client) GetMedleys() ([]MedleyEntry, error) {
+ idxRaw, err := c.readAll(MedleysIndexFileName)
+ if err != nil {
+ return nil, fmt.Errorf("read %s: %w", MedleysIndexFileName, err)
}
- var nsk *s3types.NoSuchKey
- if !errors.As(err, &nsk) {
- return nil, err
+ var idx []MedleyEntry
+ if err := json.Unmarshal(idxRaw, &idx); err != nil {
+ return nil, fmt.Errorf("unmarshal %s: %w", MedleysIndexFileName, err)
}
- slog.Warn("index.json missing, falling back to listing", slog.String("prefix", c.prefix))
- return c.scanByListing(prefix)
+ return idx, nil
}
-func (c *S3Client) scanFromIndex(prefix string) ([]*Page, error) {
+func (c *S3Client) Scan(prefix string) ([]*Page, error) {
out, err := c.s3cl.GetObject(context.Background(), &s3.GetObjectInput{
Bucket: aws.String(c.bucketName),
Key: aws.String(IndexFileName),
})
if err != nil {
- return nil, fmt.Errorf("get index.json: %w", err)
+ return nil, fmt.Errorf("get %s: %w", IndexFileName, err)
}
defer out.Body.Close()
raw, err := io.ReadAll(out.Body)
if err != nil {
- return nil, fmt.Errorf("read index.json: %w", err)
+ return nil, fmt.Errorf("read %s: %w", IndexFileName, err)
}
var idx Index
if err := json.Unmarshal(raw, &idx); err != nil {
- return nil, fmt.Errorf("unmarshal index.json: %w", err)
+ return nil, fmt.Errorf("unmarshal %s: %w", IndexFileName, err)
}
wantLang := ""
@@ -108,160 +100,93 @@ func (c *S3Client) scanFromIndex(prefix string) ([]*Page, error) {
fullPrefix := c.prefix + prefix
pages := make([]*Page, 0)
- for catKey, cat := range idx.Categories {
- lang, ok := strings.CutPrefix(catKey, c.prefix)
- if !ok {
- continue
- }
- if wantLang != "" && wantLang != lang {
- continue
- }
- for _, e := range cat.Pages {
- if !strings.HasPrefix(e.Link, fullPrefix) {
+
+ switch idx.SchemaVersion {
+ case 1:
+ for catKey, cat := range *idx.Categories.(*map[string]*IndexV1Category) {
+ lang, ok := strings.CutPrefix(catKey, c.prefix)
+ if !ok {
continue
}
- linkParts := strings.Split(e.Link, "/")
- nameParts := strings.Split(linkParts[len(linkParts)-1], ".")
- fileName := strings.Join(nameParts[:len(nameParts)-1], ".")
- pages = append(pages, &Page{
- Link: e.Link,
- FileName: fileName,
- Lang: lang,
- ModifiedTime: e.ModifiedTime,
- Metadata: &frontmatter.Metadata{
- Title: e.Title,
- ShortDescription: e.ShortDescription,
- ActionDate: e.ActionDate,
- PublishedTime: e.PublishedTime,
- Thumbnail: e.Thumbnail,
- Tags: e.Tags,
- Geolocation: e.Geolocation,
- Medley: e.Medley,
- MedleyPart: e.MedleyPart,
- },
- })
- }
- }
- return pages, nil
-}
-
-func (c *S3Client) scanByListing(prefix string) ([]*Page, error) {
- fullPrefix := c.prefix + prefix
-
- type candidate struct {
- key string
- lastModified time.Time
- }
- var candidates []candidate
-
- paginator := s3.NewListObjectsV2Paginator(c.s3cl, &s3.ListObjectsV2Input{
- Bucket: aws.String(c.bucketName),
- Prefix: aws.String(fullPrefix),
- })
- for paginator.HasMorePages() {
- output, err := paginator.NextPage(context.Background())
- if err != nil {
- return nil, fmt.Errorf("list S3 objects: %w", err)
- }
- for _, obj := range output.Contents {
- key := aws.ToString(obj.Key)
- if !strings.HasSuffix(key, ".md") {
+ if wantLang != "" && wantLang != lang {
continue
}
- candidates = append(candidates, candidate{key, aws.ToTime(obj.LastModified)})
- }
- }
-
- pages := make([]*Page, len(candidates))
- sem := make(chan struct{}, s3ScanConcurrency)
- var wg sync.WaitGroup
- var firstErr error
- var errMu sync.Mutex
-
- for i, cand := range candidates {
- sem <- struct{}{}
- wg.Go(func() {
- defer func() { <-sem }()
-
- head, err := c.s3cl.HeadObject(context.Background(), &s3.HeadObjectInput{
- Bucket: aws.String(c.bucketName),
- Key: aws.String(cand.key),
- })
- if err != nil {
- errMu.Lock()
- if firstErr == nil {
- firstErr = fmt.Errorf("head S3 object %s: %w", cand.key, err)
+ for _, e := range cat.Pages {
+ if !strings.HasPrefix(e.Link, fullPrefix) {
+ continue
}
- errMu.Unlock()
- return
+ fileName := e.Link[strings.LastIndex(e.Link, "/")+1 : strings.LastIndex(e.Link, ".")]
+ pages = append(pages, &Page{
+ Link: e.Link,
+ FileName: fileName,
+ Lang: lang,
+ ModifiedTime: e.ModifiedTime,
+ Metadata: &frontmatter.Metadata{
+ Title: e.Title,
+ ShortDescription: e.ShortDescription,
+ ActionDate: e.ActionDate,
+ PublishedTime: e.PublishedTime,
+ Thumbnail: e.Thumbnail,
+ Tags: e.Tags,
+ Geolocation: e.Geolocation,
+ Medley: e.Medley,
+ MedleyPart: e.MedleyPart,
+ },
+ })
}
-
- if head.ContentType == nil || !strings.Contains(*head.ContentType, "text/markdown") {
- return
+ }
+ case 2:
+ for catKey, cat := range *idx.Categories.(*map[string]*IndexV2Category) {
+ lang, ok := strings.CutPrefix(catKey, c.prefix)
+ if !ok {
+ continue
}
- meta := head.Metadata
- if meta["title"] == "" {
- return
+ if wantLang != "" && wantLang != lang {
+ continue
}
-
- publishedTime, err := time.Parse(time.RFC3339, meta["published-time"])
- if err != nil {
- errMu.Lock()
- if firstErr == nil {
- firstErr = fmt.Errorf("failed to parse published time metadata field: %w", err)
+ for codename, e := range cat.Pages {
+ if !strings.HasPrefix(e.Link, fullPrefix) {
+ continue
}
- errMu.Unlock()
- return
+ pages = append(pages, &Page{
+ Link: e.Link,
+ FileName: codename,
+ Lang: lang,
+ ModifiedTime: e.ModifiedTime,
+ Metadata: &frontmatter.Metadata{
+ Title: e.Title,
+ ShortDescription: e.ShortDescription,
+ ActionDate: e.ActionDate,
+ PublishedTime: e.PublishedTime,
+ Thumbnail: e.Thumbnail,
+ Tags: e.Tags,
+ Geolocation: e.Geolocation,
+ Medley: e.Medley,
+ MedleyPart: e.MedleyPart,
+ },
+ })
}
-
- linkParts := strings.Split(cand.key, "/")
- nameParts := strings.Split(linkParts[len(linkParts)-1], ".")
- fileName := strings.Join(nameParts[:len(nameParts)-1], ".")
- lang, _ := strings.CutPrefix(linkParts[0], c.prefix)
-
- tags := strings.Split(meta["tags"], ",")
- slices.Sort(tags)
-
- title, _ := url.QueryUnescape(meta["title"])
- shortDescription, _ := url.QueryUnescape(meta["short-description"])
- thumbnail, _ := url.QueryUnescape(meta["thumbnail"])
-
- pages[i] = &Page{
- Link: cand.key,
- FileName: fileName,
- Lang: lang,
- ModifiedTime: cand.lastModified,
- Metadata: &frontmatter.Metadata{
- Title: title,
- ShortDescription: shortDescription,
- ActionDate: meta["action-date"],
- PublishedTime: publishedTime,
- Thumbnail: thumbnail,
- Tags: tags,
- Geolocation: meta["geolocation"],
- },
- }
- })
- }
- wg.Wait()
-
- if firstErr != nil {
- return nil, firstErr
+ }
}
- out := pages[:0]
- for _, p := range pages {
- if p != nil {
- out = append(out, p)
+ medleys, _ := c.GetMedleys()
+ for _, medley := range medleys {
+ for locale, localname := range medley.Localnames {
+ l10n.T.SetPath(localname, true, locale, "Medleys", medley.Codename)
}
}
- return out, nil
+
+ return pages, nil
}
func (c *S3Client) ReadAll(path string) ([]byte, error) {
+ return c.readAll(c.prefix + path)
+}
+
+func (c *S3Client) readAll(path string) ([]byte, error) {
output, err := c.s3cl.GetObject(context.Background(), &s3.GetObjectInput{
Bucket: aws.String(c.bucketName),
- Key: aws.String(c.prefix + path),
+ Key: aws.String(path),
})
if err != nil {
return nil, fmt.Errorf("get S3 object: %w", err)
@@ -277,10 +202,40 @@ func (c *S3Client) ReadAll(path string) ([]byte, error) {
}
func (c *S3Client) ReadFrontmatter(path string) (metadata *frontmatter.Metadata, markdown []byte, err error) {
+ idxRaw, err := c.readAll(IndexFileName)
+ if err != nil {
+ return nil, nil, fmt.Errorf("read %s: %w", IndexFileName, err)
+ }
+
+ var idx Index
+ if err := json.Unmarshal(idxRaw, &idx); err != nil {
+ return nil, nil, fmt.Errorf("unmarshal %s: %w", IndexFileName, err)
+ }
+
contentBytes, err := c.ReadAll(path)
if err != nil {
return nil, nil, fmt.Errorf("failed to read file for frontmatter parsing: %w", err)
}
+ switch idx.SchemaVersion {
+ case 1:
+ return frontmatter.ParseFrontmatter(contentBytes)
+ case 2:
+ fullPath := c.prefix + path
+ page := (*idx.Categories.(*map[string]*IndexV2Category))[fullPath[:strings.LastIndex(fullPath, "/")]].Pages[fullPath[strings.LastIndex(fullPath, "/")+1:strings.LastIndex(fullPath, ".")]]
+ metadata = page.Metadata()
+
+ if !bytes.HasPrefix(contentBytes, []byte("---\n")) {
+ return metadata, contentBytes, nil
+ }
+
+ end := bytes.Index(contentBytes[4:], []byte("\n---\n"))
+ if end == -1 {
+ return metadata, contentBytes, nil
+ }
+
+ return metadata, contentBytes[end+9:], nil
+ }
+
return frontmatter.ParseFrontmatter(contentBytes)
}
diff --git a/internal/glightbox/html_renderer.go b/internal/glightbox/html_renderer.go
index b2cd855..1bb606b 100644
--- a/internal/glightbox/html_renderer.go
+++ b/internal/glightbox/html_renderer.go
@@ -151,12 +151,12 @@ func (r *GLightboxHTMLRenderer) renderGLightbox(w util.BufWriter, source []byte,
w.WriteString(strings.ReplaceAll(`
<div class="items-center flex flex-col">
- <hr class="border-t-3 border-dotted border-main-hard mt-1 mb-2 w-[80%] ml-auto mr-auto">
+ <hr class="border-t-[0.375rem] border-dotted border-main-hard my-2 w-24 ml-auto mr-auto">
<div class="grid masonry-grid-{id}">
<div class="grid-sizer grid-sizer-{id}"></div>
`+strings.Join(elements, "\n")+`
</div>
- <hr class="border-t-3 border-dotted border-main-hard mt-1 mb-2 w-[80%] ml-auto mr-auto">
+ <hr class="border-t-[0.375rem] border-dotted border-main-hard my-2 w-24 ml-auto mr-auto">
</div>
<script>
diff --git a/internal/mailer/mailer.go b/internal/mailer/mailer.go
index ffa7b30..33db4f7 100644
--- a/internal/mailer/mailer.go
+++ b/internal/mailer/mailer.go
@@ -16,7 +16,7 @@ import (
"github.com/SayaAndy/saya-today-web/internal/blog"
"github.com/SayaAndy/saya-today-web/internal/templatemanager"
- "github.com/SayaAndy/saya-today-web/locale"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/dgraph-io/ristretto/v2"
"github.com/gofiber/fiber/v2"
"github.com/wneessen/go-mail"
@@ -43,8 +43,6 @@ type Mailer struct {
hashMap map[string][]byte
hashMapMutex sync.RWMutex
-
- l map[string]*locale.LocaleConfig
}
type SubscriptionType int
@@ -55,7 +53,7 @@ const (
Specific
)
-func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string, mailAddress string, username string, password string, salt []byte, localization map[string]*locale.LocaleConfig) (*Mailer, error) {
+func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string, mailAddress string, username string, password string, salt []byte) (*Mailer, error) {
verificationCodes, err := ristretto.NewCache(&ristretto.Config[uint64, string]{
NumCounters: 10000,
MaxCost: 1 << 20, // 1 MB
@@ -111,7 +109,7 @@ func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string
End time.Time
CodeExpiry time.Time
}, 0),
- l: localization}, nil
+ }, nil
}
func (m *Mailer) GetHash(id string) []byte {
@@ -261,10 +259,9 @@ func (m *Mailer) SendVerificationCode(userId string, address string, lang string
verificationInfo := fmt.Sprintf("%s.%s", base64.RawStdEncoding.EncodeToString([]byte(userId)), base64.RawStdEncoding.EncodeToString([]byte(address)))
m.verificationCodes.Set(verificationCode, verificationInfo, int64(len(verificationInfo)+8))
- message.Subject(m.l[lang].Mail.VerifyEmail.Subject)
+ message.Subject(l10n.T.GetPath(lang, "Mail", "VerifyEmail", "Subject").(string))
msg, err := m.tm.Render("verify-email", fiber.Map{
- "L": m.l[lang],
"Lang": lang,
"VerificationCode": fmt.Sprintf("%X", verificationCode),
"ClientHost": m.clientHost,
@@ -499,11 +496,10 @@ rowLoop:
rand.Read(unsubscribeCodeBytes)
unsubscribeCode := binary.LittleEndian.Uint64(unsubscribeCodeBytes)
- unsubscribeFooter := strings.Replace(m.l[post.Lang].Mail.UnsubscribeFooter, "{}", fmt.Sprintf(`<a style="color: #273de1 !important;" href="https://%s/%s/user/unsubscribe?code=%X">`, m.clientHost, post.Lang, unsubscribeCode), 1)
+ unsubscribeFooter := strings.Replace(l10n.T.GetPath(post.Lang, "Mail", "UnsubscribeFooter").(string), "{}", fmt.Sprintf(`<a style="color: #273de1 !important;" href="https://%s/%s/user/unsubscribe?code=%X">`, m.clientHost, post.Lang, unsubscribeCode), 1)
unsubscribeFooter = strings.Replace(unsubscribeFooter, "{/}", "</a>", 1)
msgBody, err := m.tm.Render("new-post", fiber.Map{
- "L": m.l[post.Lang],
"Lang": post.Lang,
"Post": post,
"ClientHost": m.clientHost,
@@ -529,7 +525,7 @@ rowLoop:
message.SetMessageID()
message.SetDate()
message.SetBulk()
- message.Subject(m.l[post.Lang].Mail.NewPost.Subject)
+ message.Subject(l10n.T.GetPath(post.Lang, "Mail", "NewPost", "Subject").(string))
message.SetBodyString(mail.TypeTextHTML, string(msgBody))
m.unsubscribeCodes.Set(unsubscribeCode, user.userId, 40)
diff --git a/internal/router/handlers/api-v1-blog-search.go b/internal/router/handlers/api-v1-blog-search.go
index 38004a1..b59f09a 100644
--- a/internal/router/handlers/api-v1-blog-search.go
+++ b/internal/router/handlers/api-v1-blog-search.go
@@ -1,13 +1,13 @@
package handlers
import (
+ "cmp"
"encoding/json"
"fmt"
"log/slog"
"net/url"
"regexp"
"slices"
- "strings"
"time"
"github.com/SayaAndy/saya-today-web/internal/blog"
@@ -51,6 +51,10 @@ func (r *BlogSearchHandler) RateLimiter() *fiber.Handler {
func (r *BlogSearchHandler) Render(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
sort := c.Query("sort")
tz := c.Query("tz")
+ medley := c.Query("medley")
+ highlight := c.Query("highlight")
+ hideTags := c.QueryBool("hideTags", false)
+ hidePublishedTime := c.QueryBool("hidePublishedTime", false)
loc, err := time.LoadLocation(tz)
if err != nil {
@@ -85,21 +89,26 @@ func (r *BlogSearchHandler) Render(c *fiber.Ctx, supplements *router.Supplements
for _, page := range pages {
for _, tag := range page.Metadata.Tags {
if len(tags) == 0 || slices.Contains(tags, tag) {
- pageMeta = append(pageMeta, fiber.Map{
- "Link": page.Link,
- "ArticleLink": "/" + lang + "/blog/" + page.FileName,
- "Title": page.Metadata.Title,
- "PublishedTime": page.Metadata.PublishedTime.In(loc).Format("2006-01-02 15:04:05 -07:00"),
- "ActionDate": page.Metadata.ActionDate,
- "ShortDescription": page.Metadata.ShortDescription,
- "Thumbnail": page.Metadata.Thumbnail,
- "Tags": page.Metadata.Tags,
- "LikeCount": supplements.ClientCache.GetLikeCount(page.FileName),
- "Liked": supplements.ClientCache.GetLikeStatus(c.IP(), page.FileName),
- "ViewCount": supplements.ClientCache.GetViewCount(page.FileName),
- "Viewed": supplements.ClientCache.GetViewStatus(c.IP(), page.FileName),
- })
- break
+ if medley == "" || medley == page.Metadata.Medley {
+ pageMeta = append(pageMeta, fiber.Map{
+ "Link": page.Link,
+ "ArticleLink": "/" + lang + "/blog/" + page.FileName,
+ "Title": page.Metadata.Title,
+ "PublishedTime": page.Metadata.PublishedTime.In(loc).Format("2006-01-02 15:04:05 -07:00"),
+ "ActionDate": page.Metadata.ActionDate,
+ "ShortDescription": page.Metadata.ShortDescription,
+ "Thumbnail": page.Metadata.Thumbnail,
+ "Tags": page.Metadata.Tags,
+ "LikeCount": supplements.ClientCache.GetLikeCount(page.FileName),
+ "Liked": supplements.ClientCache.GetLikeStatus(c.IP(), page.FileName),
+ "ViewCount": supplements.ClientCache.GetViewCount(page.FileName),
+ "Viewed": supplements.ClientCache.GetViewStatus(c.IP(), page.FileName),
+ "Medley": page.Metadata.Medley,
+ "MedleyPart": page.Metadata.MedleyPart,
+ "ToHighlight": page.FileName == highlight,
+ })
+ break
+ }
}
}
}
@@ -107,13 +116,13 @@ func (r *BlogSearchHandler) Render(c *fiber.Ctx, supplements *router.Supplements
slices.SortFunc(pageMeta, func(a, b fiber.Map) int {
switch sort {
case "titleAsc":
- return strings.Compare(a["Title"].(string), b["Title"].(string))
+ return cmp.Compare(a["Title"].(string), b["Title"].(string))
case "titleDesc":
- return strings.Compare(b["Title"].(string), a["Title"].(string))
+ return cmp.Compare(b["Title"].(string), a["Title"].(string))
case "actionDateAsc":
- return strings.Compare(a["ActionDate"].(string), b["ActionDate"].(string))
+ return cmp.Compare(a["ActionDate"].(string), b["ActionDate"].(string))
case "actionDateDesc":
- return strings.Compare(b["ActionDate"].(string), a["ActionDate"].(string))
+ return cmp.Compare(b["ActionDate"].(string), a["ActionDate"].(string))
case "publicationDateAsc":
publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -07:00", a["PublishedTime"].(string))
publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", b["PublishedTime"].(string))
@@ -122,11 +131,15 @@ func (r *BlogSearchHandler) Render(c *fiber.Ctx, supplements *router.Supplements
publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -07:00", a["PublishedTime"].(string))
publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", b["PublishedTime"].(string))
return publishedTimeB.Compare(publishedTimeA)
+ case "medley":
+ return cmp.Compare(a["MedleyPart"].(int), b["MedleyPart"].(int))
}
return 0
})
templateMap["BlogPages"] = pageMeta
+ templateMap["HideTags"] = hideTags
+ templateMap["HidePublishedTime"] = hidePublishedTime
return fiber.StatusOK, nil
}
diff --git a/internal/router/handlers/api-v1-email-is-in-verification.go b/internal/router/handlers/api-v1-email-is-in-verification.go
index 4f05f48..b8ae162 100644
--- a/internal/router/handlers/api-v1-email-is-in-verification.go
+++ b/internal/router/handlers/api-v1-email-is-in-verification.go
@@ -6,6 +6,7 @@ import (
"strings"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -49,7 +50,7 @@ func (r *OngoingVerificationHandler) Render(c *fiber.Ctx, supplements *router.Su
if !isAllowed {
sterileDataset["striked-end-time"] = template.HTMLAttr(fmt.Sprintf("data-striked-end-time=\"%d\"", whenAllowed.UnixMilli()))
templateMap["Status"] = "Neutral"
- templateMap["Message"] = strings.ReplaceAll(supplements.Localization[lang].UserProfile.DelayTilVerification, "{}", whenAllowed.Format("2006-01-02 15:04:05 MST"))
+ templateMap["Message"] = strings.ReplaceAll(l10n.T.GetPath(lang, "UserProfile", "DelayTilVerification").(string), "{}", whenAllowed.Format("2006-01-02 15:04:05 MST"))
} else {
templateMap["Status"] = "OK"
templateMap["Message"] = ""
diff --git a/internal/router/handlers/api-v1-email-send-verification-code.go b/internal/router/handlers/api-v1-email-send-verification-code.go
index 0b53efa..ea8df4d 100644
--- a/internal/router/handlers/api-v1-email-send-verification-code.go
+++ b/internal/router/handlers/api-v1-email-send-verification-code.go
@@ -7,6 +7,7 @@ import (
"strings"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -49,27 +50,27 @@ func (r *SendVerificationCodeHandler) Render(c *fiber.Ctx, supplements *router.S
email := c.FormValue("email")
if email == "" {
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.EmailEmpty
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "EmailEmpty").(string)
return fiber.StatusUnprocessableEntity, nil
}
isTaken, err := supplements.Mailer.MailIsTaken(email)
if err != nil {
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationCodeSendingError
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationCodeSendingError").(string)
slog.Error("failed to check if address is already taken", slog.String("error", err.Error()))
return fiber.StatusUnprocessableEntity, nil
}
if isTaken {
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.EmailTaken
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "EmailTaken").(string)
return fiber.StatusUnprocessableEntity, nil
}
if isAllowed, whenAllowed, _ := supplements.Mailer.IsAllowedToRetryVerification(id); !isAllowed {
templateMap["Status"] = "Failed"
- templateMap["Message"] = strings.ReplaceAll(supplements.Localization[lang].UserProfile.DelayTilVerification, "{}", whenAllowed.Format("2006-01-02 15:04:05 MST"))
+ templateMap["Message"] = strings.ReplaceAll(l10n.T.GetPath(lang, "UserProfile", "DelayTilVerification").(string), "{}", whenAllowed.Format("2006-01-02 15:04:05 MST"))
templateMap["DataAttributes"] = map[string]any{
"striked-end-time": template.HTMLAttr(fmt.Sprintf("data-striked-end-time=\"%d\"", whenAllowed.UnixMilli())),
}
@@ -78,20 +79,20 @@ func (r *SendVerificationCodeHandler) Render(c *fiber.Ctx, supplements *router.S
if previousEmail, _, _ := supplements.Mailer.GetInfo(supplements.Mailer.GetHash(id)); previousEmail == email {
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.EmailAlreadyValidated
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "EmailAlreadyValidated").(string)
return fiber.StatusUnprocessableEntity, nil
}
if err = supplements.Mailer.SendVerificationCode(id, email, lang); err != nil {
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationCodeSendingError
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationCodeSendingError").(string)
slog.Error("failed to send a verification code", slog.String("error", err.Error()))
return fiber.StatusUnprocessableEntity, nil
}
_, endTime, codeExpiry := supplements.Mailer.IsAllowedToRetryVerification(id)
templateMap["Status"] = "OK"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationCodeSent
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationCodeSent").(string)
templateMap["DataAttributes"] = map[string]any{
"striked-end-time": template.HTMLAttr(fmt.Sprintf("data-striked-end-time=\"%d\"", endTime.UnixMilli())),
"code-expiry-time": template.HTMLAttr(fmt.Sprintf("data-code-expiry-time=\"%d\"", codeExpiry.UnixMilli())),
diff --git a/internal/router/handlers/api-v1-email-verify.go b/internal/router/handlers/api-v1-email-verify.go
index ace1870..65ad6d9 100644
--- a/internal/router/handlers/api-v1-email-verify.go
+++ b/internal/router/handlers/api-v1-email-verify.go
@@ -5,6 +5,7 @@ import (
"log/slog"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -46,19 +47,19 @@ func (r *VerifyCodeHandler) Render(c *fiber.Ctx, supplements *router.Supplements
if verificationCode == "" {
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationEmpty
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationEmpty").(string)
return fiber.StatusUnprocessableEntity, nil
}
if err = supplements.Mailer.Verify(verificationCode, lang); err != nil {
slog.Warn("verification code is invalid", slog.String("verification_code", verificationCode), slog.String("error", err.Error()))
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationFailed
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationFailed").(string)
return fiber.StatusUnprocessableEntity, nil
}
templateMap["Status"] = "OK"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.VerificationSuccess
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "VerificationSuccess").(string)
templateMap["DataAttributes"] = map[string]any{
"hide-verification-panel": template.HTMLAttr("data-code-expiry-time=\"true\""),
}
diff --git a/internal/router/handlers/api-v1-like-get.go b/internal/router/handlers/api-v1-like-get.go
index 0eaf5f6..9e2c856 100644
--- a/internal/router/handlers/api-v1-like-get.go
+++ b/internal/router/handlers/api-v1-like-get.go
@@ -42,7 +42,7 @@ func (r *GetLikeHandler) Render(c *fiber.Ctx, supplements *router.Supplements, l
return fiber.StatusBadRequest, fmt.Errorf("error getting path from referer: %w", err)
}
- if len(pathParts) != 3 && pathParts[1] != "blog" {
+ if len(pathParts) != 3 || pathParts[1] != "blog" {
return fiber.StatusBadRequest, fmt.Errorf("invalid path format: expected '/:lang/blog/:page', got '%s'", path)
}
diff --git a/internal/router/handlers/api-v1-map-get.go b/internal/router/handlers/api-v1-map-get.go
new file mode 100644
index 0000000..e19c54a
--- /dev/null
+++ b/internal/router/handlers/api-v1-map-get.go
@@ -0,0 +1,116 @@
+package handlers
+
+import (
+ "fmt"
+ "log/slog"
+ "slices"
+ "strconv"
+ "strings"
+
+ "github.com/SayaAndy/saya-today-web/internal/blog"
+ "github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/gofiber/fiber/v2"
+)
+
+type GetMapHandler struct {
+ router.BasicHandler
+}
+
+func init() {
+ router.Routes = append(router.Routes, &GetMapHandler{})
+}
+
+func (r *GetMapHandler) Filter() (method string, path string) {
+ return "GET", "/api/v1/map"
+}
+
+func (r *GetMapHandler) IsTemplated() bool {
+ return false
+}
+
+func (r *GetMapHandler) TemplatesToInject() []string {
+ return []string{"views/partials/global-map-widget.html"}
+}
+
+func (r *GetMapHandler) ToCache() router.CacheSetting {
+ return router.ByUrlAndQuery
+}
+
+func (r *GetMapHandler) ToValidateLang() router.LangSetting {
+ return router.InReferer
+}
+
+func (r *GetMapHandler) Render(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
+ codename := c.Query("codename")
+ zoom := c.QueryInt("zoom", 4)
+ zoomPosition := c.Query("zoomPosition")
+
+ pages, err := supplements.BlogClient.Scan(lang + "/")
+ status := fiber.StatusOK
+ if err != nil {
+ slog.Error("received an error while scanning blog pages",
+ slog.String("error", err.Error()),
+ slog.String("lang", lang),
+ )
+ status = fiber.StatusPartialContent
+ pages = []*blog.Page{}
+ }
+ slices.SortFunc(pages, func(a *blog.Page, b *blog.Page) int {
+ return a.Metadata.PublishedTime.Compare(b.Metadata.PublishedTime)
+ })
+
+ type MapMarker struct {
+ Index int `json:"Index"`
+ Title string `json:"Title"`
+ PageLink string `json:"PageLink"`
+ Lat float64 `json:"Lat"`
+ Long float64 `json:"Long"`
+ AccuracyMeters int64 `json:"AccuracyMeters"`
+ Thumbnail string `json:"Thumbnail"`
+ ToHighlight bool `json:"ToHighlight"`
+ }
+
+ templateMap["MapLocationLat"] = 45.4507
+ templateMap["MapLocationLong"] = 68.8319
+ templateMap["MapLocationZoom"] = zoom
+ templateMap["ZoomPosition"] = zoomPosition
+
+ mapMarkers := make([]*MapMarker, 0, len(pages))
+ for i, page := range pages {
+ geolocationParts := strings.Split(page.Metadata.Geolocation, " ")
+ if len(geolocationParts) < 2 {
+ continue
+ }
+
+ var x, y float64
+ var areaError int64
+ if len(geolocationParts) >= 2 {
+ x, _ = strconv.ParseFloat(geolocationParts[0], 64)
+ y, _ = strconv.ParseFloat(geolocationParts[1], 64)
+ }
+ if len(geolocationParts) >= 3 {
+ areaError, _ = strconv.ParseInt(geolocationParts[2], 10, 64)
+ }
+
+ toHighlight := page.FileName == codename
+ if toHighlight {
+ templateMap["MapLocationLat"] = x
+ templateMap["MapLocationLong"] = y
+ }
+
+ mapMarkers = append(mapMarkers, &MapMarker{
+ Index: i,
+ Title: page.Metadata.Title,
+ PageLink: fmt.Sprintf("/%s/blog/%s", lang, page.FileName),
+ Lat: x,
+ Long: y,
+ AccuracyMeters: areaError,
+ Thumbnail: page.Metadata.Thumbnail,
+ ToHighlight: toHighlight,
+ })
+ }
+
+ templateMap["MapMarkers"] = mapMarkers
+
+ return status, nil
+}
diff --git a/internal/router/handlers/api-v1-subs-put.go b/internal/router/handlers/api-v1-subs-put.go
index 32fba7e..12dc5cf 100644
--- a/internal/router/handlers/api-v1-subs-put.go
+++ b/internal/router/handlers/api-v1-subs-put.go
@@ -3,6 +3,7 @@ package handlers
import (
"github.com/SayaAndy/saya-today-web/internal/mailer"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -52,18 +53,18 @@ func (r *PutSubsHandler) Render(c *fiber.Ctx, supplements *router.Supplements, l
subscriptionTypeEnum = mailer.Specific
default:
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.SubscribeInvalidType
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "SubscribeInvalidType").(string)
return fiber.StatusUnprocessableEntity, nil
}
specificTags := c.FormValue("tags_picked")
if err = supplements.Mailer.Subscribe(supplements.Mailer.GetHash(c.IP()), subscriptionTypeEnum, specificTags); err != nil {
templateMap["Status"] = "Failed"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.FailedToSubscribe
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "FailedToSubscribe").(string)
return fiber.StatusUnprocessableEntity, nil
}
templateMap["Status"] = "OK"
- templateMap["Message"] = supplements.Localization[lang].UserProfile.SubscribedSuccessfully
+ templateMap["Message"] = l10n.T.GetPath(lang, "UserProfile", "SubscribedSuccessfully").(string)
return fiber.StatusOK, nil
}
diff --git a/internal/router/handlers/lang-blog-title.go b/internal/router/handlers/lang-blog-title.go
index 49744c1..76dc335 100644
--- a/internal/router/handlers/lang-blog-title.go
+++ b/internal/router/handlers/lang-blog-title.go
@@ -10,6 +10,7 @@ import (
"github.com/SayaAndy/saya-today-web/internal/blog"
"github.com/SayaAndy/saya-today-web/internal/frontmatter"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
"github.com/yuin/goldmark"
)
@@ -70,11 +71,15 @@ func (r *BlogPageHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements,
if err != nil {
return nil, fmt.Errorf("failed to read frontmatter of the desired blog post: %w", err)
}
+ title := metadata.Title
+ if metadata.Medley != "" {
+ title += " // " + l10n.T.GetPath(lang, "Medleys", metadata.Medley).(string)
+ }
return []router.MetaField{
- {Property: "og:title", Content: metadata.Title},
+ {Property: "og:title", Content: title},
{Property: "og:description", Content: fmt.Sprintf("%s [%s]", metadata.ShortDescription, metadata.ActionDate)},
- {Property: "og:image", Content: fmt.Sprintf(supplements.PhotoStorage.Thumbnail320p.BaseUrl, metadata.Thumbnail)},
+ {Property: "og:image", Content: fmt.Sprintf(supplements.PhotoStorage.Thumbnail560p.BaseUrl, metadata.Thumbnail)},
{Property: "og:url", Content: fmt.Sprintf("%s/%s/blog/%s", templateMap["CanonicalEndpoint"], lang, c.Params("title"))},
{Property: "og:type", Content: "website"},
{Name: "twitter:card", Content: "summary_large_image"},
@@ -86,11 +91,15 @@ func (r *BlogPageHandler) AddLinkedData(c *fiber.Ctx, supplements *router.Supple
if err != nil {
return nil, fmt.Errorf("failed to read frontmatter of the desired blog post: %w", err)
}
+ title := metadata.Title
+ if metadata.Medley != "" {
+ title += " // " + l10n.T.GetPath(lang, "Medleys", metadata.Medley).(string)
+ }
return map[string]any{
"@context": "https://schema.org",
"@type": "Article",
- "headline": metadata.Title,
+ "headline": title,
"description": fmt.Sprintf("%s [%s]", metadata.ShortDescription, metadata.ActionDate),
"author": map[string]string{"@type": "Person", "name": "Saya Andy"},
"datePublished": metadata.PublishedTime.UTC().Format(time.RFC3339),
@@ -102,10 +111,11 @@ func (r *BlogPageHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplemen
if err != nil {
return fiber.StatusBadRequest, fmt.Errorf("failed to get path from referer: %w", err)
}
+ title := pathParts[2]
- metadata, parsedMarkdown, err := readBlogPost(supplements.MarkdownRenderer, supplements.BlogClient, lang+"/"+pathParts[2])
+ metadata, parsedMarkdown, err := readBlogPost(supplements.MarkdownRenderer, supplements.BlogClient, lang+"/"+title)
if err != nil {
- return fiber.StatusNotFound, fmt.Errorf("failed to find '%s' post: %w", pathParts[2], err)
+ return fiber.StatusNotFound, fmt.Errorf("failed to find '%s' post: %w", title, err)
}
geolocationParts := strings.Split(metadata.Geolocation, " ")
@@ -122,12 +132,15 @@ func (r *BlogPageHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplemen
templateMap["MapLocationY"] = y
templateMap["MapLocationAreaMeters"] = areaError
templateMap["Title"] = metadata.Title
+ templateMap["Codename"] = title
templateMap["ParsedMarkdown"] = template.HTML(parsedMarkdown)
templateMap["PublishedDate"] = metadata.PublishedTime.Format("2006-01-02 15:04:05 -07:00")
templateMap["ActionDate"] = metadata.ActionDate
templateMap["ShortDescription"] = metadata.ShortDescription
+ templateMap["Thumbnail"] = metadata.Thumbnail
+ templateMap["Medley"] = metadata.Medley
- go supplements.ClientCache.View(c.IP(), pathParts[2])
+ go supplements.ClientCache.View(c.IP(), title)
return fiber.StatusOK, nil
}
@@ -143,7 +156,13 @@ func (r *BlogPageHandler) RenderHeader(c *fiber.Ctx, supplements *router.Supplem
return fiber.StatusNotFound, fmt.Errorf("could not read '%s' for metadata: %w", path, err)
}
+ pageTitle := metadata.Title
+ if metadata.Medley != "" {
+ pageTitle += " // " + l10n.T.GetPath(lang, "Medleys", metadata.Medley).(string)
+ }
+
templateMap["Title"] = metadata.Title
+ templateMap["PageTitle"] = pageTitle
return fiber.StatusOK, nil
}
diff --git a/internal/router/handlers/lang-blog.go b/internal/router/handlers/lang-blog.go
index 79ec9f3..c31c2eb 100644
--- a/internal/router/handlers/lang-blog.go
+++ b/internal/router/handlers/lang-blog.go
@@ -11,6 +11,7 @@ import (
"github.com/SayaAndy/saya-today-web/internal/blog"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -57,18 +58,16 @@ func (r *CatalogueHandler) SitemapInfo(supplements *router.Supplements) []router
func (r *CatalogueHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta []router.MetaField, err error) {
return []router.MetaField{
- {Property: "og:title", Content: supplements.Localization[lang].BlogSearch.Header},
- {Property: "og:description", Content: supplements.Localization[lang].BlogSearch.Description},
+ {Property: "og:title", Content: l10n.T.GetPath(lang, "BlogSearch", "Header").(string)},
+ {Property: "og:description", Content: l10n.T.GetPath(lang, "BlogSearch", "Description").(string)},
{Property: "og:url", Content: fmt.Sprintf("%s/%s/blog", templateMap["CanonicalEndpoint"], lang)},
{Property: "og:type", Content: "website"},
}, nil
}
func (r *CatalogueHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
- querySort := c.Query("sort")
- if querySort == "" {
- querySort = "publicationDateDesc"
- }
+ querySort := c.Query("sort", "publicationDateDesc")
+ previousBlogPage := c.Query("codename")
encodedQuery := c.Request().URI().QueryString()
decodedQuery, _ := url.QueryUnescape(string(encodedQuery))
@@ -87,13 +86,14 @@ func (r *CatalogueHandler) RenderBody(c *fiber.Ctx, supplements *router.Suppleme
templateMap["Tags"] = tagsArray
templateMap["QuerySort"] = querySort
templateMap["QueryTags"] = strings.Join(queryTags, ",")
- templateMap["Title"] = supplements.Localization[lang].BlogSearch.Header
+ templateMap["Title"] = l10n.T.GetPath(lang, "BlogSearch", "Header").(string)
+ templateMap["PreviousBlogPage"] = previousBlogPage
return fiber.StatusOK, nil
}
func (r *CatalogueHandler) RenderHeader(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
- templateMap["Title"] = supplements.Localization[lang].BlogSearch.Header
+ templateMap["Title"] = l10n.T.GetPath(lang, "BlogSearch", "Header").(string)
return fiber.StatusOK, nil
}
diff --git a/internal/router/handlers/lang-map.go b/internal/router/handlers/lang-map.go
index d452bc7..32ab35e 100644
--- a/internal/router/handlers/lang-map.go
+++ b/internal/router/handlers/lang-map.go
@@ -1,13 +1,6 @@
package handlers
import (
- "fmt"
- "log/slog"
- "slices"
- "strconv"
- "strings"
-
- "github.com/SayaAndy/saya-today-web/internal/blog"
"github.com/SayaAndy/saya-today-web/internal/router"
"github.com/gofiber/fiber/v2"
)
@@ -49,61 +42,5 @@ func (r *MapHandler) SitemapInfo(supplements *router.Supplements) []router.Sitem
}
func (r *MapHandler) Render(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
- pages, err := supplements.BlogClient.Scan(lang + "/")
- slices.SortFunc(pages, func(a *blog.Page, b *blog.Page) int {
- return a.Metadata.PublishedTime.Compare(b.Metadata.PublishedTime)
- })
- status := fiber.StatusOK
- if err != nil {
- slog.Error("received an error while scanning b2 pages",
- slog.String("error", err.Error()),
- slog.String("lang", lang),
- )
- status = fiber.StatusPartialContent
- pages = []*blog.Page{}
- }
-
- type MapMarker struct {
- Index int `json:"Index"`
- Title string `json:"Title"`
- PageLink string `json:"PageLink"`
- Lat float64 `json:"Lat"`
- Long float64 `json:"Long"`
- AccuracyMeters int64 `json:"AccuracyMeters"`
- Thumbnail string `json:"Thumbnail"`
- }
-
- mapMarkers := make([]*MapMarker, 0, len(pages))
- for i, page := range pages {
- geolocationParts := strings.Split(page.Metadata.Geolocation, " ")
- if len(geolocationParts) < 2 {
- continue
- }
-
- var x, y float64
- var areaError int64
- if len(geolocationParts) >= 2 {
- x, _ = strconv.ParseFloat(geolocationParts[0], 64)
- y, _ = strconv.ParseFloat(geolocationParts[1], 64)
- }
- if len(geolocationParts) >= 3 {
- areaError, _ = strconv.ParseInt(geolocationParts[2], 10, 64)
- }
-
- mapMarkers = append(mapMarkers, &MapMarker{
- Index: i,
- Title: page.Metadata.Title,
- PageLink: fmt.Sprintf("/%s/blog/%s", lang, page.FileName),
- Lat: x,
- Long: y,
- AccuracyMeters: areaError,
- Thumbnail: page.Metadata.Thumbnail,
- })
- }
-
- templateMap["MapMarkers"] = mapMarkers
- templateMap["MapLocationLat"] = 45.4507
- templateMap["MapLocationLong"] = 68.8319
-
- return status, nil
+ return fiber.StatusOK, nil
}
diff --git a/internal/router/handlers/lang-user-unsubscribe.go b/internal/router/handlers/lang-user-unsubscribe.go
index 4fbd244..e22d854 100644
--- a/internal/router/handlers/lang-user-unsubscribe.go
+++ b/internal/router/handlers/lang-user-unsubscribe.go
@@ -4,6 +4,7 @@ import (
"log/slog"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -39,24 +40,24 @@ func (r *UnsubscribeHandler) Render(c *fiber.Ctx, supplements *router.Supplement
if unsubscribeCode == "" {
statusColor = "0, 0, 255"
statusEmoji = "(╭ರ_•́)"
- statusText = supplements.Localization[lang].UnsubscribePage.UnsetCode
+ statusText = l10n.T.GetPath(lang, "UnsubscribePage", "UnsetCode").(string)
status = fiber.ErrBadRequest.Code
} else if clientError, serverError := supplements.Mailer.Unsubscribe(unsubscribeCode); clientError != nil {
slog.Info("got a client error when unsubscribing", slog.String("error", clientError.Error()))
statusColor = "255, 0, 0"
statusEmoji = "(͠≖~≖ ͡ )"
- statusText = supplements.Localization[lang].UnsubscribePage.InvalidCode
+ statusText = l10n.T.GetPath(lang, "UnsubscribePage", "InvalidCode").(string)
status = fiber.ErrBadRequest.Code
} else if serverError != nil {
slog.Error("got a server error when unsubscribing", slog.String("error", serverError.Error()))
statusColor = "255, 128, 0"
statusEmoji = "( ˶°ㅁ°) !!"
- statusText = supplements.Localization[lang].UnsubscribePage.OnServerError
+ statusText = l10n.T.GetPath(lang, "UnsubscribePage", "OnServerError").(string)
status = fiber.ErrInternalServerError.Code
} else {
statusColor = "0, 255, 0"
statusEmoji = "♡⸜(˶˃ ᵕ ˂˶)⸝♡"
- statusText = supplements.Localization[lang].UnsubscribePage.Success
+ statusText = l10n.T.GetPath(lang, "UnsubscribePage", "Success").(string)
status = fiber.StatusOK
}
diff --git a/internal/router/handlers/lang-user.go b/internal/router/handlers/lang-user.go
index 0dda529..085a9aa 100644
--- a/internal/router/handlers/lang-user.go
+++ b/internal/router/handlers/lang-user.go
@@ -6,6 +6,7 @@ import (
"github.com/SayaAndy/saya-today-web/internal/mailer"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -40,15 +41,15 @@ func (r *UserHandler) ToValidateLang() router.LangSetting {
func (r *UserHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta []router.MetaField, err error) {
return []router.MetaField{
{Name: "robots", Content: "noindex,nofollow"},
- {Property: "og:title", Content: supplements.Localization[lang].UserProfile.Header},
- {Property: "og:description", Content: supplements.Localization[lang].UserProfile.Description},
+ {Property: "og:title", Content: l10n.T.GetPath(lang, "UserProfile", "Header").(string)},
+ {Property: "og:description", Content: l10n.T.GetPath(lang, "UserProfile", "Description").(string)},
{Property: "og:url", Content: fmt.Sprintf("%s/%s/user", templateMap["CanonicalEndpoint"], lang)},
{Property: "og:type", Content: "website"},
}, nil
}
func (r *UserHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
- templateMap["Title"] = supplements.Localization[lang].UserProfile.Header
+ templateMap["Title"] = l10n.T.GetPath(lang, "UserProfile", "Header").(string)
email, _, err := supplements.Mailer.GetInfo(supplements.Mailer.GetHash(c.IP()))
if err != nil {
@@ -83,6 +84,6 @@ func (r *UserHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements,
}
func (r *UserHandler) RenderHeader(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
- templateMap["Title"] = supplements.Localization[lang].UserProfile.Header
+ templateMap["Title"] = l10n.T.GetPath(lang, "UserProfile", "Header").(string)
return fiber.StatusOK, nil
}
diff --git a/internal/router/handlers/lang.go b/internal/router/handlers/lang.go
index 25b5036..cc0b33b 100644
--- a/internal/router/handlers/lang.go
+++ b/internal/router/handlers/lang.go
@@ -6,6 +6,7 @@ import (
"time"
"github.com/SayaAndy/saya-today-web/internal/router"
+ "github.com/SayaAndy/saya-today-web/l10n"
"github.com/gofiber/fiber/v2"
)
@@ -51,8 +52,8 @@ func (r *HomeHandler) SitemapInfo(supplements *router.Supplements) []router.Site
func (r *HomeHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (meta []router.MetaField, err error) {
return []router.MetaField{
- {Property: "og:title", Content: supplements.Localization[lang].HomePage.Header},
- {Property: "og:description", Content: supplements.Localization[lang].HomePage.HomePageDescription},
+ {Property: "og:title", Content: l10n.T.GetPath(lang, "HomePage", "Header").(string)},
+ {Property: "og:description", Content: l10n.T.GetPath(lang, "HomePage", "HomePageDescription").(string)},
{Property: "og:image", Content: fmt.Sprintf(
supplements.PhotoStorage.HomePageGifs.BaseUrl,
supplements.PhotoStorage.HomePageGifs.Indexes[rand.Int()%len(supplements.PhotoStorage.HomePageGifs.Indexes)],
@@ -63,7 +64,7 @@ func (r *HomeHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lan
}
func (r *HomeHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
- templateMap["Title"] = supplements.Localization[lang].HomePage.Header
+ templateMap["Title"] = l10n.T.GetPath(lang, "HomePage", "Header").(string)
templateMap["FilledHeartCount"] = uint(40)
templateMap["OutlineHeartCount"] = uint(40)
templateMap["GifUrl"] = fmt.Sprintf(
@@ -75,6 +76,6 @@ func (r *HomeHandler) RenderBody(c *fiber.Ctx, supplements *router.Supplements,
}
func (r *HomeHandler) RenderHeader(c *fiber.Ctx, supplements *router.Supplements, lang string, templateMap fiber.Map) (statusCode int, err error) {
- templateMap["Title"] = supplements.Localization[lang].HomePage.Header
+ templateMap["Title"] = l10n.T.GetPath(lang, "HomePage", "Header").(string)
return fiber.StatusOK, nil
}
diff --git a/internal/router/handlers/root.go b/internal/router/handlers/root.go
index 6a011e4..4fc4e31 100644
--- a/internal/router/handlers/root.go
+++ b/internal/router/handlers/root.go
@@ -47,11 +47,8 @@ func (r *RootHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lan
{Property: "og:url", Content: fmt.Sprint(templateMap["CanonicalEndpoint"]) + "/"},
{Property: "og:type", Content: "website"},
}
- if supplements.Meta.GoogleSiteVerification != "" {
- meta = append(meta, router.MetaField{Name: "google-site-verification", Content: supplements.Meta.GoogleSiteVerification})
- }
- if supplements.Meta.YandexVerification != "" {
- meta = append(meta, router.MetaField{Name: "yandex-verification", Content: supplements.Meta.YandexVerification})
+ for _, field := range supplements.Meta {
+ meta = append(meta, router.MetaField{Name: field.Name, Content: field.Value})
}
return meta, nil
}
diff --git a/internal/router/router.go b/internal/router/router.go
index 65e35a4..a11a934 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -23,7 +23,6 @@ import (
"github.com/SayaAndy/saya-today-web/internal/mailer"
"github.com/SayaAndy/saya-today-web/internal/tailwind"
"github.com/SayaAndy/saya-today-web/internal/templatemanager"
- "github.com/SayaAndy/saya-today-web/locale"
"github.com/dgraph-io/ristretto/v2"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
@@ -95,7 +94,6 @@ type Route interface {
type Supplements struct {
DB *sql.DB
BlogClient blog.Client
- Localization map[string]*locale.LocaleConfig
AvailableLanguages []config.AvailableLanguageConfig
ClientCache *ClientCache
PageCache *ristretto.Cache[string, []byte]
@@ -104,9 +102,9 @@ type Supplements struct {
BlogTrigger *blogtrigger.BlogTriggerScheduler
TemplateManager *templatemanager.TemplateManager
MarkdownRenderer goldmark.Markdown
- Meta config.MetaConfig
+ Meta []config.MetaConfig
PhotoStorage config.PhotoStorageConfig
- StaticStorage config.PhotoTypeConfig
+ StaticStorage config.StaticStorageConfig
}
type Router struct {
@@ -149,16 +147,7 @@ func NewRouter(cfg *config.Config) (*Router, error) {
supplements.BlogClient, err = blog.NewClientMap[cfg.BlogPages.Storage.Type](&cfg.BlogPages.Storage)
if err != nil {
- return nil, fmt.Errorf("fail to initialize b2 client: %w", err)
- }
-
- supplements.Localization = make(map[string]*locale.LocaleConfig, len(cfg.AvailableLanguages))
- for _, lang := range cfg.AvailableLanguages {
- localeCfg, err := locale.InitConfig(cfg.LocalePath + lang.LocFile)
- if err != nil {
- return nil, fmt.Errorf("fail to initialize a locale: %w", err)
- }
- supplements.Localization[lang.Name] = localeCfg
+ return nil, fmt.Errorf("fail to initialize blog client: type %s: %w", cfg.BlogPages.Storage.Type, err)
}
supplements.MarkdownRenderer = goldmark.New(
@@ -200,7 +189,7 @@ func NewRouter(cfg *config.Config) (*Router, error) {
}
supplements.Mailer, err = mailer.NewMailer(supplements.DB, cfg.Mail.ClientHost, cfg.Mail.MailHost,
- cfg.Mail.PublicName, cfg.Mail.MailAddress, cfg.Mail.Username, cfg.Mail.Password, []byte(cfg.Mail.Salt), supplements.Localization)
+ cfg.Mail.PublicName, cfg.Mail.MailAddress, cfg.Mail.Username, cfg.Mail.Password, []byte(cfg.Mail.Salt))
if err != nil {
return nil, fmt.Errorf("fail to initialize mailer: %w", err)
}
@@ -314,13 +303,12 @@ func (r *Router) InitRoutes() (err error) {
}
defaultMap := fiber.Map{
- "L": r.supplements.Localization[lang],
- "Lang": lang,
- "Path": trimmedPath,
- "QueryString": queryString,
- "CanonicalEndpoint": r.canonicalEndpoint,
- "StaticStorageBaseUrl": r.supplements.StaticStorage.BaseUrl,
- "ThumbnailBaseUrl": r.supplements.PhotoStorage.Thumbnail320p.BaseUrl,
+ "Lang": lang,
+ "Path": trimmedPath,
+ "QueryString": queryString,
+ "CanonicalEndpoint": r.canonicalEndpoint,
+ "StaticStorage": r.supplements.StaticStorage,
+ "PhotoStorage": r.supplements.PhotoStorage,
}
statusCode, err := currentRoute.Render(c, r.supplements, lang, defaultMap)
@@ -394,6 +382,11 @@ func (r *Router) Listen() error {
case "unix":
unixConfig := r.endpoint.Config.(*config.UnixConfig)
endpoint, _ := strings.CutPrefix(unixConfig.Path, "unix://")
+
+ if err := os.Remove(endpoint); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("error while cleaning up existing unix socket: %w", err)
+ }
+
ln, err := net.Listen("unix", endpoint)
if err != nil {
return fmt.Errorf("error while initializing unix listener: %w", err)
@@ -446,8 +439,8 @@ func (r *Router) generalPage(c *fiber.Ctx, route Route, lang string) error {
path := c.Path()
method := c.Method()
trimmedPath := strings.Trim(path, "/")
- cacheKey := ""
queryString := c.Request().URI().QueryString()
+ cacheKey := ""
switch route.ToCache() {
case ByUrlOnly:
@@ -462,12 +455,12 @@ func (r *Router) generalPage(c *fiber.Ctx, route Route, lang string) error {
}
valueMap := fiber.Map{
- "L": r.supplements.Localization[lang],
- "Lang": lang,
- "QueryString": queryString,
- "CanonicalEndpoint": r.canonicalEndpoint,
- "StaticStorageBaseUrl": r.supplements.StaticStorage.BaseUrl,
- "ThumbnailBaseUrl": r.supplements.PhotoStorage.Thumbnail320p.BaseUrl,
+ "Lang": lang,
+ "Path": trimmedPath,
+ "QueryString": queryString,
+ "CanonicalEndpoint": r.canonicalEndpoint,
+ "StaticStorage": r.supplements.StaticStorage,
+ "PhotoStorage": r.supplements.PhotoStorage,
}
var err error
@@ -484,10 +477,52 @@ func (r *Router) generalPage(c *fiber.Ctx, route Route, lang string) error {
valueMap["LinkedData"] = template.JS(ldBytes)
}
+ syntheticReferer := path
+ if len(queryString) > 0 {
+ syntheticReferer += "?" + string(queryString)
+ }
+ c.Request().Header.Set("Referer", syntheticReferer)
+
+ parts := []struct {
+ name string
+ key string
+ render func(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (int, error)
+ }{
+ {"top-embeds", "RenderedTopEmbeds", route.RenderTopEmbeds},
+ {"header", "RenderedHeader", route.RenderHeader},
+ {"body", "RenderedBody", route.RenderBody},
+ {"footer", "RenderedFooter", route.RenderFooter},
+ {"bottom-embeds", "RenderedBottomEmbeds", route.RenderBottomEmbeds},
+ }
+
+ for _, p := range parts {
+ statusCode, err := p.render(c, r.supplements, lang, valueMap)
+ if err != nil {
+ slog.Error("failed to render segment for full page",
+ slog.String("path", path),
+ slog.String("segment", p.name),
+ slog.String("error", err.Error()),
+ )
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(statusCode).SendString(err.Error())
+ }
+ segContent, err := r.supplements.TemplateManager.Render("general-page-"+p.name, valueMap, route.TemplatesToInject()...)
+ if err != nil {
+ slog.Error("failed to render segment template for full page",
+ slog.String("path", path),
+ slog.String("segment", p.name),
+ slog.String("error", err.Error()),
+ )
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate segment")
+ }
+ valueMap[p.key] = template.HTML(segContent)
+ }
+
content, err := r.supplements.TemplateManager.Render("general-page", valueMap)
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")
+ slog.Warn("failed to generate full page", slog.String("path", path), slog.String("error", err.Error()))
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate full page")
}
go r.supplements.PageCache.SetWithTTL(cacheKey, content, int64(len(content)), route.CacheDuration())
@@ -528,11 +563,12 @@ func (r *Router) generalPageSegment(c *fiber.Ctx, part string) error {
cacheKey := ""
trimmedPath := strings.Trim(path, "/")
+ requestQuery := string(c.Request().URI().QueryString())
switch route.ToCache() {
case ByUrlOnly:
cacheKey = fmt.Sprintf("%s.%s.%s", method, part, trimmedPath)
case ByUrlAndQuery:
- cacheKey = fmt.Sprintf("%s.%s.%s.%s", method, part, trimmedPath, queryString)
+ cacheKey = fmt.Sprintf("%s.%s.%s.%s.%s", method, part, trimmedPath, queryString, requestQuery)
}
if route.ToCache() != Disabled {
@@ -544,13 +580,12 @@ func (r *Router) generalPageSegment(c *fiber.Ctx, part string) error {
var statusCode int
defaultMap := fiber.Map{
- "L": r.supplements.Localization[lang],
- "Lang": lang,
- "Path": strings.Trim(path, "/"),
- "QueryString": queryString,
- "CanonicalEndpoint": r.canonicalEndpoint,
- "StaticStorageBaseUrl": r.supplements.StaticStorage.BaseUrl,
- "ThumbnailBaseUrl": r.supplements.PhotoStorage.Thumbnail320p.BaseUrl,
+ "Lang": lang,
+ "Path": strings.Trim(path, "/"),
+ "QueryString": queryString,
+ "CanonicalEndpoint": r.canonicalEndpoint,
+ "StaticStorage": r.supplements.StaticStorage,
+ "PhotoStorage": r.supplements.PhotoStorage,
}
switch part {
@@ -642,8 +677,7 @@ func (r *Router) getAndValidateLang(c *fiber.Ctx, langSetting LangSetting, defau
return lang, nil
}
}
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return "", c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("lang value is invalid: '%s' is not considered an available language", lang))
+ return "", fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("lang value is invalid: '%s' is not considered an available language", lang))
}
func GetPathFromReferer(c *fiber.Ctx) (path string, pathParts []string, queryString string, err error) {
diff --git a/internal/tailwind/transformer.go b/internal/tailwind/transformer.go
index c9da9df..f90489f 100644
--- a/internal/tailwind/transformer.go
+++ b/internal/tailwind/transformer.go
@@ -45,7 +45,7 @@ func (t *TailwindTransformer) Transform(node *ast.Document, reader text.Reader,
node.SetAttribute([]byte("class"), []byte("text-base/[2] font-gentium tracking-[.0125rem]"))
case *ast.Blockquote:
- node.SetAttribute([]byte("class"), []byte("border-l-2 border-main-medium bg-paper bg-background-dark p-1 mb-2 italic font-thin"))
+ node.SetAttribute([]byte("class"), []byte("border-l-[0.125rem] border-main-medium bg-paper bg-background-dark p-1 mb-2 italic font-thin"))
case *ast.CodeSpan:
node.SetAttribute([]byte("class"), []byte("bg-paper bg-background-dark"))
diff --git a/internal/templatemanager/templatemanager.go b/internal/templatemanager/templatemanager.go
index 9d14e15..ff1214a 100644
--- a/internal/templatemanager/templatemanager.go
+++ b/internal/templatemanager/templatemanager.go
@@ -8,6 +8,8 @@ import (
"path/filepath"
"strings"
"time"
+
+ "github.com/SayaAndy/saya-today-web/l10n"
)
type TemplateManager struct {
@@ -38,6 +40,10 @@ var templateFuncMap = template.FuncMap{
"fdiv": func(a, b int) float64 {
return float64(a) / float64(b)
},
+ "l": func(path ...any) any {
+ return l10n.T.GetPath(path...)
+ },
+ "join": strings.Join,
}
func NewTemplateManager(templates ...TemplateManagerTemplates) (*TemplateManager, error) {
diff --git a/locale/localization.en.yaml b/l10n/en.yaml
index 9571aca..9571aca 100644
--- a/locale/localization.en.yaml
+++ b/l10n/en.yaml
diff --git a/locale/localization.ru.yaml b/l10n/ru.yaml
index 1818998..1818998 100644
--- a/locale/localization.ru.yaml
+++ b/l10n/ru.yaml
diff --git a/l10n/translator-hook.go b/l10n/translator-hook.go
new file mode 100644
index 0000000..ff5837c
--- /dev/null
+++ b/l10n/translator-hook.go
@@ -0,0 +1,39 @@
+package l10n
+
+var hooks map[string]*TranslatorHook
+
+type TranslatorHook struct {
+ t *Translator
+ prefix string
+}
+
+func NewTranslatorHook(prefix string) *TranslatorHook {
+ if _, ok := hooks[prefix]; !ok {
+ hooks[prefix] = &TranslatorHook{T, prefix}
+ }
+ return hooks[prefix]
+}
+
+func (h *TranslatorHook) Set(key string, val any, toOverwrite bool) (alreadySet bool, err error) {
+ return h.t.Set(h.prefix+key, val, toOverwrite)
+}
+
+func (h *TranslatorHook) SetPath(val any, toOverwrite bool, path ...any) (alreadySet bool, err error) {
+ return h.t.SetPath(val, toOverwrite, append([]any{h.prefix}, path...)...)
+}
+
+func (h *TranslatorHook) Get(key string) any {
+ return h.t.Get(h.prefix + "." + key)
+}
+
+func (h *TranslatorHook) GetPath(path ...any) any {
+ return h.t.GetPath(append([]any{h.prefix}, path...)...)
+}
+
+func (h *TranslatorHook) GetDefault(key string, def any) any {
+ return h.t.GetDefault(h.prefix+"."+key, def)
+}
+
+func (h *TranslatorHook) GetDefaultPath(def any, path ...any) any {
+ return h.t.GetDefaultPath(def, append([]any{h.prefix}, path...)...)
+}
diff --git a/l10n/translator.go b/l10n/translator.go
new file mode 100644
index 0000000..4d4b4eb
--- /dev/null
+++ b/l10n/translator.go
@@ -0,0 +1,167 @@
+package l10n
+
+import (
+ "errors"
+ "fmt"
+ "log/slog"
+ "os"
+ "strings"
+ "sync"
+
+ "gopkg.in/yaml.v3"
+)
+
+var T *Translator
+
+func init() {
+ var err error
+ if T, err = NewTranslator("ru", "en"); err != nil {
+ slog.Error("fail to load localization", slog.String("error", err.Error()))
+ os.Exit(1)
+ }
+}
+
+type Translator struct {
+ l map[string]any
+ mu sync.RWMutex
+}
+
+func NewTranslator(locales ...string) (*Translator, error) {
+ t := &Translator{l: make(map[string]any)}
+ errs := make([]error, 0)
+
+ for _, locale := range locales {
+ contentBytes, err := os.ReadFile(fmt.Sprintf("l10n/%s.yaml", locale))
+ if err != nil {
+ errs = append(errs, fmt.Errorf("opening one of files: l10n/%s.yaml: %w", locale, err))
+ continue
+ }
+
+ var content map[string]any
+ if err := yaml.Unmarshal(contentBytes, &content); err != nil {
+ errs = append(errs, fmt.Errorf("parsing one of files: l10n/%s.yaml: %w", locale, err))
+ continue
+ }
+
+ errs = append(errs, t.initMap("."+locale, content)...)
+ }
+
+ return t, errors.Join(errs...)
+}
+
+func (t *Translator) Set(key string, val any, toOverwrite bool) (alreadySet bool, err error) {
+ t.mu.RLock()
+ if _, alreadySet = t.l[key]; !alreadySet || (alreadySet && toOverwrite) {
+ t.mu.RUnlock()
+ return alreadySet, errors.Join(t.setSafe(key, val)...)
+ }
+ t.mu.RUnlock()
+ return alreadySet, nil
+}
+
+func (t *Translator) SetPath(val any, toOverwrite bool, path ...any) (alreadySet bool, err error) {
+ key, err := formatPath(path...)
+ if err != nil {
+ return false, err
+ }
+ return t.Set(key, val, toOverwrite)
+}
+
+func (t *Translator) Get(key string) any {
+ return t.GetDefault(key, key[strings.LastIndex(key, ".")+1:])
+}
+
+func (t *Translator) GetPath(path ...any) any {
+ return t.GetDefaultPath(path[len(path)-1], path...)
+}
+
+func (t *Translator) GetDefault(key string, def any) any {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+ if v, ok := t.l[key]; ok {
+ return v
+ }
+ slog.Warn("failed to find localization for a key", slog.String("key", key))
+ return def
+}
+
+func (t *Translator) GetDefaultPath(def any, path ...any) any {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+ key, err := formatPath(path...)
+ if err != nil {
+ slog.Error("get localized value by path: path is invalid", slog.String("error", err.Error()))
+ return def
+ }
+ return t.GetDefault(key, def)
+}
+
+func (t *Translator) initSlice(parent string, level []any) []error {
+ errs := make([]error, 0)
+ for i, v := range level {
+ newParent := parent + fmt.Sprintf("[%d]", i)
+ t.setSafe(newParent, v)
+ }
+ return errs
+}
+
+func (t *Translator) initMap(parent string, level map[string]any) []error {
+ errs := make([]error, 0)
+ for k, v := range level {
+ key, err := formatPath(k)
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+
+ errs = append(errs, t.setSafe(parent+key, v)...)
+ }
+ return errs
+}
+
+func (t *Translator) setSafe(k string, v any) []error {
+ errs := make([]error, 0)
+ t.mu.Lock()
+ switch vv := v.(type) {
+ case int:
+ t.l[k] = vv
+ t.mu.Unlock()
+ case float64:
+ t.l[k] = vv
+ t.mu.Unlock()
+ case string:
+ t.l[k] = vv
+ t.mu.Unlock()
+ case []any:
+ t.mu.Unlock()
+ errs = append(errs, t.initSlice(k, vv)...)
+ case map[string]any:
+ t.mu.Unlock()
+ errs = append(errs, t.initMap(k, vv)...)
+ default:
+ t.mu.Unlock()
+ errs = append(errs, fmt.Errorf("invalid value type: %s, %v", k, vv))
+ }
+ return errs
+}
+
+func formatPath(path ...any) (string, error) {
+ var key string
+ for _, part := range path {
+ switch k := part.(type) {
+ case int:
+ key += fmt.Sprintf("[%d]", k)
+ case string:
+ key += "." + strings.ReplaceAll(k, ".", "\\.")
+ case []any:
+ add, err := formatPath(k...)
+ if err != nil {
+ return "", fmt.Errorf("while formatting nested slice: %w", err)
+ }
+ key += add
+ default:
+ return "", fmt.Errorf("invalid key part: after %s follows %v", key, k)
+ }
+ }
+ return key, nil
+}
diff --git a/locale/localization.go b/locale/localization.go
deleted file mode 100644
index 87b8ea1..0000000
--- a/locale/localization.go
+++ /dev/null
@@ -1,144 +0,0 @@
-package locale
-
-import (
- "os"
-
- "github.com/go-playground/validator/v10"
- "gopkg.in/yaml.v3"
-)
-
-type LocaleConfig struct {
- TagsLabel string `yaml:"TagsLabel" json:"TagsLabel"`
- BlogSearch BlogSearchConfig `yaml:"BlogSearch" json:"BlogSearch"`
- GlobalMap GlobalMapConfig `yaml:"GlobalMap" json:"GlobalMap"`
- HomePage HomePageConfig `yaml:"HomePage" json:"HomePage"`
- UnsubscribePage UnsubscribePageConfig `yaml:"UnsubscribePage" json:"UnsubscribePage"`
- Metadata MetadataConfig `yaml:"Metadata" json:"Metadata"`
- Mail MailConfig `yaml:"Mail" json:"Mail"`
- UserProfile UserProfileConfig `yaml:"UserProfile" json:"UserProfile"`
-}
-
-type BlogSearchConfig struct {
- Header string `yaml:"Header" json:"Header"`
- Description string `yaml:"Description" json:"Description"`
- TagsHeader string `yaml:"TagsHeader" json:"TagsHeader"`
- OrderByHeader string `yaml:"OrderByHeader" json:"OrderByHeader"`
- TitleOrdered string `yaml:"TitleOrdered" json:"TitleOrdered"`
- ActionDateOrdered string `yaml:"ActionDateOrdered" json:"ActionDateOrdered"`
- PublicationDateOrdered string `yaml:"PublicationDateOrdered" json:"PublicationDateOrdered"`
- ChooseAllTags string `yaml:"ChooseAllTags" json:"ChooseAllTags"`
-}
-
-type GlobalMapConfig struct {
- Header string `yaml:"Header" json:"Header"`
- Description string `yaml:"Description" json:"Description"`
-}
-
-type HomePageConfig struct {
- Header string `yaml:"Header" json:"Header"`
- Contacts string `yaml:"Contacts" json:"Contacts"`
- DidYouKnowThat string `yaml:"DidYouKnowThat" json:"DidYouKnowThat"`
- SidebarDescription string `yaml:"SidebarDescription" json:"SidebarDescription"`
- BackDescription string `yaml:"BackDescription" json:"BackDescription"`
- HomePageDescription string `yaml:"HomePageDescription" json:"HomePageDescription"`
- BlogSearchDescription string `yaml:"BlogSearchDescription" json:"BlogSearchDescription"`
- MarkerMapDescription string `yaml:"MarkerMapDescription" json:"MarkerMapDescription"`
- ThemeSwitchDescription string `yaml:"ThemeSwitchDescription" json:"ThemeSwitchDescription"`
- AccountDescription string `yaml:"AccountDescription" json:"AccountDescription"`
- Hymn1 string `yaml:"Hymn1" json:"Hymn1"`
- Hymn2 string `yaml:"Hymn2" json:"Hymn2"`
- Hymn3 string `yaml:"Hymn3" json:"Hymn3"`
- Hymn4 string `yaml:"Hymn4" json:"Hymn4"`
-}
-
-type UnsubscribePageConfig struct {
- Header string `yaml:"Header" json:"Header"`
- UnsetCode string `yaml:"UnsetCode" json:"UnsetCode"`
- InvalidCode string `yaml:"InvalidCode" json:"InvalidCode"`
- OnServerError string `yaml:"OnServerError" json:"OnServerError"`
- Success string `yaml:"Success" json:"Success"`
-}
-
-type MetadataConfig struct {
- Action string `yaml:"Action" json:"Action"`
- Published string `yaml:"Published" json:"Published"`
-}
-
-type MailConfig struct {
- UnsubscribeFooter string `yaml:"UnsubscribeFooter" json:"UnsubscribeFooter"`
- VerifyEmail VerifyEmailConfig `yaml:"VerifyEmail" json:"VerifyEmail"`
- NewPost NewPostConfig `yaml:"NewPost" json:"NewPost"`
-}
-
-type VerifyEmailConfig struct {
- Subject string `yaml:"Subject" json:"Subject"`
- Welcome string `yaml:"Welcome" json:"Welcome"`
- Intro string `yaml:"Intro" json:"Intro"`
- GotoLink string `yaml:"GotoLink" json:"GotoLink"`
- InputCode string `yaml:"InputCode" json:"InputCode"`
- IfRandom string `yaml:"IfRandom" json:"IfRandom"`
-}
-
-type NewPostConfig struct {
- Subject string `yaml:"Subject" json:"Subject"`
- CapturedOn string `yaml:"CapturedOn" json:"CapturedOn"`
- Intro string `yaml:"Intro" json:"Intro"`
-}
-
-type UserProfileConfig struct {
- Header string `yaml:"Header" json:"Header"`
- Description string `yaml:"Description" json:"Description"`
- EmailHeader string `yaml:"EmailHeader" json:"EmailHeader"`
- VerificationCodeHeader string `yaml:"VerificationCodeHeader" json:"VerificationCodeHeader"`
- TypeEmail string `yaml:"TypeEmail" json:"TypeEmail"`
- SendCodeButton string `yaml:"SendCodeButton" json:"SendCodeButton"`
- VerifyButton string `yaml:"VerifyButton" json:"VerifyButton"`
- VerificationCodeSent string `yaml:"VerificationCodeSent" json:"VerificationCodeSent"`
- DelayTilVerification string `yaml:"DelayTilVerification" json:"DelayTilVerification"`
- FailedEmailRender string `yaml:"FailedEmailRender" json:"FailedEmailRender"`
- VerificationCodeSendingError string `yaml:"VerificationCodeSendingError" json:"VerificationCodeSendingError"`
- EmailAlreadyValidated string `yaml:"EmailAlreadyValidated" json:"EmailAlreadyValidated"`
- EmailTaken string `yaml:"EmailTaken" json:"EmailTaken"`
- EmailEmpty string `yaml:"EmailEmpty" json:"EmailEmpty"`
- VerificationSuccess string `yaml:"VerificationSuccess" json:"VerificationSuccess"`
- VerificationFailed string `yaml:"VerificationFailed" json:"VerificationFailed"`
- VerificationEmpty string `yaml:"VerificationEmpty" json:"VerificationEmpty"`
- SubscriptionHeader string `yaml:"SubscriptionHeader" json:"SubscriptionHeader"`
- TagsNone string `yaml:"TagsNone" json:"TagsNone"`
- TagsAll string `yaml:"TagsAll" json:"TagsAll"`
- TagsSpecific string `yaml:"TagsSpecific" json:"TagsSpecific"`
- TagDoesNotExist string `yaml:"TagDoesNotExist" json:"TagDoesNotExist"`
- TagAlreadyAdded string `yaml:"TagAlreadyAdded" json:"TagAlreadyAdded"`
- SaveButton string `yaml:"SaveButton" json:"SaveButton"`
- SubscribeInvalidType string `yaml:"SubscribeInvalidType" json:"SubscribeInvalidType"`
- FailedToSubscribe string `yaml:"FailedToSubscribe" json:"FailedToSubscribe"`
- SubscribedSuccessfully string `yaml:"SubscribedSuccessfully" json:"SubscribedSuccessfully"`
- RefreshPage string `yaml:"RefreshPage" json:"RefreshPage"`
-}
-
-func LoadConfig(path string, config *LocaleConfig) error {
- fileBytes, err := os.ReadFile(path)
- if err != nil {
- return err
- }
-
- if err = yaml.Unmarshal(fileBytes, config); err != nil {
- return err
- }
-
- return nil
-}
-
-func InitConfig(path string) (*LocaleConfig, error) {
- config := &LocaleConfig{}
- if err := LoadConfig(path, config); err != nil {
- return nil, err
- }
-
- validate := validator.New(validator.WithRequiredStructEnabled())
- if err := validate.Struct(config); err != nil {
- return nil, err
- }
-
- return config, nil
-}
diff --git a/static/favicon.ico b/static/favicon.ico
new file mode 100644
index 0000000..99d5fbf
--- /dev/null
+++ b/static/favicon.ico
Binary files differ
diff --git a/static/input.css b/static/input.css
index 9114514..e0c5c81 100644
--- a/static/input.css
+++ b/static/input.css
@@ -22,30 +22,40 @@
--color-main-dark-hard: var(--color-main-dark-hard);
--color-main-dark-medium: var(--color-main-dark-medium);
--color-main-dark-soft: var(--color-main-dark-soft);
- --shadow-elevation-flat: 0px 1px 1px 0px rgba(0, 0, 0, 0.2);
+ --shadow-elevation-flat: 0px 0.0625rem 0.0625rem 0px rgba(0, 0, 0, 0.2);
+ --inset-shadow-elevation-flat: inset 0px 0.0625rem 0.125rem 0px rgba(0, 0, 0, 0.15);
--shadow-elevation-0: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12);
- --shadow-elevation-1: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12);
- --shadow-elevation-2: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12);
- --shadow-elevation-3: 0px 3px 3px -2px rgba(0, 0, 0, 0.2), 0px 3px 4px 0px rgba(0, 0, 0, 0.14), 0px 1px 8px 0px rgba(0, 0, 0, 0.12);
- --shadow-elevation-4: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12);
- --shadow-elevation-5: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 5px 8px 0px rgba(0, 0, 0, 0.14), 0px 1px 14px 0px rgba(0, 0, 0, 0.12);
- --shadow-elevation-6: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12);
- --shadow-elevation-7: 0px 4px 5px -2px rgba(0, 0, 0, 0.2), 0px 7px 10px 1px rgba(0, 0, 0, 0.14), 0px 2px 16px 1px rgba(0, 0, 0, 0.12);
- --shadow-elevation-8: 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12);
- --shadow-elevation-9: 0px 5px 6px -3px rgba(0, 0, 0, 0.2), 0px 9px 12px 1px rgba(0, 0, 0, 0.14), 0px 3px 16px 2px rgba(0, 0, 0, 0.12);
- --shadow-elevation-10: 0px 6px 6px -3px rgba(0, 0, 0, 0.2), 0px 10px 14px 1px rgba(0, 0, 0, 0.14), 0px 4px 18px 3px rgba(0, 0, 0, 0.12);
- --shadow-elevation-11: 0px 6px 7px -4px rgba(0, 0, 0, 0.2), 0px 11px 15px 1px rgba(0, 0, 0, 0.14), 0px 4px 20px 3px rgba(0, 0, 0, 0.12);
- --shadow-elevation-12: 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 12px 17px 2px rgba(0, 0, 0, 0.14), 0px 5px 22px 4px rgba(0, 0, 0, 0.12);
+ --inset-shadow-elevation-0: inset 0px 0px 0px 0px rgba(0, 0, 0, 0);
+ --shadow-elevation-1: 0px 0.125rem 0.0625rem -0.0625rem rgba(0, 0, 0, 0.2), 0px 0.0625rem 0.0625rem 0px rgba(0, 0, 0, 0.14), 0px 0.0625rem 0.1875rem 0px rgba(0, 0, 0, 0.12);
+ --inset-shadow-elevation-1: inset 0px 0.125rem 0.25rem 0px rgba(0, 0, 0, 0.2);
+ --shadow-elevation-2: 0px 0.1875rem 0.0625rem -0.125rem rgba(0, 0, 0, 0.2), 0px 0.125rem 0.125rem 0px rgba(0, 0, 0, 0.14), 0px 0.0625rem 0.3125rem 0px rgba(0, 0, 0, 0.12);
+ --inset-shadow-elevation-2: inset 0px 0.1875rem 0.375rem 0px rgba(0, 0, 0, 0.22);
+ --shadow-elevation-3: 0px 0.1875rem 0.1875rem -0.125rem rgba(0, 0, 0, 0.2), 0px 0.1875rem 0.25rem 0px rgba(0, 0, 0, 0.14), 0px 0.0625rem 0.5rem 0px rgba(0, 0, 0, 0.12);
+ --inset-shadow-elevation-3: inset 0px 0.25rem 0.5rem 0px rgba(0, 0, 0, 0.25);
+ --shadow-elevation-4: 0px 0.125rem 0.25rem -0.0625rem rgba(0, 0, 0, 0.2), 0px 0.25rem 0.3125rem 0px rgba(0, 0, 0, 0.14), 0px 0.0625rem 0.625rem 0px rgba(0, 0, 0, 0.12);
+ --inset-shadow-elevation-4: inset 0px 0.3125rem 0.625rem 0px rgba(0, 0, 0, 0.27);
+ --shadow-elevation-5: 0px 0.1875rem 0.3125rem -0.0625rem rgba(0, 0, 0, 0.2), 0px 0.3125rem 0.5rem 0px rgba(0, 0, 0, 0.14), 0px 0.0625rem 0.875rem 0px rgba(0, 0, 0, 0.12);
+ --inset-shadow-elevation-5: inset 0px 0.375rem 0.75rem 0px rgba(0, 0, 0, 0.3);
+ --shadow-elevation-6: 0px 0.1875rem 0.3125rem -0.0625rem rgba(0, 0, 0, 0.2), 0px 0.375rem 0.625rem 0px rgba(0, 0, 0, 0.14), 0px 0.0625rem 1.125rem 0px rgba(0, 0, 0, 0.12);
+ --inset-shadow-elevation-6: inset 0px 0.4375rem 0.875rem 0px rgba(0, 0, 0, 0.32);
+ --shadow-elevation-7: 0px 0.25rem 0.3125rem -0.125rem rgba(0, 0, 0, 0.2), 0px 0.4375rem 0.625rem 0.0625rem rgba(0, 0, 0, 0.14), 0px 0.125rem 1rem 0.0625rem rgba(0, 0, 0, 0.12);
+ --inset-shadow-elevation-7: inset 0px 0.5rem 1rem 0px rgba(0, 0, 0, 0.35);
+ --shadow-elevation-8: 0px 0.3125rem 0.3125rem -0.1875rem rgba(0, 0, 0, 0.2), 0px 0.5rem 0.625rem 0.0625rem rgba(0, 0, 0, 0.14), 0px 0.1875rem 0.875rem 0.125rem rgba(0, 0, 0, 0.12);
+ --inset-shadow-elevation-8: inset 0px 0.5625rem 1.125rem 0px rgba(0, 0, 0, 0.37);
+ --shadow-elevation-9: 0px 0.3125rem 0.375rem -0.1875rem rgba(0, 0, 0, 0.2), 0px 0.5625rem 0.75rem 0.0625rem rgba(0, 0, 0, 0.14), 0px 0.1875rem 1rem 0.125rem rgba(0, 0, 0, 0.12);
+ --inset-shadow-elevation-9: inset 0px 0.625rem 1.25rem 0px rgba(0, 0, 0, 0.4);
+ --shadow-elevation-10: 0px 0.375rem 0.375rem -0.1875rem rgba(0, 0, 0, 0.2), 0px 0.625rem 0.875rem 0.0625rem rgba(0, 0, 0, 0.14), 0px 0.25rem 1.125rem 0.1875rem rgba(0, 0, 0, 0.12);
+ --inset-shadow-elevation-10: inset 0px 0.6875rem 1.375rem 0px rgba(0, 0, 0, 0.42);
+ --shadow-elevation-11: 0px 0.375rem 0.4375rem -0.25rem rgba(0, 0, 0, 0.2), 0px 0.6875rem 0.9375rem 0.0625rem rgba(0, 0, 0, 0.14), 0px 0.25rem 1.25rem 0.1875rem rgba(0, 0, 0, 0.12);
+ --inset-shadow-elevation-11: inset 0px 0.75rem 1.5rem 0px rgba(0, 0, 0, 0.45);
+ --shadow-elevation-12: 0px 0.4375rem 0.5rem -0.25rem rgba(0, 0, 0, 0.2), 0px 0.75rem 1.0625rem 0.125rem rgba(0, 0, 0, 0.14), 0px 0.3125rem 1.375rem 0.25rem rgba(0, 0, 0, 0.12);
+ --inset-shadow-elevation-12: inset 0px 0.8125rem 1.625rem 0px rgba(0, 0, 0, 0.47);
}
html {
- font-size: 12px;
+ font-size: clamp(11px, min(1dvw, 1dvh) + 7.6px, 16px);
--crossed-out: url("data:image/svg+xml,<svg id='patternId' width='100%' height='100%' xmlns='http://www.w3.org/2000/svg'><defs><pattern id='a' patternUnits='userSpaceOnUse' width='30' height='30' patternTransform='scale(1) rotate(65)'><rect x='0' y='0' width='100%' height='100%' fill='%232b2b3100' /><path d='M30 15L22.5 30 15 15 22.5 0zm-15 0L7.5 30 0 15 7.5 0z' stroke-width='0.6' stroke='%23ffffffff' fill='none' /></pattern></defs><rect width='800%' height='800%' transform='translate(0,0)' fill='url(%23a)' /></svg>");
--bg-paper: url(data:image/webp;base64,UklGRi4BAABXRUJQVlA4ICIBAABQCQCdASqAAIAAPm02lUmkIqIkIImIgA2JaW3rMFmGDAOKLAoJtP591ByFR74Tun2DP+Lygpfsz5FH7+cKgxi2Islfd03i5AJdXUfU0SDc8SXYAAD++hlzeYAJWQEF8ZdkJdlcA+5btZg0FMGaso8SHoBph0qAOQysElJ5C5SJFNoKIV1J550UyL7Y9Gh0/c/uzLnBmr8lvz7k1WeA81H4S5ViU/wBqa7aM1m8D9rDtrk8cSTqx+xWF6MS8UfZGSsks6CT/igtgv6syFN9uNA3LjnjiCJKYOvFcMkX+jB2578GWVo3nyLaM1QNPYCxn3TiUbnf7U0MdD2NsBpg1+Jd1wu6yW/U+goN+IQS6Nt4RMWltVcZHlxrneKJi0SobO0AAA==);
- --split-left-background: unset;
- --split-left-repeat: no-repeat;
- --split-left-size: cover;
- --split-left-position: left;
--split-right-background: unset;
--split-right-repeat: no-repeat;
--split-right-size: cover;
@@ -77,39 +87,37 @@ body {
scrollbar-color: var(--color-background-light) var(--color-main-soft);
}
-@media screen and (min-width: 640px) and (min-height: 640px) {
- html {
- font-size: 14px;
- }
+body.glightbox-open {
+ height: 100dvh;
}
@media screen and (min-width: 900px) and (min-height: 900px) {
html {
- font-size: 16px;
+ font-size: clamp(16px, min(1dvw, 1dvh) + 7px, 20px);
}
}
@media screen and (min-width: 1400px) and (min-height: 1400px) {
html {
- font-size: 20px;
+ font-size: clamp(20px, min(1dvw, 1dvh) + 6px, 24px);
}
}
@media screen and (min-width: 1920px) and (min-height: 1920px) {
html {
- font-size: 24px;
+ font-size: calc(min(1dvw, 1dvh) + 4.8px);
}
}
[data-theme="olive"] {
--color-accent-light: var(--color-lime-300);
--color-accent-deep: var(--color-lime-600);
- --color-main-dark-hard: #4b513a;
- --color-main-dark-medium: #8a9d8a;
- --color-main-dark-soft: #a5b8a5;
- --color-main-light-soft: #ffffdd;
+ --color-main-dark-hard: oklch(42.269% 0.03716 119.926);
+ --color-main-dark-medium: oklch(67.552% 0.03493 145.278);
+ --color-main-dark-soft: oklch(76.256% 0.03392 145.352);
+ --color-main-light-soft: oklch(99.13% 0.04397 107.223);
--color-main-light-medium: var(--color-main-light-soft);
- --color-main-light-hard: #fffff0;
+ --color-main-light-hard: oklch(99.598% 0.01951 106.845);
--color-main: var(--color-main-dark-hard);
--color-main-hard: var(--color-main-dark-hard);
--color-main-medium: var(--color-main-dark-medium);
@@ -133,15 +141,31 @@ body {
--squares-and-triangles-background-height-coef: 10;
}
+@media (prefers-color-scheme: dark) {
+ [data-theme="olive"] {
+ --color-accent-light: var(--color-lime-600);
+ --color-accent-deep: var(--color-lime-300);
+ --color-main: var(--color-main-light-hard);
+ --color-main-hard: var(--color-main-light-hard);
+ --color-main-medium: var(--color-main-light-medium);
+ --color-main-soft: var(--color-main-light-soft);
+ --color-background: var(--color-main-dark-hard);
+ --color-background-dark: var(--color-main-dark-hard);
+ --color-background-medium: var(--color-main-dark-medium);
+ --color-background-light: var(--color-main-dark-soft);
+ --color-sidebar-stroke: var(--color-main-light-hard);
+ }
+}
+
[data-theme="lettuce"] {
--color-accent-light: var(--color-lime-300);
--color-accent-deep: var(--color-lime-600);
- --color-main-dark-hard: #384a0c;
- --color-main-dark-medium: #617c1f;
- --color-main-dark-soft: #9bb665;
- --color-main-light-soft: #f5eee2;
+ --color-main-dark-hard: oklch(38.049% 0.087 124.935);
+ --color-main-dark-medium: oklch(54.648% 0.12144 124.626);
+ --color-main-dark-soft: oklch(73.722% 0.11138 124.213);
+ --color-main-light-soft: oklch(95.132% 0.01754 81.278);
--color-main-light-medium: var(--color-main-light-soft);
- --color-main-light-hard: #fff9ef;
+ --color-main-light-hard: oklch(98.403% 0.01457 80.636);
--color-main: var(--color-main-dark-hard);
--color-main-hard: var(--color-main-dark-hard);
--color-main-medium: var(--color-main-dark-medium);
@@ -157,8 +181,8 @@ body {
--color-leaflet-link: var(--color-main-medium);
--color-leaflet-link-hover: var(--color-main-soft);
--interlocked-hexagons-background:
- url("https://storage.yandexcloud.kz/sayauz-static/themes/lettuce/linen-fabric.webp"),
- url("https://storage.yandexcloud.kz/sayauz-static/themes/lettuce/lettuce-tiles.svg");
+ url("https://cdn.saya.uz/stats/themes/lettuce/linen-fabric.webp"),
+ url("https://cdn.saya.uz/stats/themes/lettuce/lettuce-tiles.svg");
--interlocked-hexagons-background-repeat: repeat, repeat;
--interlocked-hexagons-background-size: 11dvh, 20dvh;
--squares-and-triangles-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23bbd095%22%2F%3E%3Cpath%20fill%3D%22%23f5ede1%22%20d%3D%22M0%200v6.77L6.72%200zm8.91%200L0%208.96v3.86L12.83%200zm6.11%200L0%2015.06v3.87L18.9%200zm6.11%200L0%2021.12v3.92L25%200zM25%200l25%2024.99v-3.87L28.87%200zm25%2024.99%2025%2024.98L100%2025%2075%200zm-50%20.05v3.87l21.13%2021.12H25l-25-25zm25%2024.99h3.87L50%2028.9v-3.87zm0%200L0%2075l25%2025%2025-24.99zM50%2075l25-24.98h-3.87L50%2071.14v3.87zm0%200v3.87L71.13%20100H75L50%2075.01zm25%2025h3.87L100%2078.88v-3.87zm25-24.99v-3.87L78.87%2050.03H75L100%2075zM31.1%200%2050%2018.93v-3.87L34.98%200h-3.87zm6.07%200L50%2012.82V8.96L41.04%200zm6.1%200L50%206.77V0h-6.72zM25%202.19%202.14%2025.04H25zm0%2022.85v22.8l22.81-22.8zM75%203.92l21.13%2021.12L75%2046.16%2053.87%2025.04zm0%202.19L56.06%2025.04l18.89%2018.88%2018.94-18.88L75%206.1zm0%203.92%2015.07%2015L75%2040.06%2059.98%2025.04zm0%202.18L62.17%2025.04%2075%2037.86l12.83-12.82L75%2012.2zm0%203.82%208.96%209L75%2033.95%2066.04%2025zm0%202.24-6.72%206.77L75%2031.76l6.77-6.72h-.05zM0%2031.09v3.87l15.02%2015.07h3.87zm50%200L31.1%2050.03h3.88L50%2034.96V31.1zM0%2037.2v3.87l8.91%208.96h3.92zm50%200L37.17%2050.03h3.87L50%2041.07zM0%2043.3v6.73h6.72zm50%200-6.72%206.73H50zm0%206.73v6.71l6.72-6.71zm8.96%200L50%2058.93v3.92l12.83-12.82zm6.06%200L50%2065.03v3.88l18.94-18.88zm16.09%200L100%2068.9v-3.87l-15.02-15zm6.11%200L100%2062.85v-3.92l-8.91-8.9zm6.06%200%206.72%206.71v-6.71zM75%2052.2%2052.19%2075H75zM75%2075v22.8L97.81%2075zM25%2053.9l21.13%2021.12L25%2096.13%203.87%2075.01zm0%202.2L6.1%2074.95l18.95%2018.88%2018.9-18.88L25%2056.08zm0%203.9%2015.02%2015.01L25%2090.03%209.98%2075zm0%202.19L12.17%2075%2025%2087.84%2037.83%2075%2025%2062.2zm0%203.87L33.96%2075%2025%2083.97%2016.04%2075zm0%202.23-6.72%206.72L25%2081.73l6.72-6.72L25%2068.3zm25%2012.83v3.87L65.02%20100h3.87zm50%200L81.1%20100h3.88L100%2084.99zm-50%206.06v3.86l8.96%208.96h3.87zm50%200L87.22%20100h3.87l8.91-8.96zm-50%206.1V100h6.72zm50%200L93.28%20100H100z%22%2F%3E%3C%2Fsvg%3E");
@@ -167,66 +191,76 @@ body {
--squares-and-triangles-background-height-coef: 10;
}
+@media (prefers-color-scheme: dark) {
+ [data-theme="lettuce"] {
+ --color-accent-light: var(--color-lime-600);
+ --color-accent-deep: var(--color-lime-300);
+ --color-main: var(--color-main-light-hard);
+ --color-main-hard: var(--color-main-light-hard);
+ --color-main-medium: var(--color-main-light-medium);
+ --color-main-soft: var(--color-main-light-soft);
+ --color-background: var(--color-main-dark-hard);
+ --color-background-dark: var(--color-main-dark-hard);
+ --color-background-medium: var(--color-main-dark-medium);
+ --color-background-light: var(--color-main-dark-soft);
+ --color-sidebar-stroke: var(--color-main-light-hard);
+ }
+}
+
[data-theme="night"] {
- --color-accent-light: var(--color-indigo-900);
- --color-accent-deep: var(--color-indigo-500);
- --color-main-light-hard: var(--color-sky-100);
- --color-main-light-medium: var(--color-sky-200);
- --color-main-light-soft: var(--color-sky-300);
- --color-main-dark-hard: #13131b;
+ --color-accent-light: var(--color-indigo-500);
+ --color-accent-deep: var(--color-indigo-800);
+ --color-main-light-hard: var(--color-indigo-300);
+ --color-main-light-medium: var(--color-indigo-200);
+ --color-main-light-soft: var(--color-indigo-200);
+ --color-main-dark-hard: oklch(19.064% 0.01614 284.757);
--color-main-dark-medium: var(--color-main-dark-hard);
- --color-main-dark-soft: #222230;
- --color-main: var(--color-main-light-hard);
- --color-main-hard: var(--color-main-light-hard);
- --color-main-medium: var(--color-main-light-medium);
- --color-main-soft: var(--color-main-light-soft);
- --color-background: var(--color-main-dark-soft);
- --color-background-dark: var(--color-main-dark-hard);
- --color-background-medium: var(--color-main-dark-medium);
- --color-background-light: var(--color-main-dark-soft);
+ --color-main-dark-soft: oklch(25.846% 0.02619 284.414);
+ --color-main: var(--color-main-dark-hard);
+ --color-main-hard: var(--color-main-dark-hard);
+ --color-main-medium: var(--color-main-dark-medium);
+ --color-main-soft: var(--color-main-dark-soft);
+ --color-background: var(--color-main-light-soft);
+ --color-background-dark: var(--color-main-light-hard);
+ --color-background-medium: var(--color-main-light-medium);
+ --color-background-light: var(--color-main-light-soft);
--color-sidebar-text: var(--color-main-hard);
- --color-sidebar-stroke: var(--color-main-light-soft);
+ --color-sidebar-stroke: var(--color-main-dark-soft);
--color-secondary: var(--color-main-soft);
--color-leaflet-text: var(--color-background-dark);
--color-leaflet-link: var(--color-background-dark);
--color-leaflet-link-hover: var(--color-background-light);
--interlocked-hexagons-background:
- radial-gradient(
- 33.5% 25% at 100% 82%,
- rgba(190, 227, 248, 0.2) 0%,
- rgba(197, 179, 246, 0.13) 33.33%,
- rgba(255, 0, 0, 0) 100%
- ),
- radial-gradient(
- 54% 25% at 55% 100%,
- rgba(190, 227, 248, 0.2) 0%,
- rgba(197, 179, 246, 0.13) 33.33%,
- rgba(255, 0, 0, 0) 100%
- ),
- radial-gradient(
- 37% 19.5% at 20% 100%,
- rgba(176, 220, 248, 0.3) 0%,
- rgba(197, 172, 247, 0.2) 33.33%,
- rgba(255, 0, 0, 0) 100%
+ linear-gradient(
+ 180deg,
+ #4c51bf 0%,
+ rgba(89, 108, 214, 0.75) 12%,
+ rgba(107, 136, 231, 0.5) 24%,
+ rgba(163, 191, 250, 0) 48%
),
radial-gradient(
- 63% 34.5% at 66% 77%,
- rgba(190, 227, 248, 0.15) 0%,
- rgba(197, 179, 246, 0.1) 33.33%,
- rgba(255, 0, 0, 0) 100%
+ 100% 37.5% at 50% 100%,
+ #f687b3 0%,
+ rgba(245, 151, 227, 0.75) 25%,
+ rgba(217, 182, 245, 0.5) 50%,
+ rgba(190, 227, 248, 0) 100%
+ ),
+ linear-gradient(
+ 360deg,
+ #f6ad55 0%,
+ rgba(247, 183, 98, 0.75) 14%,
+ rgba(249, 192, 112, 0.5) 28%,
+ rgba(251, 211, 141, 0) 56%
),
linear-gradient(
- 0deg,
- #2f5883 0%,
- #2f4e7a 4.75%,
- #2e4472 9.5%,
- #2a3065 19%,
- #272e57 26.5%,
- #242b4b 34%,
- #1f2437 49%,
- #1c2030 57.75%,
- #1a1b29 66.5%,
- #13131b 84%
+ 90deg,
+ #bee3f8 0%,
+ #b7e0f7 14.29%,
+ #b1ddf7 28.57%,
+ #aadaf6 42.86%,
+ #a4d7f6 57.14%,
+ #9dd3f5 71.43%,
+ #90cdf4 100%
);
--interlocked-hexagons-background-repeat: no-repeat;
--interlocked-hexagons-background-size: 100dvw 100dvh;
@@ -236,47 +270,117 @@ body {
--squares-and-triangles-background-height-coef: 5;
}
+@media (prefers-color-scheme: dark) {
+ [data-theme="night"] {
+ --color-accent-light: var(--color-indigo-900);
+ --color-accent-deep: var(--color-indigo-500);
+ --color-main: var(--color-main-light-hard);
+ --color-main-hard: var(--color-main-light-hard);
+ --color-main-medium: var(--color-main-light-medium);
+ --color-main-soft: var(--color-main-light-soft);
+ --color-background: var(--color-main-dark-soft);
+ --color-background-dark: var(--color-main-dark-hard);
+ --color-background-medium: var(--color-main-dark-medium);
+ --color-background-light: var(--color-main-dark-soft);
+ --color-sidebar-stroke: var(--color-main-light-soft);
+ --interlocked-hexagons-background:
+ radial-gradient(
+ 33.5% 25% at 100% 82%,
+ rgba(190, 227, 248, 0.2) 0%,
+ rgba(197, 179, 246, 0.13) 33.33%,
+ rgba(255, 0, 0, 0) 100%
+ ),
+ radial-gradient(
+ 54% 25% at 55% 100%,
+ rgba(190, 227, 248, 0.2) 0%,
+ rgba(197, 179, 246, 0.13) 33.33%,
+ rgba(255, 0, 0, 0) 100%
+ ),
+ radial-gradient(
+ 37% 19.5% at 20% 100%,
+ rgba(176, 220, 248, 0.3) 0%,
+ rgba(197, 172, 247, 0.2) 33.33%,
+ rgba(255, 0, 0, 0) 100%
+ ),
+ radial-gradient(
+ 63% 34.5% at 66% 77%,
+ rgba(190, 227, 248, 0.15) 0%,
+ rgba(197, 179, 246, 0.1) 33.33%,
+ rgba(255, 0, 0, 0) 100%
+ ),
+ linear-gradient(
+ 0deg,
+ #2f5883 0%,
+ #2f4e7a 4.75%,
+ #2e4472 9.5%,
+ #2a3065 19%,
+ #272e57 26.5%,
+ #242b4b 34%,
+ #1f2437 49%,
+ #1c2030 57.75%,
+ #1a1b29 66.5%,
+ #13131b 84%
+ );
+ }
+}
+
[data-theme="ram"] {
- --color-accent-light: var(--color-orange-800);
- --color-accent-deep: var(--color-orange-300);
- --color-main-light-hard: #fefad8;
- --color-main-light-medium: #f6e4c5;
- --color-main-light-soft: #e8d4b8;
- --color-main-dark-hard: #413b34;
- --color-main-dark-medium: #62584f;
- --color-main-dark-soft: #6d6258;
- --color-main: var(--color-main-light-hard);
- --color-main-hard: var(--color-main-light-hard);
- --color-main-medium: var(--color-main-light-medium);
- --color-main-soft: var(--color-main-light-soft);
- --color-background: var(--color-main-dark-soft);
- --color-background-dark: var(--color-main-dark-hard);
- --color-background-medium: var(--color-main-dark-medium);
- --color-background-light: var(--color-main-dark-soft);
+ --color-accent-light: var(--color-orange-300);
+ --color-accent-deep: var(--color-orange-800);
+ --color-main-light-hard: oklch(97.95% 0.0443 101.66);
+ --color-main-light-medium: oklch(92.541% 0.04519 81.305);
+ --color-main-light-soft: oklch(87.909% 0.04338 76.287);
+ --color-main-dark-hard: oklch(35.615% 0.01442 71.558);
+ --color-main-dark-medium: oklch(46.724% 0.01927 64.275);
+ --color-main-dark-soft: oklch(50.352% 0.02092 64.558);
+ --color-main: var(--color-main-dark-hard);
+ --color-main-hard: var(--color-main-dark-hard);
+ --color-main-medium: var(--color-main-dark-medium);
+ --color-main-soft: var(--color-main-dark-soft);
+ --color-background: var(--color-main-light-hard);
+ --color-background-dark: var(--color-main-light-hard);
+ --color-background-medium: var(--color-main-light-medium);
+ --color-background-light: var(--color-main-light-soft);
--color-sidebar-text: var(--color-main-hard);
- --color-sidebar-stroke: var(--color-main-light-soft);
+ --color-sidebar-stroke: var(--color-main-dark-soft);
--color-secondary: var(--color-main-soft);
--color-leaflet-text: var(--color-background-dark);
--color-leaflet-link: var(--color-background-dark);
--color-leaflet-link-hover: var(--color-background-light);
- --interlocked-hexagons-background: url("https://storage.yandexcloud.kz/sayauz-static/themes/ram/ram-background.webp");
+ --interlocked-hexagons-background: url("https://cdn.saya.uz/stats/themes/ram/ram-background.webp");
--interlocked-hexagons-background-repeat: repeat;
--interlocked-hexagons-background-size: auto;
- --squares-and-triangles-background: url("https://storage.yandexcloud.kz/sayauz-static/themes/ram/ram-sidebar.webp");
+ --squares-and-triangles-background: url("https://cdn.saya.uz/stats/themes/ram/ram-sidebar.webp");
--squares-and-triangles-background-repeat: repeat;
--squares-and-triangles-background-width-coef: 5;
--squares-and-triangles-background-height-coef: 15;
}
+@media (prefers-color-scheme: dark) {
+ [data-theme="ram"] {
+ --color-accent-light: var(--color-orange-800);
+ --color-accent-deep: var(--color-orange-300);
+ --color-main: var(--color-main-light-hard);
+ --color-main-hard: var(--color-main-light-hard);
+ --color-main-medium: var(--color-main-light-medium);
+ --color-main-soft: var(--color-main-light-soft);
+ --color-background: var(--color-main-dark-soft);
+ --color-background-dark: var(--color-main-dark-hard);
+ --color-background-medium: var(--color-main-dark-medium);
+ --color-background-light: var(--color-main-dark-soft);
+ --color-sidebar-stroke: var(--color-main-light-soft);
+ }
+}
+
[data-theme="lilac"] {
--color-accent-light: var(--color-indigo-300);
--color-accent-deep: var(--color-indigo-800);
- --color-main-dark-hard: #343b58;
- --color-main-dark-medium: #414868;
- --color-main-dark-soft: #6c6e75;
- --color-main-light-hard: #e3e6ff;
- --color-main-light-medium: #d1d6ff;
- --color-main-light-soft: #c3caff;
+ --color-main-dark-hard: oklch(35.926% 0.05132 273.187);
+ --color-main-dark-medium: oklch(40.944% 0.05461 274.279);
+ --color-main-dark-soft: oklch(53.885% 0.01118 273.226);
+ --color-main-light-hard: oklch(93.022% 0.03408 280.254);
+ --color-main-light-medium: oklch(88.545% 0.05677 279.69);
+ --color-main-light-soft: oklch(85.154% 0.07433 278.817);
--color-main: var(--color-main-dark-hard);
--color-main-hard: var(--color-main-dark-hard);
--color-main-medium: var(--color-main-dark-medium);
@@ -298,8 +402,24 @@ body {
--squares-and-triangles-background-repeat: repeat;
--squares-and-triangles-background-width-coef: 5;
--squares-and-triangles-background-height-coef: 5;
- --split-left-background: url("https://storage.yandexcloud.kz/sayauz-static/themes/lilac/split-left.webp");
- --split-right-background: url("https://storage.yandexcloud.kz/sayauz-static/themes/lilac/split-right.webp");
+ --split-right-background: url("https://cdn.saya.uz/stats/themes/lilac/split-right-light.webp");
+}
+
+@media (prefers-color-scheme: dark) {
+ [data-theme="lilac"] {
+ --color-accent-light: var(--color-indigo-800);
+ --color-accent-deep: var(--color-indigo-300);
+ --color-main: var(--color-main-light-hard);
+ --color-main-hard: var(--color-main-light-hard);
+ --color-main-medium: var(--color-main-light-medium);
+ --color-main-soft: var(--color-main-light-soft);
+ --color-background: var(--color-main-dark-hard);
+ --color-background-dark: var(--color-main-dark-hard);
+ --color-background-medium: var(--color-main-dark-medium);
+ --color-background-light: var(--color-main-dark-soft);
+ --color-sidebar-stroke: var(--color-main-dark-hard);
+ --split-right-background: url("https://cdn.saya.uz/stats/themes/lilac/split-right-dark.webp");
+ }
}
a,
@@ -314,16 +434,16 @@ a:hover,
}
#map-container {
- color: var(--color-leaflet-text) !important;
+ color: var(--color-main-hard) !important;
}
#map-container a {
- color: var(--color-leaflet-link) !important;
+ color: var(--color-main-medium) !important;
text-decoration: underline;
}
#map-container a:hover {
- color: var(--color-leaflet-link-hover) !important;
+ color: var(--color-main-soft) !important;
text-decoration: underline;
}
@@ -348,6 +468,26 @@ form.crossable.htmx-request input {
filter: grayscale(1);
}
+.multitone {
+ position: relative;
+ z-index: 1;
+}
+
+.multitone::before {
+ filter: url(#multitone);
+ background-image: var(--multitone-bg);
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: -1;
+}
+
.bg-interlocked-hexagons {
background-image: var(--interlocked-hexagons-background);
background-repeat: var(--interlocked-hexagons-background-repeat);
@@ -359,13 +499,6 @@ form.crossable.htmx-request input {
background-repeat: var(--squares-and-triangles-background-repeat);
}
-.bg-split-left {
- background-image: var(--split-left-background);
- background-repeat: var(--split-left-repeat);
- background-size: var(--split-left-size);
- background-position: var(--split-left-position);
-}
-
.bg-split-right {
background-image: var(--split-right-background);
background-repeat: var(--split-right-repeat);
@@ -406,14 +539,6 @@ form.crossable.htmx-request input {
calc(var(--squares-and-triangles-background-height-coef) * 1rem);
}
-.inset-shadow {
- box-shadow:
- inset 0dvh 1dvh 1dvh -1dvh rgba(75, 81, 58, 0.5),
- inset 1dvh 0dvh 1dvh -1dvh rgba(75, 81, 58, 0.5),
- inset -1dvh 0dvh 1dvh -1dvh rgba(75, 81, 58, 0.5),
- inset 0dvh -0.5dvh rgba(75, 81, 58, 0.5);
-}
-
.text-stroke-\(--color-sidebar-stroke\) {
-webkit-text-stroke-color: var(--color-sidebar-stroke);
}
@@ -426,7 +551,7 @@ form.crossable.htmx-request input {
position: fixed;
width: 12rem;
height: 12rem;
- box-shadow: 0.25rem 0.25rem 0.25rem var(--color-main-soft);
+ box-shadow: 0px 0.3125rem 0.3125rem -0.1875rem rgba(0, 0, 0, 0.2), 0px 0.5rem 0.625rem 0.0625rem rgba(0, 0, 0, 0.14), 0px 0.1875rem 0.875rem 0.125rem rgba(0, 0, 0, 0.12);
border-radius: 50%;
opacity: 0;
visibility: hidden;
@@ -461,7 +586,7 @@ form.crossable.htmx-request input {
.theme-icon i {
position: absolute;
- filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
+ filter: drop-shadow(0 0.125rem 0.25rem rgba(0, 0, 0, 0.3));
z-index: 1;
}
@@ -487,7 +612,7 @@ form.crossable.htmx-request input {
box-shadow: 0rem 0rem 1rem var(--color-main-hard);
border: 0.5rem solid
color-mix(in srgb, var(--color-main-soft) 90%, transparent);
- backdrop-filter: blur(2px);
+ backdrop-filter: blur(0.125rem);
top: 35%;
left: 35%;
width: 30%;
@@ -512,6 +637,10 @@ form.crossable.htmx-request input {
transition-timing-function: var(--ease-in-out);
}
+.accent-button {
+ box-shadow: 0px 0.1875rem 0.1875rem -0.125rem rgba(0, 0, 0, 0.2), 0px 0.1875rem 0.25rem 0px rgba(0, 0, 0, 0.14), 0px 0.0625rem 0.5rem 0px rgba(0, 0, 0, 0.12);
+}
+
.accent-button:hover,
.accent-button.active:active:hover {
cursor: pointer;
@@ -525,6 +654,7 @@ form.crossable.htmx-request input {
--tw-gradient-position: from 45deg in oklab;
--tw-gradient-to: color-mix(in oklab, var(--color-accent-dark) 30%, transparent);
--tw-gradient-via: color-mix(in oklab, var(--color-accent-light) 50%, transparent);
+ box-shadow: inset 0px 0.25rem 0.5rem 0px rgba(0, 0, 0, 0.25);
}
.accent-button.active:hover,
@@ -540,7 +670,7 @@ form.crossable.htmx-request input {
}
.custom-svg-marker svg {
- filter: drop-shadow(2px 2px 2px rgba(0, 0, 0, 0.3));
+ filter: drop-shadow(0.125rem 0.125rem 0.125rem rgba(0, 0, 0, 0.3));
transition: transform 0.2s;
}
@@ -596,6 +726,15 @@ form.crossable.htmx-request input {
width: 10rem;
}
+.grid-item picture {
+ mask-image:
+ linear-gradient(to right, #FFFFFF calc(100% - 0.5rem), transparent 100%),
+ linear-gradient(to left, #FFFFFF calc(100% - 0.5rem), transparent 100%),
+ linear-gradient(to top, #FFFFFF calc(100% - 0.5rem), transparent 100%),
+ linear-gradient(to bottom, #FFFFFF calc(100% - 0.5rem), transparent 100%);
+ mask-composite: intersect;
+}
+
.grid-sizer {
width: 10.5rem;
}
@@ -619,6 +758,10 @@ form.crossable.htmx-request input {
opacity: 0;
}
+.grid-item:hover picture {
+ mask-image: none;
+}
+
.grid-item-2x {
width: 20rem;
}
@@ -718,126 +861,29 @@ form.crossable.htmx-request input {
height: 100% !important;
}
-body[data-theme~="night"] .leaflet-tile,
-body[data-theme~="ram"] .leaflet-tile {
- filter: invert(1) hue-rotate(190deg) contrast(1.1) brightness(1.2) !important;
+.leaflet-container .leaflet-control-attribution {
+ background: color-mix(in oklch, var(--color-background-medium) 80%, transparent) !important;
}
-@media (min-aspect-ratio: 0.8/1) {
- .ar-gt-0\.8\:mr-6 {
- margin-right: 1.5rem;
- }
-
- .ar-gt-0\.8\:w-fit {
- width: fit-content;
- }
-
- .ar-gt-0\.8\:w-\[100dvh\] {
- width: 100dvh;
- flex-basis: 100dvh;
- }
-
- .ar-gt-0\.8\:min-w-fit {
- min-width: fit-content;
- }
-
- .ar-gt-0\.8\:min-w-1\/10 {
- min-width: 10%;
- }
-
- .ar-gt-0\.8\:max-w-1\/4 {
- max-width: 25%;
- }
-
- .ar-gt-0\.8\:items-center {
- align-items: center;
- }
+.leaflet-popup-content-wrapper, .leaflet-popup-tip {
+ background: var(--color-background-medium) !important;
}
-@media (max-aspect-ratio: 0.8/1) {
- .bg-pattern-body {
- background-size: 10dvh;
- }
-
- .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-\[8dvh\] {
- height: 8dvh;
- }
-
- .ar-lt-0\.8\:h-\[10dvh\] {
- height: 10dvh;
- }
-
- .ar-lt-0\.8\:h-\[30dvh\] {
- height: 30dvh;
- }
-
- .ar-lt-0\.8\:h-\[90dvh\] {
- height: 90dvh;
- }
-
- .ar-lt-0\.8\:w-\[8dvh\] {
- width: 8dvh;
- }
-
- .ar-lt-0\.8\:w-\[80\%\] {
- width: 80%;
- }
-
- .ar-lt-0\.8\:w-\[90dvw\] {
- width: 90dvw;
- }
-
- .ar-lt-0\.8\:w-\[100dvw\] {
- width: 100dvw;
- }
-
- .ar-lt-0\.8\:max-h-\[30\%\] {
- max-height: 30%;
- }
-
- .ar-lt-0\.8\:max-h-\[80\%\] {
- max-height: 80%;
- }
-
- .ar-lt-0\.8\:max-h-\[92dvh\] {
- max-height: 92dvh;
- }
-
- .ar-lt-0\.8\:text-\[5dvh\] {
- font-size: 5dvh;
- }
-
- .ar-lt-0\.8\:hidden {
- display: none;
- }
+.leaflet-bar a {
+ background-color: var(--color-background-medium) !important;
}
-@media (min-aspect-ratio: 0.6/1) {
- .ar-gt-0\.6\:flex-3\/5 {
- flex: 60%;
- }
+.leaflet-popup-content-wrapper, .leaflet-popup-tip {
+ color: var(--color-main-medium) !important;
+}
- .ar-gt-0\.6\:flex-2\/5 {
- flex: 40%;
- }
+.leaflet-marker-icon.marker-cluster {
+ color: var(--color-main-dark-hard) !important;
}
-@media (max-aspect-ratio: 0.6/1) {
- .ar-lt-0\.6\:flex-col {
- flex-direction: column;
- }
+.leaflet-attribution-flag {
+ visibility: hidden;
+ width: 0;
}
.home-page-heart {
@@ -858,8 +904,8 @@ body[data-theme~="ram"] .leaflet-tile {
}
.ram-frame {
- width: 20vmin;
- height: 20vmin;
+ width: 8rem;
+ height: 8rem;
user-select: none;
-webkit-user-select: none;
}
diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html
index 3920bdb..06ef625 100644
--- a/views/layouts/general-page.html
+++ b/views/layouts/general-page.html
@@ -13,22 +13,32 @@
.Content}}content="{{.}}"
{{end}}
/>
- {{- if eq .Property "og:description" }}
+ {{- if eq .Property "og:title" }}
+ <title>{{.Content}}</title>
+ {{- end }} {{- if eq .Property "og:description" }}
<meta name="description" content="{{.Content}}" />
{{- end }} {{- if eq .Property "og:url" }}
<link rel="canonical" href="{{.Content}}" />
{{- end }} {{- end }}
<link
rel="stylesheet"
- href="{{ .StaticStorageBaseUrl }}/fonts/fonts.css"
+ href="{{ .StaticStorage.BaseUrl }}/fonts/fonts.css"
/>
<link
rel="stylesheet"
- href="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.css"
+ href="{{ .StaticStorage.BaseUrl }}/libs/leaflet/1.9.4/leaflet.css"
/>
<link
rel="stylesheet"
- href="{{ .StaticStorageBaseUrl }}/libs/glightbox/3.3.0/css/glightbox.min.css"
+ href="{{ .StaticStorage.BaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.css"
+ />
+ <link
+ rel="stylesheet"
+ href="{{ .StaticStorage.BaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css"
+ />
+ <link
+ rel="stylesheet"
+ href="{{ .StaticStorage.BaseUrl }}/libs/glightbox/3.3.0/css/glightbox.min.css"
/>
<link rel="stylesheet" href="/output.css" />
<link
@@ -43,11 +53,21 @@
type="image/svg+xml"
media="(prefers-color-scheme: dark)"
/>
+ <link
+ rel="icon"
+ href="/favicon.ico"
+ type="image/x-icon"
+ />
{{- with .LinkedData }}
<script type="application/ld+json">
{{ . }}
</script>
{{- end }}
+ <script src="{{ .StaticStorage.BaseUrl }}/libs/htmx/2.0.6/htmx.min.js"></script>
+ <script src="{{ .StaticStorage.BaseUrl }}/libs/leaflet/1.9.4/leaflet.js"></script>
+ <script src="{{ .StaticStorage.BaseUrl }}/libs/protomaps-leaflet/5.0.0/dist/protomaps-leaflet.js"></script>
+ <script src="{{ .StaticStorage.BaseUrl }}/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js"></script>
+ <script src="{{ .StaticStorage.BaseUrl }}/libs/glightbox/3.3.0/js/glightbox.min.js"></script>
</head>
<body
@@ -102,6 +122,17 @@
<path d="M17.71 1c0.79 0.05 1.58 0 2.37 0a7.47 7.47 0 0 1 0.94 0.09 9 9 0 0 1 1.26 0.16 0.47 0.47 0 0 1 0.29 0.28 4.56 4.56 0 0 1 0.25 1.14 32.83 32.83 0 0 1 0.28 3.72c0 0.09 0.51 0.32 0.6 -0.14l0.06 -0.47a21.7 21.7 0 0 0 0.16 -3.22 4.7 4.7 0 0 0 -0.28 -1.5 1.53 1.53 0 0 0 -1 -0.93 8.59 8.59 0 0 0 -1.49 -0.1 9.73 9.73 0 0 0 -1.08 0c-0.79 0.06 -1.56 0.2 -2.35 0.25a0.34 0.34 0 0 0 -0.34 0.34 0.34 0.34 0 0 0 0.33 0.38Z" fill="var(--color-accent-deep)" fill-rule="evenodd" stroke-width="1"></path>
<path d="M23.82 18.17c0 -0.42 -0.05 -0.84 -0.4 -0.78s-0.26 0.35 -0.27 0.81a28.84 28.84 0 0 1 -0.27 3.08 4.63 4.63 0 0 1 -0.24 1.14 0.46 0.46 0 0 1 -0.3 0.28 7.6 7.6 0 0 1 -1.25 0.16 7.63 7.63 0 0 1 -0.95 0.09l-2.36 0a0.35 0.35 0 1 0 0 0.69c0.79 0.05 1.56 0.19 2.35 0.25a9.53 9.53 0 0 0 1.07 0 8.52 8.52 0 0 0 1.49 -0.1 1.55 1.55 0 0 0 1 -0.93 4.7 4.7 0 0 0 0.28 -1.5 23 23 0 0 0 -0.15 -3.19Z" fill="var(--color-accent-deep)" fill-rule="evenodd" stroke-width="1"></path>
</g></symbol>
+ <symbol id="icon-like" viewBox="-2 -2 28 28" fill="none">
+ <path fill="currentColor" d="M22.155 6.29294c-0.8113 -1.34837 -1.8229 -2.56561 -3 -3.61 -1.6282 -1.47261 -3.6782 -2.396134 -5.86 -2.6399952C11.1388 -0.137008 8.97321 0.252665 7.015 1.17294c-1.6708 0.75913 -3.14018 1.8997 -4.29 3.33C1.59785 5.92036 0.798705 7.56988 0.385003 9.33294c-0.010341 0.04271 -0.012051 0.08704 -0.005029 0.13041 0.007022 0.04337 0.022633 0.08491 0.045919 0.12216 0.023286 0.03726 0.053777 0.06949 0.089685 0.09481 0.035908 0.02532 0.07651 0.04321 0.119425 0.05262 0.083317 0.01503 0.169224 -0.00254 0.239953 -0.04907 0.070728 -0.04653 0.120867 -0.11846 0.140044 -0.20093 0.42729 -1.65655 1.22384 -3.19495 2.33 -4.5 1.0777 -1.30186 2.44846 -2.32993 4 -3 1.83266 -0.80027 3.84 -1.117035 5.83 -0.92 1.9654 0.25006 3.804 1.10643 5.26 2.45 1.0733 0.9746 1.9996 2.09963 2.75 3.34 0.77 1.20382 1.2366 2.57635 1.36 3.99996 0.1304 1.5877 -0.0565 3.1854 -0.55 4.7 -0.5032 1.5017 -1.4221 2.8298 -2.65 3.83 -0.7981 0.673 -1.7331 1.1644 -2.74 1.44 -1.5427 0.4432 -3.1354 0.6885 -4.74 0.73 -0.0778 0.0052 -0.1505 0.0404 -0.2028 0.0983 -0.0523 0.0579 -0.08 0.1338 -0.0772 0.2117 0 0.039 0.0078 0.0775 0.023 0.1134 0.0152 0.0358 0.0374 0.0683 0.0654 0.0953 0.028 0.0271 0.0612 0.0482 0.0976 0.0622 0.0363 0.014 0.0751 0.0205 0.114 0.0191 1.6629 -0.0085 3.3184 -0.2201 4.93 -0.63 1.0928 -0.2824 2.1146 -0.7899 3 -1.49 1.3927 -1.0653 2.454 -2.5046 3.06 -4.15 0.5754 -1.6287 0.8235 -3.3551 0.73 -5.08 -0.1076 -1.59917 -0.6055 -3.14765 -1.45 -4.50996Z" stroke-width="1"></path>
+ <path fill="currentColor" d="M15.935 15.903c0.6791 -0.2938 1.2468 -0.7964 1.6208 -1.4347 0.3741 -0.6384 0.5349 -1.3794 0.4592 -2.1153 -0.0284 -0.1682 -0.0994 -0.3264 -0.2061 -0.4594 -0.1068 -0.133 -0.2458 -0.2365 -0.4039 -0.3007 -0.4529 -0.1228 -0.927 -0.1466 -1.39 -0.0699 -1.22 0.07 -3.05 0.38 -4.54 0.54 -0.4 0.05 -0.78 0.08 -1.11 0.1 -0.0689 0.0127 -0.1312 0.0492 -0.1761 0.1031 -0.0448 0.0539 -0.0693 0.1217 -0.0693 0.1919 0 0.0701 0.0245 0.1379 0.0693 0.1918 0.0449 0.0539 0.1072 0.0904 0.1761 0.1032l5.66 -0.18h0.83c0.0328 0.5028 -0.0896 1.0037 -0.3508 1.4347 -0.2611 0.431 -0.6483 0.7715 -1.1092 0.9753 -0.4499 0.3065 -0.9606 0.5125 -1.4972 0.6039 -0.5367 0.0915 -1.0868 0.0663 -1.6128 -0.074 -0.4102 -0.1727 -0.7888 -0.4126 -1.12 -0.71 -0.3351 -0.2774 -0.6136 -0.6169 -0.82 -1 -0.0184 -0.0407 -0.0446 -0.0774 -0.0771 -0.108 -0.0326 -0.0305 -0.0709 -0.0544 -0.1127 -0.0702 -0.0418 -0.0158 -0.0862 -0.0232 -0.1309 -0.0218 -0.04465 0.0014 -0.08858 0.0116 -0.12929 0.03 -0.04037 0.0174 -0.07686 0.0426 -0.10734 0.0743 -0.03049 0.0316 -0.05435 0.069 -0.07018 0.11 -0.01584 0.041 -0.02333 0.0847 -0.02203 0.1286 0.00129 0.044 0.01133 0.0872 0.02955 0.1272 0.21114 0.4969 0.52069 0.9459 0.90999 1.32 0.3786 0.3811 0.8137 0.7015 1.29 0.95 0.6592 0.2289 1.361 0.3082 2.0546 0.2321 0.6936 -0.0762 1.3616 -0.3057 1.9554 -0.6721Z" stroke-width="1"></path>
+ <path fill="currentColor" d="M8.33499 10.9829c0.22252 0.0613 0.45748 0.0613 0.68 0 0.17653 -0.0617 0.33498 -0.1663 0.46112 -0.3044 0.12615 -0.1381 0.21605 -0.3053 0.26164 -0.4867 0.04559 -0.1814 0.04545 -0.37124 -0.00041 -0.55256 -0.04586 -0.18131 -0.136 -0.34841 -0.26235 -0.4863 -1 -1.24 -3 0.82 -1.74 1.71996 0.1869 0.0887 0.39382 0.1266 0.6 0.11Z" stroke-width="1"></path>
+ <path fill="currentColor" d="M19.1653 10.343c0.2247 0.0401 0.456 0.0194 0.67 -0.06 0.1784 -0.0611 0.3386 -0.1658 0.4661 -0.30464 0.1275 -0.13885 0.2183 -0.3074 0.264 -0.4903 0.0457 -0.1829 0.045 -0.37432 -0.0022 -0.55685 -0.0472 -0.18253 -0.1393 -0.35035 -0.2679 -0.48818 -1 -1.23 -3 0.83 -1.74 1.72997 0.1838 0.1117 0.3949 0.1705 0.61 0.17Z" stroke-width="1"></path>
+ <path fill="currentColor" d="M9.63505 20.0929c-0.10233 -0.204 -0.25778 -0.3768 -0.45 -0.5 0.19629 -0.3235 0.27075 -0.7064 0.21 -1.08 -0.04172 -0.3535 -0.15757 -0.6943 -0.34 -1 -0.13496 -0.1997 -0.31256 -0.3671 -0.52 -0.49 0.04212 -0.0952 0.06912 -0.1964 0.08 -0.3 0.05325 -0.3277 0.02651 -0.6634 -0.07796 -0.9786 -0.10447 -0.3151 -0.28357 -0.6003 -0.52204 -0.8314 -0.51428 -0.3306 -1.1194 -0.4915 -1.73 -0.46 -0.74972 -0.022 -1.49478 0.125 -2.18 0.43 0.05982 -0.8997 -0.19079 -1.7927 -0.71 -2.53 -0.9 -1 -2.54 -0.13 -2.71 1.36 -0.017326 0.6116 0.060262 1.2222 0.23 1.81 0.28 1 0.23 0.95 0.48 1.33 -0.22002 0.2395 -0.405175 0.5088 -0.55 0.8 -0.1 0.34 -0.28 0.74 -0.12 1 0.16 0.26 0.3 0.23 0.47 0.06 0.17 -0.17 0.09 -0.44 0.14 -0.6 0.07428 -0.2866 0.22256 -0.5487 0.43 -0.76 0.16 -0.21 0.28 -0.22 0.47 -0.4 1.85 -1.77 4.62 -1.97 5.12 -1.28 0.85 1.16 -1.16 1.29 -2.9 2.37 -0.03825 0.011 -0.07372 0.03 -0.10405 0.0557 -0.03034 0.0258 -0.05483 0.0577 -0.07187 0.0936 -0.01703 0.036 -0.0262 0.0752 -0.02691 0.1149 -0.0007 0.0398 0.00708 0.0793 0.02283 0.1158 0.01228 0.0374 0.03203 0.0719 0.05805 0.1014 0.02602 0.0295 0.05777 0.0534 0.09332 0.0702 0.03554 0.0168 0.07414 0.0263 0.11345 0.0277 0.0393 0.0015 0.07849 -0.0051 0.11518 -0.0193 0.41 -0.06 0.77 -0.06 1.12 -0.11 1 -0.17 1.25 -0.66 2.16 -0.51 0.15573 0.127 0.26274 0.3039 0.30284 0.5008 0.0401 0.1968 0.01083 0.4015 -0.08284 0.5792 -0.45993 0.315 -0.97588 0.539 -1.52 0.66 -0.36935 0.1765 -0.72678 0.377 -1.07 0.6 -0.03798 0.0103 -0.07348 0.0282 -0.10433 0.0526 -0.03085 0.0244 -0.05641 0.0549 -0.07512 0.0895 -0.01871 0.0346 -0.03018 0.0727 -0.03371 0.1119 -0.00353 0.0391 0.00094 0.0786 0.01316 0.116 0.01027 0.038 0.02815 0.0735 0.05258 0.1044 0.02442 0.0308 0.05486 0.0564 0.08948 0.0751 0.03461 0.0187 0.07267 0.0302 0.11185 0.0337 0.03919 0.0035 0.07869 -0.0009 0.11609 -0.0132 0.36103 -0.003 0.7216 -0.0264 1.08 -0.07 0.15662 -0.0212 0.31072 -0.0581 0.46 -0.11 0.36406 -0.2158 0.79585 -0.2872 1.21 -0.2 0.03286 0.0062 0.06407 0.0192 0.09162 0.0381 0.02755 0.019 0.05084 0.0435 0.06838 0.0719 0.03012 0.0772 0.03012 0.1629 0 0.24 -0.05277 0.2543 -0.17376 0.4893 -0.35 0.68 -0.20205 0.2292 -0.43028 0.434 -0.68 0.61 -0.66869 0.4607 -1.44234 0.7461 -2.25 0.83 -0.80071 0.1179 -1.61838 -0.0039 -2.35 -0.35 -0.53395 -0.2824 -0.95681 -0.737 -1.2 -1.29 -0.27045 -0.5855 -0.42982 -1.2162 -0.47 -1.86 -0.00126 -0.0442 -0.0114 -0.0877 -0.02981 -0.1279 -0.01841 -0.0403 -0.04473 -0.0764 -0.07738 -0.1062 -0.03265 -0.0299 -0.07097 -0.0529 -0.11269 -0.0676 -0.04171 -0.0148 -0.08596 -0.021 -0.13012 -0.0183 -0.044675 0.0026 -0.088408 0.0139 -0.12869 0.0334s-0.076321 0.0468 -0.106051 0.0802c-0.029729 0.0334 -0.052563 0.0724 -0.067194 0.1147 -0.014631 0.0423 -0.02077 0.0871 -0.018065 0.1317 0.012623 0.8862 0.235395 1.7567 0.65 2.54 0.30359 0.5453 0.74942 0.9981 1.29 1.31 0.8694 0.479 1.8607 0.6912 2.85 0.61 0.9936 -0.0912 1.94725 -0.4355 2.77 -1 0.4107 -0.274 0.77525 -0.6115 1.08 -1 0.2272 -0.2941 0.3845 -0.6361 0.46 -1 0.04902 -0.304 -0.00001 -0.6157 -0.14 -0.89Zm-7.81 -3.72 -0.09 -1c0 -0.75 -0.13 -1.73 0.37 -2.19 0.1 0 0.45 -0.59 0.88 0.22 0.2375 0.4432 0.38371 0.9294 0.43 1.43 -0.00039 0.0506 0.01118 0.1005 0.03377 0.1457 0.0226 0.0451 0.05557 0.0843 0.09623 0.1143 -0.63566 0.3358 -1.2158 0.7676 -1.72 1.28Z" stroke-width="1"></path>
+ </symbol>
+ <symbol id="icon-view" viewBox="-2 -2 28 28" fill="none">
+ <path fill="currentColor" fill-rule="evenodd" d="M23.6534 10.5263c-1.3628 -1.56103 -3.0387 -2.81795 -4.9189 -3.68916 -1.8052 -0.8978 -3.7485 -1.48585 -5.7486 -1.7396 -0.998 -0.11306 -2.0067 -0.09289 -2.99934 0.05999 -2.63062 0.3739 -5.14267 1.33771 -7.34828 2.81934 -0.84337 0.55535 -1.63325 1.18793 -2.359454 1.88956 -0.08425 0.0868 -0.161162 0.18047 -0.2299462 0.27997C0.0169406 10.1973 0 10.2562 0 10.3163s0.0169406 0.1191 0.0488798 0.17c0.0179573 0.0358 0.0429176 0.0677 0.0734052 0.0937 0.030488 0.0261 0.065885 0.0457 0.104099 0.0578 0.038214 0.0122 0.078469 0.0165 0.118385 0.0128 0.039917 -0.0037 0.078686 -0.0154 0.114015 -0.0343C1.5971 9.42178 2.95486 8.45777 4.45785 7.77692c0.959 -0.48729 1.9636 -0.87909 2.9993 -1.16973 0.88979 -0.25558 1.79943 -0.43617 2.71935 -0.53987 0.8961 -0.11518 1.8033 -0.11518 2.6994 0 1.8879 0.28843 3.7154 0.88632 5.4087 1.76959 1.6883 0.80557 3.2029 1.93308 4.459 3.31919 0.0877 0.152 0.1339 0.3244 0.1339 0.4999s-0.0462 0.3479 -0.1339 0.4999c-0.2004 0.4189 -0.4747 0.7983 -0.8098 1.1197 -0.9132 0.9438 -1.9179 1.7945 -2.9993 2.5394 -1.0941 0.7277 -2.2767 1.3123 -3.5192 1.7396 -0.786 0.2536 -1.6039 0.395 -2.4294 0.4199 -1.2488 0.0246 -2.4978 -0.0321 -3.73917 -0.1699 -1.55221 -0.2376 -3.01813 -0.8673 -4.259 -1.8296 -1.29902 -0.9356 -2.40844 -2.1095 -3.26924 -3.4592 -0.02265 -0.0404 -0.05333 -0.0757 -0.09014 -0.1039 -0.03681 -0.0281 -0.07898 -0.0484 -0.12392 -0.0596 -0.04494 -0.0113 -0.0917 -0.0132 -0.13741 -0.0057s-0.08941 0.0243 -0.12842 0.0492c-0.07659 0.0529 -0.13044 0.1327 -0.15082 0.2235 -0.02039 0.0908 -0.0058 0.1859 0.04085 0.2664 0.8813 1.4449 2.02784 2.7102 3.37921 3.7292 1.34805 1.0808 2.9532 1.7938 4.65891 2.0695 1.26545 0.179 2.54115 0.2758 3.81915 0.2899 0.8996 0.0048 1.7952 -0.1198 2.6593 -0.3699 1.3434 -0.4251 2.6244 -1.0269 3.8092 -1.7896 1.1699 -0.7705 2.2556 -1.6619 3.2392 -2.6594 0.5478 -0.5108 0.9601 -1.1498 1.1997 -1.8595 0.0999 -0.2934 0.1302 -0.606 0.0886 -0.9131 -0.0417 -0.3071 -0.1541 -0.6003 -0.3285 -0.8565Z" clip-rule="evenodd" stroke-width="1"></path>
+ <path fill="currentColor" fill-rule="evenodd" d="M10.6964 9.1166c0.7827 -0.2444 1.6136 -0.29247 2.4194 -0.13997 0.3938 0.09551 0.7634 0.27195 1.0852 0.51809 0.3219 0.24613 0.589 0.55658 0.7844 0.91158 0.0189 0.0362 0.0448 0.0683 0.0762 0.0944 0.0314 0.0262 0.0677 0.0458 0.1067 0.0579 0.039 0.0121 0.08 0.0163 0.1207 0.0125 0.0407 -0.0038 0.0802 -0.0157 0.1163 -0.0348 0.0713 -0.04 0.124 -0.1066 0.1465 -0.1853 0.0224 -0.0786 0.0129 -0.163 -0.0266 -0.2346 -0.213 -0.44629 -0.5169 -0.84312 -0.8922 -1.16507 -0.3753 -0.32195 -0.8138 -0.56192 -1.2873 -0.70452 -0.9344 -0.26583 -1.92 -0.29674 -2.8693 -0.08998 -0.53345 0.13369 -1.02665 0.39446 -1.4375 0.76005 -0.41085 0.36559 -0.72716 0.82514 -0.92194 1.33942 -0.41881 1.0115 -0.41881 2.1478 0 3.1593 0.4668 0.9845 1.29879 1.7483 2.31944 2.1295 0.9793 0.3819 2.0556 0.438 3.0693 0.16 0.8982 -0.2612 1.6632 -0.8546 2.1395 -1.6596 0.4789 -0.7747 0.6995 -1.6814 0.6299 -2.5894 -0.0061 -0.0449 -0.0211 -0.0881 -0.0441 -0.127 -0.0231 -0.039 -0.0536 -0.073 -0.0899 -0.1 -0.0363 -0.027 -0.0777 -0.0465 -0.1216 -0.0573 -0.044 -0.0108 -0.0897 -0.0127 -0.1344 -0.0057 -0.0457 0.0049 -0.0899 0.019 -0.1301 0.0415 -0.0401 0.0224 -0.0752 0.0528 -0.1033 0.0893 -0.028 0.0364 -0.0484 0.0782 -0.0598 0.1227 -0.0114 0.0446 -0.0137 0.091 -0.0067 0.1364 -0.0016 0.7166 -0.2252 1.4151 -0.6398 1.9996 -0.3854 0.5945 -0.9884 1.0144 -1.6796 1.1697 -0.8061 0.1738 -1.6466 0.0863 -2.3995 -0.2499 -0.7468 -0.2609 -1.36555 -0.7974 -1.72958 -1.4997 -0.29453 -0.7244 -0.29453 -1.5351 0 -2.2595 0.1115 -0.3727 0.31161 -0.7129 0.58321 -0.99148 0.27161 -0.27856 0.60667 -0.48722 0.97647 -0.60812Z" clip-rule="evenodd" stroke-width="1"></path>
+ </symbol>
</svg>
<script>
@@ -142,8 +173,6 @@
}
</script>
- <script src="{{ .StaticStorageBaseUrl }}/libs/glightbox/3.3.0/js/glightbox.min.js"></script>
-
<script>
function sfc32(a, b, c, d) {
return function () {
@@ -184,18 +213,91 @@
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;
+ function calculateRem(remNum) {
+ return remNum * parseFloat(getComputedStyle(document.documentElement).fontSize);
+ }
- const smallerDimension = Math.min(
- viewportWidth,
- viewportHeight,
- );
+ const _probe = document.createElement('div');
+ _probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none';
+ document.body.appendChild(_probe);
+ const _ctx = document.createElement('canvas').getContext('2d');
+
+ function resolveColor(cssValue) {
+ // Step 1: let the DOM resolve color-mix/oklch/var to its computed form
+ _probe.style.backgroundColor = '';
+ _probe.style.backgroundColor = cssValue;
+ const computed = getComputedStyle(_probe).backgroundColor;
+ if (!computed) return [0,0,0];
+
+ // Step 2: funnel through canvas to force sRGB
+ _ctx.fillStyle = '#000';
+ _ctx.fillStyle = computed;
+ const s = _ctx.fillStyle; // "#rrggbb" or "rgba(...)"
- const vminValue = (smallerDimension / 100) * percent;
+ if (s.startsWith('#')) {
+ return [
+ parseInt(s.slice(1,3),16)/255,
+ parseInt(s.slice(3,5),16)/255,
+ parseInt(s.slice(5,7),16)/255,
+ ];
+ }
+ const m = s.match(/[\d.]+/g);
+ return [+m[0]/255, +m[1]/255, +m[2]/255];
+ }
+
+ function oklchMix(c1, c2, t) {
+ // Use the browser's color-mix() to interpolate in oklch
+ return `color-mix(in oklch, ${c1} ${(1-t)*100}%, ${c2})`;
+ }
+
+ function makeMultitoneOklch(id, stops, ...vars) {
+ if (vars.length < 2) {
+ console.error('Less than 2 colors set in input');
+ return;
+ }
+
+ const root = getComputedStyle(document.body);
+ const rs = [], gs = [], bs = [];
+
+ for (let i = 0; i < vars.length - 1; i++) {
+ const c1 = root.getPropertyValue(vars[i]).trim();
+ const c2 = root.getPropertyValue(vars[i+1]).trim();
+
+ if (!c1) {
+ console.error(`CSS variable ${vars[i]} not found`);
+ return;
+ }
+ if (!c2) {
+ console.error(`CSS variable ${vars[i+1]} not found`);
+ return;
+ }
+
+ for (let i = 0; i < stops; i++) {
+ const t = i / (stops - 1);
+ const [r, g, b] = resolveColor(oklchMix(c1, c2, t));
+ rs.push(r.toFixed(4));
+ gs.push(g.toFixed(4));
+ bs.push(b.toFixed(4));
+ }
+ }
- return vminValue;
+ let svg = document.getElementById(id + '-svg');
+ if (svg) svg.remove();
+ document.body.insertAdjacentHTML('beforeend', `
+ <svg id="${id}-svg" width="0" height="0" style="position:absolute">
+ <filter id="${id}">
+ <feColorMatrix type="matrix" values="
+ 0.299 0.587 0.114 0 0
+ 0.299 0.587 0.114 0 0
+ 0.299 0.587 0.114 0 0
+ 0 0 0 1 0"/>
+ <feComponentTransfer color-interpolation-filters="sRGB">
+ <feFuncR type="table" tableValues="${rs.join(' ')}"/>
+ <feFuncG type="table" tableValues="${gs.join(' ')}"/>
+ <feFuncB type="table" tableValues="${bs.join(' ')}"/>
+ </feComponentTransfer>
+ </filter>
+ </svg>`);
}
function switchThemeDecor(theme) {
@@ -207,7 +309,7 @@
theme = document.body.getAttribute("data-theme");
}
- vmin = calculateVmin(1);
+ rem = calculateRem(1);
var clWidth = document.documentElement.clientWidth;
var clHeight = document.documentElement.clientHeight;
@@ -226,11 +328,11 @@
"z-0",
"absolute",
);
- const x = Math.floor(rand() * clWidth);
- const y = Math.floor(rand() * clHeight * 0.8);
const rot = rand() * 90;
const animDuration = rand() * 10 + 3;
const sz = Math.floor(rand() * 8) + 12;
+ const x = Math.floor(rand() * clWidth - 1.42 * sz); // 1.42 ~ sqrt(2)
+ const y = Math.floor(rand() * clHeight * 0.8);
star.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rot}deg)`;
star.style.animationDuration = `${animDuration}s`;
star.style.fontSize = `${sz}px`;
@@ -241,7 +343,7 @@
const frameCenters = new Array(4);
for (i = 1; i <= 4; i++) {
let frame = document.createElement("img");
- frame.src = `{{ .StaticStorageBaseUrl }}/themes/ram/ram-frame${i}.webp`;
+ frame.src = `{{ .StaticStorage.BaseUrl }}/themes/ram/ram-frame${i}.webp`;
frame.classList.add(
"ram-frame",
"theme-decor",
@@ -250,14 +352,14 @@
);
let x, y;
outerLoop: while (true) {
- x = Math.floor(rand() * clWidth - 20 * vmin);
- y = Math.floor(rand() * clHeight - 20 * vmin);
+ x = Math.floor(rand() * clWidth - 22.64 * rem) + 11.32 * rem; // 11.32 ~ 8 * sqrt(2)
+ y = Math.floor(rand() * clHeight - 22.64 * rem) + 11.32 * rem;
for (j = 0; j < i - 1; j++) {
const xd = frameCenters[j].x - x;
const yd = frameCenters[j].y - y;
if (
Math.sqrt(xd * xd + yd * yd) <
- 30 * vmin
+ 23 * rem
) {
continue outerLoop;
}
@@ -300,20 +402,68 @@
localStorage.setItem("theme", theme);
document.body.setAttribute("data-theme", theme);
switchThemeDecor(theme);
+ switchColorScheme(matchMedia('(prefers-color-scheme: dark)').matches);
}
const savedTheme = localStorage.getItem("theme") || "lilac";
switchTheme(savedTheme);
+
+ function switchColorScheme(isNightMode) {
+ if (isNightMode) {
+ makeMultitoneOklch('multitone', 4, '--color-background-dark', '--color-accent-light', '--color-background-light');
+ } else {
+ makeMultitoneOklch('multitone', 4, '--color-background-light', '--color-accent-light', '--color-background-dark');
+ }
+ }
+
+ const mq = matchMedia('(prefers-color-scheme: dark)');
+ switchColorScheme(mq.matches);
+ mq.addEventListener('change', (e) => switchColorScheme(e.matches));
</script>
- <div
- id="general-page-top-embeds"
- class="flex"
- hx-get="/api/v1/general-page/top-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
- hx-trigger="load"
- hx-target="this"
- hx-swap="innerHTML"
- ></div>
+ <script src="{{ .StaticStorage.BaseUrl }}/libs/htmx/2.0.6/htmx.min.js"></script>
+ <script src="{{ .StaticStorage.BaseUrl }}/libs/imagesloaded/5.0.0/imagesloaded.pkgd.min.js"></script>
+ <script src="{{ .StaticStorage.BaseUrl }}/libs/packery/3.0.0/dist/packery.pkgd.min.js"></script>
+
+ <script>
+ // Enable fitWidth-like attribute for Packery. Source: https://codepen.io/desandro/pen/kVmLYz
+ var __resetLayout = Packery.prototype._resetLayout;
+ Packery.prototype._resetLayout = function () {
+ __resetLayout.call(this);
+ var parentSize = getSize(this.element.parentNode);
+ var colW = this.columnWidth + this.gutter;
+ this.fitWidth =
+ Math.floor((parentSize.innerWidth + this.gutter) / colW) *
+ colW;
+ this.packer.width = this.fitWidth;
+ this.packer.height = Number.POSITIVE_INFINITY;
+ this.packer.reset();
+ };
+
+ Packery.prototype._getContainerSize = function () {
+ var emptyWidth = 0;
+ for (var i = 0, len = this.packer.spaces.length; i < len; i++) {
+ var space = this.packer.spaces[i];
+ if (
+ space.y === 0 &&
+ space.height === Number.POSITIVE_INFINITY
+ ) {
+ emptyWidth += space.width;
+ }
+ }
+
+ return {
+ width: this.fitWidth - this.gutter - emptyWidth,
+ height: this.maxY - this.gutter,
+ };
+ };
+
+ Packery.prototype.needsResizeLayout = function () {
+ return true;
+ };
+ </script>
+
+ <div id="general-page-top-embeds" class="flex">{{ .RenderedTopEmbeds }}</div>
<div
id="general-page-top-embeds-trigger"
hx-get="/api/v1/general-page/top-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
@@ -475,23 +625,19 @@
</nav>
<div
- class="flex flex-1 z-1 bg-paper bg-background shadow-elevation-3 @container/body"
+ class="flex flex-1 min-h-0 z-1 bg-paper bg-background shadow-elevation-3 @container/body"
>
<div
- class="bg-paper bg-background-dark border-r-2 border-dashed border-main-hard"
+ class="bg-paper bg-background-dark border-r-[0.25rem] border-dashed border-main-hard"
></div>
<div
- class="max-h-full max-w-full flex flex-col grow bg-paper bg-background-dark relative pt-4 pr-0 md:pr-4"
+ class="max-h-full max-w-full flex flex-col grow bg-paper bg-background-dark relative md:pr-4"
>
<header
id="general-page-header"
- class="max-xs:hidden [@media(max-height:32rem)]:hidden flex flex-col ml-8 basis-8"
- hx-get="/api/v1/general-page/header{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
- hx-trigger="load"
- hx-target="this"
- hx-swap="innerHTML"
- ></header>
+ class="max-xs:hidden [@media(max-height:32rem)]:hidden pt-4 flex flex-col ml-8 basis-8"
+ >{{ .RenderedHeader }}</header>
<div
id="general-page-header-trigger"
hx-get="/api/v1/general-page/header{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
@@ -501,33 +647,26 @@
></div>
<hr
- class="max-xs:hidden [@media(max-height:32rem)]:hidden border-t-4 border-dotted border-main-hard mb-2"
+ class="max-xs:hidden [@media(max-height:32rem)]:hidden border-t-[0.5rem] border-dotted border-main-hard mb-2"
/>
<main
id="general-page-body"
- class="relative flex flex-col grow overflow-y-auto"
- hx-get="/api/v1/general-page/body{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
- hx-trigger="load"
- hx-target="this"
- hx-swap="innerHTML"
- ></main>
+ class="relative flex-1 overflow-y-auto @container-size inset-shadow-elevation-8"
+ >{{ .RenderedBody }}</main>
<div
id="general-page-body-trigger"
hx-get="/api/v1/general-page/body{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
hx-trigger="popstate from:window"
hx-target="#general-page-body"
hx-swap="innerHTML"
+ hx-include="#general-page-body .htmx-exit-trigger"
></div>
<footer
id="general-page-footer"
class="flex flex-row basis-4 my-1"
- hx-get="/api/v1/general-page/footer{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
- hx-trigger="load"
- hx-target="this"
- hx-swap="innerHTML"
- ></footer>
+ >{{ .RenderedFooter }}</footer>
<div
id="general-page-footer-trigger"
hx-get="/api/v1/general-page/footer{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
@@ -535,71 +674,17 @@
hx-target="#general-page-footer"
hx-swap="innerHTML"
></div>
-
- <hr
- class="border-t-4 border-dotted border-main-hard mb-2"
- />
</div>
<div
- class="bg-paper bg-background-dark shadow-2xl border-l-2 border-dotted border-main-hard"
+ class="bg-paper bg-background-dark shadow-2xl border-l-[0.25rem] border-dotted border-main-hard"
></div>
</div>
</div>
<div class="grow bg-split-right"></div>
- <script src="{{ .StaticStorageBaseUrl }}/libs/htmx/2.0.6/htmx.min.js"></script>
- <script src="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.js"></script>
- <script src="{{ .StaticStorageBaseUrl }}/libs/imagesloaded/5.0.0/imagesloaded.pkgd.min.js"></script>
- <script src="{{ .StaticStorageBaseUrl }}/libs/packery/3.0.0/dist/packery.pkgd.min.js"></script>
-
- <script>
- // Enable fitWidth-like attribute for Packery. Source: https://codepen.io/desandro/pen/kVmLYz
- var __resetLayout = Packery.prototype._resetLayout;
- Packery.prototype._resetLayout = function () {
- __resetLayout.call(this);
- var parentSize = getSize(this.element.parentNode);
- var colW = this.columnWidth + this.gutter;
- this.fitWidth =
- Math.floor((parentSize.innerWidth + this.gutter) / colW) *
- colW;
- this.packer.width = this.fitWidth;
- this.packer.height = Number.POSITIVE_INFINITY;
- this.packer.reset();
- };
-
- Packery.prototype._getContainerSize = function () {
- var emptyWidth = 0;
- for (var i = 0, len = this.packer.spaces.length; i < len; i++) {
- var space = this.packer.spaces[i];
- if (
- space.y === 0 &&
- space.height === Number.POSITIVE_INFINITY
- ) {
- emptyWidth += space.width;
- }
- }
-
- return {
- width: this.fitWidth - this.gutter - emptyWidth,
- height: this.maxY - this.gutter,
- };
- };
-
- Packery.prototype.needsResizeLayout = function () {
- return true;
- };
- </script>
-
- <div
- id="general-page-bottom-embeds"
- class="flex"
- hx-get="/api/v1/general-page/bottom-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
- hx-trigger="load"
- hx-target="this"
- hx-swap="innerHTML"
- ></div>
+ <div id="general-page-bottom-embeds" class="flex">{{ .RenderedBottomEmbeds }}</div>
<div
id="general-page-bottom-embeds-trigger"
hx-get="/api/v1/general-page/bottom-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
@@ -612,7 +697,6 @@
const themeWheel = document.getElementById("theme-wheel");
const themeButton = document.getElementById("sidebar-theme-button");
const themeIcons = document.querySelectorAll(".theme-icon");
- const themeWheelRadius = convertRemToPixels(6);
themeIcons.forEach((icon) => {
if (icon.getAttribute("data-theme") == savedTheme) {
@@ -620,17 +704,6 @@
}
});
- function calculateVmax(percent) {
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
-
- const largerDimension = Math.max(viewportWidth, viewportHeight);
-
- const vmaxValue = (largerDimension / 100) * percent;
-
- return vmaxValue;
- }
-
function positionThemeIcons() {
const sliceAngle = 360 / themeIcons.length;
@@ -641,7 +714,7 @@
const clipPath = createSliceClipPath(
startAngle,
endAngle,
- themeWheelRadius,
+ convertRemToPixels(6),
);
icon.style.clipPath = clipPath;
});
@@ -676,6 +749,7 @@
const buttonRect = themeButton.getBoundingClientRect();
const buttonCenterX = buttonRect.left + buttonRect.width / 2;
const buttonCenterY = buttonRect.top + buttonRect.height / 2;
+ const themeWheelRadius = convertRemToPixels(6);
const margin = 0;
diff --git a/views/messages/new-post.html b/views/messages/new-post.html
index 98f0b52..4e586e2 100644
--- a/views/messages/new-post.html
+++ b/views/messages/new-post.html
@@ -1,15 +1,15 @@
{{ define "body" }}
-<p>{{ .L.Mail.NewPost.Intro }}</p>
+<p>{{ l $.Lang "Mail" "NewPost" "Intro" }}</p>
<table>
<tr>
- <td rowspan="3"><a href="https://{{ .ClientHost }}/{{ .Lang }}/blog/{{ .Post.FileName }}"><img src="{{ printf .ThumbnailBaseUrl .Post.Metadata.Thumbnail }}"></a></td>
+ <td rowspan="3"><a href="https://{{ .ClientHost }}/{{ .Lang }}/blog/{{ .Post.FileName }}"><img src="{{ printf .PhotoStorage.Thumbnail320p.BaseUrl .Post.Metadata.Thumbnail }}"></a></td>
<td class="darkened" style="font-size: 24px"><a style="color: #273de1 !important;" href="https://{{ .ClientHost }}/{{ .Lang }}/blog/{{ .Post.FileName }}">{{ .Post.Metadata.Title }}</a></td>
</tr>
<tr>
<td class="darkened" style="font-size: 16px">{{ .Post.Metadata.ShortDescription }}</td>
</tr>
<tr>
- <td class="darkened" style="font-size: 16px">{{ .L.Mail.NewPost.CapturedOn }} {{ .Post.Metadata.ActionDate }}</td>
+ <td class="darkened" style="font-size: 16px">{{ l $.Lang "Mail" "NewPost" "CapturedOn" }} {{ .Post.Metadata.ActionDate }}</td>
</tr>
</table>
{{ end }}
diff --git a/views/messages/verify-email.html b/views/messages/verify-email.html
index dd6ef33..e6fff76 100644
--- a/views/messages/verify-email.html
+++ b/views/messages/verify-email.html
@@ -1,10 +1,10 @@
{{ define "body" }}
-<p>{{ .L.Mail.VerifyEmail.Welcome }}</p>
-<p>{{ .L.Mail.VerifyEmail.Intro }}</p>
+<p>{{ l $.Lang "Mail" "VerifyEmail" "Welcome" }}</p>
+<p>{{ l $.Lang "Mail" "VerifyEmail" "Intro" }}</p>
<ol>
- <li>{{ .L.Mail.VerifyEmail.GotoLink }} <a href="https://{{ .ClientHost }}/{{ .Lang }}/user?email_code={{ .VerificationCode }}" class="outlier">&gt;&gt;&gt;</a></li>
- <li>{{ .L.Mail.VerifyEmail.InputCode }} <span class="outlier">{{ .VerificationCode }}</span><span></li>
+ <li>{{ l $.Lang "Mail" "VerifyEmail" "GotoLink" }} <a href="https://{{ .ClientHost }}/{{ .Lang }}/user?email_code={{ .VerificationCode }}" class="outlier">&gt;&gt;&gt;</a></li>
+ <li>{{ l $.Lang "Mail" "VerifyEmail" "InputCode" }} <span class="outlier">{{ .VerificationCode }}</span><span></li>
</ol>
-<p>{{ .L.Mail.VerifyEmail.IfRandom }}</p>
+<p>{{ l $.Lang "Mail" "VerifyEmail" "IfRandom" }}</p>
<a href="https://{{ .ClientHost }}" class="outlier">{{ .ClientHost }}</a>
{{ end }} \ No newline at end of file
diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html
index 8e1cf72..ca8f578 100644
--- a/views/pages/blog-catalogue.html
+++ b/views/pages/blog-catalogue.html
@@ -1,17 +1,6 @@
{{ define "body" }}
<!-- Streamline Icons: https://streamlinehq.com -->
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
- <symbol id="icon-like" viewBox="-2 -2 28 28" fill="none">
- <path fill="currentColor" d="M22.155 6.29294c-0.8113 -1.34837 -1.8229 -2.56561 -3 -3.61 -1.6282 -1.47261 -3.6782 -2.396134 -5.86 -2.6399952C11.1388 -0.137008 8.97321 0.252665 7.015 1.17294c-1.6708 0.75913 -3.14018 1.8997 -4.29 3.33C1.59785 5.92036 0.798705 7.56988 0.385003 9.33294c-0.010341 0.04271 -0.012051 0.08704 -0.005029 0.13041 0.007022 0.04337 0.022633 0.08491 0.045919 0.12216 0.023286 0.03726 0.053777 0.06949 0.089685 0.09481 0.035908 0.02532 0.07651 0.04321 0.119425 0.05262 0.083317 0.01503 0.169224 -0.00254 0.239953 -0.04907 0.070728 -0.04653 0.120867 -0.11846 0.140044 -0.20093 0.42729 -1.65655 1.22384 -3.19495 2.33 -4.5 1.0777 -1.30186 2.44846 -2.32993 4 -3 1.83266 -0.80027 3.84 -1.117035 5.83 -0.92 1.9654 0.25006 3.804 1.10643 5.26 2.45 1.0733 0.9746 1.9996 2.09963 2.75 3.34 0.77 1.20382 1.2366 2.57635 1.36 3.99996 0.1304 1.5877 -0.0565 3.1854 -0.55 4.7 -0.5032 1.5017 -1.4221 2.8298 -2.65 3.83 -0.7981 0.673 -1.7331 1.1644 -2.74 1.44 -1.5427 0.4432 -3.1354 0.6885 -4.74 0.73 -0.0778 0.0052 -0.1505 0.0404 -0.2028 0.0983 -0.0523 0.0579 -0.08 0.1338 -0.0772 0.2117 0 0.039 0.0078 0.0775 0.023 0.1134 0.0152 0.0358 0.0374 0.0683 0.0654 0.0953 0.028 0.0271 0.0612 0.0482 0.0976 0.0622 0.0363 0.014 0.0751 0.0205 0.114 0.0191 1.6629 -0.0085 3.3184 -0.2201 4.93 -0.63 1.0928 -0.2824 2.1146 -0.7899 3 -1.49 1.3927 -1.0653 2.454 -2.5046 3.06 -4.15 0.5754 -1.6287 0.8235 -3.3551 0.73 -5.08 -0.1076 -1.59917 -0.6055 -3.14765 -1.45 -4.50996Z" stroke-width="1"></path>
- <path fill="currentColor" d="M15.935 15.903c0.6791 -0.2938 1.2468 -0.7964 1.6208 -1.4347 0.3741 -0.6384 0.5349 -1.3794 0.4592 -2.1153 -0.0284 -0.1682 -0.0994 -0.3264 -0.2061 -0.4594 -0.1068 -0.133 -0.2458 -0.2365 -0.4039 -0.3007 -0.4529 -0.1228 -0.927 -0.1466 -1.39 -0.0699 -1.22 0.07 -3.05 0.38 -4.54 0.54 -0.4 0.05 -0.78 0.08 -1.11 0.1 -0.0689 0.0127 -0.1312 0.0492 -0.1761 0.1031 -0.0448 0.0539 -0.0693 0.1217 -0.0693 0.1919 0 0.0701 0.0245 0.1379 0.0693 0.1918 0.0449 0.0539 0.1072 0.0904 0.1761 0.1032l5.66 -0.18h0.83c0.0328 0.5028 -0.0896 1.0037 -0.3508 1.4347 -0.2611 0.431 -0.6483 0.7715 -1.1092 0.9753 -0.4499 0.3065 -0.9606 0.5125 -1.4972 0.6039 -0.5367 0.0915 -1.0868 0.0663 -1.6128 -0.074 -0.4102 -0.1727 -0.7888 -0.4126 -1.12 -0.71 -0.3351 -0.2774 -0.6136 -0.6169 -0.82 -1 -0.0184 -0.0407 -0.0446 -0.0774 -0.0771 -0.108 -0.0326 -0.0305 -0.0709 -0.0544 -0.1127 -0.0702 -0.0418 -0.0158 -0.0862 -0.0232 -0.1309 -0.0218 -0.04465 0.0014 -0.08858 0.0116 -0.12929 0.03 -0.04037 0.0174 -0.07686 0.0426 -0.10734 0.0743 -0.03049 0.0316 -0.05435 0.069 -0.07018 0.11 -0.01584 0.041 -0.02333 0.0847 -0.02203 0.1286 0.00129 0.044 0.01133 0.0872 0.02955 0.1272 0.21114 0.4969 0.52069 0.9459 0.90999 1.32 0.3786 0.3811 0.8137 0.7015 1.29 0.95 0.6592 0.2289 1.361 0.3082 2.0546 0.2321 0.6936 -0.0762 1.3616 -0.3057 1.9554 -0.6721Z" stroke-width="1"></path>
- <path fill="currentColor" d="M8.33499 10.9829c0.22252 0.0613 0.45748 0.0613 0.68 0 0.17653 -0.0617 0.33498 -0.1663 0.46112 -0.3044 0.12615 -0.1381 0.21605 -0.3053 0.26164 -0.4867 0.04559 -0.1814 0.04545 -0.37124 -0.00041 -0.55256 -0.04586 -0.18131 -0.136 -0.34841 -0.26235 -0.4863 -1 -1.24 -3 0.82 -1.74 1.71996 0.1869 0.0887 0.39382 0.1266 0.6 0.11Z" stroke-width="1"></path>
- <path fill="currentColor" d="M19.1653 10.343c0.2247 0.0401 0.456 0.0194 0.67 -0.06 0.1784 -0.0611 0.3386 -0.1658 0.4661 -0.30464 0.1275 -0.13885 0.2183 -0.3074 0.264 -0.4903 0.0457 -0.1829 0.045 -0.37432 -0.0022 -0.55685 -0.0472 -0.18253 -0.1393 -0.35035 -0.2679 -0.48818 -1 -1.23 -3 0.83 -1.74 1.72997 0.1838 0.1117 0.3949 0.1705 0.61 0.17Z" stroke-width="1"></path>
- <path fill="currentColor" d="M9.63505 20.0929c-0.10233 -0.204 -0.25778 -0.3768 -0.45 -0.5 0.19629 -0.3235 0.27075 -0.7064 0.21 -1.08 -0.04172 -0.3535 -0.15757 -0.6943 -0.34 -1 -0.13496 -0.1997 -0.31256 -0.3671 -0.52 -0.49 0.04212 -0.0952 0.06912 -0.1964 0.08 -0.3 0.05325 -0.3277 0.02651 -0.6634 -0.07796 -0.9786 -0.10447 -0.3151 -0.28357 -0.6003 -0.52204 -0.8314 -0.51428 -0.3306 -1.1194 -0.4915 -1.73 -0.46 -0.74972 -0.022 -1.49478 0.125 -2.18 0.43 0.05982 -0.8997 -0.19079 -1.7927 -0.71 -2.53 -0.9 -1 -2.54 -0.13 -2.71 1.36 -0.017326 0.6116 0.060262 1.2222 0.23 1.81 0.28 1 0.23 0.95 0.48 1.33 -0.22002 0.2395 -0.405175 0.5088 -0.55 0.8 -0.1 0.34 -0.28 0.74 -0.12 1 0.16 0.26 0.3 0.23 0.47 0.06 0.17 -0.17 0.09 -0.44 0.14 -0.6 0.07428 -0.2866 0.22256 -0.5487 0.43 -0.76 0.16 -0.21 0.28 -0.22 0.47 -0.4 1.85 -1.77 4.62 -1.97 5.12 -1.28 0.85 1.16 -1.16 1.29 -2.9 2.37 -0.03825 0.011 -0.07372 0.03 -0.10405 0.0557 -0.03034 0.0258 -0.05483 0.0577 -0.07187 0.0936 -0.01703 0.036 -0.0262 0.0752 -0.02691 0.1149 -0.0007 0.0398 0.00708 0.0793 0.02283 0.1158 0.01228 0.0374 0.03203 0.0719 0.05805 0.1014 0.02602 0.0295 0.05777 0.0534 0.09332 0.0702 0.03554 0.0168 0.07414 0.0263 0.11345 0.0277 0.0393 0.0015 0.07849 -0.0051 0.11518 -0.0193 0.41 -0.06 0.77 -0.06 1.12 -0.11 1 -0.17 1.25 -0.66 2.16 -0.51 0.15573 0.127 0.26274 0.3039 0.30284 0.5008 0.0401 0.1968 0.01083 0.4015 -0.08284 0.5792 -0.45993 0.315 -0.97588 0.539 -1.52 0.66 -0.36935 0.1765 -0.72678 0.377 -1.07 0.6 -0.03798 0.0103 -0.07348 0.0282 -0.10433 0.0526 -0.03085 0.0244 -0.05641 0.0549 -0.07512 0.0895 -0.01871 0.0346 -0.03018 0.0727 -0.03371 0.1119 -0.00353 0.0391 0.00094 0.0786 0.01316 0.116 0.01027 0.038 0.02815 0.0735 0.05258 0.1044 0.02442 0.0308 0.05486 0.0564 0.08948 0.0751 0.03461 0.0187 0.07267 0.0302 0.11185 0.0337 0.03919 0.0035 0.07869 -0.0009 0.11609 -0.0132 0.36103 -0.003 0.7216 -0.0264 1.08 -0.07 0.15662 -0.0212 0.31072 -0.0581 0.46 -0.11 0.36406 -0.2158 0.79585 -0.2872 1.21 -0.2 0.03286 0.0062 0.06407 0.0192 0.09162 0.0381 0.02755 0.019 0.05084 0.0435 0.06838 0.0719 0.03012 0.0772 0.03012 0.1629 0 0.24 -0.05277 0.2543 -0.17376 0.4893 -0.35 0.68 -0.20205 0.2292 -0.43028 0.434 -0.68 0.61 -0.66869 0.4607 -1.44234 0.7461 -2.25 0.83 -0.80071 0.1179 -1.61838 -0.0039 -2.35 -0.35 -0.53395 -0.2824 -0.95681 -0.737 -1.2 -1.29 -0.27045 -0.5855 -0.42982 -1.2162 -0.47 -1.86 -0.00126 -0.0442 -0.0114 -0.0877 -0.02981 -0.1279 -0.01841 -0.0403 -0.04473 -0.0764 -0.07738 -0.1062 -0.03265 -0.0299 -0.07097 -0.0529 -0.11269 -0.0676 -0.04171 -0.0148 -0.08596 -0.021 -0.13012 -0.0183 -0.044675 0.0026 -0.088408 0.0139 -0.12869 0.0334s-0.076321 0.0468 -0.106051 0.0802c-0.029729 0.0334 -0.052563 0.0724 -0.067194 0.1147 -0.014631 0.0423 -0.02077 0.0871 -0.018065 0.1317 0.012623 0.8862 0.235395 1.7567 0.65 2.54 0.30359 0.5453 0.74942 0.9981 1.29 1.31 0.8694 0.479 1.8607 0.6912 2.85 0.61 0.9936 -0.0912 1.94725 -0.4355 2.77 -1 0.4107 -0.274 0.77525 -0.6115 1.08 -1 0.2272 -0.2941 0.3845 -0.6361 0.46 -1 0.04902 -0.304 -0.00001 -0.6157 -0.14 -0.89Zm-7.81 -3.72 -0.09 -1c0 -0.75 -0.13 -1.73 0.37 -2.19 0.1 0 0.45 -0.59 0.88 0.22 0.2375 0.4432 0.38371 0.9294 0.43 1.43 -0.00039 0.0506 0.01118 0.1005 0.03377 0.1457 0.0226 0.0451 0.05557 0.0843 0.09623 0.1143 -0.63566 0.3358 -1.2158 0.7676 -1.72 1.28Z" stroke-width="1"></path>
- </symbol>
- <symbol id="icon-view" viewBox="-2 -2 28 28" fill="none">
- <path fill="currentColor" fill-rule="evenodd" d="M23.6534 10.5263c-1.3628 -1.56103 -3.0387 -2.81795 -4.9189 -3.68916 -1.8052 -0.8978 -3.7485 -1.48585 -5.7486 -1.7396 -0.998 -0.11306 -2.0067 -0.09289 -2.99934 0.05999 -2.63062 0.3739 -5.14267 1.33771 -7.34828 2.81934 -0.84337 0.55535 -1.63325 1.18793 -2.359454 1.88956 -0.08425 0.0868 -0.161162 0.18047 -0.2299462 0.27997C0.0169406 10.1973 0 10.2562 0 10.3163s0.0169406 0.1191 0.0488798 0.17c0.0179573 0.0358 0.0429176 0.0677 0.0734052 0.0937 0.030488 0.0261 0.065885 0.0457 0.104099 0.0578 0.038214 0.0122 0.078469 0.0165 0.118385 0.0128 0.039917 -0.0037 0.078686 -0.0154 0.114015 -0.0343C1.5971 9.42178 2.95486 8.45777 4.45785 7.77692c0.959 -0.48729 1.9636 -0.87909 2.9993 -1.16973 0.88979 -0.25558 1.79943 -0.43617 2.71935 -0.53987 0.8961 -0.11518 1.8033 -0.11518 2.6994 0 1.8879 0.28843 3.7154 0.88632 5.4087 1.76959 1.6883 0.80557 3.2029 1.93308 4.459 3.31919 0.0877 0.152 0.1339 0.3244 0.1339 0.4999s-0.0462 0.3479 -0.1339 0.4999c-0.2004 0.4189 -0.4747 0.7983 -0.8098 1.1197 -0.9132 0.9438 -1.9179 1.7945 -2.9993 2.5394 -1.0941 0.7277 -2.2767 1.3123 -3.5192 1.7396 -0.786 0.2536 -1.6039 0.395 -2.4294 0.4199 -1.2488 0.0246 -2.4978 -0.0321 -3.73917 -0.1699 -1.55221 -0.2376 -3.01813 -0.8673 -4.259 -1.8296 -1.29902 -0.9356 -2.40844 -2.1095 -3.26924 -3.4592 -0.02265 -0.0404 -0.05333 -0.0757 -0.09014 -0.1039 -0.03681 -0.0281 -0.07898 -0.0484 -0.12392 -0.0596 -0.04494 -0.0113 -0.0917 -0.0132 -0.13741 -0.0057s-0.08941 0.0243 -0.12842 0.0492c-0.07659 0.0529 -0.13044 0.1327 -0.15082 0.2235 -0.02039 0.0908 -0.0058 0.1859 0.04085 0.2664 0.8813 1.4449 2.02784 2.7102 3.37921 3.7292 1.34805 1.0808 2.9532 1.7938 4.65891 2.0695 1.26545 0.179 2.54115 0.2758 3.81915 0.2899 0.8996 0.0048 1.7952 -0.1198 2.6593 -0.3699 1.3434 -0.4251 2.6244 -1.0269 3.8092 -1.7896 1.1699 -0.7705 2.2556 -1.6619 3.2392 -2.6594 0.5478 -0.5108 0.9601 -1.1498 1.1997 -1.8595 0.0999 -0.2934 0.1302 -0.606 0.0886 -0.9131 -0.0417 -0.3071 -0.1541 -0.6003 -0.3285 -0.8565Z" clip-rule="evenodd" stroke-width="1"></path>
- <path fill="currentColor" fill-rule="evenodd" d="M10.6964 9.1166c0.7827 -0.2444 1.6136 -0.29247 2.4194 -0.13997 0.3938 0.09551 0.7634 0.27195 1.0852 0.51809 0.3219 0.24613 0.589 0.55658 0.7844 0.91158 0.0189 0.0362 0.0448 0.0683 0.0762 0.0944 0.0314 0.0262 0.0677 0.0458 0.1067 0.0579 0.039 0.0121 0.08 0.0163 0.1207 0.0125 0.0407 -0.0038 0.0802 -0.0157 0.1163 -0.0348 0.0713 -0.04 0.124 -0.1066 0.1465 -0.1853 0.0224 -0.0786 0.0129 -0.163 -0.0266 -0.2346 -0.213 -0.44629 -0.5169 -0.84312 -0.8922 -1.16507 -0.3753 -0.32195 -0.8138 -0.56192 -1.2873 -0.70452 -0.9344 -0.26583 -1.92 -0.29674 -2.8693 -0.08998 -0.53345 0.13369 -1.02665 0.39446 -1.4375 0.76005 -0.41085 0.36559 -0.72716 0.82514 -0.92194 1.33942 -0.41881 1.0115 -0.41881 2.1478 0 3.1593 0.4668 0.9845 1.29879 1.7483 2.31944 2.1295 0.9793 0.3819 2.0556 0.438 3.0693 0.16 0.8982 -0.2612 1.6632 -0.8546 2.1395 -1.6596 0.4789 -0.7747 0.6995 -1.6814 0.6299 -2.5894 -0.0061 -0.0449 -0.0211 -0.0881 -0.0441 -0.127 -0.0231 -0.039 -0.0536 -0.073 -0.0899 -0.1 -0.0363 -0.027 -0.0777 -0.0465 -0.1216 -0.0573 -0.044 -0.0108 -0.0897 -0.0127 -0.1344 -0.0057 -0.0457 0.0049 -0.0899 0.019 -0.1301 0.0415 -0.0401 0.0224 -0.0752 0.0528 -0.1033 0.0893 -0.028 0.0364 -0.0484 0.0782 -0.0598 0.1227 -0.0114 0.0446 -0.0137 0.091 -0.0067 0.1364 -0.0016 0.7166 -0.2252 1.4151 -0.6398 1.9996 -0.3854 0.5945 -0.9884 1.0144 -1.6796 1.1697 -0.8061 0.1738 -1.6466 0.0863 -2.3995 -0.2499 -0.7468 -0.2609 -1.36555 -0.7974 -1.72958 -1.4997 -0.29453 -0.7244 -0.29453 -1.5351 0 -2.2595 0.1115 -0.3727 0.31161 -0.7129 0.58321 -0.99148 0.27161 -0.27856 0.60667 -0.48722 0.97647 -0.60812Z" clip-rule="evenodd" stroke-width="1"></path>
- </symbol>
<symbol id="icon-arrange-letter-asc" viewBox="-2 -2 28 28" fill="none">
<path fill="var(--color-accent-light)" d="M11.9999 20.9053c5.1771 0 9.3739 -4.1969 9.3739 -9.374 0 -5.17702 -4.1968 -9.37389 -9.3739 -9.37389 -5.17706 0 -9.37392 4.19687 -9.37392 9.37389 0 5.1771 4.19686 9.374 9.37392 9.374Z" stroke-width="1"></path>
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M5.90674 4.49902V18.5599" stroke-width="1"></path>
@@ -55,64 +44,64 @@
<path fill="currentColor" d="M23.2453 20.5533c-0.04 -0.2181 -0.1001 -0.4321 -0.1797 -0.6391 -0.1361 -0.3547 -0.3173 -0.6903 -0.5393 -0.9986 -0.3034 -0.4388 -0.637 -0.8559 -0.9986 -1.2482 -0.5284 -0.5741 -1.0956 -1.1113 -1.6976 -1.6078 -0.8288 -0.679 -1.6976 -1.2981 -2.5763 -1.9971 0.2631 -0.5055 0.4803 -1.0335 0.649 -1.5778 0.248 -0.8172 0.4349 -1.6516 0.5592 -2.4965 0.1569 -0.81258 0.2305 -1.63902 0.2197 -2.46652 -0.0078 -0.57747 -0.1055 -1.1502 -0.2896 -1.6976 -0.1575 -0.4947 -0.3476 -0.97841 -0.5692 -1.44796 -0.1562 -0.33202 -0.347 -0.64665 -0.5691 -0.93868 -0.3781 -0.48797 -0.8217 -0.92146 -1.3182 -1.28818 -0.7357 -0.52062 -1.53 -0.95298 -2.3666 -1.288183 -0.5827 -0.254108 -1.1835 -0.464396 -1.7975 -0.629112 -0.6518 -0.1693231 -1.324 -0.246631 -1.9972 -0.22967613C8.94606 0.0473495 8.12871 0.212165 7.34773 0.491599c-0.76731 0.278516 -1.50629 0.629611 -2.20689 1.048521 -0.60025 0.33964 -1.16553 0.73768 -1.68762 1.18832C1.21637 4.69567 0.757023 8.98961 0.727065 9.45895 0.620124 10.7287 0.804559 12.0061 1.2663 13.1937c0.12146 0.3013 0.26153 0.5948 0.41941 0.8787 0.22968 0.3995 0.48931 0.7889 0.75893 1.1784 0.5038 0.7726 1.12352 1.463 1.83741 2.0471 0.49826 0.396 1.06292 0.7003 1.66765 0.8987 0.69046 0.2254 1.39862 0.3924 2.11701 0.4993 0.49142 0.0823 0.99015 0.1124 1.4879 0.0899 0.77439 -0.05 1.53939 -0.1977 2.27679 -0.4394 0.7156 -0.2507 1.4101 -0.5579 2.0771 -0.9187 0.1351 0.1597 0.2617 0.3264 0.3794 0.4993 0.4294 0.6591 0.769 1.3681 1.1684 1.9972 0.2809 0.4341 0.5877 0.851 0.9187 1.2482 0.3126 0.3904 0.646 0.7638 0.9986 1.1184 0.9711 0.9019 2.1935 1.4871 3.505 1.6777 0.262 0.0471 0.5307 0.0413 0.7904 -0.0169 0.2597 -0.0582 0.5052 -0.1678 0.7219 -0.3222 0.2168 -0.1544 0.4005 -0.3506 0.5404 -0.5771 0.1398 -0.2264 0.233 -0.4785 0.2741 -0.7415 0.0404 -0.1745 0.0704 -0.3512 0.0898 -0.5292 0.0097 -0.1763 0.0097 -0.353 0 -0.5293 0.0036 -0.234 -0.0131 -0.4679 -0.0499 -0.699Zm-1.1584 1.4679c-0.0509 0.2532 -0.1979 0.4768 -0.4102 0.6239 -0.2122 0.1471 -0.4732 0.2062 -0.7282 0.165 -1.0467 -0.1308 -2.0325 -0.5647 -2.836 -1.2482 -0.3522 -0.313 -0.6856 -0.6464 -0.9986 -0.9986 -0.3312 -0.341 -0.6381 -0.7047 -0.9187 -1.0885 -0.4393 -0.6191 -0.8388 -1.3081 -1.2981 -1.9372 -0.1219 -0.1597 -0.2519 -0.313 -0.3895 -0.4594l0.1298 -0.0899c0.2213 -0.1586 0.4315 -0.3321 0.6292 -0.5192 0.0617 -0.0665 0.096 -0.1539 0.096 -0.2447 0 -0.0908 -0.0343 -0.1782 -0.096 -0.2447 -0.0637 -0.0634 -0.1499 -0.099 -0.2397 -0.099 -0.0898 0 -0.176 0.0356 -0.2397 0.099 -0.1708 0.1582 -0.3545 0.3018 -0.5492 0.4294 -0.1917 0.1358 -0.3918 0.2592 -0.5991 0.3695 -0.6447 0.3275 -1.3121 0.6078 -1.9972 0.8388 -0.6937 0.2123 -1.4121 0.3332 -2.13702 0.3595 -0.55091 0.0138 -1.10132 -0.0432 -1.63769 -0.1697 -0.5569 -0.1063 -1.10451 -0.2566 -1.63769 -0.4494 -0.62749 -0.2261 -1.19946 -0.5835 -1.67763 -1.0485 -0.49431 -0.4831 -0.93003 -1.0228 -1.29817 -1.6077 -0.22967 -0.3595 -0.46934 -0.719 -0.67904 -1.1185 -0.13067 -0.2462 -0.24735 -0.4996 -0.34951 -0.7589 -0.38354 -1.05 -0.52354 -2.1734 -0.40942 -3.28536 0.45935 -5.29254 3.14556 -6.55076 3.91448 -6.99014 0.63297 -0.38101 1.30193 -0.69877 1.99718 -0.94867 0.6757 -0.25505 1.38606 -0.40655 2.10703 -0.44936 0.56148 -0.01552 1.12238 0.04495 1.66768 0.17975 0.5643 0.13703 1.1183 0.31389 1.6576 0.52925 0.6401 0.2469 1.2581 0.54755 1.8474 0.89873 0.5688 0.32061 1.076 0.7399 1.4979 1.23825 0.1894 0.23504 0.3534 0.48943 0.4893 0.75893 0.2209 0.42872 0.4111 0.87254 0.5692 1.32813 0.1632 0.46345 0.2607 0.94748 0.2896 1.43797 0.0241 0.77857 -0.026 1.55767 -0.1498 2.32672 -0.0854 0.6722 -0.2088 1.3389 -0.3695 1.9972 -0.1609 0.6599 -0.3954 1.2996 -0.699 1.9073 -0.1608 0.3485 -0.3513 0.6826 -0.5692 0.9986 -0.2143 0.3034 -0.4549 0.5874 -0.719 0.8488 -0.0288 0.0275 -0.052 0.0605 -0.0681 0.097 -0.0161 0.0364 -0.0249 0.0757 -0.0258 0.1156 -0.0019 0.0805 0.0283 0.1585 0.084 0.2168 0.0556 0.0582 0.1321 0.092 0.2126 0.0939 0.0805 0.0019 0.1585 -0.0283 0.2168 -0.0839 0.2935 -0.2844 0.5639 -0.5916 0.8088 -0.9187 0.1099 -0.1498 0.1997 -0.3096 0.2996 -0.4694 0.8088 0.6791 1.6177 1.3182 2.3866 1.9972 0.5434 0.4957 1.0506 1.0297 1.5179 1.5978 0.3133 0.3747 0.6034 0.7682 0.8688 1.1783 0.1584 0.2534 0.2858 0.525 0.3794 0.8088 0.0505 0.1638 0.0905 0.3305 0.1198 0.4993 0.0099 0.1597 0.0099 0.3197 0 0.4794l-0.0599 0.8088Z" stroke-width="1"></path>
</symbol>
</svg>
-<div class="flex flex-row ar-lt-0.8:flex-col text-base h-full">
- <div class="relative flex shrink-0 ar-gt-0.8:min-w-1/10 ar-gt-0.8:max-w-1/4 ar-gt-0.8:w-fit ar-lt-0.8:w-full ar-lt-0.8:max-h-[30%]">
- <div class="absolute w-full h-full inset-shadow-[0_0_0.4rem_black] pointer-events-none z-20"></div>
+<div class="flex md:flex-row flex-col text-base h-full">
+ <div class="relative flex shrink-0 w-full max-h-[30%] md:min-w-1/10 md:max-w-1/4 md:max-h-full md:h-full md:w-fit shadow-elevation-6">
+ <div class="absolute w-full h-full pointer-events-none z-20"></div>
<form hx-get="/api/v1/blog-search" hx-vals='{"lang": "{{ .Lang }}"}' hx-target=".blog-cards" hx-swap="innerHTML"
- class="tags-list relative flex flex-col w-full bg-paper bg-background-light">
- <div class="flex flex-col overflow-y-auto">
+ class="tags-list relative flex flex-col w-full">
+ <div class="flex flex-col bg-paper bg-background-light overflow-y-auto inset-shadow-elevation-6">
<fieldset>
- <legend class="font-bold w-full text-center">{{ .L.BlogSearch.OrderByHeader }}</legend>
- <div class="flex flex-col ar-lt-0.8:grid grid-cols-3 grid-rows-2 grid-flow-col">
- <div>
+ <legend class="font-bold w-full text-center">{{ l $.Lang "BlogSearch" "OrderByHeader" }}</legend>
+ <div class="md:flex flex-col grid grid-cols-3 grid-rows-2 grid-flow-col">
+ <div class="m-1">
<label class="flex justify-start gap-1 items-center">
<input type="radio" id="sortTitleAsc" name="sort" value="titleAsc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "titleAsc" }}checked{{ end }}>
- {{ .L.BlogSearch.TitleOrdered }}
+ {{ l $.Lang "BlogSearch" "TitleOrdered" }}
<svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-letter-asc"/></svg>
</label>
</div>
- <div>
+ <div class="m-1">
<label class="flex justify-start gap-1 items-center">
<input type="radio" id="sortTitleDesc" name="sort" value="titleDesc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "titleDesc" }}checked{{ end }}>
- {{ .L.BlogSearch.TitleOrdered }}
+ {{ l $.Lang "BlogSearch" "TitleOrdered" }}
<svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-letter-desc"/></svg>
</label>
</div>
- <div>
+ <div class="m-1">
<label class="flex justify-start gap-1 items-center">
<input type="radio" id="sortActionDateAsc" name="sort" value="actionDateAsc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "actionDateAsc" }}checked{{ end }}>
- {{ .L.BlogSearch.ActionDateOrdered }}
+ {{ l $.Lang "BlogSearch" "ActionDateOrdered" }}
<svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-number-asc"/></svg>
</label>
</div>
- <div>
+ <div class="m-1">
<label class="flex justify-start gap-1 items-center">
<input type="radio" id="sortActionDateDesc" name="sort" value="actionDateDesc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "actionDateDesc" }}checked{{ end }}>
- {{ .L.BlogSearch.ActionDateOrdered }}
+ {{ l $.Lang "BlogSearch" "ActionDateOrdered" }}
<svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-number-desc"/></svg>
</label>
</div>
- <div>
+ <div class="m-1">
<label class="flex justify-start gap-1 items-center">
<input type="radio" id="sortPublicationDateAsc" name="sort" value="publicationDateAsc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "publicationDateAsc" }}checked{{ end }}>
- {{ .L.BlogSearch.PublicationDateOrdered }}
+ {{ l $.Lang "BlogSearch" "PublicationDateOrdered" }}
<svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-number-asc"/></svg>
</label>
</div>
- <div>
+ <div class="m-1">
<label class="flex justify-start gap-1 items-center">
<input type="radio" id="sortPublicationDateDesc" name="sort" value="publicationDateDesc" class="bg-accent-light border-transparent text-accent-deep focus:border-transparent focus:bg-accent-light focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep" {{ if eq .QuerySort "publicationDateDesc" }}checked{{ end }}>
- {{ .L.BlogSearch.PublicationDateOrdered }}
+ {{ l $.Lang "BlogSearch" "PublicationDateOrdered" }}
<svg viewBox="0 0 24 24" class="h-8"><use href="#icon-arrange-number-desc"/></svg>
</label>
</div>
</div>
</fieldset>
<fieldset class="mt-1">
- <legend class="font-bold w-full text-center">{{ .L.BlogSearch.TagsHeader }}</legend>
- <div class="flex flex-col ar-lt-0.8:grid grid-cols-3 xs:grid-cols-4 sm:grid-cols-5 grid-flow-row-dense">
+ <legend class="font-bold w-full text-center">{{ l $.Lang "BlogSearch" "TagsHeader" }}</legend>
+ <div class="md:flex flex-col grid grid-cols-3 xs:grid-cols-4 sm:grid-cols-5 grid-flow-row-dense">
<div class="m-1">
- <label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();" class="bg-accent-light rounded border-transparent text-accent-deep focus:border-transparent focus:bg-background-dark focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep"> {{ .L.BlogSearch.ChooseAllTags }}</label>
+ <label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();" class="bg-accent-light rounded border-transparent text-accent-deep focus:border-transparent focus:bg-background-dark focus:ring-1 focus:ring-offset-2 focus:ring-accent-deep"> {{ l $.Lang "BlogSearch" "ChooseAllTags" }}</label>
</div>
{{- range .Tags }}
<div class="m-1">
@@ -122,24 +111,21 @@
</div>
</fieldset>
</div>
- <hr class="border-t-2 border-dotted border-main-hard my-0.5">
- <div class="flex flex-row bg-paper bg-background-dark p-0.5 rounded-1 h-12">
- <button type="button" onclick="defaultSearchParams(); cleanHrefParams();" class="flex flex-1 justify-center text-main-hard bg-paper bg-accent-light cursor-pointer hover:bg-accent-deep active:text-background-dark transition-colors duration-500">
- <svg viewBox="0 0 24 24"><use href="#icon-camera-settings-flip"/></svg>
+ <hr class="border-t-[0.375rem] border-dotted border-main-hard my-2 mx-auto w-24">
+ <div class="flex flex-row p-0.5 rounded-1 h-12">
+ <button type="button" onclick="defaultSearchParams(); cleanHrefParams();" class="accent-button grow h-full">
+ <svg class="h-full mx-auto" viewBox="0 0 24 24"><use href="#icon-camera-settings-flip"/></svg>
</button>
- <div class="w-1 bg-paper bg-background-light grow-0"></div>
- <button type="submit" onclick="setHrefParams();" class="flex flex-1 justify-center text-main-hard bg-paper bg-accent-light cursor-pointer hover:bg-accent-deep active:text-background-dark transition-colors duration-500">
- <svg viewBox="0 0 24 24"><use href="#icon-search-magnifier"/></svg>
+ <button type="submit" onclick="setHrefParams();" class="accent-button grow h-full">
+ <svg class="h-full mx-auto" viewBox="0 0 24 24"><use href="#icon-search-magnifier"/></svg>
</button>
</div>
</form>
</div>
- <div class="w-[0.5dvh] border-r-2 border-dotted border-main-hard bg-paper bg-background-dark"></div>
-
<div class="relative flex grow h-full">
- <div class="absolute w-full h-full inset-0 inset-shadow-[0_0_0.4rem_black] pointer-events-none z-20"></div>
- <div hx-get="/api/v1/blog-search" hx-vals='js:{lang: "{{ .Lang }}", tz: clientTimeZone}' hx-target="this" hx-swap="innerHTML" hx-trigger="load" hx-include=".tags-list"
+ <div class="absolute w-full h-full inset-0 inset-shadow-elevation-6 pointer-events-none z-20"></div>
+ <div hx-get="/api/v1/blog-search" hx-vals='js:{lang: "{{ .Lang }}", tz: clientTimeZone, highlight: "{{ .PreviousBlogPage }}"}' hx-target="this" hx-swap="innerHTML" hx-trigger="load" hx-include=".tags-list"
class="blog-cards relative grow flex flex-col overflow-y-auto bg-paper bg-background-light pr-2">
Loading...
</div>
diff --git a/views/pages/blog-page.html b/views/pages/blog-page.html
index 8978eb8..3d3c623 100644
--- a/views/pages/blog-page.html
+++ b/views/pages/blog-page.html
@@ -1,59 +1,50 @@
{{ define "body" }}
-<div class="absolute w-full h-full inset-0 inset-shadow-[0_0_0.4rem_black] pointer-events-none z-20"></div>
-<article class="relative bg-paper bg-background-light flex flex-col px-8 py-4 overflow-y-auto">
- <h1 class="max-xs:block [@media(max-height:32rem)]:block hidden font-gentium text-2xl font-bold italic text-main-hard tracking-[.0125rem] mb-1">{{ .Title }}</h1>
- <p class="block grow text-lg font-gentium text-secondary">
- <b>{{ .L.Metadata.Published }}</b>:
- <time datetime="{{ .PublishedDate }}" hx-get="/api/v1/tz" hx-vals='js:{timestamp: "{{ .PublishedDate }}", tz: clientTimeZone}' hx-target="this" hx-swap="innerHTML" hx-trigger="load">
- {{ .PublishedDate }}
- </time>
- </p>
- <p class="block grow text-lg font-gentium text-secondary"><b>{{ .L.Metadata.Action }}</b>: {{ .ActionDate }}</p>
- <p class="block grow text-md font-gentium font-thin text-main-hard">{{ .ShortDescription }}</p>
- <hr class="block border-t-2 border-dotted border-main-hard mt-1 mb-2 w-full ml-auto mr-auto">
- {{ .ParsedMarkdown }}
+<input class="htmx-exit-trigger" type="hidden" name="codename" value="{{ .Codename }}">
+<div class="relative bg-paper bg-background-light flex flex-col py-4 overflow-y-auto">
+ <div class="flex flex-col sm:flex-row h-[30cqb] sm:h-[15cqb] shadow-elevation-6">
+ <div class="grow shrink-2 flex-1 overflow-y-auto -mt-4 z-1 shadow-elevation-3">
+ <div class="multitone min-h-full" style="--multitone-bg: url({{ printf $.PhotoStorage.Thumbnail560p.BaseUrl .Thumbnail }});">
+ <div class="ml-8 py-4">
+ <h1 class="max-xs:flex flex-row content-center [@media(max-height:32rem)]:block hidden font-gentium text-2xl font-bold italic text-main-hard tracking-[.0125rem] mb-1">
+ <div hx-get="/api/v1/like" hx-target="this" hx-swap="outerHTML" hx-trigger="load"></div>
+ {{ .Title }}
+ </h1>
+ <p class="block grow text-lg font-gentium text-secondary">
+ <b>{{ l $.Lang "Metadata" "Published" }}</b>:
+ <time datetime="{{ .PublishedDate }}" hx-get="/api/v1/tz" hx-vals='js:{timestamp: "{{ .PublishedDate }}", tz: clientTimeZone}' hx-target="this" hx-swap="innerHTML" hx-trigger="load">
+ {{ .PublishedDate }}
+ </time>
+ </p>
+ <p class="block grow text-lg font-gentium text-secondary"><b>{{ l $.Lang "Metadata" "Action" }}</b>: {{ .ActionDate }}</p>
+ <p class="block grow text-md font-gentium font-thin text-main-hard">{{ .ShortDescription }}</p>
+ </div>
+ </div>
+ </div>
+ <hr class="block sm:hidden border-t-[0.1875rem] bg-paper bg-background-dark border-dotted border-main-hard w-full">
+ {{- if .Medley }}
+ <div class="overflow-y-auto grow-2 shrink-0 flex-1 bg-paper bg-background-dark shadow-elevation-3 sm:-mt-4 z-2">
+ <h2 class="text-2xl font-gentium font-extrabold text-main-hard tracking-[.0125rem] ml-2 mb-2 border-b-[0.25rem] border-dotted w-fit">
+ {{ l .Lang "Medleys" .Medley }}
+ </h2>
+ <div hx-get="/api/v1/blog-search" hx-vals='js:{lang: "{{ .Lang }}", tz: clientTimeZone, medley: "{{ .Medley }}", sort: "medley", hideTags: true, hidePublishedTime: true, highlight: "{{ .Codename }}"}' hx-target="this" hx-swap="innerHTML" hx-trigger="load"
+ class="grow flex flex-col pr-2">
+ Loading...
+ </div>
+ </div>
+ {{- end }}
+ </div>
+ <hr class="block border-t-[0.375rem] w-24 mx-auto my-4 border-dotted border-main-hard">
+ <article class="px-8 flex flex-col">
+ {{ .ParsedMarkdown }}
+ </article>
{{- if .MapLocationX }}
- <hr class="border-t-3 border-dotted border-main-hard mt-1 mb-2 w-[80%] ml-auto mr-auto">
- <div id="map-container" class="relative p-1 flex-none ml-auto mr-auto w-[60%] ar-lt-0.8:w-[80%] h-[40dvh] ar-lt-0.8:h-[30dvh]"></div>
- <hr class="border-t-3 border-dotted border-main-hard mt-1 mb-2 w-[80%] ml-auto mr-auto">
+ <hr class="border-t-[0.375rem] border-dotted border-main-hard my-2 w-24 mx-auto">
+ <div id="map-outer-container" class="relative p-1 flex-none mx-auto w-[80%] lg:w-[60%] h-[30dvh] md:h-[40dvh]"
+ hx-get="/api/v1/map" hx-vals='{"lang": "{{ .Lang }}", "codename": "{{ .Codename }}", "zoom": 8}' hx-target="this" hx-swap="innerHTML" hx-trigger="load">
+ </div>
+ <hr class="border-t-[0.375rem] border-dotted border-main-hard my-2 w-24 mx-auto">
<script>
- function initLocationMap(id, x, y, relativeErrorMeters = 0) {
- map = L.map(id).setView([x, y], 8);
-
- L.tileLayer('https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', {
- maxZoom: 20,
- attribution: '<a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> | Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
- }).addTo(map);
-
- if (relativeErrorMeters != 0) {
- var circle = L.circle([x, y], {
- color: '#8a9d8a',
- fillColor: '#8a9d8a',
- fillOpacity: 0.15,
- radius: relativeErrorMeters
- }).addTo(map);
- }
-
- var marker = L.marker([x, y], {
- title: '{{ .Title }}',
- }).addTo(map)
- .bindPopup('<span><b>{{ .Title }}</b><br><a href="https://www.openstreetmap.org/#map=13/{{ .MapLocationX }}/{{ .MapLocationY }}">{{ .MapLocationX }}, {{ .MapLocationY }}</a></span>')
- .openPopup();
- }
-
- if (map) {
- map.remove();
- map = null;
- }
-
- setTimeout(() => initLocationMap(
- 'map-container',
- {{ .MapLocationX }},
- {{ .MapLocationY }},
- {{ .MapLocationAreaMeters }}
- ), 1000);
-
var lightbox = GLightbox({
selector: '.glightbox',
moreLength: 0
diff --git a/views/pages/global-map.html b/views/pages/global-map.html
index 248d0b9..4d1853f 100644
--- a/views/pages/global-map.html
+++ b/views/pages/global-map.html
@@ -3,22 +3,23 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <meta name="description" content="{{ .L.GlobalMap.Description }}" />
- <meta property="og:title" content="{{ .L.GlobalMap.Header }}" />
+ <meta name="description" content="{{ l $.Lang "GlobalMap" "Description" }}" />
+ <title>{{ l $.Lang "GlobalMap" "Header" }} // SAYA.UZ</title>
+ <meta property="og:title" content="{{ l $.Lang "GlobalMap" "Header" }}" />
<meta
property="og:description"
- content="{{ .L.GlobalMap.Description }}"
+ content="{{ l $.Lang "GlobalMap" "Description" }}"
/>
<meta
property="og:url"
content="{{ .CanonicalEndpoint }}/{{ .Lang }}/map"
/>
<meta property="og:type" content="site" />
- <title>{{ .L.GlobalMap.Header }} // SAYA.UZ</title>
+ <title>{{ l $.Lang "GlobalMap" "Header" }} // SAYA.UZ</title>
<link rel="canonical" href="{{ .CanonicalEndpoint }}/{{ .Lang }}/map" />
<link
rel="stylesheet"
- href="{{ .StaticStorageBaseUrl }}/fonts/fonts.css"
+ href="{{ .StaticStorage.BaseUrl }}/fonts/fonts.css"
/>
{{ block "header" . }}{{ end }}
<link rel="stylesheet" href="/output.css" />
@@ -35,17 +36,26 @@
media="(prefers-color-scheme: dark)"
/>
<link
+ rel="icon"
+ href="/favicon.ico"
+ type="image/x-icon"
+ />
+ <link
rel="stylesheet"
- href="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.css"
+ href="{{ .StaticStorage.BaseUrl }}/libs/leaflet/1.9.4/leaflet.css"
/>
<link
rel="stylesheet"
- href="{{ .StaticStorageBaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.css"
+ href="{{ .StaticStorage.BaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.css"
/>
<link
rel="stylesheet"
- href="{{ .StaticStorageBaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css"
+ href="{{ .StaticStorage.BaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css"
/>
+ <script src="{{ .StaticStorage.BaseUrl }}/libs/htmx/2.0.6/htmx.min.js"></script>
+ <script src="{{ .StaticStorage.BaseUrl }}/libs/leaflet/1.9.4/leaflet.js"></script>
+ <script src="{{ .StaticStorage.BaseUrl }}/libs/protomaps-leaflet/5.0.0/dist/protomaps-leaflet.js"></script>
+ <script src="{{ .StaticStorage.BaseUrl }}/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js"></script>
</head>
<body
@@ -210,21 +220,14 @@
</div>
</nav>
- <div
- id="map-container"
- class="bg-paper bg-background-dark flex z-10 w-full h-full"
- ></div>
-
- <script src="{{ .StaticStorageBaseUrl }}/libs/htmx/2.0.6/htmx.min.js"></script>
- <script src="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.js"></script>
- <script src="{{ .StaticStorageBaseUrl }}/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js"></script>
+ <div id="map-outer-container" class="bg-paper bg-background-dark flex z-10 w-full h-full"
+ hx-get="/api/v1/map" hx-vals='{"lang": "{{ .Lang }}", "codename": "{{ .Codename }}", "zoom": 4, "zoomPosition": "topright"}' hx-target="this" hx-swap="innerHTML" hx-trigger="load">
+ </div>
<script>
const themeWheel = document.getElementById("theme-wheel");
const themeButton = document.getElementById("sidebar-theme-button");
const themeIcons = document.querySelectorAll(".theme-icon");
- const themeWheelRadius = convertRemToPixels(6);
- const THUMB_BASE = "{{ .ThumbnailBaseUrl }}";
themeIcons.forEach((icon) => {
if (icon.getAttribute("data-theme") == savedTheme) {
@@ -245,6 +248,7 @@
function positionThemeIcons() {
const sliceAngle = 360 / themeIcons.length;
+ const themeWheelRadius = convertRemToPixels(6);
themeIcons.forEach((icon, index) => {
const startAngle = index * sliceAngle;
@@ -288,6 +292,7 @@
const buttonRect = themeButton.getBoundingClientRect();
const buttonCenterX = buttonRect.left + buttonRect.width / 2;
const buttonCenterY = buttonRect.top + buttonRect.height / 2;
+ const themeWheelRadius = convertRemToPixels(6);
const margin = 0;
@@ -361,95 +366,5 @@
}
});
</script>
-
- <script>
- var map;
- var markers = L.markerClusterGroup();
-
- function createCustomIcon(color, borderColor = "var(--color-main-soft)") {
- const svgIcon = `
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
- <path d="M12 1C7.5 1 4 4.5 4 9c0 6.5 8 14 8 14s8-7.5 8-14c0-4.5-3.5-8-8-8zm0 11c-1.5 0-3-1.5-3-3s1.5-3 3-3 3 1.5 3 3-1.5 3-3 3z"
- fill="${color}"
- stroke="${borderColor}"
- stroke-width="1"
- stroke-linejoin="round"/>
- </svg>
- `;
-
- return L.divIcon({
- html: svgIcon,
- className: 'custom-svg-marker',
- iconSize: [32, 32],
- iconAnchor: [16, 32],
- popupAnchor: [0, -32]
- });
- }
-
- function initLocationMap() {
- map = L.map('map-container').setView([{{ .MapLocationLat }}, {{ .MapLocationLong }}], 4);
-
- L.tileLayer('https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', {
- maxZoom: 20,
- attribution: '<a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> | Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
- zoomControl: false
- }).addTo(map);
-
- L.control.zoom({
- position: 'topright'
- }).addTo(map);
-
- map.addLayer(markers);
- }
-
- function markerToHsl(str, newCoef) {
- let hash = 0;
- for (let i = 0; i < str.length / 2; i++) {
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
- hash = str.charCodeAt(str.length - i - 1) + ((hash << 5) - hash);
- hash %= 360;
- }
-
- return [hash, (20 + newCoef * 80).toFixed(0), (20 + newCoef * 75).toFixed(0)];
- }
-
- function mapAddMarker(x, y, relativeErrorMeters = 0, title = "", link = "", thumbnail = "", newCoef = 1) {
- const [h, s, l] = markerToHsl(title, newCoef);
- const color = `hsl(${h} ${s}% ${l}%)`;
-
- if (relativeErrorMeters != 0) {
- var circle = L.circle([x, y], {
- color: color,
- fillColor: color,
- fillOpacity: 0.15,
- radius: relativeErrorMeters
- }).addTo(map);
- }
-
- var marker = L.marker([x, y], {
- icon: createCustomIcon(color),
- title: title,
- }).bindPopup(`
- <div class="flex flex-row justify-center justify-items-center">
- <img onclick="location.href='${link}';" class="cursor-pointer object-cover rounded-[10%] select-none w-12 h-12 mr-1" src="${THUMB_BASE.replace('%s', thumbnail)}">
- <span><b><a href="${link}">${title}</a></b><br><a href="https://www.openstreetmap.org/#map=13/${x}/${y}">${x}, ${y}</a></span>
- </div>`);
-
- markers.addLayer(marker);
- }
-
- document.addEventListener('DOMContentLoaded', () => {
- initLocationMap();
- {{- range .MapMarkers }}
- mapAddMarker({{ .Lat }}, {{ .Long }}, {{ .AccuracyMeters }}, {{ .Title }}, {{ .PageLink }}, {{ .Thumbnail }}, {{ fdiv .Index (len $.MapMarkers) }});
- {{- end }}
- });
-
- window.addEventListener('resize', function() {
- setTimeout(() => {
- map.invalidateSize();
- }, 100);
- });
- </script>
</body>
</html>
diff --git a/views/pages/home-page.html b/views/pages/home-page.html
index 7de9420..d494a08 100644
--- a/views/pages/home-page.html
+++ b/views/pages/home-page.html
@@ -17,24 +17,23 @@
<path d="M4.59 15.35a39.94 39.94 0 0 0 -2.94 3.25 0.35 0.35 0 1 0 0.53 0.46c0.59 -0.6 1.69 -1.51 2.89 -2.58 0.63 -0.57 1.29 -1.17 1.9 -1.79s1.42 -1.52 2 -2.21 -0.14 -1 -0.82 -0.39c-0.52 0.46 -1.08 0.94 -1.67 1.47s-1.28 1.19 -1.89 1.79Z" fill="var(--color-accent-deep)" stroke-width="1"></path>
</g></symbol>
</svg>
-<div class="absolute w-full h-full inset-0 inset-shadow-[0_0_0.4rem_black] pointer-events-none z-20"></div>
-<div class="relative bg-paper bg-background-light font-gentium flex flex-row ar-lt-0.6:flex-col w-full h-full overflow-y-auto text-base/loose tracking-wide z-10">
- <div class="ar-gt-0.6:flex-3/5 justify-center content-start p-8">
+<div class="relative bg-paper bg-background-light font-gentium flex flex-col sm:flex-row w-full h-full overflow-y-auto text-base/loose tracking-wide z-10">
+ <div class="sm:flex-3/5 justify-center content-start p-8">
<div class="flex flex-col justify-start items-start bg-paper bg-background-dark mb-8 shadow-[0.4rem_0.4rem_0.4rem_black]">
<p class="text-3xl/loose sm:text-4xl/loose lg:text-5xl/loose tracking-widest uppercase w-full font-m-plus font-thin italic text-left
- border-l-4 border-solid border-background-dark
- bg-linear-to-r from-background-dark to-background-light">{{ .L.HomePage.Hymn1 }}</p>
- <hr class="w-full border-t-2 border-dotted border-main-hard mb-1">
- <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ .L.HomePage.Hymn2 }}</p>
- <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ .L.HomePage.Hymn3 }}</p>
- <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ .L.HomePage.Hymn4 }}</p>
+ border-l-[0.25rem] border-solid border-background-dark
+ bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "Hymn1" }}</p>
+ <hr class="w-full border-t-[0.125rem] border-dotted border-main-hard mb-1">
+ <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ l $.Lang "HomePage" "Hymn2" }}</p>
+ <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ l $.Lang "HomePage" "Hymn3" }}</p>
+ <p class="text-xl/loose sm:text-2xl/loose lg:text-3xl/loose -indent-8 ml-12 p-1">{{ l $.Lang "HomePage" "Hymn4" }}</p>
</div>
<div class="flex flex-col justify-center items-center">
<p class="text-xl sm:text-2xl lg:text-3xl p-1 w-full font-m-plus font-thin italic text-left
- border-l-1.5 border-solid border-background-dark
- bg-linear-to-r from-background-dark to-background-light">{{ .L.HomePage.DidYouKnowThat }}</p>
- <hr class="w-full border-t-2 border-dotted border-main-hard mb-1">
+ border-l-[0.125rem] border-solid border-background-dark
+ bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "DidYouKnowThat" }}</p>
+ <hr class="w-full border-t-[0.125rem] border-dotted border-main-hard mb-1">
<ul class="w-full text-left list-[circle]">
<li class="ml-4 mb-1">{{ index .FunFacts 0 }}</li>
<li class="ml-4 mb-1">{{ index .FunFacts 1 }}</li>
@@ -43,12 +42,12 @@
</div>
</div>
- <div class="ar-gt-0.6:flex-2/5 justify-center content-start p-8">
+ <div class="sm:flex-2/5 justify-center content-start p-8">
<div class="flex flex-col justify-center items-start mb-4">
<p class="text-xl sm:text-2xl lg:text-3xl p-1 w-full font-m-plus font-thin italic text-left
- border-l-1.5 border-solid border-background-dark
- bg-linear-to-r from-background-dark to-background-light">{{ .L.HomePage.Contacts }}</p>
- <hr class="w-full border-t-2 border-dotted border-main-hard mb-1">
+ border-l-[0.125rem] border-solid border-background-dark
+ bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "Contacts" }}</p>
+ <hr class="w-full border-t-[0.125rem] border-dotted border-main-hard mb-1">
<a href="https://t.me/EarlInisMona">
<svg class="w-4 h-4 mr-1 inline" viewBox="0 0 24 24"><use href="#icon-telegram"/></svg>
<span class="font-bold">Telegram:</span>
@@ -74,7 +73,7 @@
<div class="home-page-heart z-50">♡</div>
{{- end }}
<img src="{{ .GifUrl }}"
- class="home-page-gif z-49 -rotate-12 max-w-[70%] max-h-[70%] border-4 border-background-dark border-inset">
+ class="home-page-gif z-49 -rotate-12 max-w-[70%] max-h-[70%] border-[0.25rem] border-background-dark border-inset">
</div>
<script>
@@ -121,23 +120,23 @@
<div class="flex flex-col justify-center items-center">
<p class="text-xl sm:text-2xl lg:text-3xl p-1 w-full font-m-plus font-thin italic text-left
- border-l-1.5 border-solid border-color-background-dark
- bg-linear-to-r from-background-dark to-background-light">{{ .L.HomePage.SidebarDescription }}</p>
- <hr class="w-full border-t-2 border-dotted border-main-hard mb-1">
+ border-l-[0.125rem] border-solid border-color-background-dark
+ bg-linear-to-r from-background-dark to-background-light">{{ l $.Lang "HomePage" "SidebarDescription" }}</p>
+ <hr class="w-full border-t-[0.125rem] border-dotted border-main-hard mb-1">
<div class="w-full grid grid-cols-[1fr_4fr] content-center items-center
[&>*:nth-child(4n+2)]:bg-background-medium/50 [&>*:nth-child(4n+3)]:bg-background-medium/50" style="container-type: inline-size;">
<svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-back"/></svg>
- <p class="p-1 w-[85cqi]">{{ .L.HomePage.BackDescription }}</p>
+ <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "BackDescription" }}</p>
<svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-logo"/></svg>
- <p class="p-1 w-[85cqi]">{{ .L.HomePage.HomePageDescription }}</p>
+ <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "HomePageDescription" }}</p>
<svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-newspaper"/></svg>
- <p class="p-1 w-[85cqi]">{{ .L.HomePage.BlogSearchDescription }}</p>
+ <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "BlogSearchDescription" }}</p>
<svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-map"/></svg>
- <p class="p-1 w-[85cqi]">{{ .L.HomePage.MarkerMapDescription }}</p>
+ <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "MarkerMapDescription" }}</p>
<svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-palette"/></svg>
- <p class="p-1 w-[85cqi]">{{ .L.HomePage.ThemeSwitchDescription }}</p>
+ <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "ThemeSwitchDescription" }}</p>
<svg viewBox="0 0 24 24" class="h-full w-[15cqi] context-center text-main-hard"><use href="#icon-account"/></svg>
- <p class="p-1 w-[85cqi]">{{ .L.HomePage.AccountDescription }}</p>
+ <p class="p-1 w-[85cqi]">{{ l $.Lang "HomePage" "AccountDescription" }}</p>
</div>
</div>
</div>
diff --git a/views/pages/language-pick.html b/views/pages/language-pick.html
index f4119e4..9576a65 100644
--- a/views/pages/language-pick.html
+++ b/views/pages/language-pick.html
@@ -1,7 +1,7 @@
{{ define "body" }}
<div class="flex flex-col justify-center text-3xl h-full">
{{- range .AvailableLanguages }}
- <hr class="border-t-2 border-dotted border-main-hard">
+ <hr class="border-t-[0.125rem] border-dotted border-main-hard">
<div class="nth-[2n+1_of_div]:bg-background-medium/50 nth-[2n_of_div]:bg-background-light/50 p-2.5 flex flex-row justify-center justify-items-center">
<a href="/{{ .Name }}" class="w-12 h-12 basis-12 shrink-0 mr-4">
<img onclick="return changeUrl('{{ .Name }}');" class="w-full h-full aspect-square cursor-pointer object-cover rounded-md select-none" src="{{ .Flag }}">
@@ -11,6 +11,6 @@
</a>
</div>
{{- end }}
- <hr class="border-t-2 border-dotted border-main-hard">
+ <hr class="border-t-[0.125rem] border-dotted border-main-hard">
</div>
{{ end }}
diff --git a/views/pages/unsubscribe-page.html b/views/pages/unsubscribe-page.html
index a917746..12ff1f0 100644
--- a/views/pages/unsubscribe-page.html
+++ b/views/pages/unsubscribe-page.html
@@ -16,7 +16,12 @@
type="image/svg+xml"
media="(prefers-color-scheme: dark)"
/>
- <title>{{ .L.UnsubscribePage.Header }} // SAYA.UZ</title>
+ <link
+ rel="icon"
+ href="/favicon.ico"
+ type="image/x-icon"
+ />
+ <title>{{ l $.Lang "UnsubscribePage" "Header" }} // SAYA.UZ</title>
<style>
.bg-paper {
background-image: url(data:image/webp;base64,UklGRi4BAABXRUJQVlA4ICIBAABQCQCdASqAAIAAPm02lUmkIqIkIImIgA2JaW3rMFmGDAOKLAoJtP591ByFR74Tun2DP+Lygpfsz5FH7+cKgxi2Islfd03i5AJdXUfU0SDc8SXYAAD++hlzeYAJWQEF8ZdkJdlcA+5btZg0FMGaso8SHoBph0qAOQysElJ5C5SJFNoKIV1J550UyL7Y9Gh0/c/uzLnBmr8lvz7k1WeA81H4S5ViU/wBqa7aM1m8D9rDtrk8cSTqx+xWF6MS8UfZGSsks6CT/igtgv6syFN9uNA3LjnjiCJKYOvFcMkX+jB2578GWVo3nyLaM1QNPYCxn3TiUbnf7U0MdD2NsBpg1+Jd1wu6yW/U+goN+IQS6Nt4RMWltVcZHlxrneKJi0SobO0AAA==);
diff --git a/views/pages/user-page.html b/views/pages/user-page.html
index 0dd006a..a94dcb3 100644
--- a/views/pages/user-page.html
+++ b/views/pages/user-page.html
@@ -1,74 +1,74 @@
{{ define "body" }}
-<div class="absolute w-full h-full inset-0 inset-shadow-[0_0_0.4rem_black] pointer-events-none z-20"></div>
+<div class="absolute w-full h-full inset-0 inset-shadow-elevation-6 pointer-events-none z-20"></div>
<div class="relative bg-paper bg-background-light flex flex-col px-8 py-4">
<form id="email-form" class="w-full mb-2 flex flex-col relative crossable"
hx-get="/api/v1/email/is-in-verification" hx-target="#email-message" hx-swap="outerHTML" hx-trigger="load" hx-indicator="this">
<div class="flex flex-row w-full mb-4 z-21">
<div class="flex flex-col w-full sm:flex-3/5 grow-0">
<label class="block text-main-medium font-bold mb-1 z-22">
- {{ .L.UserProfile.EmailHeader }}
+ {{ l $.Lang "UserProfile" "EmailHeader" }}
</label>
- <input class="bg-background-medium appearance-none border-background-medium rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-[0_0_0.4rem_black] focus:outline-none focus:bg-background-light focus:text-main-hard z-22"
- id="email-input" name="email" type="text" value="{{ .Email }}" placeholder="{{ .L.UserProfile.TypeEmail }}" maxlength="64"
+ <input class="bg-background-medium appearance-none border-background-medium rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-elevation-6 focus:outline-none focus:bg-background-light focus:text-main-hard z-22"
+ id="email-input" name="email" type="text" value="{{ .Email }}" placeholder="{{ l $.Lang "UserProfile" "TypeEmail" }}" maxlength="64"
data-validated-email="{{ .Email }}" onchange="checkIfValidated(this);">
</div>
</div>
<button class="w-max mx-auto bg-main-hard hover:bg-main-medium active:bg-main-soft active:inset-shadow-[0.2em_0.2em_0.2rem_black] transition-colors transition-300 text-background-light font-bold py-2 px-4 rounded z-22"
hx-post="/api/v1/email/send-verification-code" hx-target="#email-message" hx-swap="outerHTML" hx-indicator="#email-form">
- {{ .L.UserProfile.SendCodeButton }}
+ {{ l $.Lang "UserProfile" "SendCodeButton" }}
</button>
</form>
- <div id="email-message" class="bg-paper bg-background-dark text-main-soft z-26 w-full inset-shadow-[0_0_0.4rem_black] py-2 px-4 text-center"></div>
+ <div id="email-message" class="bg-paper bg-background-dark text-main-soft z-26 w-full inset-shadow-elevation-6 py-2 px-4 text-center"></div>
- <hr class="my-2 border-t-4 border-dotted border-main-hard">
+ <hr class="my-2 border-t-[0.25rem] border-dotted border-main-hard">
<form id="email-verification-form" class="w-full mb-2 flex flex-col relative hidden crossable"
{{ if .EmailCode }}hx-post="/api/v1/email/verify" hx-target="#verification-message" hx-swap="outerHTML" hx-trigger="load" hx-indicator="this"{{ end }}>
<div class="flex flex-row w-full mb-4 z-21">
<div class="flex flex-col w-full sm:flex-2/5 grow-0">
<label class="block text-main-medium font-bold ml-1 z-22">
- {{ .L.UserProfile.VerificationCodeHeader }}
+ {{ l $.Lang "UserProfile" "VerificationCodeHeader" }}
</label>
- <input class="bg-background-medium appearance-none border-background-medium rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-[0_0_0.4rem_black] focus:outline-none focus:bg-background-light focus:text-main-hard z-22"
+ <input class="bg-background-medium appearance-none border-background-medium rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-elevation-6 focus:outline-none focus:bg-background-light focus:text-main-hard z-22"
name="email_code" type="text" value="{{ .EmailCode }}" placeholder="0123456789ABCDEF" maxlength="16" autocapitalize="characters">
</div>
</div>
<button class="w-max mx-auto bg-main-hard hover:bg-main-medium active:bg-main-soft active:inset-shadow-[0.2em_0.2em_0.2rem_black] transition-colors transition-300 text-background-light font-bold py-2 px-4 rounded z-22"
hx-post="/api/v1/email/verify" hx-target="#verification-message" hx-swap="outerHTML" hx-indicator="#email-verification-form">
- {{ .L.UserProfile.VerifyButton }}
+ {{ l $.Lang "UserProfile" "VerifyButton" }}
</button>
</form>
- <div id="verification-message" class="bg-paper bg-background-dark text-main-soft z-26 w-full inset-shadow-[0_0_0.4rem_black] py-2 px-4 text-center"></div>
+ <div id="verification-message" class="bg-paper bg-background-dark text-main-soft z-26 w-full inset-shadow-elevation-6 py-2 px-4 text-center"></div>
</div>
-<hr class="my-4 border-t-4 border-dotted border-main-hard">
+<hr class="my-4 border-t-[0.25rem] border-dotted border-main-hard">
-<div class="bg-paper bg-background-light flex flex-col inset-shadow-[0_0_0.4rem_black] px-8 py-4">
+<div class="bg-paper bg-background-light flex flex-col inset-shadow-elevation-6 px-8 py-4">
<form id="subs-form" class="w-full mb-2 flex flex-col relative crossable">
<div class="flex flex-col w-full mb-4 z-21">
<label class="block text-main-medium font-bold mb-1 z-22">
- {{ .L.UserProfile.SubscriptionHeader }}
+ {{ l $.Lang "UserProfile" "SubscriptionHeader" }}
</label>
<div class="flex flex-col sm:flex-row w-full">
<div class="m-1 w-1/3">
- <label><input class="mr-1" type="radio" id="tagsNone" name="tags" value="none" {{ if eq .TagsPicked "none" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ .L.UserProfile.TagsNone }}</label>
+ <label><input class="mr-1" type="radio" id="tagsNone" name="tags" value="none" {{ if eq .TagsPicked "none" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ l $.Lang "UserProfile" "TagsNone" }}</label>
</div>
<div class="m-1 w-1/3">
- <label><input class="mr-1" type="radio" id="tagsAll" name="tags" value="all" {{ if eq .TagsPicked "all" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ .L.UserProfile.TagsAll }}</label>
+ <label><input class="mr-1" type="radio" id="tagsAll" name="tags" value="all" {{ if eq .TagsPicked "all" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ l $.Lang "UserProfile" "TagsAll" }}</label>
</div>
<div class="m-1 w-1/3">
- <label><input class="mr-1" type="radio" id="tagsSpecific" name="tags" value="specific" {{ if eq .TagsPicked "specific" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ .L.UserProfile.TagsSpecific }}</label>
+ <label><input class="mr-1" type="radio" id="tagsSpecific" name="tags" value="specific" {{ if eq .TagsPicked "specific" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ l $.Lang "UserProfile" "TagsSpecific" }}</label>
</div>
</div>
<div id="pick-tags-form" class="flex flex-col w-full relative crossable">
- <input type="text" class="bg-background-medium appearance-none border-background-medium rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-[0_0_0.4rem_black] focus:outline-none focus:bg-background-light focus:text-main-hard z-22"
+ <input type="text" class="bg-background-medium appearance-none border-background-medium rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-elevation-6 focus:outline-none focus:bg-background-light focus:text-main-hard z-22"
list="existing-tags" onchange="addTag(this.value);" onkeydown="if (event.keyCode === 13) {addTag(this.value); this.value=''; return false;}">
<datalist id="existing-tags">
{{- range .ExistingTags }}
<option value="{{ .Name }}">
{{- end }}
</datalist>
- <div id="tags-picked-list" class="bg-paper bg-background-dark flex flex-wrap inset-shadow-[0_0_0.4rem_black] p-2 m-4 w-full">
+ <div id="tags-picked-list" class="bg-paper bg-background-dark flex flex-wrap inset-shadow-elevation-6 p-2 m-4 w-full">
{{- range .TagsPickedList }}
<div id="tagPicked{{ . }}" class="bg-paper bg-main-hard text-background-light m-2 flex flex-row">
<p class="py-1 px-2">{{ . }}</p>
@@ -80,11 +80,11 @@
</div>
<button class="w-max mx-auto bg-color-main-hard hover:bg-main-medium active:bg-color-main-soft active:inset-shadow-[0.2em_0.2em_0.2rem_black] transition-colors transition-300 text-background-light font-bold py-2 px-4 rounded z-22"
hx-put="/api/v1/subs" hx-target="#subs-message" hx-swap="outerHTML" hx-indicator="#subs-form">
- {{ .L.UserProfile.SaveButton }}
+ {{ l $.Lang "UserProfile" "SaveButton" }}
</button>
</div>
</form>
- <div id="subs-message" class="bg-paper bg-background-dark text-main-soft z-26 w-full inset-shadow-[0_0_0.4rem_black] py-2 px-4 text-center"></div>
+ <div id="subs-message" class="bg-paper bg-background-dark text-main-soft z-26 w-full inset-shadow-elevation-6 py-2 px-4 text-center"></div>
</div>
<script>
@@ -168,7 +168,7 @@ function addTag(name) {
found = [...existingTags].find(opt => opt.value===name);
if (!found) {
subsMessage.classList.remove("hidden");
- subsMessage.innerHTML = "{{ .L.UserProfile.TagDoesNotExist }}".replace("{}", name);
+ subsMessage.innerHTML = "{{ l $.Lang "UserProfile" "TagDoesNotExist" }}".replace("{}", name);
return;
}
@@ -177,7 +177,7 @@ function addTag(name) {
found = [...subscribedTags].find(opt => opt.value===name);
if (found) {
subsMessage.classList.remove("hidden");
- subsMessage.innerHTML = "{{ .L.UserProfile.TagAlreadyAdded }}".replace("{}", name);
+ subsMessage.innerHTML = "{{ l $.Lang "UserProfile" "TagAlreadyAdded" }}".replace("{}", name);
return;
}
diff --git a/views/partials/blog-page-like-button.html b/views/partials/blog-page-like-button.html
index 54b2914..50ceae9 100644
--- a/views/partials/blog-page-like-button.html
+++ b/views/partials/blog-page-like-button.html
@@ -1,5 +1,5 @@
<button hx-put="/api/v1/like" hx-vals='{"like": {{ not .Liked }}}' hx-target="this" hx-swap="outerHTML" hx-trigger="click"
- class="accent-button rounded-xl flex flex-row shrink-0 w-fit h-full mx-2 my-auto{{ if .Liked }} active{{ end }}">
+ class="accent-button rounded-xl relative shrink-0 w-fit aspect-square mr-4 p-2 my-auto{{ if .Liked }} active{{ end }}">
{{ if .Liked }}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="Like-Hand-1--Streamline-Freehand" class="h-10">
<desc>
@@ -12,7 +12,7 @@
<path d="M13.94 4.33a2.57 2.57 0 0 1 1.43 0.88 4.52 4.52 0 0 1 1 1.54 0.57 0.57 0 0 0 0.56 0.37 0.57 0.57 0 0 0 0.53 -0.41 2.81 2.81 0 0 1 1 -1.41 6.9 6.9 0 0 1 1.69 -1 0.49 0.49 0 0 0 0.31 -0.43 0.5 0.5 0 0 0 -0.24 -0.47 8.18 8.18 0 0 1 -2.26 -1.47 2.39 2.39 0 0 1 -0.71 -1.46 0.34 0.34 0 0 0 -0.34 -0.34 0.35 0.35 0 0 0 -0.34 0.35 0.29 0.29 0 0 0 -0.27 0.23 4.41 4.41 0 0 1 -1 1.63 3.53 3.53 0 0 1 -1.48 1 0.47 0.47 0 0 0 -0.32 0.5 0.48 0.48 0 0 0 0.44 0.49Zm2 -1.47a5.53 5.53 0 0 0 0.82 -1.44 3.34 3.34 0 0 0 0.59 1 6.91 6.91 0 0 0 1.53 1.3 5.93 5.93 0 0 0 -1.48 1 6.91 6.91 0 0 0 -0.51 0.57 4.8 4.8 0 0 0 -0.7 -0.85 4 4 0 0 0 -1 -0.74 5.76 5.76 0 0 0 0.71 -0.84Z" fill="var(--color-accent-deep)" fill-rule="evenodd" stroke-width="1"></path>
</g>
</svg>
- <div class="ml-auto bg-accent-light rounded-full w-6 h-6 text-main">{{ .LikedCount }}</div>
+ <div class="absolute left-10 -top-2 bg-accent-light rounded-full w-6 h-6 text-main! font-m-plus! font-normal! tracking-normal! not-italic! text-lg!">{{ .LikedCount }}</div>
{{ else }}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="Like-Hand--Streamline-Freehand" class="h-10">
<desc>
@@ -23,6 +23,6 @@
<path d="M3.53 9.34a2.22 2.22 0 0 0 -1.24 -0.88 5.37 5.37 0 0 0 -2 -0.26 0.3 0.3 0 0 0 -0.29 0.31 0.3 0.3 0 0 0 0.32 0.29 4.36 4.36 0 0 1 1.44 0.25 1.74 1.74 0 0 1 1 0.69A9.16 9.16 0 0 1 3 11.8c0 0.64 0.06 1.38 0.09 2.14 0 1.07 0.08 2.2 0.11 3.22s0.06 2.19 0 2.84c0 0.23 -0.12 0.45 -0.07 0.51A2.7 2.7 0 0 1 2 21a3.34 3.34 0 0 1 -1.49 0 0.34 0.34 0 0 0 -0.22 0.65 4.22 4.22 0 0 0 1.82 0.13 3.37 3.37 0 0 0 1.79 -0.69 1.82 1.82 0 0 0 0.26 -0.82 37.13 37.13 0 0 0 0.14 -5.13c0 -1 -0.1 -2 -0.19 -2.83a12.48 12.48 0 0 0 -0.58 -2.97Z" fill="var(--color-accent-deep)" fill-rule="evenodd" stroke-width="1"></path>
</g>
</svg>
- <div class="ml-auto bg-accent-deep rounded-full w-6 h-6 text-background">{{ .LikedCount }}</div>
+ <div class="absolute left-10 -top-2 bg-accent-deep rounded-full w-6 h-6 text-background! font-m-plus! font-normal! tracking-normal! not-italic! text-lg!">{{ .LikedCount }}</div>
{{ end }}
</button>
diff --git a/views/partials/catalogue-blog-cards.html b/views/partials/catalogue-blog-cards.html
index 0a93797..6466262 100644
--- a/views/partials/catalogue-blog-cards.html
+++ b/views/partials/catalogue-blog-cards.html
@@ -1,8 +1,8 @@
{{- range .BlogPages }}
-<hr class="first-of-type:hidden border-t-2 border-dotted border-main-hard">
-<div class="w-full flex flex-row items-stretch">
- <div class="relative min-h-24 flex flex-col w-28 overflow-hidden shrink-0 mask-r-from-70% mask-r-to-100%">
- <img onclick="return changeUrl('{{ .ArticleLink }}');" class="h-full min-h-24 w-full cursor-pointer select-none object-cover block absolute" src="{{ printf $.ThumbnailBaseUrl .Thumbnail }}">
+<hr class="first-of-type:hidden border-t-[0.375rem] my-1 w-24 mx-auto border-dotted border-main-hard">
+<div {{ if .ToHighlight }}id="blog-page-highlighted"{{ end }} class="w-full flex flex-row items-stretch mask-t-from-[calc(100%-0.5rem)] mask-t-to-100% mask-b-from-[calc(100%-0.5rem)] mask-b-to-100% {{ if .ToHighlight }}bg-accent-light bg-paper{{ end }}">
+ <div class="relative min-h-24 flex flex-col w-32 overflow-hidden shrink-0 mask-r-from-[calc(100%-1rem)] mask-r-to-100%">
+ <img onclick="return changeUrl('{{ .ArticleLink }}');" class="h-full min-h-24 w-full cursor-pointer select-none object-cover block absolute" src="{{ printf $.PhotoStorage.Thumbnail320p.BaseUrl .Thumbnail }}">
<div class="w-full h-6 flex flex-row mt-auto relative">
<div class="flex-1/2 flex flex-row justify-center select-none bg-linear-180 {{ if .Liked }}from-accent-deep/40 to-accent-deep/80 text-background-dark{{ else }}from-accent-light/40 to-accent-light/80 text-main-hard{{ end }}">
<svg viewBox="0 0 24 24"><use href="#icon-like"/></svg>
@@ -14,16 +14,26 @@
</div>
</div>
</div>
- <div class="grow w-full h-fit flex flex-col justify-center my-auto py-1 px-1">
+ <div class="grow w-full h-fit flex flex-col justify-center my-auto py-1 px-2">
<div class="flex flex-row gap-2 max-w-full">
<a href="{{ .ArticleLink }}" onclick="return changeUrl('{{ .ArticleLink }}');" class="grow-2 text-base cursor-pointer font-m-plus">
<span class="font-extrabold">{{ .Title }}</span>
<span class="italic">[{{ .ActionDate }}]</span>
</a>
+ {{- if not $.HidePublishedTime }}
<p class="basis-56 grow text-base italic text-secondary text-right">{{ .PublishedTime }}</p>
+ {{- end }}
</div>
- <p class="font-gentium">{{ .ShortDescription }}</p>
+ <p class="font-m-plus">{{ .ShortDescription }}</p>
+ {{- if not $.HideTags }}
<p class="font-m-plus text-sm">{{ $.L.TagsLabel }} {{ template "catalogue-blog-card-tags.html" . }}</p>
+ {{- end }}
</div>
</div>
-{{- end }} \ No newline at end of file
+{{- if .ToHighlight }}
+<script>
+ element = document.getElementById("blog-page-highlighted");
+ element.scrollIntoView({ behavior: "smooth", block: "center" });
+</script>
+{{- end }}
+{{- end }}
diff --git a/views/partials/general-page-body.html b/views/partials/general-page-body.html
index d697c14..b71a0d1 100644
--- a/views/partials/general-page-body.html
+++ b/views/partials/general-page-body.html
@@ -1 +1,2 @@
+<input class="htmx-exit-trigger" type="hidden" name="dummy" value="">
{{ block "body" . }}{{ end }} \ No newline at end of file
diff --git a/views/partials/general-page-header.html b/views/partials/general-page-header.html
index 995ebf3..a75d32f 100644
--- a/views/partials/general-page-header.html
+++ b/views/partials/general-page-header.html
@@ -1,9 +1,9 @@
-<title>{{ .Title }} // SAYA.UZ</title>
+<title>{{ or .PageTitle .Title }}</title>
<div class="flex flex-row mb-2">
{{ block "header" . }}{{ end }}
- <p
- class="text-4xl font-m-plus font-bold italic text-shadow-[0_2px_2px_var(--color-main-soft)] text-left my-auto"
+ <h1
+ class="text-4xl font-m-plus font-bold italic text-shadow-[0_0.125rem_0.125rem_var(--color-main-soft)] text-left my-auto"
>
{{ .Title }}
- </p>
+ </h1>
</div>
diff --git a/views/partials/global-map-widget.html b/views/partials/global-map-widget.html
new file mode 100644
index 0000000..f50020f
--- /dev/null
+++ b/views/partials/global-map-widget.html
@@ -0,0 +1,109 @@
+<div
+ id="map-container"
+ class="bg-paper bg-background-dark flex z-10 w-full h-full"
+></div>
+
+<script>
+(function() {
+ var map;
+ var markers = L.markerClusterGroup();
+ const THUMB_BASE = "{{ .PhotoStorage.Thumbnail320p.BaseUrl }}";
+
+ function createCustomIcon(color, borderColor = "var(--color-main-soft)") {
+ const svgIcon = `
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M12 1C7.5 1 4 4.5 4 9c0 6.5 8 14 8 14s8-7.5 8-14c0-4.5-3.5-8-8-8zm0 11c-1.5 0-3-1.5-3-3s1.5-3 3-3 3 1.5 3 3-1.5 3-3 3z"
+ fill="${color}"
+ stroke="${borderColor}"
+ stroke-width="1"
+ stroke-linejoin="round"/>
+ </svg>
+ `;
+
+ return L.divIcon({
+ html: svgIcon,
+ className: 'custom-svg-marker',
+ iconSize: [32, 32],
+ iconAnchor: [16, 32],
+ popupAnchor: [0, -32]
+ });
+ }
+
+ function initLocationMap() {
+ map = L.map('map-container').setView([{{ .MapLocationLat }}, {{ .MapLocationLong }}], {{ .MapLocationZoom }});
+
+ protomapsL.leafletLayer({
+ url: '{{ .StaticStorage.Map.BaseUrl }}/{{ .StaticStorage.Map.PMTiles }}',
+ maxZoom: 15,
+ flavor: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
+ lang: '{{ .Lang }}'
+ }).addTo(map);
+
+ {{- with .ZoomPosition }}
+ L.control.zoom({
+ position: '{{ . }}'
+ }).addTo(map);
+ {{- end }}
+
+ map.addLayer(markers);
+ }
+
+ function markerToHsl(str, newCoef) {
+ let hash = 0;
+ for (let i = 0; i < str.length / 2; i++) {
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
+ hash = str.charCodeAt(str.length - i - 1) + ((hash << 5) - hash);
+ hash %= 360;
+ }
+
+ return [hash, (20 + newCoef * 80).toFixed(0), (20 + newCoef * 75).toFixed(0)];
+ }
+
+ function mapAddMarker(x, y, relativeErrorMeters = 0, title = "", link = "", thumbnail = "", newCoef = 1, toHighlight = false) {
+ const [h, s, l] = markerToHsl(title, newCoef);
+ const color = `hsl(${h} ${s}% ${l}%)`;
+
+ if (relativeErrorMeters != 0) {
+ var circle = L.circle([x, y], {
+ color: color,
+ fillColor: color,
+ fillOpacity: 0.15,
+ radius: relativeErrorMeters
+ }).addTo(map);
+ }
+
+ var marker = L.marker([x, y], {
+ icon: createCustomIcon(color),
+ title: title,
+ }).bindPopup(`
+ <div class="flex flex-row justify-center justify-items-center">
+ <img onclick="location.href='${link}';" class="cursor-pointer object-cover rounded-[10%] select-none w-12 h-12 mr-1" src="${THUMB_BASE.replace('%s', thumbnail)}">
+ <span><b><a href="${link}">${title}</a></b><br><a href="https://www.openstreetmap.org/#map=13/${x}/${y}">${x}, ${y}</a></span>
+ </div>`);
+
+ markers.addLayer(marker);
+
+ if (toHighlight) {
+ markers.zoomToShowLayer(marker, () => marker.openPopup());
+ }
+ }
+
+ initLocationMap();
+ {{- range .MapMarkers }}
+ mapAddMarker({{ .Lat }}, {{ .Long }}, {{ .AccuracyMeters }}, {{ .Title }}, {{ .PageLink }}, {{ .Thumbnail }}, {{ fdiv .Index (len $.MapMarkers) }}, {{ .ToHighlight }});
+ {{- end }}
+
+ function onResize() {
+ setTimeout(() => {
+ if (map) map.invalidateSize();
+ }, 300);
+ }
+ window.addEventListener('resize', onResize);
+ new MutationObserver((muts, obs) => {
+ if (!document.getElementById('map-container')) {
+ window.removeEventListener('resize', onResize);
+ obs.disconnect();
+ }
+ }).observe(document.body, { childList: true, subtree: true });
+})();
+</script>
diff --git a/views/partials/personal-page-status.html b/views/partials/personal-page-status.html
index 078fb24..41033a9 100644
--- a/views/partials/personal-page-status.html
+++ b/views/partials/personal-page-status.html
@@ -1,4 +1,4 @@
-<div id="{{ .StatusId }}" class='z-26 w-full inset-shadow-[0_0_0.4rem_black]
+<div id="{{ .StatusId }}" class='z-26 w-full inset-shadow-elevation-6
{{if eq .Status "OK"}}bg-green-700 text-green-100
{{else if eq .Status "Failed"}}bg-red-700 text-red-100
{{else}}bg-paper bg-background-dark text-main-soft{{end}}