summaryrefslogtreecommitdiff
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build-and-deploy-prod.yml2
-rw-r--r--.github/workflows/build-and-deploy-stage.yml2
-rw-r--r--config/config.go86
-rw-r--r--config/config.local.yaml28
-rw-r--r--config/config.prod.yaml30
-rw-r--r--config/config.stage.yaml29
-rw-r--r--deploy/inventory.yml2
-rw-r--r--deploy/playbook.yml28
-rw-r--r--go.mod1
-rw-r--r--go.sum61
-rw-r--r--internal/blog/index.go32
-rw-r--r--internal/blog/s3.go173
-rw-r--r--internal/router/handlers/root.go3
-rw-r--r--internal/router/router.go36
-rw-r--r--main.go2
-rw-r--r--static/favicon-dark.svg16
-rw-r--r--static/favicon-light.svg16
-rw-r--r--static/favicon.icobin0 -> 5694 bytes
-rw-r--r--static/input.css125
-rw-r--r--views/layouts/general-page.html1026
-rw-r--r--views/pages/global-map.html18
-rw-r--r--views/pages/robots.txt4
-rw-r--r--views/pages/unsubscribe-page.html12
23 files changed, 543 insertions, 1189 deletions
diff --git a/.github/workflows/build-and-deploy-prod.yml b/.github/workflows/build-and-deploy-prod.yml
index 97afdf7..d89cc76 100644
--- a/.github/workflows/build-and-deploy-prod.yml
+++ b/.github/workflows/build-and-deploy-prod.yml
@@ -80,4 +80,4 @@ jobs:
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 }}"
+ ansible-playbook -i inventory.yml -l prod playbook.yml --private-key ~/.ssh/id_ed25519 -u svc_github -e saya_today_web_auth_salt="${{ secrets.AUTH_SALT }}" -e saya_today_web_b2_key_id="${{ secrets.B2_KEY_ID }}" -e saya_today_web_b2_application_key="${{ secrets.B2_APPLICATION_KEY }}" -e saya_today_web_environment=prod -e saya_today_web_tag=${{ github.ref_name }} -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 }}"
diff --git a/.github/workflows/build-and-deploy-stage.yml b/.github/workflows/build-and-deploy-stage.yml
index ffee091..e699856 100644
--- a/.github/workflows/build-and-deploy-stage.yml
+++ b/.github/workflows/build-and-deploy-stage.yml
@@ -86,4 +86,4 @@ jobs:
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 }}"
+ ansible-playbook -i inventory.yml -l stage playbook.yml --private-key ~/.ssh/id_ed25519 -u svc_github -e saya_today_web_auth_salt="${{ secrets.AUTH_SALT }}" -e saya_today_web_b2_key_id="${{ secrets.B2_KEY_ID }}" -e saya_today_web_b2_application_key="${{ secrets.B2_APPLICATION_KEY }}" -e saya_today_web_environment=stage -e saya_today_web_tag=commit-${{ steps.ghss_vars.outputs.sha_short }} -e saya_today_web_mail_salt="${{ secrets.MAIL_SALT }}" -e saya_today_web_mail_host="${{ secrets.MAIL_HOST }}" -e saya_today_web_mail_address="${{ secrets.MAIL_ADDRESS }}" -e saya_today_web_mail_username="${{ secrets.MAIL_USERNAME }}" -e saya_today_web_mail_password="${{ secrets.MAIL_PASSWORD }}"
diff --git a/config/config.go b/config/config.go
index 0b1570a..536a333 100644
--- a/config/config.go
+++ b/config/config.go
@@ -12,7 +12,6 @@ import (
type Config struct {
LogLevel slog.Level `json:"LogLevel" yaml:"logLevel" validate:"required"`
- Endpoint EndpointConfig `json:"Endpoint" yaml:"endpoint" validate:"required"`
BlogPages BlogPagesConfig `json:"BlogPages" yaml:"blogPages" validate:"required"`
FactGiver FactGiverConfig `json:"FactGiver" yaml:"factGiver" validate:"required"`
LocalePath string `json:"LocalePath" yaml:"localePath" validate:"required,filepath"`
@@ -26,90 +25,12 @@ type Config struct {
AllowOrigins []string `json:"AllowOrigins" yaml:"allowOrigins"`
}
-type EndpointConfig struct {
- Type string `json:"Type" yaml:"type" validate:"required,oneof=http unix"`
- Config any `json:"Config" yaml:"config" validate:"required"`
-}
-
-func (ec *EndpointConfig) UnmarshalJSON(data []byte) error {
- var tmp struct {
- Type string `json:"Type"`
- Config json.RawMessage `json:"Config"`
- }
-
- if err := json.Unmarshal(data, &tmp); err != nil {
- return err
- }
-
- ec.Type = tmp.Type
-
- switch tmp.Type {
- case "http":
- var httpConfig HttpConfig
- if err := json.Unmarshal(tmp.Config, &httpConfig); err != nil {
- return fmt.Errorf("unmarshal HttpConfig: %w", err)
- }
- ec.Config = &httpConfig
- case "unix":
- var unixConfig UnixConfig
- if err := json.Unmarshal(tmp.Config, &unixConfig); err != nil {
- return fmt.Errorf("unmarshal UnixConfig: %w", err)
- }
- ec.Config = &unixConfig
- default:
- return fmt.Errorf("unsupported storage type: %s", tmp.Type)
- }
-
- return nil
-}
-
-func (ec *EndpointConfig) UnmarshalYAML(value *yaml.Node) error {
- var tmp struct {
- Type string `yaml:"type"`
- Config yaml.Node `yaml:"config"`
- }
-
- if err := value.Decode(&tmp); err != nil {
- return err
- }
-
- ec.Type = tmp.Type
-
- switch tmp.Type {
- case "http":
- var httpConfig HttpConfig
- if err := tmp.Config.Decode(&httpConfig); err != nil {
- return fmt.Errorf("unmarshal HttpConfig: %w", err)
- }
- ec.Config = &httpConfig
- case "unix":
- var unixConfig UnixConfig
- if err := tmp.Config.Decode(&unixConfig); err != nil {
- return fmt.Errorf("unmarshal UnixConfig: %w", err)
- }
- ec.Config = &unixConfig
- default:
- return fmt.Errorf("unsupported storage type: %s", tmp.Type)
- }
-
- return nil
-}
-
-type HttpConfig struct {
- ListenOn string `json:"ListenOn" yaml:"listenOn" validate:"required"`
-}
-
-type UnixConfig struct {
- Path string `json:"Path" yaml:"path" validate:"required"`
- Chmod string `json:"Chmod" yaml:"chmod" validate:"oneof=0600 0660 0666"`
-}
-
type BlogPagesConfig struct {
Storage StorageConfig `json:"Storage" yaml:"storage" validate:"required"`
}
type StorageConfig struct {
- Type string `json:"Type" yaml:"type" validate:"required,oneof=b2 s3"`
+ Type string `json:"type" yaml:"type,oneof=b2 s3"`
Config any `json:"Config" yaml:"config" validate:"required"`
}
@@ -238,7 +159,6 @@ type TriggerConfig struct {
type MetaConfig struct {
GoogleSiteVerification string `json:"GoogleSiteVerification" yaml:"googleSiteVerification"`
- YandexVerification string `json:"YandexVerification" yaml:"yandexVerification"`
}
type PhotoStorageConfig struct {
@@ -282,10 +202,6 @@ func InitConfig(path string) (*Config, error) {
return nil, err
}
- if config.Endpoint.Type == "unix" && config.Endpoint.Config.(*UnixConfig).Chmod == "" {
- config.Endpoint.Config.(*UnixConfig).Chmod = "0660"
- }
-
validate := validator.New(validator.WithRequiredStructEnabled())
if err := validate.Struct(config); err != nil {
return nil, err
diff --git a/config/config.local.yaml b/config/config.local.yaml
index d3e2e33..7769438 100644
--- a/config/config.local.yaml
+++ b/config/config.local.yaml
@@ -1,30 +1,22 @@
logLevel: debug
-endpoint:
- type: http
- config:
- listenOn: ":3000"
blogPages:
storage:
- type: s3
+ type: b2
config:
- bucketName: sayauz-pages
- region: kz1
+ bucketName: sayana-pages
+ region: eu-central-003
prefix: "stage-"
- endpoint: "https://storage.yandexcloud.kz"
- usePathStyle: true
- accessKeyID: "${S3_ACCESS_KEY_ID}"
- secretAccessKey: "${S3_SECRET_ACCESS_KEY}"
+ keyID: "${B2_KEY_ID}"
+ applicationKey: "${B2_APPLICATION_KEY}"
factGiver:
storage:
- type: s3
+ type: b2
config:
- bucketName: sayauz-pages
- region: kz1
+ bucketName: sayana-pages
+ region: eu-central-003
prefix: ""
- endpoint: "https://storage.yandexcloud.kz"
- usePathStyle: true
- accessKeyID: "${S3_ACCESS_KEY_ID}"
- secretAccessKey: "${S3_SECRET_ACCESS_KEY}"
+ keyID: "${B2_KEY_ID}"
+ applicationKey: "${B2_APPLICATION_KEY}"
factsFileName: "facts-*.txt"
localePath: ./locale/
availableLanguages:
diff --git a/config/config.prod.yaml b/config/config.prod.yaml
index e5785f8..91f606f 100644
--- a/config/config.prod.yaml
+++ b/config/config.prod.yaml
@@ -1,31 +1,22 @@
logLevel: info
-endpoint:
- type: unix
- config:
- path: "/etc/sayauz/prod.socket"
- chmod: '0666'
blogPages:
storage:
- type: s3
+ type: b2
config:
- bucketName: sayauz-pages
- region: kz1
+ bucketName: sayana-pages
+ region: eu-central-003
prefix: "prod-"
- endpoint: "https://storage.yandexcloud.kz"
- usePathStyle: true
- accessKeyID: "${S3_ACCESS_KEY_ID}"
- secretAccessKey: "${S3_SECRET_ACCESS_KEY}"
+ keyID: "${B2_KEY_ID}"
+ applicationKey: "${B2_APPLICATION_KEY}"
factGiver:
storage:
- type: s3
+ type: b2
config:
- bucketName: sayauz-pages
- region: kz1
+ bucketName: sayana-pages
+ region: eu-central-003
prefix: ""
- endpoint: "https://storage.yandexcloud.kz"
- usePathStyle: true
- accessKeyID: "${S3_ACCESS_KEY_ID}"
- secretAccessKey: "${S3_SECRET_ACCESS_KEY}"
+ keyID: "${B2_KEY_ID}"
+ applicationKey: "${B2_APPLICATION_KEY}"
factsFileName: "facts-*.txt"
localePath: ./locale/
availableLanguages:
@@ -56,7 +47,6 @@ mail:
canonicalEndpoint: "https://${FQDN}"
meta:
googleSiteVerification: "${GOOGLE_SITE_VERIFICATION}"
- yandexVerification: "${YANDEX_VERIFICATION}"
photoStorage:
full:
baseUrl: https://storage.yandexcloud.kz/sayauz-photos/%s
diff --git a/config/config.stage.yaml b/config/config.stage.yaml
index c2e337b..3f1381b 100644
--- a/config/config.stage.yaml
+++ b/config/config.stage.yaml
@@ -1,31 +1,22 @@
logLevel: debug
-endpoint:
- type: unix
- config:
- path: "/etc/sayauz/stage.socket"
- chmod: '0666'
blogPages:
storage:
- type: s3
+ type: b2
config:
- bucketName: sayauz-pages
- region: kz1
+ bucketName: sayana-pages
+ region: eu-central-003
prefix: "stage-"
- endpoint: "https://storage.yandexcloud.kz"
- usePathStyle: true
- accessKeyID: "${S3_ACCESS_KEY_ID}"
- secretAccessKey: "${S3_SECRET_ACCESS_KEY}"
+ keyID: "${B2_KEY_ID}"
+ applicationKey: "${B2_APPLICATION_KEY}"
factGiver:
storage:
- type: s3
+ type: b2
config:
- bucketName: sayauz-pages
- region: kz1
+ bucketName: sayana-pages
+ region: eu-central-003
prefix: ""
- endpoint: "https://storage.yandexcloud.kz"
- usePathStyle: true
- accessKeyID: "${S3_ACCESS_KEY_ID}"
- secretAccessKey: "${S3_SECRET_ACCESS_KEY}"
+ keyID: "${B2_KEY_ID}"
+ applicationKey: "${B2_APPLICATION_KEY}"
factsFileName: "facts-*.txt"
localePath: ./locale/
availableLanguages:
diff --git a/deploy/inventory.yml b/deploy/inventory.yml
index 0a63140..d3acab8 100644
--- a/deploy/inventory.yml
+++ b/deploy/inventory.yml
@@ -8,7 +8,6 @@ all:
saya_today_web_hostname: sayauz-prod
saya_today_web_ipv4_address: 172.16.0.17
saya_today_web_listen_address: saya.uz
- saya_today_web_unix_sockets_path: '/etc/sayauz'
stage:
hosts:
@@ -18,4 +17,3 @@ all:
saya_today_web_hostname: sayauz-stage
saya_today_web_ipv4_address: 172.16.0.18
saya_today_web_listen_address: stage.saya.uz
- saya_today_web_unix_sockets_path: '/etc/sayauz'
diff --git a/deploy/playbook.yml b/deploy/playbook.yml
index ae3e936..8fb9088 100644
--- a/deploy/playbook.yml
+++ b/deploy/playbook.yml
@@ -2,27 +2,9 @@
- name: Deploy saya-today-web Docker Container
hosts: all
remote_user: svc_github
-
- vars:
- docker_volumes:
- - "{{ saya_today_web_name }}-volume:/data:rw"
tasks:
- - name: Initialize Unix socket volume
- when: >-
- saya_today_web_unix_sockets_path is defined and
- saya_today_web_unix_sockets_path is string and
- saya_today_web_unix_sockets_path | length > 0
- block:
- - name: Ensure Unix socket volume exists
- community.docker.docker_volume:
- name: "sayana_sockets"
- - name: Include Unix socket volume in docker volumes list
- ansible.builtin.set_fact:
- docker_volumes: >-
- {{ docker_volumes + ["sayana_sockets:" ~ saya_today_web_unix_sockets_path ~ ":rw"] }}
-
- - name: Create a data volume
+ - name: Create a volume
community.docker.docker_volume:
name: "{{ saya_today_web_name }}-volume"
@@ -32,8 +14,8 @@
hostname: "{{ saya_today_web_hostname }}"
image: "ghcr.io/sayaandy/saya-today-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 }}"
+ B2_KEY_ID: "{{ saya_today_web_b2_key_id }}"
+ B2_APPLICATION_KEY: "{{ saya_today_web_b2_application_key }}"
ENVIRONMENT: "{{ saya_today_web_environment }}"
AUTH_SALT: "{{ saya_today_web_auth_salt }}"
MAIL_HOST: "{{ saya_today_web_mail_host }}"
@@ -43,12 +25,12 @@
MAIL_SALT: "{{ saya_today_web_mail_salt }}"
FQDN: "{{ saya_today_web_listen_address }}"
GOOGLE_SITE_VERIFICATION: "{{ saya_today_google_site_verification | default('') }}"
- YANDEX_VERIFICATION: "{{ saya_today_yandex_verification | default('') }}"
network_mode: caddy
networks:
- name: caddy
ipv4_address: "{{ saya_today_web_ipv4_address }}"
- volumes: "{{ docker_volumes }}"
+ volumes:
+ - "{{ saya_today_web_name }}-volume:/data:rw"
restart_policy: unless-stopped
no_log: true
...
diff --git a/go.mod b/go.mod
index 779e641..7d44551 100644
--- a/go.mod
+++ b/go.mod
@@ -19,6 +19,7 @@ require (
require (
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect
diff --git a/go.sum b/go.sum
index d277091..597f315 100644
--- a/go.sum
+++ b/go.sum
@@ -2,40 +2,78 @@ github.com/Backblaze/blazer v0.7.2 h1:UWNHMLB+Nf+UmbO2qkVvgriODLEMz4kIyr2Hm+DVXQ
github.com/Backblaze/blazer v0.7.2/go.mod h1:T4y3EYa9IQ5J0PKc/C/J8/CEnSd3qa/lgNw938wZg10=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
+github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
+github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 h1:gx1AwW1Iyk9Z9dD9F4akX5gnN3QZwUB20GGKH/I+Rho=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10/go.mod h1:qqY157uZoqm5OXq/amuaBJyC9hgBCBQnsaWnPe905GY=
+github.com/aws/aws-sdk-go-v2/config v1.32.14 h1:opVIRo/ZbbI8OIqSOKmpFaY7IwfFUOCCXBsUpJOwDdI=
+github.com/aws/aws-sdk-go-v2/config v1.32.14/go.mod h1:U4/V0uKxh0Tl5sxmCBZ3AecYny4UNlVmObYjKuuaiOo=
github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU=
github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w=
github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU=
github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 h1:ieLCO1JxUWuxTZ1cRd0GAaeX7O6cIxnwk7tc1LsQhC4=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15/go.mod h1:e3IzZvQ3kAWNykvE0Tr0RDZCMFInMvhku3qNpcIQXhM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
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.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
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.99.0 h1:hlSuz394kV0vhv9drL5lhuEFbEOEP1VyQpy15qWh1Pk=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
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/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI=
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.15 h1:lFd1+ZSEYJZYvv9d6kXzhkZu07si3f+GQ1AaYwa2LUM=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.15/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 h1:dzztQ1YmfPrxdrOiuZRMF6fuOwWlWpD2StNLTceKpys=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk=
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio=
+github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
+github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -53,6 +91,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.20.0 h1:9IMrnnVSWjfSh3E54gWmWCHbloQJLh6f9+nwyKfLNpc=
+github.com/go-co-op/gocron/v2 v2.20.0/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -63,6 +103,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
+github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw=
+github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/fiber/v2 v2.52.13 h1:TOKP64iqC9b5P49VrBW5tHhUOvDyrtJ0xePEfzJbCbk=
github.com/gofiber/fiber/v2 v2.52.13/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
@@ -71,6 +113,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
+github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
+github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -83,12 +127,20 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
+github.com/mattn/go-runewidth v0.0.22 h1:76lXsPn6FyHtTY+jt2fTTvsMUCZq1k0qwRsAMuxzKAk=
+github.com/mattn/go-runewidth v0.0.22/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
+github.com/mattn/go-sqlite3 v1.14.41 h1:8p7Pwz5NHkEbWSqc/ygU4CBGubhFFkpgP9KwcdkAHNA=
+github.com/mattn/go-sqlite3 v1.14.41/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
+github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
+github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -100,6 +152,8 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
+github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
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=
@@ -114,10 +168,17 @@ 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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
+golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
+golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
+golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/blog/index.go b/internal/blog/index.go
deleted file mode 100644
index 861a90e..0000000
--- a/internal/blog/index.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package blog
-
-import "time"
-
-const IndexFileName = "index.json"
-
-const IndexSchemaVersion = 1
-
-type IndexEntry struct {
- Link string `json:"link"`
- ModifiedTime time.Time `json:"modifiedTime"`
- Title string `json:"title"`
- ShortDescription string `json:"shortDescription"`
- ActionDate string `json:"actionDate"`
- PublishedTime time.Time `json:"publishedTime"`
- Thumbnail string `json:"thumbnail"`
- Tags []string `json:"tags"`
- Geolocation string `json:"geolocation"`
- Medley string `json:"medley,omitempty"`
- MedleyPart int `json:"medleyPart,omitempty"`
-}
-
-type IndexCategory 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"`
-}
diff --git a/internal/blog/s3.go b/internal/blog/s3.go
index bbd5238..86a6463 100644
--- a/internal/blog/s3.go
+++ b/internal/blog/s3.go
@@ -2,15 +2,10 @@ package blog
import (
"context"
- "encoding/json"
- "errors"
"fmt"
"io"
- "log/slog"
- "net/url"
"slices"
"strings"
- "sync"
"time"
"github.com/SayaAndy/saya-today-web/config"
@@ -19,11 +14,8 @@ import (
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
@@ -58,7 +50,6 @@ func NewS3Client(cfg *config.StorageConfig) (Client, error) {
}
s3Opts = append(s3Opts, func(o *s3.Options) {
o.UsePathStyle = s3cfg.UsePathStyle
- o.DisableLogOutputChecksumValidationSkipped = true
})
s3cl := s3.NewFromConfig(awsCfg, s3Opts...)
@@ -67,195 +58,79 @@ func NewS3Client(cfg *config.StorageConfig) (Client, error) {
}
func (c *S3Client) Scan(prefix string) ([]*Page, error) {
- pages, err := c.scanFromIndex(prefix)
- if err == nil {
- return pages, nil
- }
-
- var nsk *s3types.NoSuchKey
- if !errors.As(err, &nsk) {
- return nil, err
- }
-
- slog.Warn("index.json missing, falling back to listing", slog.String("prefix", c.prefix))
- return c.scanByListing(prefix)
-}
-
-func (c *S3Client) scanFromIndex(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)
- }
- defer out.Body.Close()
-
- raw, err := io.ReadAll(out.Body)
- if err != nil {
- return nil, fmt.Errorf("read index.json: %w", err)
- }
-
- var idx Index
- if err := json.Unmarshal(raw, &idx); err != nil {
- return nil, fmt.Errorf("unmarshal index.json: %w", err)
- }
-
- wantLang := ""
- if i := strings.Index(prefix, "/"); i > 0 {
- wantLang = prefix[:i]
- }
+ pages := []*Page{}
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) {
- 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,
- },
- })
- }
+ input := &s3.ListObjectsV2Input{
+ Bucket: aws.String(c.bucketName),
+ Prefix: aws.String(fullPrefix),
}
- 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, input)
- 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") {
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),
+ Key: aws.String(key),
})
if err != nil {
- errMu.Lock()
- if firstErr == nil {
- firstErr = fmt.Errorf("head S3 object %s: %w", cand.key, err)
- }
- errMu.Unlock()
- return
+ return nil, fmt.Errorf("head S3 object %s: %w", key, err)
}
if head.ContentType == nil || !strings.Contains(*head.ContentType, "text/markdown") {
- return
+ continue
}
+
meta := head.Metadata
if meta["title"] == "" {
- return
+ 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)
- }
- errMu.Unlock()
- return
+ return nil, fmt.Errorf("failed to parse published time metadata field: %w", err)
}
- linkParts := strings.Split(cand.key, "/")
+ linkParts := strings.Split(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"])
+ lang, _ := strings.CutPrefix(linkParts[0], c.prefix)
- pages[i] = &Page{
- Link: cand.key,
+ pages = append(pages, &Page{
+ Link: key,
FileName: fileName,
Lang: lang,
- ModifiedTime: cand.lastModified,
+ ModifiedTime: aws.ToTime(obj.LastModified),
Metadata: &frontmatter.Metadata{
- Title: title,
- ShortDescription: shortDescription,
+ Title: meta["title"],
+ ShortDescription: meta["short-description"],
ActionDate: meta["action-date"],
PublishedTime: publishedTime,
- Thumbnail: thumbnail,
+ Thumbnail: meta["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)
+ })
}
}
- return out, nil
+
+ return pages, nil
}
func (c *S3Client) ReadAll(path string) ([]byte, error) {
diff --git a/internal/router/handlers/root.go b/internal/router/handlers/root.go
index 6a011e4..a8890fa 100644
--- a/internal/router/handlers/root.go
+++ b/internal/router/handlers/root.go
@@ -50,9 +50,6 @@ func (r *RootHandler) AddMeta(c *fiber.Ctx, supplements *router.Supplements, lan
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})
- }
return meta, nil
}
diff --git a/internal/router/router.go b/internal/router/router.go
index 65e35a4..5230e74 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -7,11 +7,8 @@ import (
"fmt"
"html/template"
"log/slog"
- "net"
"net/url"
- "os"
"slices"
- "strconv"
"strings"
"time"
@@ -115,7 +112,6 @@ type Router struct {
templatedRoutes map[string]map[string]Route
templatedPathMatcher *PathMatcher
canonicalEndpoint string
- endpoint config.EndpointConfig
}
func NewRouter(cfg *config.Config) (*Router, error) {
@@ -260,7 +256,7 @@ func NewRouter(cfg *config.Config) (*Router, error) {
templatedRoutes := make(map[string]map[string]Route)
templatedPathMatcher := NewPathMatcher()
- return &Router{supplements, app, templatedRoutes, templatedPathMatcher, cfg.CanonicalEndpoint, cfg.Endpoint}, nil
+ return &Router{supplements, app, templatedRoutes, templatedPathMatcher, cfg.CanonicalEndpoint}, nil
}
func (r *Router) InitRoutes() (err error) {
@@ -309,8 +305,6 @@ func (r *Router) InitRoutes() (err error) {
cacheKey = fmt.Sprintf("%s.full-page.%s", method, trimmedPath)
case ByUrlAndQuery:
cacheKey = fmt.Sprintf("%s.full-page.%s.%s", method, trimmedPath, queryString)
- case Disabled:
- c.Set("Cache-Control", "no-store, no-cache, must-revalidate")
}
defaultMap := fiber.Map{
@@ -373,7 +367,6 @@ func (r *Router) InitRoutes() (err error) {
c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
return c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("unknown segment '%s'", part))
}
- c.Set("Cache-Control", "no-store, no-cache, must-revalidate")
err := r.generalPageSegment(c, part)
return err
})
@@ -389,30 +382,10 @@ func (r *Router) InitRoutes() (err error) {
return nil
}
-func (r *Router) Listen() error {
- switch r.endpoint.Type {
- case "unix":
- unixConfig := r.endpoint.Config.(*config.UnixConfig)
- endpoint, _ := strings.CutPrefix(unixConfig.Path, "unix://")
- ln, err := net.Listen("unix", endpoint)
- if err != nil {
- return fmt.Errorf("error while initializing unix listener: %w", err)
- }
- chmod, _ := strconv.ParseUint(unixConfig.Chmod[1:], 8, 32)
- os.Chmod(unixConfig.Path, os.FileMode(chmod))
- if err := r.app.Listener(ln); err != nil {
- return fmt.Errorf("error while running fiber server: %w", err)
- }
- case "http":
- httpConfig := r.endpoint.Config.(*config.HttpConfig)
- fmt.Print(httpConfig)
- if err := r.app.Listen(httpConfig.ListenOn); err != nil {
- return fmt.Errorf("error while running fiber server: %w", err)
- }
- default:
- return fmt.Errorf("error with initializing fiber server: invalid endpoint type (supported are unix and http)")
+func (r *Router) Listen(endpoint string) error {
+ if err := r.app.Listen(endpoint); err != nil {
+ return fmt.Errorf("error while running fiber server: %w", err)
}
-
return nil
}
@@ -436,7 +409,6 @@ func (r *Router) Close() (err error) {
}
slog.Debug("closing page cache")
r.supplements.PageCache.Close()
-
return errors.Join(allErrors...)
}
diff --git a/main.go b/main.go
index 9d90916..3691e5b 100644
--- a/main.go
+++ b/main.go
@@ -47,7 +47,7 @@ func main() {
routerShutdown := make(chan struct{}, 1)
go func() {
- if err := app.Listen(); err != nil {
+ if err := app.Listen(":3000"); err != nil {
slog.Error("error while running fiber server", slog.String("error", err.Error()))
if nestedErr := app.Close(); nestedErr != nil {
slog.Error("error while shutting down fiber server", slog.String("error", err.Error()))
diff --git a/static/favicon-dark.svg b/static/favicon-dark.svg
deleted file mode 100644
index f09a1bf..0000000
--- a/static/favicon-dark.svg
+++ /dev/null
@@ -1,16 +0,0 @@
-<svg role="img" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" id="Lutris--Streamline-Simple-Icons" height="120" width="120">
- <desc>
- Lutris Streamline Icon: https://streamlinehq.com
- </desc>
- <defs>
- <linearGradient gradientTransform="rotate(150, 0.5, 0.5)" x1="50%" y1="0%" x2="50%" y2="100%" id="gradient">
- <stop stop-color="hsl(31.4, 100%, 70.8%)" stop-opacity="1" offset="0%"></stop><stop stop-color="hsl(17, 100%, 31%)" stop-opacity="1" offset="100%"></stop>
- </linearGradient>
- <filter id="filter" x="-20%" y="-20%" width="140%" height="140%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
- <feTurbulence type="fractalNoise" baseFrequency="0.005 0.003" numOctaves="1" seed="2" stitchTiles="stitch" x="0%" y="0%" width="100%" height="100%" result="turbulence"></feTurbulence>
- <feGaussianBlur stdDeviation="20 0" x="0%" y="0%" width="100%" height="100%" in="turbulence" edgeMode="duplicate" result="blur"></feGaussianBlur>
- <feBlend mode="color-dodge" x="0%" y="0%" width="100%" height="100%" in="SourceGraphic" in2="blur" result="blend"></feBlend>
- </filter>
- </defs>
- <path d="m106.155 94.45 0.005 -0.01c-6.465 16.215 -26.09 26.16 -47.235 25.525C26.5 119.965 0 92.4 0 59.53S26.38 0.005 58.925 0.005c8.965 0 17.465 2.03 25.075 5.65 0.405 -0.885 1.355 -2.72 2.255 -2.785 1.19 -0.085 1.87 0.685 2.63 1.545 0.77 0.86 2.3 2.145 2.3 2.145s6.965 -2.405 14.775 1.885c7.815 4.29 8.915 5.58 10.45 8.58 0.76 1.505 0.975 4.145 1 6.41a3.98 3.98 0 0 0 -0.35 -0.015c-2.48 0 -4.8 2.275 -4.8 5.4 0 1.315 0.41 2.48 1.075 3.39l-0.05 0.035a7.525 7.525 0 0 0 -0.66 0.05 93.52 93.52 0 0 0 -1.945 -0.71 12.65 12.65 0 0 1 -4.1 -2.36 7.01 7.01 0 0 0 -5.98 -10.56c-1.915 0 -3.65 0.78 -4.91 2.05 -2.36 -1.355 -5.87 -2.41 -12.635 -2.825l-2.035 -0.055c-11.41 0.06 -18.055 1.395 -29.895 6.505 -3.015 1.415 -6.03 3.075 -8.925 5.005 -2.115 1.5 -3.195 3.35 -3.545 5.685a6.63 6.63 0 0 0 6.15 6.865h0.210c6.35 0.3 10.195 9.95 10.315 12.485 0.02 0.25 0.02 0.115 0.015 0.4 -0.16 3.635 -1.85 6.335 -5.44 6.23a6.155 6.155 0 0 1 -4.88 -2.47c-0.315 -0.385 -0.515 -0.86 -0.795 -1.27 -3.33 -5.405 -8.66 -6.80 -13.855 -7.615 -2.19 -0.34 -5.365 -0.61 -6.55 1.25a41.4 41.4 0 0 0 -2.885 15.315c-0.1 25.18 20.205 45.59 45.13 45.59 12.875 0 26.745 -4.76 34.965 -13.5l-0.175 0.15c-8.86 7.365 -23.3 9.705 -30.135 9.705 -21.51 0 -39.09 -16.16 -39.09 -37.89 0 -6.38 1.44 -11.98 4.07 -16.8 2.475 0.915 4.735 2.415 6.4 5.11a1.2 1.2 0 0 0 0.065 0.1050c0.32 0.46 0.555 0.985 0.91 1.42 2.12 2.62 4.405 3.29 6.71 3.4h0.05c2.15 0.065 3.84 -0.6 5.12 -1.71 1.735 -1.5 2.75 -3.95 2.885 -6.91v-0.07c0.01 -0.425 0 -0.265 -0.02 -0.56 -0.12 -1.88 -1.665 -6.59 -4.53 -10.135 -1.33 -1.655 -2.935 -3.035 -4.75 -3.87l0.6 -0.37c3.78 -2.285 11.82 -4.885 22.96 -3.19 5.65 0.865 10.275 2.095 17.415 4.395 8.285 2.67 12.895 6.395 19.27 7.135 0.75 0.085 1.505 0.09 2.25 0.015 2.05 5.645 3.17 11.75 3.17 18.105 0 10.34 -2.95 19.975 -8.055 28.1zm9.735 -61.37s-0.575 1.005 -1.82 1.61c-0.515 0.25 -1.41 -0.375 -2.25 0.5 -1.795 3.63 2.58 6.66 4.615 6.575 2.04 -0.085 3.65 -2.16 3.56 -3.965 -0.085 -2.79 -4.1 -4.72 -4.1 -4.72zm1.17 -7.16c1.275 0 2.31 1.3 2.31 2.9 0 1.6 -1.035 2.9 -2.31 2.9 -1.27 0 -2.3 -1.3 -2.3 -2.9 0 -1.6 1.03 -2.9 2.3 -2.9zm-16.46 -4.755c2.46 0 4.45 2.015 4.45 4.5a4.475 4.475 0 0 1 -4.45 4.49 4.475 4.475 0 0 1 -4.45 -4.495c0 -2.48 1.995 -4.495 4.45 -4.495z" fill="url(#gradient)" filter="url(#filter)" stroke-width="5"></path>
-</svg>
diff --git a/static/favicon-light.svg b/static/favicon-light.svg
deleted file mode 100644
index e7ed6eb..0000000
--- a/static/favicon-light.svg
+++ /dev/null
@@ -1,16 +0,0 @@
-<svg role="img" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" id="Lutris--Streamline-Simple-Icons" height="120" width="120">
- <desc>
- Lutris Streamline Icon: https://streamlinehq.com
- </desc>
- <defs>
- <linearGradient gradientTransform="rotate(150, 0.5, 0.5)" x1="50%" y1="0%" x2="50%" y2="100%" id="gradient">
- <stop stop-color="hsl(30.1, 100%, 44.1%)" stop-opacity="1" offset="0%"></stop><stop stop-color="hsl(23.1, 90.7%, 25.3%)" stop-opacity="1" offset="100%"></stop>
- </linearGradient>
- <filter id="filter" x="-20%" y="-20%" width="140%" height="140%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
- <feTurbulence type="fractalNoise" baseFrequency="0.005 0.003" numOctaves="1" seed="2" stitchTiles="stitch" x="0%" y="0%" width="100%" height="100%" result="turbulence"></feTurbulence>
- <feGaussianBlur stdDeviation="20 0" x="0%" y="0%" width="100%" height="100%" in="turbulence" edgeMode="duplicate" result="blur"></feGaussianBlur>
- <feBlend mode="color-dodge" x="0%" y="0%" width="100%" height="100%" in="SourceGraphic" in2="blur" result="blend"></feBlend>
- </filter>
- </defs>
- <path d="m106.155 94.45 0.005 -0.01c-6.465 16.215 -26.09 26.16 -47.235 25.525C26.5 119.965 0 92.4 0 59.53S26.38 0.005 58.925 0.005c8.965 0 17.465 2.03 25.075 5.65 0.405 -0.885 1.355 -2.72 2.255 -2.785 1.19 -0.085 1.87 0.685 2.63 1.545 0.77 0.86 2.3 2.145 2.3 2.145s6.965 -2.405 14.775 1.885c7.815 4.29 8.915 5.58 10.45 8.58 0.76 1.505 0.975 4.145 1 6.41a3.98 3.98 0 0 0 -0.35 -0.015c-2.48 0 -4.8 2.275 -4.8 5.4 0 1.315 0.41 2.48 1.075 3.39l-0.05 0.035a7.525 7.525 0 0 0 -0.66 0.05 93.52 93.52 0 0 0 -1.945 -0.71 12.65 12.65 0 0 1 -4.1 -2.36 7.01 7.01 0 0 0 -5.98 -10.56c-1.915 0 -3.65 0.78 -4.91 2.05 -2.36 -1.355 -5.87 -2.41 -12.635 -2.825l-2.035 -0.055c-11.41 0.06 -18.055 1.395 -29.895 6.505 -3.015 1.415 -6.03 3.075 -8.925 5.005 -2.115 1.5 -3.195 3.35 -3.545 5.685a6.63 6.63 0 0 0 6.15 6.865h0.210c6.35 0.3 10.195 9.95 10.315 12.485 0.02 0.25 0.02 0.115 0.015 0.4 -0.16 3.635 -1.85 6.335 -5.44 6.23a6.155 6.155 0 0 1 -4.88 -2.47c-0.315 -0.385 -0.515 -0.86 -0.795 -1.27 -3.33 -5.405 -8.66 -6.80 -13.855 -7.615 -2.19 -0.34 -5.365 -0.61 -6.55 1.25a41.4 41.4 0 0 0 -2.885 15.315c-0.1 25.18 20.205 45.59 45.13 45.59 12.875 0 26.745 -4.76 34.965 -13.5l-0.175 0.15c-8.86 7.365 -23.3 9.705 -30.135 9.705 -21.51 0 -39.09 -16.16 -39.09 -37.89 0 -6.38 1.44 -11.98 4.07 -16.8 2.475 0.915 4.735 2.415 6.4 5.11a1.2 1.2 0 0 0 0.065 0.1050c0.32 0.46 0.555 0.985 0.91 1.42 2.12 2.62 4.405 3.29 6.71 3.4h0.05c2.15 0.065 3.84 -0.6 5.12 -1.71 1.735 -1.5 2.75 -3.95 2.885 -6.91v-0.07c0.01 -0.425 0 -0.265 -0.02 -0.56 -0.12 -1.88 -1.665 -6.59 -4.53 -10.135 -1.33 -1.655 -2.935 -3.035 -4.75 -3.87l0.6 -0.37c3.78 -2.285 11.82 -4.885 22.96 -3.19 5.65 0.865 10.275 2.095 17.415 4.395 8.285 2.67 12.895 6.395 19.27 7.135 0.75 0.085 1.505 0.09 2.25 0.015 2.05 5.645 3.17 11.75 3.17 18.105 0 10.34 -2.95 19.975 -8.055 28.1zm9.735 -61.37s-0.575 1.005 -1.82 1.61c-0.515 0.25 -1.41 -0.375 -2.25 0.5 -1.795 3.63 2.58 6.66 4.615 6.575 2.04 -0.085 3.65 -2.16 3.56 -3.965 -0.085 -2.79 -4.1 -4.72 -4.1 -4.72zm1.17 -7.16c1.275 0 2.31 1.3 2.31 2.9 0 1.6 -1.035 2.9 -2.31 2.9 -1.27 0 -2.3 -1.3 -2.3 -2.9 0 -1.6 1.03 -2.9 2.3 -2.9zm-16.46 -4.755c2.46 0 4.45 2.015 4.45 4.5a4.475 4.475 0 0 1 -4.45 4.49 4.475 4.475 0 0 1 -4.45 -4.495c0 -2.48 1.995 -4.495 4.45 -4.495z" fill="url(#gradient)" filter="url(#filter)" stroke-width="5"></path>
-</svg>
diff --git a/static/favicon.ico b/static/favicon.ico
new file mode 100644
index 0000000..33eb226
--- /dev/null
+++ b/static/favicon.ico
Binary files differ
diff --git a/static/input.css b/static/input.css
index 5d8de42..75482d8 100644
--- a/static/input.css
+++ b/static/input.css
@@ -1,3 +1,5 @@
+@import url('https://fonts.googleapis.com/css2?family=Gentium+Plus:ital,wght@0,400;0,700;1,400;1,700&family=M+PLUS+Rounded+1c:wght@100;300;400;500;700;800;900&display=swap');
+
@import "tailwindcss";
@plugin "@tailwindcss/forms";
@@ -46,8 +48,7 @@ html {
@utility bg-paper-* {
background-image: --value([image], [url]), var(--bg-paper);
background-size: --value("auto", "cover", "contain"), 128px;
- background-repeat:
- --value("repeat", "repeat-x", "repeat-y", "no-repeat"), repeat;
+ background-repeat: --value("repeat", "repeat-x", "repeat-y", "no-repeat"), repeat;
background-blend-mode: --value("overlay", "multiply", "normal"), normal;
}
@@ -144,9 +145,7 @@ body {
--color-leaflet-text: var(--color-main-hard);
--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");
+ --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');
--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");
@@ -176,44 +175,7 @@ body {
--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%
- ),
- radial-gradient(
- 63% 34.5% at 66% 77%,
- rgba(190, 227, 248, 0.15) 0%,
- rgba(197, 179, 246, 0.1) 33.33%,
- rgba(255, 0, 0, 0) 100%
- ),
- linear-gradient(
- 0deg,
- #2f5883 0%,
- #2f4e7a 4.75%,
- #2e4472 9.5%,
- #2a3065 19%,
- #272e57 26.5%,
- #242b4b 34%,
- #1f2437 49%,
- #1c2030 57.75%,
- #1a1b29 66.5%,
- #13131b 84%
- );
+ --interlocked-hexagons-background: radial-gradient(33.5% 25% at 100% 82%, rgba(190, 227, 248, 0.2) 0%, rgba(197, 179, 246, 0.13) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(54% 25% at 55% 100%, rgba(190, 227, 248, 0.2) 0%, rgba(197, 179, 246, 0.13) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(37% 19.5% at 20% 100%, rgba(176, 220, 248, 0.3) 0%, rgba(197, 172, 247, 0.2) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(63% 34.5% at 66% 77%, rgba(190, 227, 248, 0.15) 0%, rgba(197, 179, 246, 0.1) 33.33%, rgba(255, 0, 0, 0) 100%), linear-gradient(0deg, #2f5883 0%, #2f4e7a 4.75%, #2e4472 9.5%, #2a3065 19%, #272e57 26.5%, #242b4b 34%, #1f2437 49%, #1c2030 57.75%, #1a1b29 66.5%, #13131b 84%);
--interlocked-hexagons-background-repeat: no-repeat;
--interlocked-hexagons-background-size: 100dvw 100dvh;
--squares-and-triangles-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2260%22%20height%3D%2260%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%2313131b%22%2F%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23363645%22%20stroke-width%3D%22.5%22%20d%3D%22M10%2050H0m60-40H50m0%2040v10M10%200v10m21%2030v20m0-60v20m9%2010h20M0%2030h20m0%2010h20V20H20v10zM10%2010h40v40H10zM0%200h60v60H0z%22%2F%3E%3C%2Fsvg%3E");
@@ -243,10 +205,10 @@ body {
--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://storage.yandexcloud.kz/sayauz-static/themes/ram/ram-background.webp');
--interlocked-hexagons-background-repeat: repeat;
--interlocked-hexagons-background-size: auto;
- --squares-and-triangles-background: url("https://storage.yandexcloud.kz/sayauz-static/themes/ram/ram-sidebar.webp");
+ --squares-and-triangles-background: url('https://storage.yandexcloud.kz/sayauz-static/themes/ram/ram-sidebar.webp');
--squares-and-triangles-background-repeat: repeat;
--squares-and-triangles-background-width-coef: 5;
--squares-and-triangles-background-height-coef: 15;
@@ -257,7 +219,7 @@ body {
--color-accent-deep: var(--color-indigo-800);
--color-main-dark-hard: #343b58;
--color-main-dark-medium: #414868;
- --color-main-dark-soft: #6c6e75;
+ --color-main-dark-soft: #6c6e75;
--color-main-light-hard: #e3e6ff;
--color-main-light-medium: #d1d6ff;
--color-main-light-soft: #c3caff;
@@ -280,18 +242,16 @@ 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-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');
}
-a,
-.linklike {
+a, .linklike {
color: var(--color-main-medium) !important;
text-decoration: underline;
}
-a:hover,
-.linklike:hover {
+a:hover, .linklike:hover {
color: var(--color-main-soft) !important;
}
@@ -311,7 +271,7 @@ a:hover,
.crossable.disabled::before,
.crossable.htmx-request::before {
- content: "";
+ content: '';
position: absolute;
top: 0;
left: 0;
@@ -357,18 +317,10 @@ form.crossable.htmx-request input {
.text-sidebar {
color: var(--color-main-dark-soft);
- text-shadow:
- -0.2rem 0 var(--color-main-dark-hard),
- 0 -0.2rem var(--color-main-dark-hard),
- 0.2rem 0rem var(--color-main-light-soft),
- 0 0.2rem var(--color-main-light-soft);
+ text-shadow: -0.2rem 0 var(--color-main-dark-hard), 0 -0.2rem var(--color-main-dark-hard), 0.2rem 0rem var(--color-main-light-soft), 0 0.2rem var(--color-main-light-soft);
-webkit-text-stroke-color: var(--color-sidebar-stroke);
-webkit-text-stroke-width: 0.1rem;
- background: linear-gradient(
- 135deg,
- color-mix(in srgb, var(--color-main-light-soft) 20%, transparent),
- color-mix(in srgb, var(--color-main-dark-soft) 20%, transparent)
- );
+ background: linear-gradient(135deg, color-mix(in srgb, var(--color-main-light-soft) 20%, transparent), color-mix(in srgb, var(--color-main-dark-soft) 20%, transparent));
}
.text-secondary {
@@ -386,15 +338,11 @@ form.crossable.htmx-request input {
.bg-sidebar {
background-image: var(--squares-and-triangles-background);
background-repeat: var(--squares-and-triangles-background-repeat);
- background-size: calc(
- var(--squares-and-triangles-background-width-coef) * 1rem
- )
- calc(var(--squares-and-triangles-background-height-coef) * 1rem);
+ background-size: calc(var(--squares-and-triangles-background-width-coef) * 1rem) calc(var(--squares-and-triangles-background-height-coef) * 1rem);
}
.inset-shadow {
- box-shadow:
- inset 0dvh 1dvh 1dvh -1dvh rgba(75, 81, 58, 0.5),
+ 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);
@@ -456,7 +404,7 @@ form.crossable.htmx-request input {
}
.theme-icon.active::after {
- content: "";
+ content: '';
position: absolute;
top: 0;
left: 0;
@@ -471,8 +419,7 @@ form.crossable.htmx-request input {
position: relative;
background: color-mix(in srgb, var(--color-main-soft) 75%, transparent);
box-shadow: 0rem 0rem 1rem var(--color-main-hard);
- border: 0.5rem solid
- color-mix(in srgb, var(--color-main-soft) 90%, transparent);
+ border: 0.5rem solid color-mix(in srgb, var(--color-main-soft) 90%, transparent);
backdrop-filter: blur(2px);
top: 35%;
left: 35%;
@@ -488,11 +435,7 @@ form.crossable.htmx-request input {
left: 0;
width: 100%;
height: 100%;
- background: linear-gradient(
- 135deg,
- var(--color-main-light-hard),
- var(--color-main-dark-hard)
- );
+ background: linear-gradient(135deg, var(--color-main-light-hard), var(--color-main-dark-hard));
opacity: 0.2;
z-index: 10;
content: "";
@@ -515,16 +458,14 @@ form.crossable.htmx-request input {
--border-size: 0.5rem;
}
-.themed-button:active,
-.themed-button.active {
+.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 > * {
+.themed-button:active > *, .themed-button.active > * {
transform: translateY(calc(var(--border-size) / 2));
}
@@ -532,10 +473,7 @@ form.crossable.htmx-request input {
--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);
+ 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 {
@@ -548,7 +486,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(2px 2px 2px rgba(0,0,0,0.3));
transition: transform 0.2s;
}
@@ -646,8 +584,7 @@ form.crossable.htmx-request input {
}
@container main (width >= 76rem) {
- .grid-item,
- .grid-sizer {
+ .grid-item, .grid-sizer {
width: 18rem;
}
@@ -671,20 +608,12 @@ form.crossable.htmx-request input {
}
.goverlay {
- background: color-mix(
- in srgb,
- var(--color-background-light) 50%,
- transparent
- ) !important;
+ background: color-mix(in srgb, var(--color-background-light) 50%, transparent) !important;
backdrop-filter: blur(1rem);
}
.glightbox-clean .gslide-description {
- background: color-mix(
- in srgb,
- var(--color-background-dark) 50%,
- transparent
- ) !important;
+ background: color-mix(in srgb, var(--color-background-dark) 50%, transparent) !important;
backdrop-filter: blur(1rem);
}
diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html
index 0bd3454..93567d3 100644
--- a/views/layouts/general-page.html
+++ b/views/layouts/general-page.html
@@ -1,725 +1,469 @@
-<!doctype html>
+<!DOCTYPE html>
<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- {{- range .Meta }}
- <meta
- {{with
- .Name}}name="{{.}}"
- {{end}}{{with
- .Property}}property="{{.}}"
- {{end}}{{with
- .Content}}content="{{.}}"
- {{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"
- />
- <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"
- />
- <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"
- />
- <link rel="stylesheet" href="/output.css" />
- <link
- rel="icon"
- href="/favicon-light.svg"
- type="image/svg+xml"
- media="(prefers-color-scheme: light)"
- />
- <link
- rel="icon"
- href="/favicon-dark.svg"
- type="image/svg+xml"
- media="(prefers-color-scheme: dark)"
- />
- {{- with .LinkedData }}
- <script type="application/ld+json">
- {{ . }}
- </script>
- {{- end }}
- </head>
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ {{- range .Meta }}
+ <meta {{with .Name}}name="{{.}}" {{end}}{{with .Property}}property="{{.}}" {{end}}{{with .Content}}content="{{.}}"{{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 }}/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">
+ <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">
+ <link href="/output.css" rel="stylesheet">
+ {{- with .LinkedData }}
+ <script type="application/ld+json">
+ {{ . }}
+ </script>
+ {{- end }}
+</head>
- <body
- data-theme="ram"
- class="font-m-plus text-main-hard overflow-hidden bg-interlocked-hexagons h-dvh flex flex-row"
- >
- <script>
- let map = null;
- </script>
+<body data-theme="ram" class="font-m-plus text-main-hard overflow-hidden bg-interlocked-hexagons h-dvh flex flex-row">
- <script>
- function changeUrl(path, toAppend = false) {
- const urlParams = new URLSearchParams(window.location.search);
- newPathname = location.pathname.endsWith("/")
- ? location.pathname.slice(0, -1)
- : location.pathname;
- while (path.slice(-3) == "/.." && toAppend) {
- newPathname = newPathname.replace(/\/[^\/]+$/, "");
- path = path.slice(0, -3);
- }
- const newPath = toAppend ? `${newPathname}${path}` : `${path}`;
+ <script>
+ let map = null;
+ </script>
- history.pushState(
- {},
- "",
- `${newPath}${urlParams.entries.length == 0 ? "" : "?" + urlParams.toString()}`,
- );
- window.dispatchEvent(new Event("popstate"));
- return false;
+ <script>
+ function changeUrl(path, toAppend = false) {
+ const urlParams = new URLSearchParams(window.location.search);
+ newPathname = location.pathname.endsWith('/') ? location.pathname.slice(0, -1) : location.pathname;
+ while (path.slice(-3) == '/..' && toAppend) {
+ newPathname = newPathname.replace(/\/[^\/]+$/, '');
+ path = path.slice(0, -3);
}
+ const newPath = toAppend ? `${newPathname}${path}` : `${path}`;
- function convertRemToPixels(rem) {
- return (
- rem *
- parseFloat(
- getComputedStyle(document.documentElement).fontSize,
- )
- );
- }
- </script>
+ history.pushState({}, '', `${newPath}${urlParams.entries.length == 0 ? '' : '?' + urlParams.toString()}`);
+ window.dispatchEvent(new Event('popstate'));
+ return false;
+ };
- <script src="{{ .StaticStorageBaseUrl }}/libs/glightbox/3.3.0/js/glightbox.min.js"></script>
+ function convertRemToPixels(rem) {
+ return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
+ }
+ </script>
- <script>
- function sfc32(a, b, c, d) {
- return function () {
- a |= 0;
- b |= 0;
- c |= 0;
- d |= 0;
- let t = (((a + b) | 0) + d) | 0;
- d = (d + 1) | 0;
- a = b ^ (b >>> 9);
- b = (c + (c << 3)) | 0;
- c = (c << 21) | (c >>> 11);
- c = (c + t) | 0;
- return (t >>> 0) / 4294967296;
- };
+ <script src="{{ .StaticStorageBaseUrl }}/libs/glightbox/3.3.0/js/glightbox.min.js"></script>
+
+ <script>
+ function sfc32(a, b, c, d) {
+ return function() {
+ a |= 0; b |= 0; c |= 0; d |= 0;
+ let t = (a + b | 0) + d | 0;
+ d = d + 1 | 0;
+ a = b ^ b >>> 9;
+ b = c + (c << 3) | 0;
+ c = (c << 21 | c >>> 11);
+ c = c + t | 0;
+ return (t >>> 0) / 4294967296;
}
+ }
- function cyrb128(str) {
- let h1 = 1779033703,
- h2 = 3144134277,
- h3 = 1013904242,
- h4 = 2773480762;
- for (let i = 0, k; i < str.length; i++) {
- k = str.charCodeAt(i);
- h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
- h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
- h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
- h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
- }
- h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
- h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
- h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
- h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
- ((h1 ^= h2 ^ h3 ^ h4), (h2 ^= h1), (h3 ^= h1), (h4 ^= h1));
- return [h1 >>> 0, h2 >>> 0, h3 >>> 0, h4 >>> 0];
+ function cyrb128(str) {
+ let h1 = 1779033703, h2 = 3144134277,
+ h3 = 1013904242, h4 = 2773480762;
+ for (let i = 0, k; i < str.length; i++) {
+ k = str.charCodeAt(i);
+ h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
+ h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
+ h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
+ h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
}
+ h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
+ h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
+ h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
+ h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
+ h1 ^= (h2 ^ h3 ^ h4), h2 ^= h1, h3 ^= h1, h4 ^= h1;
+ return [h1>>>0, h2>>>0, h3>>>0, h4>>>0];
+ }
- var seed = cyrb128("{{ .Title }}");
- var rand = sfc32(seed[0], seed[1], seed[2], seed[3]);
+ 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 calculateVmin(percent) {
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
- const smallerDimension = Math.min(
- viewportWidth,
- viewportHeight,
- );
+ const smallerDimension = Math.min(viewportWidth, viewportHeight);
- const vminValue = (smallerDimension / 100) * percent;
+ const vminValue = (smallerDimension / 100) * percent;
- return vminValue;
- }
+ return vminValue;
+ }
- function switchThemeDecor(theme) {
- document.querySelectorAll(".theme-decor").forEach((e) => {
- e.remove();
- });
+ function switchThemeDecor(theme) {
+ document.querySelectorAll('.theme-decor').forEach((e) => {
+ e.remove();
+ });
- if (theme == "" || typeof theme == "undefined") {
- theme = document.body.getAttribute("data-theme");
- }
+ if (theme == "" || typeof theme == "undefined") {
+ theme = document.body.getAttribute('data-theme');
+ }
- vmin = calculateVmin(1);
- var clWidth = document.documentElement.clientWidth;
- var clHeight = document.documentElement.clientHeight;
+ vmin = calculateVmin(1);
+ var clWidth = document.documentElement.clientWidth;
+ var clHeight = document.documentElement.clientHeight;
- switch (theme) {
- case "olive":
- break;
- case "lettuce":
- break;
- case "night":
- for (i = 0; i < 25; i++) {
- let star = document.createElement("div");
- star.append("*");
- star.classList.add(
- "night-star",
- "theme-decor",
- "z-0",
- "absolute",
- );
- const x = Math.floor(rand() * clWidth);
- const y = Math.floor(rand() * clHeight * 0.8);
- const rot = rand() * 90;
- const animDuration = rand() * 10 + 3;
- const sz = Math.floor(rand() * 8) + 12;
- star.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rot}deg)`;
- star.style.animationDuration = `${animDuration}s`;
- star.style.fontSize = `${sz}px`;
- document.body.append(star);
- }
- break;
- case "ram":
- const frameCenters = new Array(4);
- for (i = 1; i <= 4; i++) {
- let frame = document.createElement("img");
- frame.src = `{{ .StaticStorageBaseUrl }}/themes/ram/ram-frame${i}.webp`;
- frame.classList.add(
- "ram-frame",
- "theme-decor",
- "z-0",
- "absolute",
- );
- let x, y;
- outerLoop: while (true) {
- x = Math.floor(rand() * clWidth - 20 * vmin);
- y = Math.floor(rand() * clHeight - 20 * vmin);
- for (j = 0; j < i - 1; j++) {
- const xd = frameCenters[j].x - x;
- const yd = frameCenters[j].y - y;
- if (
- Math.sqrt(xd * xd + yd * yd) <
- 30 * vmin
- ) {
- continue outerLoop;
- }
+ switch (theme) {
+ case "olive": break;
+ case "lettuce": break;
+ case "night":
+ for (i = 0; i < 25; i++) {
+ let star = document.createElement("div");
+ star.append("*");
+ star.classList.add("night-star", "theme-decor", "z-0", "absolute");
+ const x = Math.floor(rand() * clWidth);
+ const y = Math.floor(rand() * clHeight * 0.8);
+ const rot = rand() * 90;
+ const animDuration = (rand() * 10) + 3;
+ const sz = Math.floor(rand() * 8) + 12;
+ star.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rot}deg)`;
+ star.style.animationDuration = `${animDuration}s`;
+ star.style.fontSize = `${sz}px`;
+ document.body.append(star);
+ }
+ break;
+ case "ram":
+ const frameCenters = new Array(4);
+ for (i = 1; i <= 4; i++) {
+ let frame = document.createElement("img");
+ frame.src = `{{ .StaticStorageBaseUrl }}/themes/ram/ram-frame${i}.webp`;
+ frame.classList.add("ram-frame", "theme-decor", "z-0", "absolute");
+ let x, y;
+ outerLoop:
+ while (true) {
+ x = Math.floor(rand() * clWidth - 20*vmin);
+ y = Math.floor(rand() * clHeight - 20*vmin);
+ for (j = 0; j < i-1; j++) {
+ const xd = frameCenters[j].x - x;
+ const yd = frameCenters[j].y - y;
+ if (Math.sqrt(xd*xd + yd*yd) < 30*vmin) {
+ continue outerLoop;
}
- break;
}
- frameCenters[i - 1] = { x: x, y: y };
- const rot = rand() * 90 - 45;
- frame.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rot}deg)`;
- document.body.append(frame);
+ break;
}
- break;
- case "lilac":
- break;
- }
+ frameCenters[i-1] = {x: x, y: y};
+ const rot = rand() * 90 - 45;
+ frame.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rot}deg)`;
+ document.body.append(frame);
+ }
+ break;
+ case "lilac": break;
}
+ }
- function switchTheme(theme) {
- if (theme == "" || typeof theme == "undefined") {
- const currentTheme =
- document.body.getAttribute("data-theme");
- switch (currentTheme) {
- case "olive":
- theme = "lettuce";
- break;
- case "lettuce":
- theme = "night";
- break;
- case "night":
- theme = "ram";
- break;
- case "ram":
- theme = "lilac";
- break;
- case "lilac":
- theme = "olive";
- break;
- }
+ function switchTheme(theme) {
+ if (theme == "" || typeof theme == "undefined") {
+ const currentTheme = document.body.getAttribute('data-theme');
+ switch (currentTheme) {
+ case "olive": theme = "lettuce"; break;
+ case "lettuce": theme = "night"; break;
+ case "night": theme = "ram"; break;
+ case "ram": theme = "lilac"; break;
+ case "lilac": theme = "olive"; break;
}
- localStorage.setItem("theme", theme);
- document.body.setAttribute("data-theme", theme);
- switchThemeDecor(theme);
}
+ localStorage.setItem('theme', theme);
+ document.body.setAttribute('data-theme', theme);
+ switchThemeDecor(theme);
+ }
- const savedTheme = localStorage.getItem("theme") || "lilac";
- switchTheme(savedTheme);
- </script>
+ const savedTheme = localStorage.getItem('theme') || 'lilac';
+ switchTheme(savedTheme);
+ </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>
- <div
- id="general-page-top-embeds-trigger"
- hx-get="/api/v1/general-page/top-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
- hx-trigger="popstate from:window"
- hx-target="#general-page-top-embeds"
- hx-swap="innerHTML"
- ></div>
+ <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>
+ <div id="general-page-top-embeds-trigger" hx-get="/api/v1/general-page/top-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-top-embeds" hx-swap="innerHTML"></div>
- <script>
- const clientTimeZone =
- Intl.DateTimeFormat().resolvedOptions().timeZone;
- </script>
+ <script>
+ const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ </script>
- <div class="grow bg-split-left"></div>
+ <div class="grow bg-split-left"></div>
- <div class="flex flex-col sm:flex-row">
- <div
- id="sidebar"
- class="bg-sidebar flex flex-row sticky w-dvw h-fit xs:w-[90dvw] sm:left-0 sm:flex-col sm:fixed sm:w-fit z-20 shadow-[0_0_1rem_black]"
- >
- <nav
- aria-label="Main navigation"
- class="text-[3rem] flex flex-row sm:flex-col gap-0 relative z-20"
- >
- <a
- onclick="return changeUrl('/..', true);"
- href="../"
- id="sidebar-back-button"
- class="themed-button themed-button-sidebar w-fit h-fit hidden"
- draggable="false"
- >
- <i
- class="fas fa-circle-left flex faw-18 h-18 content-center text-center text-sidebar"
- ></i>
- </a>
- <a
- onclick="return changeUrl('/{{ .Lang }}');"
- href="/{{ .Lang }}"
- id="sidebar-home-button"
- class="themed-button themed-button-sidebar w-fit h-fit hidden"
- draggable="false"
- >
- <i
- class="fas fa-home flex faw-18 h-18 content-center text-center text-sidebar"
- ></i>
- </a>
- <a
- onclick="return changeUrl('/{{ .Lang }}/blog');"
- href="/{{ .Lang }}/blog"
- id="sidebar-blog-button"
- class="themed-button themed-button-sidebar w-fit h-fit hidden"
- draggable="false"
- >
- <i
- class="fas fa-search flex faw-18 h-18 content-center text-center text-sidebar"
- ></i>
- </a>
- <a
- href="/{{ .Lang }}/map"
- id="sidebar-map-button"
- class="themed-button themed-button-sidebar w-fit h-fit hidden"
- draggable="false"
- >
- <i
- class="fas fa-map flex faw-18 h-18 content-center text-center text-sidebar"
- ></i>
- </a>
- <div
- id="toolbar-theme-button"
- onclick="toggleThemeWheel()"
- class="themed-button themed-button-sidebar w-fit h-fit"
- draggable="false"
- >
- <i
- id="toolbar-theme-button-content"
- class="fas fa-swatchbook flex faw-18 h-18 content-center text-center text-sidebar"
- ></i>
- </div>
- <a
- onclick="return changeUrl('/{{ .Lang }}/user');"
- href="/{{ .Lang }}/user"
- id="sidebar-user-button"
- class="themed-button themed-button-sidebar w-fit h-fit hidden"
- draggable="false"
- >
- <i
- class="fas fa-user flex faw-18 h-18 content-center text-center text-sidebar"
- ></i>
- </a>
- </nav>
- <div class="theme-wheel" id="theme-wheel">
- <div class="theme-icon" data-theme="olive"></div>
- <div class="theme-icon" data-theme="lettuce"></div>
- <div class="theme-icon" data-theme="night"></div>
- <div class="theme-icon" data-theme="ram"></div>
- <div class="theme-icon" data-theme="lilac"></div>
- <div class="theme-pin"></div>
+ <div class="flex flex-col sm:flex-row">
+ <div id="sidebar" class="bg-sidebar flex
+ flex-row sticky w-dvw h-fit xs:w-[90dvw]
+ sm:left-0 sm:flex-col sm:fixed sm:w-fit
+ z-20 shadow-[0_0_1rem_black]">
+ <nav aria-label="Main navigation" class="text-[3rem] flex flex-row sm:flex-col gap-0 relative z-20">
+ <a onclick="return changeUrl('/..', true);" href="../" id="sidebar-back-button" class="themed-button themed-button-sidebar w-fit h-fit hidden" draggable="false">
+ <i class="fas fa-circle-left flex faw-18 h-18 content-center text-center text-sidebar"></i>
+ </a>
+ <a onclick="return changeUrl('/{{ .Lang }}');" href="/{{ .Lang }}" id="sidebar-home-button" class="themed-button themed-button-sidebar w-fit h-fit hidden" draggable="false">
+ <i class="fas fa-home flex faw-18 h-18 content-center text-center text-sidebar"></i>
+ </a>
+ <a onclick="return changeUrl('/{{ .Lang }}/blog');" href="/{{ .Lang }}/blog" id="sidebar-blog-button" class="themed-button themed-button-sidebar w-fit h-fit hidden" draggable="false">
+ <i class="fas fa-search flex faw-18 h-18 content-center text-center text-sidebar"></i>
+ </a>
+ <a href="/{{ .Lang }}/map" id="sidebar-map-button" class="themed-button themed-button-sidebar w-fit h-fit hidden" draggable="false">
+ <i class="fas fa-map flex faw-18 h-18 content-center text-center text-sidebar"></i>
+ </a>
+ <div id="toolbar-theme-button" onclick="toggleThemeWheel();" class="themed-button themed-button-sidebar w-fit h-fit" draggable="false">
+ <i id="toolbar-theme-button-content" class="fas fa-swatchbook flex faw-18 h-18 content-center text-center text-sidebar"></i>
</div>
- <script>
- sidebarHomeButton = document.getElementById(
- "sidebar-home-button",
- );
- sidebarMapButton =
- document.getElementById("sidebar-map-button");
- sidebarBlogButton = document.getElementById(
- "sidebar-blog-button",
- );
- sidebarBackButton = document.getElementById(
- "sidebar-back-button",
- );
- sidebarUserButton = document.getElementById(
- "sidebar-user-button",
- );
-
- function updateSidebar() {
- if (
- location.pathname == "" ||
- location.pathname == "/"
- ) {
- sidebarHomeButton.classList.add("hidden");
- sidebarMapButton.classList.add("hidden");
- sidebarBlogButton.classList.add("hidden");
- sidebarBackButton.classList.add("hidden");
- sidebarUserButton.classList.add("hidden");
- } else {
- sidebarHomeButton.classList.remove("hidden");
- sidebarMapButton.classList.remove("hidden");
- sidebarBlogButton.classList.remove("hidden");
- sidebarBackButton.classList.remove("hidden");
- sidebarUserButton.classList.remove("hidden");
- lang = location.pathname.split("/")[1];
- sidebarHomeButton.setAttribute("href", `/${lang}`);
- sidebarHomeButton.setAttribute(
- "onclick",
- `return changeUrl('/${lang}', false);`,
- );
- sidebarMapButton.setAttribute(
- "href",
- `/${lang}/map`,
- );
- sidebarBlogButton.setAttribute(
- "href",
- `/${lang}/blog`,
- );
- sidebarBlogButton.setAttribute(
- "onclick",
- `return changeUrl('/${lang}/blog', false);`,
- );
- sidebarUserButton.setAttribute(
- "href",
- `/${lang}/user`,
- );
- sidebarUserButton.setAttribute(
- "onclick",
- `return changeUrl('/${lang}/user', false);`,
- );
- }
- }
- window.addEventListener("popstate", updateSidebar);
- window.addEventListener("load", updateSidebar, {
- once: true,
- });
- </script>
+ <a onclick="return changeUrl('/{{ .Lang }}/user');" href="/{{ .Lang }}/user" id="sidebar-user-button" class="themed-button themed-button-sidebar w-fit h-fit hidden" draggable="false">
+ <i class="fas fa-user flex faw-18 h-18 content-center text-center text-sidebar"></i>
+ </a>
+ </nav>
+ <div class="theme-wheel" id="theme-wheel">
+ <div class="theme-icon" data-theme="olive"></div>
+ <div class="theme-icon" data-theme="lettuce"></div>
+ <div class="theme-icon" data-theme="night"></div>
+ <div class="theme-icon" data-theme="ram"></div>
+ <div class="theme-icon" data-theme="lilac"></div>
+ <div class="theme-pin"></div>
</div>
+ <script>
+ sidebarHomeButton = document.getElementById("sidebar-home-button");
+ sidebarMapButton = document.getElementById("sidebar-map-button");
+ sidebarBlogButton = document.getElementById("sidebar-blog-button");
+ sidebarBackButton = document.getElementById("sidebar-back-button");
+ sidebarUserButton = document.getElementById("sidebar-user-button");
- <div
- class="bg-paper bg-background-dark @container/main flex z-10 w-dvw xs:w-[90dvw] sm:w-[85dvw] md:w-[80dvw] lg:w-[75dvw] xl:w-[70dvw] 2xl:w-[60dvw] 3xl:w-[50dvw] grow-0 h-[calc(100dvh-5rem)] sm:h-dvh shadow-[0_0_1rem_black]"
- >
- <div
- class="bg-paper bg-background-dark border-r-2 border-dashed border-main-hard"
- ></div>
+ function updateSidebar() {
+ if (location.pathname == "" || location.pathname == "/") {
+ sidebarHomeButton.classList.add('hidden');
+ sidebarMapButton.classList.add('hidden');
+ sidebarBlogButton.classList.add('hidden');
+ sidebarBackButton.classList.add('hidden');
+ sidebarUserButton.classList.add('hidden');
+ } else {
+ sidebarHomeButton.classList.remove('hidden');
+ sidebarMapButton.classList.remove('hidden');
+ sidebarBlogButton.classList.remove('hidden');
+ sidebarBackButton.classList.remove('hidden');
+ sidebarUserButton.classList.remove('hidden');
+ lang = location.pathname.split("/")[1];
+ sidebarHomeButton.setAttribute('href', `/${lang}`);
+ sidebarHomeButton.setAttribute('onclick', `return changeUrl('/${lang}', false);`);
+ sidebarMapButton.setAttribute('href', `/${lang}/map`);
+ sidebarBlogButton.setAttribute('href', `/${lang}/blog`);
+ sidebarBlogButton.setAttribute('onclick', `return changeUrl('/${lang}/blog', false);`);
+ sidebarUserButton.setAttribute('href', `/${lang}/user`);
+ sidebarUserButton.setAttribute('onclick', `return changeUrl('/${lang}/user', false);`);
+ }
+ }
+ window.addEventListener("popstate", updateSidebar);
+ window.addEventListener("load", updateSidebar, {once: true});
+ </script>
+ </div>
- <div
- class="max-h-full max-w-full flex flex-col grow bg-paper bg-background-dark relative pt-4 ml-8 pr-4"
- >
- <header
- id="general-page-header"
- class="max-xs:hidden [@media(max-height:32rem)]:hidden flex flex-col basis-8"
- hx-get="/api/v1/general-page/header{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
- hx-trigger="load"
- hx-target="this"
- hx-swap="innerHTML"
- ></header>
- <div
- id="general-page-header-trigger"
- hx-get="/api/v1/general-page/header{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
- hx-trigger="popstate from:window"
- hx-target="#general-page-header"
- hx-swap="innerHTML"
- ></div>
+ <div class="bg-paper bg-background-dark @container/main flex z-10 w-dvw xs:w-[90dvw] sm:w-[85dvw] md:w-[80dvw] lg:w-[75dvw] xl:w-[70dvw] 2xl:w-[60dvw] 3xl:w-[50dvw] grow-0 h-[calc(100dvh-5rem)] sm:h-dvh shadow-[0_0_1rem_black]">
+ <div class="bg-paper bg-background-dark border-r-2 border-dashed border-main-hard"></div>
- <hr
- class="max-xs:hidden [@media(max-height:32rem)]:hidden border-t-4 border-dotted border-main-hard mb-2"
- />
+ <div class="max-h-full max-w-full flex flex-col grow bg-paper bg-background-dark relative pt-4 ml-8 pr-4">
+ <header id="general-page-header" class="max-xs:hidden [@media(max-height:32rem)]:hidden flex flex-col basis-8"
+ hx-get="/api/v1/general-page/header{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML">
+ </header>
+ <div id="general-page-header-trigger" hx-get="/api/v1/general-page/header{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-header" hx-swap="innerHTML"></div>
- <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>
- <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"
- ></div>
+ <hr class="max-xs:hidden [@media(max-height:32rem)]:hidden border-t-4 border-dotted border-main-hard mb-2">
- <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>
- <div
- id="general-page-footer-trigger"
- hx-get="/api/v1/general-page/footer{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
- hx-trigger="popstate from:window"
- hx-target="#general-page-footer"
- hx-swap="innerHTML"
- ></div>
+ <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>
+ <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"></div>
- <hr
- class="border-t-4 border-dotted border-main-hard mb-2"
- />
- </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>
+ <div id="general-page-footer-trigger" hx-get="/api/v1/general-page/footer{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-footer" hx-swap="innerHTML"></div>
- <div
- class="pr-[0.5dvw] bg-paper bg-background-dark shadow-2xl border-l-2 border-dotted border-main-hard"
- ></div>
+ <hr class="border-t-4 border-dotted border-main-hard mb-2">
</div>
+
+ <div class="pr-[0.5dvw] bg-paper bg-background-dark shadow-2xl border-l-2 border-dotted border-main-hard"></div>
</div>
+ </div>
- <div class="grow bg-split-right"></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 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();
- };
+ <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;
- }
+ 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,
- };
+ return {
+ width: this.fitWidth - this.gutter - emptyWidth,
+ height: this.maxY - this.gutter
};
+ };
- Packery.prototype.needsResizeLayout = function () {
- return true;
- };
- </script>
+ 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-trigger"
- hx-get="/api/v1/general-page/bottom-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}"
- hx-trigger="popstate from:window"
- hx-target="#general-page-bottom-embeds"
- hx-swap="innerHTML"
- ></div>
+ <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-trigger" hx-get="/api/v1/general-page/bottom-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-bottom-embeds" hx-swap="innerHTML"></div>
- <script>
- const themeWheel = document.getElementById("theme-wheel");
- const themeButton = document.getElementById("toolbar-theme-button");
- const themeIcons = document.querySelectorAll(".theme-icon");
- const themeWheelRadius = convertRemToPixels(6);
+ <script>
+ const themeWheel = document.getElementById('theme-wheel');
+ const themeButton = document.getElementById('toolbar-theme-button');
+ const themeIcons = document.querySelectorAll('.theme-icon');
+ const themeWheelRadius = convertRemToPixels(6);
- themeIcons.forEach((icon) => {
- if (icon.getAttribute("data-theme") == savedTheme) {
- icon.classList.add("active");
- }
- });
-
- function calculateVmax(percent) {
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
-
- const largerDimension = Math.max(viewportWidth, viewportHeight);
+ themeIcons.forEach(icon => {
+ if (icon.getAttribute('data-theme') == savedTheme) {
+ icon.classList.add('active');
+ }
+ });
- const vmaxValue = (largerDimension / 100) * percent;
+ function calculateVmax(percent) {
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
- return vmaxValue;
- }
+ const largerDimension = Math.max(viewportWidth, viewportHeight);
- function positionThemeIcons() {
- const sliceAngle = 360 / themeIcons.length;
+ const vmaxValue = (largerDimension / 100) * percent;
- themeIcons.forEach((icon, index) => {
- const startAngle = index * sliceAngle;
- const endAngle = (index + 1) * sliceAngle;
+ return vmaxValue;
+ }
- const clipPath = createSliceClipPath(
- startAngle,
- endAngle,
- themeWheelRadius,
- );
- icon.style.clipPath = clipPath;
- });
- }
+ function positionThemeIcons() {
+ const sliceAngle = 360 / themeIcons.length;
- function createSliceClipPath(startAngle, endAngle, radius) {
- const centerX = radius;
- const centerY = radius;
+ themeIcons.forEach((icon, index) => {
+ const startAngle = index * sliceAngle;
+ const endAngle = (index + 1) * sliceAngle;
- let points = `${centerX}px ${centerY}px`;
+ const clipPath = createSliceClipPath(startAngle, endAngle, themeWheelRadius);
+ icon.style.clipPath = clipPath;
+ });
+ }
- const segments = Math.max(
- 2,
- Math.ceil(Math.abs(endAngle - startAngle) / 5),
- );
- const angleStep = (endAngle - startAngle) / segments;
+ function createSliceClipPath(startAngle, endAngle, radius) {
+ const centerX = radius;
+ const centerY = radius;
- for (let i = 0; i <= segments; i++) {
- const angle =
- ((startAngle + angleStep * i - 90) * Math.PI) / 180;
- const x = centerX + Math.cos(angle) * radius;
- const y = centerY + Math.sin(angle) * radius;
- points += `, ${x}px ${y}px`;
- }
+ let points = `${centerX}px ${centerY}px`;
- points += `, ${centerX}px ${centerY}px`;
+ const segments = Math.max(2, Math.ceil(Math.abs(endAngle - startAngle) / 5));
+ const angleStep = (endAngle - startAngle) / segments;
- return `polygon(${points})`;
+ for (let i = 0; i <= segments; i++) {
+ const angle = ((startAngle + angleStep * i - 90) * Math.PI) / 180;
+ const x = centerX + Math.cos(angle) * radius;
+ const y = centerY + Math.sin(angle) * radius;
+ points += `, ${x}px ${y}px`;
}
- function positionThemeWheel() {
- const buttonRect = themeButton.getBoundingClientRect();
- const buttonCenterX = buttonRect.left + buttonRect.width / 2;
- const buttonCenterY = buttonRect.top + buttonRect.height / 2;
-
- const margin = 0;
+ points += `, ${centerX}px ${centerY}px`;
+
+ return `polygon(${points})`;
+ }
- let wheelX = buttonCenterX - themeWheelRadius;
- let wheelY = buttonCenterY - themeWheelRadius;
+ function positionThemeWheel() {
+ const buttonRect = themeButton.getBoundingClientRect();
+ const buttonCenterX = buttonRect.left + buttonRect.width / 2;
+ const buttonCenterY = buttonRect.top + buttonRect.height / 2;
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
+ const margin = 0;
- if (wheelX < margin) {
- wheelX = margin;
- } else if (
- wheelX + themeWheelRadius * 2 >
- viewportWidth - margin
- ) {
- wheelX = viewportWidth - themeWheelRadius * 2 - margin;
- }
+ let wheelX = buttonCenterX - themeWheelRadius;
+ let wheelY = buttonCenterY - themeWheelRadius;
- if (wheelY < margin) {
- wheelY = margin;
- } else if (
- wheelY + themeWheelRadius * 2 >
- viewportHeight - margin
- ) {
- wheelY = viewportHeight - themeWheelRadius * 2 - margin;
- }
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
- themeWheel.style.left = wheelX + "px";
- themeWheel.style.top = wheelY + "px";
+ if (wheelX < margin) {
+ wheelX = margin;
+ } else if (wheelX + (themeWheelRadius * 2) > viewportWidth - margin) {
+ wheelX = viewportWidth - (themeWheelRadius * 2) - margin;
}
- function hideThemeWheel() {
- themeWheel.classList.remove("active");
+ if (wheelY < margin) {
+ wheelY = margin;
+ } else if (wheelY + (themeWheelRadius * 2) > viewportHeight - margin) {
+ wheelY = viewportHeight - (themeWheelRadius * 2) - margin;
}
- function showThemeWheel() {
- positionThemeWheel();
- positionThemeIcons();
- themeWheel.classList.add("active");
- }
+ themeWheel.style.left = wheelX + 'px';
+ themeWheel.style.top = wheelY + 'px';
+ }
- function toggleThemeWheel() {
- if (themeWheel.classList.contains("active")) {
- hideThemeWheel();
- } else {
- showThemeWheel();
- }
+ function hideThemeWheel() {
+ themeWheel.classList.remove('active');
+ }
+
+ function showThemeWheel() {
+ positionThemeWheel();
+ positionThemeIcons();
+ themeWheel.classList.add('active');
+ }
+
+ function toggleThemeWheel() {
+ if (themeWheel.classList.contains('active')) {
+ hideThemeWheel();
+ } else {
+ showThemeWheel();
}
+ }
- themeIcons.forEach((icon) => {
- icon.addEventListener("click", (e) => {
- e.stopPropagation();
- const theme = icon.getAttribute("data-theme");
- switchTheme(theme);
- themeIcons.forEach((icon) => {
- icon.classList.remove("active");
- });
- icon.classList.add("active");
- });
+ themeIcons.forEach(icon => {
+ icon.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const theme = icon.getAttribute('data-theme');
+ switchTheme(theme);
+ themeIcons.forEach(icon => {
+ icon.classList.remove('active');
+ })
+ icon.classList.add('active');
});
+ });
- themeWheel.addEventListener("mouseleave", hideThemeWheel);
+ themeWheel.addEventListener('mouseleave', hideThemeWheel);
- document.addEventListener("click", (e) => {
- if (
- !themeWheel.contains(e.target) &&
- e.target !== themeButton &&
- !e.target.classList.contains("toolbar-theme-button-content")
- ) {
- hideThemeWheel();
- }
- });
- </script>
+ document.addEventListener('click', (e) => {
+ if (!themeWheel.contains(e.target) && e.target !== themeButton && !e.target.classList.contains("toolbar-theme-button-content")) {
+ hideThemeWheel();
+ }
+ });
+ </script>
- <script>
- let decorResizeTimeout;
- window.addEventListener("resize", function () {
- clearTimeout(decorResizeTimeout);
- decorResizeTimeout = setTimeout(switchThemeDecor, 300);
- });
- </script>
- </body>
+ <script>
+ let decorResizeTimeout;
+ window.addEventListener('resize', function() {
+ clearTimeout(decorResizeTimeout);
+ decorResizeTimeout = setTimeout(switchThemeDecor, 300);
+ });
+ </script>
+
+</body>
</html>
diff --git a/views/pages/global-map.html b/views/pages/global-map.html
index ce9d5b1..aa6ddfd 100644
--- a/views/pages/global-map.html
+++ b/views/pages/global-map.html
@@ -18,10 +18,6 @@
<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
@@ -33,19 +29,7 @@
href="{{ .StaticStorageBaseUrl }}/libs/font-awesome/7.2.0/css/solid.min.css"
/>
{{ block "header" . }}{{ end }}
- <link rel="stylesheet" href="/output.css" />
- <link
- rel="icon"
- href="/favicon-light.svg"
- type="image/svg+xml"
- media="(prefers-color-scheme: light)"
- />
- <link
- rel="icon"
- href="/favicon-dark.svg"
- type="image/svg+xml"
- media="(prefers-color-scheme: dark)"
- />
+ <link href="/output.css" rel="stylesheet" />
<link
rel="stylesheet"
href="{{ .StaticStorageBaseUrl }}/libs/leaflet/1.9.4/leaflet.css"
diff --git a/views/pages/robots.txt b/views/pages/robots.txt
index aaef3f4..1e5bebb 100644
--- a/views/pages/robots.txt
+++ b/views/pages/robots.txt
@@ -1,7 +1,5 @@
User-agent: *
Allow: /
-Disallow: /api/v1/email/
-Disallow: /ru/user/
-Disallow: /en/user/
+Disallow: /api/
Sitemap: {{ .CanonicalEndpoint }}/sitemap.xml \ No newline at end of file
diff --git a/views/pages/unsubscribe-page.html b/views/pages/unsubscribe-page.html
index a917746..c9de473 100644
--- a/views/pages/unsubscribe-page.html
+++ b/views/pages/unsubscribe-page.html
@@ -4,18 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex,nofollow" />
- <link
- rel="icon"
- href="/favicon-light.svg"
- type="image/svg+xml"
- media="(prefers-color-scheme: light)"
- />
- <link
- rel="icon"
- href="/favicon-dark.svg"
- type="image/svg+xml"
- media="(prefers-color-scheme: dark)"
- />
<title>{{ .L.UnsubscribePage.Header }} // SAYA.UZ</title>
<style>
.bg-paper {