58 files changed, 1942 insertions, 1480 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 }}" @@ -42,3 +42,5 @@ config*.json .envrc /.ansible + +.DS_Store @@ -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 @@ -5,15 +5,15 @@ go 1.26 require ( github.com/Backblaze/blazer v0.7.2 github.com/dgraph-io/ristretto/v2 v2.4.0 - github.com/go-co-op/gocron/v2 v2.21.1 + github.com/go-co-op/gocron/v2 v2.21.2 github.com/go-playground/validator/v10 v10.30.2 github.com/gofiber/fiber/v2 v2.52.13 github.com/golang-migrate/migrate/v4 v4.19.1 github.com/mattn/go-sqlite3 v1.14.44 - github.com/valyala/fasthttp v1.70.0 - github.com/wneessen/go-mail v0.7.2 + github.com/valyala/fasthttp v1.71.0 + github.com/wneessen/go-mail v0.7.3 github.com/yuin/goldmark v1.8.2 - golang.org/x/crypto v0.50.0 + golang.org/x/crypto v0.51.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -40,7 +40,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 github.com/aws/smithy-go v1.25.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect @@ -58,6 +58,6 @@ require ( github.com/mattn/go-runewidth v0.0.23 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.36.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect ) @@ -26,8 +26,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/ku github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 h1:03xatSQO4+AM1lTAbnRg5OK528EUg744nW7F73U8DKw= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23/go.mod h1:M8l3mwgx5ToK7wot2sBBce/ojzgnPzZXUV445gTSyE8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1 h1:mxuT1xE+dI54NW3RkNjP8DUT5HXqbkiAFvfdyDFwE5c= -github.com/aws/aws-sdk-go-v2/service/s3 v1.100.1/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0 h1:etqBTKY581iwLL/H/S2sVgk3C9lAsTJFeXWFDsDcWOU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0/go.mod h1:L2dcoOgS2VSgbPLvpak2NyUPsO1TBN7M45Z4H7DlRc4= github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc= github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI= github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE= @@ -53,8 +53,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/go-co-op/gocron/v2 v2.21.1 h1:QYOK6iOQVCut+jDcs4zRdWRTBHRxRCEeeFi1TnAmgbU= -github.com/go-co-op/gocron/v2 v2.21.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U= +github.com/go-co-op/gocron/v2 v2.21.2 h1:bD8/YwkojYHgXFr3iEulL148KBdTbKVxUZzFKpXcdbY= +github.com/go-co-op/gocron/v2 v2.21.2/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -104,22 +104,22 @@ github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA= -github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE= -github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8= -github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= +github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k= +github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA= +github.com/wneessen/go-mail v0.7.3 h1:g3DravXC5SMlVdboFrQA8Jx95A8sOzoBeS5F+vzNRK0= +github.com/wneessen/go-mail v0.7.3/go.mod h1:QGhBX0yNbc1J+Mkjcu7z2rpj4B4l+BmDY8gYznPC9sk= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/blog/b2.go b/internal/blog/b2.go index 0caa0ba..f2ac890 100644 --- a/internal/blog/b2.go +++ b/internal/blog/b2.go @@ -2,6 +2,7 @@ package blog import ( "context" + "encoding/json" "fmt" "slices" "strings" @@ -37,6 +38,20 @@ func NewB2Client(cfg *config.StorageConfig) (Client, error) { return &B2Client{b2cl: b2cl, bucket: bucket, prefix: b2cfg.Prefix}, nil } +func (c *B2Client) GetMedleys() ([]MedleyEntry, error) { + idxRaw, err := c.readAll(MedleysIndexFileName) + if err != nil { + return nil, fmt.Errorf("read %s: %w", MedleysIndexFileName, err) + } + + var idx []MedleyEntry + if err := json.Unmarshal(idxRaw, &idx); err != nil { + return nil, fmt.Errorf("unmarshal %s: %w", MedleysIndexFileName, err) + } + + return idx, nil +} + func (c *B2Client) Scan(prefix string) ([]*Page, error) { filePaths := []*Page{} @@ -70,17 +85,15 @@ func (c *B2Client) Scan(prefix string) ([]*Page, error) { return nil, fmt.Errorf("failed to parse published time metadata field: %w", err) } - linkParts := strings.Split(obj.Name(), "/") - nameParts := strings.Split(linkParts[len(linkParts)-1], ".") - fileName := strings.Join(nameParts[:len(linkParts)-1], ".") - + link := obj.Name() + fileName := link[strings.LastIndex(link, "/")+1 : strings.LastIndex(link, ".")] tags := strings.Split(attrs.Info["tags"], ",") slices.Sort(tags) - lang, _ := strings.CutPrefix(linkParts[0], c.prefix) + lang, _ := strings.CutPrefix(link[0:strings.Index(link, "/")], c.prefix) filePaths = append(filePaths, &Page{ - Link: obj.Name(), + Link: link, FileName: fileName, Lang: lang, ModifiedTime: attrs.LastModified, @@ -104,7 +117,11 @@ func (c *B2Client) Scan(prefix string) ([]*Page, error) { } func (c *B2Client) ReadAll(path string) ([]byte, error) { - obj := c.bucket.Object(c.prefix + path) + return c.readAll(c.prefix + path) +} + +func (c *B2Client) readAll(path string) ([]byte, error) { + obj := c.bucket.Object(path) if obj == nil { return nil, fmt.Errorf("failed to reference object in B2 bucket") } diff --git a/internal/blog/client.go b/internal/blog/client.go index 56bbee4..53eb0fd 100644 --- a/internal/blog/client.go +++ b/internal/blog/client.go @@ -17,6 +17,7 @@ type Page struct { type Client interface { Scan(prefix string) ([]*Page, error) + GetMedleys() ([]MedleyEntry, error) ReadAll(path string) ([]byte, error) ReadFrontmatter(path string) (metadata *frontmatter.Metadata, markdown []byte, err error) } diff --git a/internal/blog/index.go b/internal/blog/index.go index 861a90e..a09b59e 100644 --- a/internal/blog/index.go +++ b/internal/blog/index.go @@ -1,10 +1,17 @@ package blog -import "time" +import ( + "encoding/json" + "fmt" + "time" + + "github.com/SayaAndy/saya-today-web/internal/frontmatter" +) const IndexFileName = "index.json" +const MedleysIndexFileName = "medleys.json" -const IndexSchemaVersion = 1 +const IndexSchemaVersion = 2 type IndexEntry struct { Link string `json:"link"` @@ -20,13 +27,77 @@ type IndexEntry struct { MedleyPart int `json:"medleyPart,omitempty"` } -type IndexCategory struct { +func (e IndexEntry) Metadata() *frontmatter.Metadata { + return &frontmatter.Metadata{ + Title: e.Title, + ShortDescription: e.ShortDescription, + ActionDate: e.ActionDate, + PublishedTime: e.PublishedTime, + Thumbnail: e.Thumbnail, + Tags: e.Tags, + Geolocation: e.Geolocation, + Medley: e.Medley, + MedleyPart: e.MedleyPart, + } +} + +type IndexV2Category struct { + GeneratedAt time.Time `json:"generatedAt"` + Pages map[string]IndexEntry `json:"pages"` +} + +type IndexV1Category struct { GeneratedAt time.Time `json:"generatedAt"` Pages []IndexEntry `json:"pages"` } type Index struct { - SchemaVersion int `json:"schemaVersion"` - GeneratedAt time.Time `json:"generatedAt"` - Categories map[string]IndexCategory `json:"categories"` + SchemaVersion int `json:"schemaVersion"` + GeneratedAt time.Time `json:"generatedAt"` + Categories any `json:"categories"` +} + +func (idx *Index) UnmarshalJSON(data []byte) error { + var tmp struct { + SchemaVersion int `json:"schemaVersion"` + GeneratedAt time.Time `json:"generatedAt"` + Categories json.RawMessage `json:"categories"` + } + + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + + idx.SchemaVersion = tmp.SchemaVersion + idx.GeneratedAt = tmp.GeneratedAt + + switch tmp.SchemaVersion { + case 1: + var categories map[string]*IndexV1Category + if err := json.Unmarshal(tmp.Categories, &categories); err != nil { + return fmt.Errorf("unmarshal map[string]*IndexV1Category: %w", err) + } + idx.Categories = &categories + case 2: + var categories map[string]*IndexV2Category + if err := json.Unmarshal(tmp.Categories, &categories); err != nil { + return fmt.Errorf("unmarshal map[string]*IndexV2Category: %w", err) + } + idx.Categories = &categories + default: + return fmt.Errorf("unsupported index version: %d", tmp.SchemaVersion) + } + + return nil +} + +type MedleyEntry struct { + Codename string `json:"codename"` + Localnames map[string]string `json:"localnames"` + Content []string `json:"content"` +} + +type MedleyPageEntry struct { + Codename string `json:"codename"` + Position int `json:"position"` } diff --git a/internal/blog/s3.go b/internal/blog/s3.go index bbd5238..3c1a932 100644 --- a/internal/blog/s3.go +++ b/internal/blog/s3.go @@ -1,29 +1,22 @@ package blog import ( + "bytes" "context" "encoding/json" - "errors" "fmt" "io" - "log/slog" - "net/url" - "slices" "strings" - "sync" - "time" "github.com/SayaAndy/saya-today-web/config" "github.com/SayaAndy/saya-today-web/internal/frontmatter" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/aws/aws-sdk-go-v2/aws" awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" - s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" ) -const s3ScanConcurrency = 32 - type S3Client struct { prefix string bucketName string @@ -66,39 +59,38 @@ func NewS3Client(cfg *config.StorageConfig) (Client, error) { return &S3Client{s3cfg.Prefix, s3cfg.BucketName, s3cl}, nil } -func (c *S3Client) Scan(prefix string) ([]*Page, error) { - pages, err := c.scanFromIndex(prefix) - if err == nil { - return pages, nil +func (c *S3Client) GetMedleys() ([]MedleyEntry, error) { + idxRaw, err := c.readAll(MedleysIndexFileName) + if err != nil { + return nil, fmt.Errorf("read %s: %w", MedleysIndexFileName, err) } - var nsk *s3types.NoSuchKey - if !errors.As(err, &nsk) { - return nil, err + var idx []MedleyEntry + if err := json.Unmarshal(idxRaw, &idx); err != nil { + return nil, fmt.Errorf("unmarshal %s: %w", MedleysIndexFileName, err) } - slog.Warn("index.json missing, falling back to listing", slog.String("prefix", c.prefix)) - return c.scanByListing(prefix) + return idx, nil } -func (c *S3Client) scanFromIndex(prefix string) ([]*Page, error) { +func (c *S3Client) Scan(prefix string) ([]*Page, error) { out, err := c.s3cl.GetObject(context.Background(), &s3.GetObjectInput{ Bucket: aws.String(c.bucketName), Key: aws.String(IndexFileName), }) if err != nil { - return nil, fmt.Errorf("get index.json: %w", err) + return nil, fmt.Errorf("get %s: %w", IndexFileName, err) } defer out.Body.Close() raw, err := io.ReadAll(out.Body) if err != nil { - return nil, fmt.Errorf("read index.json: %w", err) + return nil, fmt.Errorf("read %s: %w", IndexFileName, err) } var idx Index if err := json.Unmarshal(raw, &idx); err != nil { - return nil, fmt.Errorf("unmarshal index.json: %w", err) + return nil, fmt.Errorf("unmarshal %s: %w", IndexFileName, err) } wantLang := "" @@ -108,160 +100,93 @@ func (c *S3Client) scanFromIndex(prefix string) ([]*Page, error) { fullPrefix := c.prefix + prefix pages := make([]*Page, 0) - for catKey, cat := range idx.Categories { - lang, ok := strings.CutPrefix(catKey, c.prefix) - if !ok { - continue - } - if wantLang != "" && wantLang != lang { - continue - } - for _, e := range cat.Pages { - if !strings.HasPrefix(e.Link, fullPrefix) { + + switch idx.SchemaVersion { + case 1: + for catKey, cat := range *idx.Categories.(*map[string]*IndexV1Category) { + lang, ok := strings.CutPrefix(catKey, c.prefix) + if !ok { continue } - linkParts := strings.Split(e.Link, "/") - nameParts := strings.Split(linkParts[len(linkParts)-1], ".") - fileName := strings.Join(nameParts[:len(nameParts)-1], ".") - pages = append(pages, &Page{ - Link: e.Link, - FileName: fileName, - Lang: lang, - ModifiedTime: e.ModifiedTime, - Metadata: &frontmatter.Metadata{ - Title: e.Title, - ShortDescription: e.ShortDescription, - ActionDate: e.ActionDate, - PublishedTime: e.PublishedTime, - Thumbnail: e.Thumbnail, - Tags: e.Tags, - Geolocation: e.Geolocation, - Medley: e.Medley, - MedleyPart: e.MedleyPart, - }, - }) - } - } - return pages, nil -} - -func (c *S3Client) scanByListing(prefix string) ([]*Page, error) { - fullPrefix := c.prefix + prefix - - type candidate struct { - key string - lastModified time.Time - } - var candidates []candidate - - paginator := s3.NewListObjectsV2Paginator(c.s3cl, &s3.ListObjectsV2Input{ - Bucket: aws.String(c.bucketName), - Prefix: aws.String(fullPrefix), - }) - for paginator.HasMorePages() { - output, err := paginator.NextPage(context.Background()) - if err != nil { - return nil, fmt.Errorf("list S3 objects: %w", err) - } - for _, obj := range output.Contents { - key := aws.ToString(obj.Key) - if !strings.HasSuffix(key, ".md") { + if wantLang != "" && wantLang != lang { continue } - candidates = append(candidates, candidate{key, aws.ToTime(obj.LastModified)}) - } - } - - pages := make([]*Page, len(candidates)) - sem := make(chan struct{}, s3ScanConcurrency) - var wg sync.WaitGroup - var firstErr error - var errMu sync.Mutex - - for i, cand := range candidates { - sem <- struct{}{} - wg.Go(func() { - defer func() { <-sem }() - - head, err := c.s3cl.HeadObject(context.Background(), &s3.HeadObjectInput{ - Bucket: aws.String(c.bucketName), - Key: aws.String(cand.key), - }) - if err != nil { - errMu.Lock() - if firstErr == nil { - firstErr = fmt.Errorf("head S3 object %s: %w", cand.key, err) + for _, e := range cat.Pages { + if !strings.HasPrefix(e.Link, fullPrefix) { + continue } - errMu.Unlock() - return + fileName := e.Link[strings.LastIndex(e.Link, "/")+1 : strings.LastIndex(e.Link, ".")] + pages = append(pages, &Page{ + Link: e.Link, + FileName: fileName, + Lang: lang, + ModifiedTime: e.ModifiedTime, + Metadata: &frontmatter.Metadata{ + Title: e.Title, + ShortDescription: e.ShortDescription, + ActionDate: e.ActionDate, + PublishedTime: e.PublishedTime, + Thumbnail: e.Thumbnail, + Tags: e.Tags, + Geolocation: e.Geolocation, + Medley: e.Medley, + MedleyPart: e.MedleyPart, + }, + }) } - - if head.ContentType == nil || !strings.Contains(*head.ContentType, "text/markdown") { - return + } + case 2: + for catKey, cat := range *idx.Categories.(*map[string]*IndexV2Category) { + lang, ok := strings.CutPrefix(catKey, c.prefix) + if !ok { + continue } - meta := head.Metadata - if meta["title"] == "" { - return + if wantLang != "" && wantLang != lang { + continue } - - publishedTime, err := time.Parse(time.RFC3339, meta["published-time"]) - if err != nil { - errMu.Lock() - if firstErr == nil { - firstErr = fmt.Errorf("failed to parse published time metadata field: %w", err) + for codename, e := range cat.Pages { + if !strings.HasPrefix(e.Link, fullPrefix) { + continue } - errMu.Unlock() - return + pages = append(pages, &Page{ + Link: e.Link, + FileName: codename, + Lang: lang, + ModifiedTime: e.ModifiedTime, + Metadata: &frontmatter.Metadata{ + Title: e.Title, + ShortDescription: e.ShortDescription, + ActionDate: e.ActionDate, + PublishedTime: e.PublishedTime, + Thumbnail: e.Thumbnail, + Tags: e.Tags, + Geolocation: e.Geolocation, + Medley: e.Medley, + MedleyPart: e.MedleyPart, + }, + }) } - - linkParts := strings.Split(cand.key, "/") - nameParts := strings.Split(linkParts[len(linkParts)-1], ".") - fileName := strings.Join(nameParts[:len(nameParts)-1], ".") - lang, _ := strings.CutPrefix(linkParts[0], c.prefix) - - tags := strings.Split(meta["tags"], ",") - slices.Sort(tags) - - title, _ := url.QueryUnescape(meta["title"]) - shortDescription, _ := url.QueryUnescape(meta["short-description"]) - thumbnail, _ := url.QueryUnescape(meta["thumbnail"]) - - pages[i] = &Page{ - Link: cand.key, - FileName: fileName, - Lang: lang, - ModifiedTime: cand.lastModified, - Metadata: &frontmatter.Metadata{ - Title: title, - ShortDescription: shortDescription, - ActionDate: meta["action-date"], - PublishedTime: publishedTime, - Thumbnail: thumbnail, - Tags: tags, - Geolocation: meta["geolocation"], - }, - } - }) - } - wg.Wait() - - if firstErr != nil { - return nil, firstErr + } } - out := pages[:0] - for _, p := range pages { - if p != nil { - out = append(out, p) + medleys, _ := c.GetMedleys() + for _, medley := range medleys { + for locale, localname := range medley.Localnames { + l10n.T.SetPath(localname, true, locale, "Medleys", medley.Codename) } } - return out, nil + + return pages, nil } func (c *S3Client) ReadAll(path string) ([]byte, error) { + return c.readAll(c.prefix + path) +} + +func (c *S3Client) readAll(path string) ([]byte, error) { output, err := c.s3cl.GetObject(context.Background(), &s3.GetObjectInput{ Bucket: aws.String(c.bucketName), - Key: aws.String(c.prefix + path), + Key: aws.String(path), }) if err != nil { return nil, fmt.Errorf("get S3 object: %w", err) @@ -277,10 +202,40 @@ func (c *S3Client) ReadAll(path string) ([]byte, error) { } func (c *S3Client) ReadFrontmatter(path string) (metadata *frontmatter.Metadata, markdown []byte, err error) { + idxRaw, err := c.readAll(IndexFileName) + if err != nil { + return nil, nil, fmt.Errorf("read %s: %w", IndexFileName, err) + } + + var idx Index + if err := json.Unmarshal(idxRaw, &idx); err != nil { + return nil, nil, fmt.Errorf("unmarshal %s: %w", IndexFileName, err) + } + contentBytes, err := c.ReadAll(path) if err != nil { return nil, nil, fmt.Errorf("failed to read file for frontmatter parsing: %w", err) } + switch idx.SchemaVersion { + case 1: + return frontmatter.ParseFrontmatter(contentBytes) + case 2: + fullPath := c.prefix + path + page := (*idx.Categories.(*map[string]*IndexV2Category))[fullPath[:strings.LastIndex(fullPath, "/")]].Pages[fullPath[strings.LastIndex(fullPath, "/")+1:strings.LastIndex(fullPath, ".")]] + metadata = page.Metadata() + + if !bytes.HasPrefix(contentBytes, []byte("---\n")) { + return metadata, contentBytes, nil + } + + end := bytes.Index(contentBytes[4:], []byte("\n---\n")) + if end == -1 { + return metadata, contentBytes, nil + } + + return metadata, contentBytes[end+9:], nil + } + return frontmatter.ParseFrontmatter(contentBytes) } diff --git a/internal/glightbox/html_renderer.go b/internal/glightbox/html_renderer.go index b2cd855..1bb606b 100644 --- a/internal/glightbox/html_renderer.go +++ b/internal/glightbox/html_renderer.go @@ -151,12 +151,12 @@ func (r *GLightboxHTMLRenderer) renderGLightbox(w util.BufWriter, source []byte, w.WriteString(strings.ReplaceAll(` <div class="items-center flex flex-col"> - <hr class="border-t-3 border-dotted border-main-hard mt-1 mb-2 w-[80%] ml-auto mr-auto"> + <hr class="border-t-[0.375rem] border-dotted border-main-hard my-2 w-24 ml-auto mr-auto"> <div class="grid masonry-grid-{id}"> <div class="grid-sizer grid-sizer-{id}"></div> `+strings.Join(elements, "\n")+` </div> - <hr class="border-t-3 border-dotted border-main-hard mt-1 mb-2 w-[80%] ml-auto mr-auto"> + <hr class="border-t-[0.375rem] border-dotted border-main-hard my-2 w-24 ml-auto mr-auto"> </div> <script> diff --git a/internal/mailer/mailer.go b/internal/mailer/mailer.go index ffa7b30..33db4f7 100644 --- a/internal/mailer/mailer.go +++ b/internal/mailer/mailer.go @@ -16,7 +16,7 @@ import ( "github.com/SayaAndy/saya-today-web/internal/blog" "github.com/SayaAndy/saya-today-web/internal/templatemanager" - "github.com/SayaAndy/saya-today-web/locale" + "github.com/SayaAndy/saya-today-web/l10n" "github.com/dgraph-io/ristretto/v2" "github.com/gofiber/fiber/v2" "github.com/wneessen/go-mail" @@ -43,8 +43,6 @@ type Mailer struct { hashMap map[string][]byte hashMapMutex sync.RWMutex - - l map[string]*locale.LocaleConfig } type SubscriptionType int @@ -55,7 +53,7 @@ const ( Specific ) -func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string, mailAddress string, username string, password string, salt []byte, localization map[string]*locale.LocaleConfig) (*Mailer, error) { +func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string, mailAddress string, username string, password string, salt []byte) (*Mailer, error) { verificationCodes, err := ristretto.NewCache(&ristretto.Config[uint64, string]{ NumCounters: 10000, MaxCost: 1 << 20, // 1 MB @@ -111,7 +109,7 @@ func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string End time.Time CodeExpiry time.Time }, 0), - l: localization}, nil + }, nil } func (m *Mailer) GetHash(id string) []byte { @@ -261,10 +259,9 @@ func (m *Mailer) SendVerificationCode(userId string, address string, lang string verificationInfo := fmt.Sprintf("%s.%s", base64.RawStdEncoding.EncodeToString([]byte(userId)), base64.RawStdEncoding.EncodeToString([]byte(address))) m.verificationCodes.Set(verificationCode, verificationInfo, int64(len(verificationInfo)+8)) - message.Subject(m.l[lang].Mail.VerifyEmail.Subject) + message.Subject(l10n.T.GetPath(lang, "Mail", "VerifyEmail", "Subject").(string)) msg, err := m.tm.Render("verify-email", fiber.Map{ - "L": m.l[lang], "Lang": lang, "VerificationCode": fmt.Sprintf("%X", verificationCode), "ClientHost": m.clientHost, @@ -499,11 +496,10 @@ rowLoop: rand.Read(unsubscribeCodeBytes) unsubscribeCode := binary.LittleEndian.Uint64(unsubscribeCodeBytes) - unsubscribeFooter := strings.Replace(m.l[post.Lang].Mail.UnsubscribeFooter, "{}", fmt.Sprintf(`<a style="color: #273de1 !important;" href="https://%s/%s/user/unsubscribe?code=%X">`, m.clientHost, post.Lang, unsubscribeCode), 1) + unsubscribeFooter := strings.Replace(l10n.T.GetPath(post.Lang, "Mail", "UnsubscribeFooter").(string), "{}", fmt.Sprintf(`<a style="color: #273de1 !important;" href="https://%s/%s/user/unsubscribe?code=%X">`, m.clientHost, post.Lang, unsubscribeCode), 1) unsubscribeFooter = strings.Replace(unsubscribeFooter, "{/}", "</a>", 1) msgBody, err := m.tm.Render("new-post", fiber.Map{ - "L": m.l[post.Lang], "Lang": post.Lang, "Post": post, "ClientHost": m.clientHost, @@ -529,7 +525,7 @@ rowLoop: message.SetMessageID() message.SetDate() message.SetBulk() - message.Subject(m.l[post.Lang].Mail.NewPost.Subject) + message.Subject(l10n.T.GetPath(post.Lang, "Mail", "NewPost", "Subject").(string)) message.SetBodyString(mail.TypeTextHTML, string(msgBody)) m.unsubscribeCodes.Set(unsubscribeCode, user.userId, 40) diff --git a/internal/router/handlers/api-v1-blog-search.go b/internal/router/handlers/api-v1-blog-search.go index 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 Binary files differnew file mode 100644 index 0000000..99d5fbf --- /dev/null +++ b/static/favicon.ico diff --git a/static/input.css b/static/input.css index b9fb166..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; @@ -72,52 +82,42 @@ html { background-size: --value([bg-size]), 128px; } -@utility faw-full { - --fa-width: 100%; -} - -@utility faw-* { - --fa-width: calc(--value(integer) * var(--spacing)); -} - body { scrollbar-width: thin; 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); @@ -141,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); @@ -165,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"); @@ -175,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; @@ -244,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); @@ -306,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, @@ -322,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; } @@ -356,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); @@ -367,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); @@ -405,10 +530,6 @@ form.crossable.htmx-request input { border-style: inset; } -.border-outset { - border-style: outset; -} - .bg-sidebar { background-image: var(--squares-and-triangles-background); background-repeat: var(--squares-and-triangles-background-repeat); @@ -418,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); } @@ -438,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; @@ -473,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; } @@ -499,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%; @@ -508,66 +621,6 @@ form.crossable.htmx-request input { border-radius: 50%; } -.themed-button::before { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient( - 135deg, - var(--color-main-light-hard), - var(--color-main-dark-hard) - ); - opacity: 0.2; - z-index: 10; - content: ""; -} - -.themed-button:active::before { - opacity: 0.4; -} - -.themed-button:hover:not(:active)::before { - opacity: 0; -} - -.themed-button { - border-top: var(--border-size) solid var(--color-main-light-hard); - border-left: var(--border-size) solid var(--color-main-light-hard); - border-bottom: var(--border-size) solid var(--color-main-dark-hard); - border-right: var(--border-size) solid var(--color-main-dark-hard); - position: relative; - --border-size: 0.5rem; -} - -.themed-button:active, -.themed-button.active { - border-top: var(--border-size) solid var(--color-main-dark-hard); - border-left: var(--border-size) solid var(--color-main-dark-hard); - border-bottom: var(--border-size) solid var(--color-main-light-hard); - border-right: var(--border-size) solid var(--color-main-light-hard); -} - -.themed-button:active > *, -.themed-button.active > * { - transform: translateY(calc(var(--border-size) / 2)); -} - -.themed-button.themed-button-sidebar { - --border-size: 0.25rem; - background-image: var(--squares-and-triangles-background); - background-repeat: var(--squares-and-triangles-background-repeat); - background-size: calc( - var(--squares-and-triangles-background-width-coef) * 0.9rem - ) - calc(var(--squares-and-triangles-background-height-coef) * 0.9rem); -} - -.themed-button.themed-button-like { - --border-size: 0.2rem; -} - .accent-button, .accent-button.active:active { --tw-duration: 1500ms; @@ -584,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; @@ -597,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, @@ -612,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; } @@ -668,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; } @@ -691,6 +758,10 @@ form.crossable.htmx-request input { opacity: 0; } +.grid-item:hover picture { + mask-image: none; +} + .grid-item-2x { width: 20rem; } @@ -790,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 { @@ -930,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 848c786..06ef625 100644 --- a/views/layouts/general-page.html +++ b/views/layouts/general-page.html @@ -13,34 +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/font-awesome/7.2.0/css/fontawesome.min.css" + href="{{ .StaticStorage.BaseUrl }}/libs/leaflet/1.9.4/leaflet.css" /> <link rel="stylesheet" - href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/brands.min.css" + href="{{ .StaticStorage.BaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.css" /> <link rel="stylesheet" - href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/solid.min.css" + href="{{ .StaticStorage.BaseUrl }}/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css" /> <link rel="stylesheet" - href="{{ .StaticStorageBaseUrl }}/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/glightbox/3.3.0/css/glightbox.min.css" /> <link rel="stylesheet" href="/output.css" /> <link @@ -55,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 @@ -114,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> @@ -154,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 () { @@ -196,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]; - const vminValue = (smallerDimension / 100) * percent; + // Step 2: funnel through canvas to force sRGB + _ctx.fillStyle = '#000'; + _ctx.fillStyle = computed; + const s = _ctx.fillStyle; // "#rrggbb" or "rgba(...)" - return vminValue; + if (s.startsWith('#')) { + return [ + parseInt(s.slice(1,3),16)/255, + parseInt(s.slice(3,5),16)/255, + parseInt(s.slice(5,7),16)/255, + ]; + } + const m = s.match(/[\d.]+/g); + return [+m[0]/255, +m[1]/255, +m[2]/255]; + } + + function oklchMix(c1, c2, t) { + // Use the browser's color-mix() to interpolate in oklch + return `color-mix(in oklch, ${c1} ${(1-t)*100}%, ${c2})`; + } + + function makeMultitoneOklch(id, stops, ...vars) { + if (vars.length < 2) { + console.error('Less than 2 colors set in input'); + return; + } + + const root = getComputedStyle(document.body); + const rs = [], gs = [], bs = []; + + for (let i = 0; i < vars.length - 1; i++) { + const c1 = root.getPropertyValue(vars[i]).trim(); + const c2 = root.getPropertyValue(vars[i+1]).trim(); + + if (!c1) { + console.error(`CSS variable ${vars[i]} not found`); + return; + } + if (!c2) { + console.error(`CSS variable ${vars[i+1]} not found`); + return; + } + + for (let i = 0; i < stops; i++) { + const t = i / (stops - 1); + const [r, g, b] = resolveColor(oklchMix(c1, c2, t)); + rs.push(r.toFixed(4)); + gs.push(g.toFixed(4)); + bs.push(b.toFixed(4)); + } + } + + let svg = document.getElementById(id + '-svg'); + if (svg) svg.remove(); + document.body.insertAdjacentHTML('beforeend', ` + <svg id="${id}-svg" width="0" height="0" style="position:absolute"> + <filter id="${id}"> + <feColorMatrix type="matrix" values=" + 0.299 0.587 0.114 0 0 + 0.299 0.587 0.114 0 0 + 0.299 0.587 0.114 0 0 + 0 0 0 1 0"/> + <feComponentTransfer color-interpolation-filters="sRGB"> + <feFuncR type="table" tableValues="${rs.join(' ')}"/> + <feFuncG type="table" tableValues="${gs.join(' ')}"/> + <feFuncB type="table" tableValues="${bs.join(' ')}"/> + </feComponentTransfer> + </filter> + </svg>`); } function switchThemeDecor(theme) { @@ -219,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; @@ -238,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`; @@ -253,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", @@ -262,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; } @@ -312,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 }}" @@ -487,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 }}" @@ -513,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 }}" @@ -547,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 }}" @@ -624,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) { @@ -632,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; @@ -653,7 +714,7 @@ const clipPath = createSliceClipPath( startAngle, endAngle, - themeWheelRadius, + convertRemToPixels(6), ); icon.style.clipPath = clipPath; }); @@ -688,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">>>></a></li> - <li>{{ .L.Mail.VerifyEmail.InputCode }} <span class="outlier">{{ .VerificationCode }}</span><span></li> + <li>{{ l $.Lang "Mail" "VerifyEmail" "GotoLink" }} <a href="https://{{ .ClientHost }}/{{ .Lang }}/user?email_code={{ .VerificationCode }}" class="outlier">>>></a></li> + <li>{{ l $.Lang "Mail" "VerifyEmail" "InputCode" }} <span class="outlier">{{ .VerificationCode }}</span><span></li> </ol> -<p>{{ .L.Mail.VerifyEmail.IfRandom }}</p> +<p>{{ l $.Lang "Mail" "VerifyEmail" "IfRandom" }}</p> <a href="https://{{ .ClientHost }}" class="outlier">{{ .ClientHost }}</a> {{ end }}
\ No newline at end of file diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html index 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: © <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 34fb870..4d1853f 100644 --- a/views/pages/global-map.html +++ b/views/pages/global-map.html @@ -3,34 +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" - /> - <link - rel="stylesheet" - href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/fontawesome.min.css" - /> - <link - rel="stylesheet" - href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/brands.min.css" - /> - <link - rel="stylesheet" - href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/solid.min.css" + href="{{ .StaticStorage.BaseUrl }}/fonts/fonts.css" /> {{ block "header" . }}{{ end }} <link rel="stylesheet" href="/output.css" /> @@ -47,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 @@ -222,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) { @@ -257,6 +248,7 @@ function positionThemeIcons() { const sliceAngle = 360 / themeIcons.length; + const themeWheelRadius = convertRemToPixels(6); themeIcons.forEach((icon, index) => { const startAngle = index * sliceAngle; @@ -300,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; @@ -373,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: © <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 b14b08c..d494a08 100644 --- a/views/pages/home-page.html +++ b/views/pages/home-page.html @@ -1,22 +1,39 @@ {{ 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="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"> +<!-- Streamline Icons: https://streamlinehq.com --> +<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true"> + <symbol id="icon-discord" viewBox="0 0 24 24"><g> + <path d="M23.8 8.8A10.45 10.45 0 0 0 20 3 13 13 0 0 0 13.59 0.4c-0.14 0 -6 -1 -9.79 2.77C1.67 5.32 0.56 8.65 0.15 11.77A15.3 15.3 0 0 0 0 14.71a8.77 8.77 0 0 0 3.46 6.44 13.74 13.74 0 0 0 4 2 13.55 13.55 0 0 0 1.91 0.52 5.32 5.32 0 0 0 1.41 0.11 16.63 16.63 0 0 0 6.12 -1.57A12.19 12.19 0 0 0 21.75 18a14.6 14.6 0 0 0 1.87 -4.4 10.74 10.74 0 0 0 0.18 -4.8Zm-1.19 4.56a13.31 13.31 0 0 1 -1.77 4.05 11 11 0 0 1 -4.43 3.73 15.5 15.5 0 0 1 -5.71 1.47 4.07 4.07 0 0 1 -1.13 -0.09 13.63 13.63 0 0 1 -1.76 -0.47 13.06 13.06 0 0 1 -3.68 -1.75 7.87 7.87 0 0 1 -2.5 -3.08A7.75 7.75 0 0 1 1 14.63a14.11 14.11 0 0 1 0 -2.75c0.32 -3 1.29 -6.15 3.27 -8.23 0.1 -0.11 2.84 -3.36 9.2 -2.5a12.09 12.09 0 0 1 5.95 2.53A9.47 9.47 0 0 1 22.86 9a9.56 9.56 0 0 1 -0.25 4.36Z" fill="var(--color-accent-deep)" fill-rule="evenodd" stroke-width="1"></path> + <path d="M20.27 11.06c-0.33 -0.59 -0.74 -1.14 -1.08 -1.69a16.78 16.78 0 0 0 -1.4 -2 6 6 0 0 0 -1.27 -1.17A6.34 6.34 0 0 0 14 5.36a31.12 31.12 0 0 0 -3.83 -0.08 12.94 12.94 0 0 0 -3 0.18c-1.41 0.35 -0.76 0.32 -2 2.72 -0.57 0.89 -1.02 1.82 -1.49 2.72a11.42 11.42 0 0 0 -0.77 1.39 1.65 1.65 0 0 0 -0.1 0.82 2.77 2.77 0 0 0 0.45 1.15 8.92 8.92 0 0 0 1.08 1.27l1.09 1.29a3.51 3.51 0 0 0 0.29 0.37 0.47 0.47 0 0 0 0.42 0.15 2 2 0 0 0 1.16 -0.62 12.55 12.55 0 0 0 1.49 -2c0.06 -0.08 0.36 -0.36 0.37 -0.52l0.93 0.11 2.91 0c1.31 0.09 0.41 -0.77 1.77 1.78a9.19 9.19 0 0 0 0.52 0.85 5.71 5.71 0 0 0 0.53 0.81 0.67 0.67 0 0 0 0.39 0.19 1.49 1.49 0 0 0 0.74 -0.11 5 5 0 0 0 1 -0.67 11.91 11.91 0 0 0 1.52 -1.42 13 13 0 0 0 1.28 -1.64 1.47 1.47 0 0 0 0.25 -0.77 3.09 3.09 0 0 0 -0.16 -0.94 6.06 6.06 0 0 0 -0.57 -1.33Zm-0.38 2.43A12.53 12.53 0 0 1 18.7 15a11.92 11.92 0 0 1 -1.4 1.28 5 5 0 0 1 -0.57 0.42l-0.14 0.08a2.55 2.55 0 0 0 -0.38 -0.48 6.07 6.07 0 0 1 -0.45 -0.74c-0.35 -0.64 -0.64 -1.32 -1 -1.95a0.73 0.73 0 0 0 -0.48 -0.31 11.39 11.39 0 0 0 -1.22 -0.09h-2.95c-0.21 0 -0.69 -0.1 -1 -0.1a1.13 1.13 0 0 0 -0.45 0.08 0.88 0.88 0 0 0 -0.34 0.29c-0.14 0.19 -0.28 0.53 -0.35 0.63a19.39 19.39 0 0 1 -1.11 1.72 1.93 1.93 0 0 1 -0.59 0.53l-0.11 -0.09L5 14.93a10 10 0 0 1 -1 -1.12 1.94 1.94 0 0 1 -0.41 -0.81 1.11 1.11 0 0 1 0.11 -0.64c0.17 -0.4 0.45 -0.79 0.62 -1.14 0.45 -0.91 0.87 -1.84 1.33 -2.75 1.29 -2.53 1.18 -2.29 2 -2.41a14.57 14.57 0 0 1 2.46 0 28 28 0 0 1 3.71 0.16A5.33 5.33 0 0 1 16 7a5.08 5.08 0 0 1 1 1 16.3 16.3 0 0 1 1.27 1.93c0.33 0.55 0.72 1.11 1 1.69a5.1 5.1 0 0 1 0.48 1.1 2.36 2.36 0 0 1 0.1 0.53 0.43 0.43 0 0 1 0.04 0.24Z" fill="var(--color-main)" fill-rule="evenodd" stroke-width="1"></path> + <path d="M7.15 8.26a1.44 1.44 0 0 0 -0.51 0.6 1.56 1.56 0 0 0 0.14 1.48A1.49 1.49 0 0 0 8.2 11a1.77 1.77 0 0 0 1.38 -1.42c0.27 -1.69 -1.45 -1.94 -2.43 -1.32Zm1.53 1.11a0.71 0.71 0 0 1 -0.6 0.53 0.44 0.44 0 0 1 -0.41 -0.2 0.65 0.65 0 0 1 -0.19 -0.54 0.68 0.68 0 0 1 0.21 -0.37c0.75 -0.55 1.17 0.02 0.99 0.58Z" fill="var(--color-main)" fill-rule="evenodd" stroke-width="1"></path> + <path d="M15 8a1.38 1.38 0 0 0 -0.77 -0.21 2.68 2.68 0 0 0 -1 0.25 3.12 3.12 0 0 0 -0.72 1 1.65 1.65 0 0 0 0.55 1.53 1.6 1.6 0 0 0 1.69 0.38 1.71 1.71 0 0 0 0.89 -1.47A1.65 1.65 0 0 0 15 8Zm-0.37 1.4a0.62 0.62 0 0 1 -0.3 0.53 0.36 0.36 0 0 1 -0.27 0 1.28 1.28 0 0 1 -0.4 -0.25c-0.15 -0.14 -0.3 -0.29 -0.28 -0.47a0.45 0.45 0 0 1 0.17 -0.34 2.73 2.73 0 0 1 0.53 -0.29 0.57 0.57 0 0 1 0.39 0.14 0.74 0.74 0 0 1 0.2 0.69Z" fill="var(--color-main)" fill-rule="evenodd" stroke-width="1"></path> + </g></symbol> + <symbol id="icon-telegram" viewBox="0 0 24 24"><g> + <path d="M23.8 9.74a10.54 10.54 0 0 0 -3.1 -5.5c-1.25 -1.19 -7.2 -6 -13.52 -3.12a12 12 0 0 0 -4.56 18.49 12.24 12.24 0 0 0 2.66 2.53 9.1 9.1 0 0 0 3.43 1.45c3.79 0.74 7.9 -0.75 10.9 -3.38s5.03 -6.67 4.19 -10.47Zm-4.87 9.68c-2.78 2.38 -6.54 3.78 -10 3.09a8.18 8.18 0 0 1 -3 -1.27 11.8 11.8 0 0 1 -2.44 -2.3 11 11 0 0 1 -2.25 -9.53 11.25 11.25 0 0 1 6.24 -7.66C8.17 1.42 12.94 -0.79 19 3.84a10 10 0 0 1 3.91 6.08c0.74 3.49 -1.1 7.08 -3.98 9.5Z" fill="var(--color-accent-deep)" fill-rule="evenodd" stroke-width="1"></path> + <path d="M17.5 6.52a8 8 0 0 0 -2.14 0.35l-0.78 0.13c-1.67 0.31 -3.36 0.63 -5 1 -1.11 0.28 -2.21 0.59 -3.29 1a5.12 5.12 0 0 0 -0.65 0.25 4.6 4.6 0 0 0 -0.64 0.34l-0.61 0.4 -0.39 0.34a0.93 0.93 0 0 0 -0.24 1.09C4 12 5 12.34 5.49 12.48l0.53 0.17 0.15 0 0 0.81 0 1 0.08 0.74 0.06 0.53A8.91 8.91 0 0 0 6.53 17a1.4 1.4 0 0 0 0.42 0.71H7a0.3 0.3 0 0 0 0.34 0.22A13.32 13.32 0 0 0 9 17.8a5.35 5.35 0 0 0 0.69 -0.17c0.23 -0.07 0.45 -0.15 0.67 -0.24 0.49 -0.19 0.94 -0.41 1.4 -0.63l0.58 0.56 0.66 0.47 0.52 0.23a1.08 1.08 0 0 0 1 -0.19 4.55 4.55 0 0 0 0.92 -1.51l1.4 -2.92c0.38 -0.76 0.78 -1.51 1.15 -2.28 0.25 -0.52 0.48 -1 0.68 -1.59A3.92 3.92 0 0 0 19 8.2a1.73 1.73 0 0 0 -0.31 -1.08 1.51 1.51 0 0 0 -1.19 -0.6ZM6.88 13.44l-0.07 -0.58c0.46 0 0.49 -0.43 5.26 -1.84l0.62 -0.16A16.71 16.71 0 0 1 11.34 12c-0.74 0.57 -1.51 1.12 -2.26 1.69l-0.46 0.52 -0.69 1.49s-0.22 0.44 -0.4 0.82c-0.07 -0.36 -0.1 -0.74 -0.14 -0.91l-0.12 -0.54 -0.19 -0.7Zm3.14 3c-0.23 0.05 -0.46 0.1 -0.68 0.17l-0.66 0.21 -0.27 0.12c0.07 -0.25 0.26 -0.76 0.31 -0.93 0.12 -0.36 0.35 -1.18 0.48 -1.5a0.3 0.3 0 0 0 0 -0.23s0.07 0 0.11 0l1.81 2c-0.35 -0.02 -0.72 0.04 -1.12 0.13Zm7.83 -8.08a3.2 3.2 0 0 1 -0.22 0.82c-0.14 0.39 -0.3 0.77 -0.46 1.14 -0.4 0.9 -0.83 1.79 -1.24 2.68l-1.31 2.95a6 6 0 0 1 -0.55 1.15 0.34 0.34 0 0 1 -0.36 0.16l-0.35 -0.14 -0.57 -0.38 -1 -0.91 -1.86 -1.93c0.78 -0.49 1.56 -1 2.32 -1.48 0.4 -0.27 0.8 -0.55 1.17 -0.85 0.2 -0.17 0.41 -0.34 0.59 -0.52a2.16 2.16 0 0 0 0.36 -0.51 2.52 2.52 0 0 0 0.14 -0.56 0.54 0.54 0 0 0 -0.51 -0.55 1.06 1.06 0 0 0 -0.24 0 3.54 3.54 0 0 0 -0.47 0.12l-1.51 0.45c-0.58 0.19 -1.14 0.38 -1.69 0.6 -1.27 0.5 -2.49 1.07 -3.78 1.55l-0.09 0 -0.53 -0.18a3.39 3.39 0 0 1 -1.22 -0.65c-0.11 -0.11 -0.11 -0.26 0.06 -0.43l0.3 -0.25 0.52 -0.32 1.16 -0.52c1.08 -0.32 2.17 -0.58 3.26 -0.8 1.66 -0.35 3.33 -0.62 5 -0.89l0.81 -0.16a10 10 0 0 1 1.6 -0.32c0.23 0 0.46 0 0.57 0.14a0.76 0.76 0 0 1 0.1 0.56Z" fill="var(--color-main)" fill-rule="evenodd" stroke-width="1"></path> + </g></symbol> + <symbol id="icon-email" viewBox="0 0 24 24"><g> + <path d="M23.58 5.09a2.73 2.73 0 0 0 -0.71 -1.43 4.24 4.24 0 0 0 -1.62 -0.34 99 99 0 0 0 -11.89 0.07C5.34 3.65 1.7 4.18 0.55 5a1.12 1.12 0 0 0 -0.35 0.68A25.75 25.75 0 0 0 0 9.6a93.2 93.2 0 0 0 0.5 10.09 0.31 0.31 0 1 0 0.61 -0.11c-0.15 -0.95 -0.29 -4 -0.31 -7.11A56 56 0 0 1 1 6.36c1.64 1.1 4.88 3.25 7.53 4.69l1.46 0.72a7.13 7.13 0 0 0 2.53 0.82 3.16 3.16 0 0 0 1.14 -0.42 8.38 8.38 0 0 0 0.79 -0.48l1.44 -1c0.34 -0.25 0.67 -0.49 1 -0.77A44.32 44.32 0 0 0 22.32 5c0 0.11 0.07 0.24 0.1 0.38a24.38 24.38 0 0 1 0.41 4.75 65 65 0 0 1 -0.36 8.17c0 0.22 -0.07 0.45 -0.1 0.58l-0.69 0c-1.79 0.18 -5.6 0.46 -9.48 0.7 -4.39 0.27 -8.86 0.5 -10.56 0.54a0.35 0.35 0 0 0 -0.35 0.35 0.36 0.36 0 0 0 0.36 0.35c2.27 0 9.45 -0.29 14.83 -0.58 2.56 -0.13 4.71 -0.27 5.73 -0.37a2.73 2.73 0 0 0 0.86 -0.19 0.6 0.6 0 0 0 0.24 -0.3 4.17 4.17 0 0 0 0.22 -0.94A65.86 65.86 0 0 0 24 10.1a26 26 0 0 0 -0.42 -5.01Zm-6.33 3.28c-1.23 0.92 -2.48 1.78 -3.44 2.36a4.31 4.31 0 0 1 -1.36 0.67 3.75 3.75 0 0 1 -1.27 -0.4c-0.9 -0.37 -2.06 -0.94 -3.29 -1.59C5.34 8.1 2.52 6.49 1.13 5.68c1.18 -0.69 4.57 -1 8.29 -1.2a106 106 0 0 1 11.74 0h0.62a38.87 38.87 0 0 1 -4.53 3.89Z" fill="var(--color-main)" stroke-width="1"></path> + <path d="M17.68 14.41c0.34 0.33 0.69 0.65 1 0.95 1 0.93 2 1.74 2.46 2.19a0.36 0.36 0 0 0 0.5 0 0.35 0.35 0 0 0 0 -0.5c-0.43 -0.51 -1.23 -1.46 -2.18 -2.49q-0.48 -0.51 -1 -1c-0.34 -0.33 -0.69 -0.66 -1 -1s-0.81 -0.68 -1.19 -1c-0.69 -0.47 -1.2 0 -0.71 0.59 0.34 0.37 0.71 0.78 1.12 1.2s0.66 0.73 1 1.06Z" fill="var(--color-accent-deep)" stroke-width="1"></path> + <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="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> @@ -25,24 +42,24 @@ </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"> - <i class="fa-brands fa-telegram faw-4 h-4 mr-1"></i> + <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> <span>@EarlInisMona</span> </a> <a href="https://discord.com/users/356057270951346176"> - <i class="fa-brands fa-discord faw-4 h-4 mr-1"></i> + <svg class="w-4 h-4 mr-1 inline" viewBox="0 0 24 24"><use href="#icon-discord"/></svg> <span class="font-bold">Discord:</span> <span>montferrat</span> </a> <a href="mailto:saya.andy@posteo.com"> - <i class="fas fa-envelope faw-4 h-4 mr-1"></i> + <svg class="w-4 h-4 mr-1 inline" viewBox="0 0 24 24"><use href="#icon-email"/></svg> <span class="font-bold">E-mail:</span> <span>saya.andy@posteo.com</span> </a> @@ -56,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> @@ -103,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}} |