summaryrefslogtreecommitdiff
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build-and-deploy-stage.yml89
-rw-r--r--.github/workflows/build-and-deploy.yml (renamed from .github/workflows/build-and-deploy-prod.yml)5
-rw-r--r--Dockerfile11
-rw-r--r--config/config.prod.yaml20
-rw-r--r--config/config.yaml (renamed from config/config.stage.yaml)2
-rw-r--r--deploy/inventory.yml6
-rw-r--r--deploy/playbook.yml1
-rw-r--r--internal/b2/client.go1
-rw-r--r--internal/lightgallery/block.go11
-rw-r--r--internal/lightgallery/html_renderer.go59
-rw-r--r--internal/lightgallery/parser.go35
-rw-r--r--internal/router/api-v1-blog-search.go112
-rw-r--r--internal/router/api-v1-tz.go37
-rw-r--r--internal/router/lang-blog-title.go79
-rw-r--r--internal/router/lang-blog.go93
-rw-r--r--internal/router/lang-map.go85
-rw-r--r--internal/router/root.go27
-rw-r--r--internal/router/tm.go18
-rw-r--r--internal/templatemanager/templatemanager.go24
-rw-r--r--locale/localization.en.yaml2
-rw-r--r--locale/localization.go5
-rw-r--r--locale/localization.ru.yaml2
-rw-r--r--main.go247
-rw-r--r--static/input.css157
-rw-r--r--views/layouts/general-page.html153
-rw-r--r--views/pages/blog-catalogue.html80
-rw-r--r--views/pages/blog-page.html20
-rw-r--r--views/pages/global-map.html269
-rw-r--r--views/partials/catalogue-blog-card-tags.html1
-rw-r--r--views/partials/catalogue-blog-cards.html16
30 files changed, 372 insertions, 1295 deletions
diff --git a/.github/workflows/build-and-deploy-stage.yml b/.github/workflows/build-and-deploy-stage.yml
deleted file mode 100644
index 104d9ac..0000000
--- a/.github/workflows/build-and-deploy-stage.yml
+++ /dev/null
@@ -1,89 +0,0 @@
-name: Docker image building and publishing onto Stage
-on:
- push:
- branches: [stage]
-jobs:
- build-and-push:
- if: github.repository == 'SayaAndy/saya-today-web'
- runs-on: ubuntu-latest
- environment: Stage
- permissions:
- packages: write
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Generate output.css with tailwindcss
- uses: ZoeyVid/tailwindcss-update@main
- with:
- input: static/input.css
- output: static/output.css
- params: "--minify"
-
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v3
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Login to GitHub Container Registry
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.repository_owner }}
- password: ${{ secrets.GHCR_TOKEN }}
-
- - name: Set github short sha
- id: ghss_vars
- run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
-
- - name: Build and push
- uses: docker/build-push-action@v6
- with:
- context: .
- file: ./Dockerfile
- push: true
- tags: |
- ghcr.io/sayaandy/saya-today-web:latest
- ghcr.io/sayaandy/saya-today-web:commit-${{ steps.ghss_vars.outputs.sha_short }}
-
- deploy:
- if: github.repository == 'SayaAndy/saya-today-web'
- runs-on: ubuntu-latest
- environment: Stage
- needs: [build-and-push]
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Set up SSH connection for pl.saya.today
- run: |
- mkdir -p ~/.ssh
- (cat <<EOF
- ${{ secrets.SVC_GITHUB_PK }}
- EOF
- ) > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
- sed -i 's/\r$//' ~/.ssh/id_ed25519
- ssh-keyscan -H pl.saya.today >> ~/.ssh/known_hosts
-
- - name: Test SSH connection
- run: |
- ssh -o ConnectTimeout=10 -i ~/.ssh/id_ed25519 svc_github@pl.saya.today "echo 'SSH connection successful'"
-
- - name: Install Ansible
- shell: bash
- run: |
- sudo apt update
- sudo apt install -y ansible
-
- - name: Set github short sha
- id: ghss_vars
- run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
-
- - name: Run Ansible playbook
- env:
- ANSIBLE_HOST_KEY_CHECKING: False
- working-directory: ./deploy
- run: |
- ansible-playbook -i inventory.yml -l stage playbook.yml --private-key ~/.ssh/id_ed25519 -u svc_github -e saya_today_web_b2_key_id=${{ secrets.B2_KEY_ID }} -e saya_today_web_b2_application_key=${{ secrets.B2_APPLICATION_KEY }} -e saya_today_web_environment=stage -e saya_today_web_tag=commit-${{ steps.ghss_vars.outputs.sha_short }}
diff --git a/.github/workflows/build-and-deploy-prod.yml b/.github/workflows/build-and-deploy.yml
index 53e307e..ac5fa1f 100644
--- a/.github/workflows/build-and-deploy-prod.yml
+++ b/.github/workflows/build-and-deploy.yml
@@ -1,4 +1,4 @@
-name: Docker image building and publishing onto Production
+name: Publish Docker image
on:
push:
tags:
@@ -37,7 +37,6 @@ jobs:
push: true
tags: |
ghcr.io/sayaandy/saya-today-web:latest
- ghcr.io/sayaandy/saya-today-web:stable
ghcr.io/sayaandy/saya-today-web:${{ github.ref_name }}
deploy:
@@ -75,4 +74,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_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 }}
+ ansible-playbook -i inventory.yml -l stage playbook.yml --private-key ~/.ssh/id_ed25519 -u svc_github -e saya_today_web_b2_key_id=${{ secrets.B2_KEY_ID }} -e saya_today_web_b2_application_key=${{ secrets.B2_APPLICATION_KEY }} -e saya_today_web_tag=${{ github.ref_name }}
diff --git a/Dockerfile b/Dockerfile
index cd3e0eb..e3d9e65 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,21 +2,18 @@ FROM golang:1.24.5-alpine3.22 AS build-stage
WORKDIR /builddir
COPY . .
-RUN go build -o sayana-web .
+RUN go build -o sayana-demo .
FROM alpine:3.22.1 AS runtime-stage
-ENV ENVIRONMENT=""
ENV B2_KEY_ID=""
ENV B2_APPLICATION_KEY=""
WORKDIR /app
-COPY --from=build-stage /builddir/sayana-web /app/sayana-web
+COPY --from=build-stage /builddir/sayana-demo /app/sayana-demo
COPY --from=build-stage /builddir/static /app/static
COPY --from=build-stage /builddir/views /app/views
COPY --from=build-stage /builddir/locale/*.yaml /app/locale/
-COPY --from=build-stage /builddir/config/config*.yaml /app/config/
+COPY --from=build-stage /builddir/config/config.yaml /app/config.yaml
-RUN apk add --no-cache tzdata
-
-ENTRYPOINT /app/sayana-web -c /app/config/config.${ENVIRONMENT}.yaml
+ENTRYPOINT [ "/app/sayana-demo", "-c", "/app/config.yaml" ]
diff --git a/config/config.prod.yaml b/config/config.prod.yaml
deleted file mode 100644
index 2d050bf..0000000
--- a/config/config.prod.yaml
+++ /dev/null
@@ -1,20 +0,0 @@
-logLevel: info
-blogPages:
- storage:
- type: b2
- config:
- bucketName: sayana-pages
- region: eu-central-003
- prefix: 'prod-'
- keyID: '${B2_KEY_ID}'
- applicationKey: '${B2_APPLICATION_KEY}'
-localePath: ./locale/
-availableLanguages:
- - name: ru
- alt: Русский
- flag: https://f003.backblazeb2.com/file/sayana-static/flags/ru.svg
- locFile: localization.ru.yaml
- - name: en
- alt: English
- flag: https://f003.backblazeb2.com/file/sayana-static/flags/gb.svg
- locFile: localization.en.yaml
diff --git a/config/config.stage.yaml b/config/config.yaml
index a34a33e..dac7b9a 100644
--- a/config/config.stage.yaml
+++ b/config/config.yaml
@@ -5,7 +5,7 @@ blogPages:
config:
bucketName: sayana-pages
region: eu-central-003
- prefix: 'stage-'
+ prefix: ''
keyID: '${B2_KEY_ID}'
applicationKey: '${B2_APPLICATION_KEY}'
localePath: ./locale/
diff --git a/deploy/inventory.yml b/deploy/inventory.yml
index dde952e..3c1220d 100644
--- a/deploy/inventory.yml
+++ b/deploy/inventory.yml
@@ -2,16 +2,14 @@ all:
children:
prod:
hosts:
- node_prod:
- ansible_host: pl.saya.today
+ pl.saya.today:
saya_today_web_name: sayana-web
saya_today_web_hostname: sayana-web
saya_today_web_ipv4_address: 172.16.0.18
stage:
hosts:
- node_stage:
- ansible_host: pl.saya.today
+ pl.saya.today:
saya_today_web_name: sayana-demo
saya_today_web_hostname: sayana-demo
saya_today_web_ipv4_address: 172.16.0.17
diff --git a/deploy/playbook.yml b/deploy/playbook.yml
index ca22ac3..773bb61 100644
--- a/deploy/playbook.yml
+++ b/deploy/playbook.yml
@@ -12,7 +12,6 @@
env:
B2_KEY_ID: "{{ saya_today_web_b2_key_id }}"
B2_APPLICATION_KEY: "{{ saya_today_web_b2_application_key }}"
- ENVIRONMENT: "{{ saya_today_web_environment }}"
network_mode: caddy
networks:
- name: caddy
diff --git a/internal/b2/client.go b/internal/b2/client.go
index 7b745a8..5322b84 100644
--- a/internal/b2/client.go
+++ b/internal/b2/client.go
@@ -84,7 +84,6 @@ func (c *B2Client) Scan(prefix string) ([]*BlogPage, error) {
PublishedTime: publishedTime,
Thumbnail: attrs.Info["thumbnail"],
Tags: strings.Split(attrs.Info["tags"], ","),
- Geolocation: attrs.Info["geolocation"],
},
})
}
diff --git a/internal/lightgallery/block.go b/internal/lightgallery/block.go
index 8eb7c9e..dade2ea 100644
--- a/internal/lightgallery/block.go
+++ b/internal/lightgallery/block.go
@@ -1,21 +1,16 @@
package lightgallery
-import (
- "time"
-
- "github.com/yuin/goldmark/ast"
-)
+import "github.com/yuin/goldmark/ast"
// LightGalleryBlock represents a light gallery block in the AST
type LightGalleryBlock struct {
ast.BaseBlock
- Images []LightGalleryImage
- Location *time.Location
+ Images []LightGalleryImage
}
type LightGalleryImage struct {
URL string
- Caption []byte
+ Caption string
}
var KindLightGalleryBlock = ast.NewNodeKind("LightGalleryBlock")
diff --git a/internal/lightgallery/html_renderer.go b/internal/lightgallery/html_renderer.go
index e20e9ec..e611f0b 100644
--- a/internal/lightgallery/html_renderer.go
+++ b/internal/lightgallery/html_renderer.go
@@ -1,13 +1,11 @@
package lightgallery
import (
- "bytes"
"fmt"
"math/rand"
"strings"
"time"
- "github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
@@ -16,18 +14,11 @@ import (
type LightGalleryHTMLRenderer struct {
html.Config
- md goldmark.Markdown
}
func NewLightGalleryHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
r := &LightGalleryHTMLRenderer{
Config: html.NewConfig(),
- md: goldmark.New(
- goldmark.WithRendererOptions(
- html.WithHardWraps(),
- html.WithXHTML(),
- ),
- ),
}
for _, opt := range opts {
opt.SetHTMLOption(&r.Config)
@@ -62,36 +53,29 @@ func (r *LightGalleryHTMLRenderer) renderLightGallery(w util.BufWriter, source [
imageUrlWithoutExt := strings.Join(imageUrlSegments[:len(imageUrlSegments)-1], ".")
imageNameParts := strings.Split(imageUrlWithoutExt, "-")
- var captionBuf bytes.Buffer
- captionHTML := img.Caption
- if err := r.md.Convert(img.Caption, &captionBuf); err == nil {
- captionHTML = captionBuf.Bytes()
- captionHTML = bytes.TrimPrefix(captionHTML, []byte("<p>"))
- captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>\n"))
- captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>"))
- }
-
dayDate, _ := time.Parse("20060102 150405", imageNameParts[len(imageNameParts)-2]+" "+imageNameParts[len(imageNameParts)-1])
- dayDate = dayDate.In(gallery.Location)
dynamicElements = append(dynamicElements, fmt.Sprintf(`{
src:
"https://f003.backblazeb2.com/file/sayana-photos/full/%s",
downloadUrl:
"https://f003.backblazeb2.com/file/sayana-photos/full/%s",
alt: "%s",
- responsive: "https://f003.backblazeb2.com/file/sayana-photos/webp-320p/%s.webp 384, https://f003.backblazeb2.com/file/sayana-photos/webp-560p/%s.webp 672, https://f003.backblazeb2.com/file/sayana-photos/webp-800p/%s.webp 960, https://f003.backblazeb2.com/file/sayana-photos/webp-1200p/%s.webp 1440, https://f003.backblazeb2.com/file/sayana-photos/webp-1600p/%s.webp 1920",
+ sources: [{
+ srcset: "https://f003.backblazeb2.com/file/sayana-photos/thumbnails/%s.webp",
+ media: "(max-width: 800px)"
+ }],
thumb:
- "https://f003.backblazeb2.com/file/sayana-photos/webp-320p/%s.webp",
+ "https://f003.backblazeb2.com/file/sayana-photos/thumbnails/%s.webp",
subHtml: `+"`"+`<div class="flex flex-row light-gallery-captions">
- <p class="grow !text-[1vmax]/[1] text-left font-spectral text-main-dark">%s</p>
- <p class="!text-[1vmax]/[1] text-right font-spectral text-secondary">%s</p>
+ <p class="grow !text-[1vmax]/[0.9] text-left font-spectral text-main-dark">%s</p>
+ <p class="!text-[1vmax]/[0.9] text-right font-spectral text-secondary">%s</p>
</div>`+"`"+`
- }`, img.URL, img.URL, util.EscapeHTML(captionHTML), imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, captionHTML, dayDate.Format("2006-01-02 15:04:05 -07:00")))
+ }`, img.URL, img.URL, img.Caption, imageUrlWithoutExt, imageUrlWithoutExt, img.Caption, dayDate.Format("2006-01-02 15:04:05 -07:00")))
}
w.WriteString(fmt.Sprintf(`
<script>
-function createLightGallery%s() {
+function createLightLibrary%s() {
const $lgContainer = document.getElementById('lg-%s');
const inlineGallery = lightGallery($lgContainer, {
container: $lgContainer,
@@ -106,7 +90,7 @@ function createLightGallery%s() {
isMobile: () => false,
slideDelay: 0,
plugins: [lgZoom, lgThumbnail],
- thumbWidth: calculateVmin(10),
+ thumbWidth: 160,
thumbHeight: "10vmin",
thumbMargin: 4
});
@@ -116,13 +100,32 @@ function createLightGallery%s() {
}, 200);
}
-document.addEventListener('DOMContentLoaded', createLightGallery%s);
-</script>`, galleryID, galleryID, strings.Join(dynamicElements, ","), galleryID))
+document.addEventListener('DOMContentLoaded', createLightLibrary%s);
+
+let resizeTimer%s;
+window.addEventListener("resize", () => {
+ clearTimeout(resizeTimer%s);
+ resizeTimer%s = setTimeout(() => {
+ const $lgContainer = document.getElementById('lg-%s');
+ $lgContainer.innerHTML = "";
+ createLightLibrary%s();
+ }, 1000);
+});
+</script>`, galleryID, galleryID, strings.Join(dynamicElements, ","), galleryID, galleryID, galleryID, galleryID, galleryID, galleryID))
}
return ast.WalkContinue, nil
}
+func escapeHTML(s string) string {
+ s = strings.ReplaceAll(s, "&", "&amp;")
+ s = strings.ReplaceAll(s, "<", "&lt;")
+ s = strings.ReplaceAll(s, ">", "&gt;")
+ s = strings.ReplaceAll(s, "\"", "&quot;")
+ s = strings.ReplaceAll(s, "'", "&#39;")
+ return s
+}
+
func generateDivId(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyz"
seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) // Seed with current time
diff --git a/internal/lightgallery/parser.go b/internal/lightgallery/parser.go
index daec797..0ce209c 100644
--- a/internal/lightgallery/parser.go
+++ b/internal/lightgallery/parser.go
@@ -2,9 +2,7 @@ package lightgallery
import (
"bytes"
- "log/slog"
- "regexp"
- "time"
+ "strings"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
@@ -24,52 +22,43 @@ func (p *LightGalleryParser) Trigger() []byte {
func (p *LightGalleryParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) {
line, _ := reader.PeekLine()
- if !bytes.HasPrefix(line, []byte("{Gallery:")) {
+ if !bytes.HasPrefix(line, []byte("{Gallery}")) {
return nil, parser.NoChildren
}
- r := regexp.MustCompile(`^\{Gallery:([A-Za-z0-9\+\-/]+)\}$`)
-
trimmed := bytes.TrimSpace(line)
- parts := r.FindSubmatch(trimmed)
- if len(parts) < 2 {
- slog.Warn("invalid gallery header format", slog.String("line", string(trimmed)), slog.Int("submatch_count", len(parts)))
+ if !bytes.Equal(trimmed, []byte("{Gallery}")) {
return nil, parser.NoChildren
}
- loc, err := time.LoadLocation(string(parts[1]))
- if err != nil {
- slog.Warn("invalid location specified for a gallery", slog.String("error", err.Error()), slog.String("line", string(trimmed)), slog.String("location", string(parts[1])))
- return nil, parser.NoChildren
- }
-
- return &LightGalleryBlock{Location: loc}, parser.NoChildren
+ return &LightGalleryBlock{}, parser.NoChildren
}
func (p *LightGalleryParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State {
line, segment := reader.PeekLine()
if len(line) == 0 || segment.Len() == 0 {
- return parser.Continue | parser.NoChildren
+ return parser.Close
}
trimmed := bytes.TrimSpace(line)
- if bytes.Equal(trimmed, []byte("{Gallery}")) || bytes.Equal(trimmed, []byte("{/Gallery}")) {
+ if bytes.Equal(trimmed, []byte("{Gallery}")) {
reader.AdvanceLine()
return parser.Close
}
gallery := node.(*LightGalleryBlock)
+ lineStr := string(trimmed)
- parts := bytes.SplitN(trimmed, []byte{'|'}, 2)
- url := bytes.TrimSpace(parts[0])
+ parts := strings.SplitN(lineStr, "|", 2)
+ url := strings.TrimSpace(parts[0])
+ caption := ""
- caption := make([]byte, 0)
if len(parts) > 1 {
- caption = bytes.TrimSpace(parts[1])
+ caption = strings.TrimSpace(parts[1])
}
gallery.Images = append(gallery.Images, LightGalleryImage{
- URL: string(url),
+ URL: url,
Caption: caption,
})
diff --git a/internal/router/api-v1-blog-search.go b/internal/router/api-v1-blog-search.go
deleted file mode 100644
index 51ace85..0000000
--- a/internal/router/api-v1-blog-search.go
+++ /dev/null
@@ -1,112 +0,0 @@
-package router
-
-import (
- "fmt"
- "log/slog"
- "net/url"
- "regexp"
- "slices"
- "strings"
- "time"
-
- "github.com/SayaAndy/saya-today-web/internal/b2"
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- tm.Add("catalogue-blog-cards", "views/partials/catalogue-blog-cards.html", "views/partials/catalogue-blog-card-tags.html")
-}
-
-func Api_V1_BlogSearch(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client) func(c *fiber.Ctx) error {
- return func(c *fiber.Ctx) error {
- sort := c.Query("sort")
- lang := c.Query("lang")
- tz := c.Query("tz")
-
- if !slices.Contains(langs, lang) {
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language", lang))
- }
-
- loc, err := time.LoadLocation(tz)
- if err != nil {
- loc = time.UTC
- slog.Warn("unable to parse a client timezone, defaulting to UTC", slog.String("error", err.Error()), slog.String("tz", tz))
- }
-
- pages, err := b2Client.Scan(lang + "/")
- if err != nil {
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrInternalServerError.Code).SendString(fmt.Sprintf("failed to scan pages for '%s' lang: %v", lang, err))
- }
-
- encodedQuery := c.Request().URI().QueryString()
- re, err := regexp.Compile(`tags\[\]=([\w]+)`)
- if err != nil {
- slog.Warn("failed to generate regex for tags gathering", slog.String("error", err.Error()))
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate regex for tags gathering")
- }
- decodedQuery, err := url.QueryUnescape(string(encodedQuery))
- matches := re.FindAllStringSubmatch(decodedQuery, -1)
-
- tags := make([]string, 0, len(matches))
- for _, match := range matches {
- tags = append(tags, string(match[1]))
- }
-
- pageMeta := make([]fiber.Map, 0, len(pages))
- for _, page := range pages {
- for _, tag := range page.Metadata.Tags {
- if len(tags) == 0 || slices.Contains(tags, tag) {
- pageMeta = append(pageMeta, fiber.Map{
- "Link": page.Link,
- "ArticleLink": "/" + lang + "/blog/" + page.FileName,
- "Title": page.Metadata.Title,
- "PublishedTime": page.Metadata.PublishedTime.In(loc).Format("2006-01-02 15:04:05 -07:00"),
- "ActionDate": page.Metadata.ActionDate,
- "ShortDescription": page.Metadata.ShortDescription,
- "Thumbnail": page.Metadata.Thumbnail,
- "Tags": page.Metadata.Tags,
- })
- break
- }
- }
- }
-
- slices.SortFunc(pageMeta, func(a, b fiber.Map) int {
- switch sort {
- case "titleAsc":
- return strings.Compare(a["Title"].(string), b["Title"].(string))
- case "titleDesc":
- return strings.Compare(b["Title"].(string), a["Title"].(string))
- case "actionDateAsc":
- return strings.Compare(a["ActionDate"].(string), b["ActionDate"].(string))
- case "actionDateDesc":
- return strings.Compare(b["ActionDate"].(string), a["ActionDate"].(string))
- case "publicationDateAsc":
- publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -07:00", a["PublishedTime"].(string))
- publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", b["PublishedTime"].(string))
- return publishedTimeA.Compare(publishedTimeB)
- case "publicationDateDesc":
- publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -07:00", a["PublishedTime"].(string))
- publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", b["PublishedTime"].(string))
- return publishedTimeB.Compare(publishedTimeA)
- }
- return 0
- })
-
- content, err := tm.Render("catalogue-blog-cards", fiber.Map{
- "BlogPages": pageMeta,
- "L": l[lang],
- })
- if err != nil {
- slog.Warn("failed to generate div", slog.String("page", "/"+lang+"/blog"), slog.String("error", err.Error()))
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div")
- }
-
- return c.Type("html").Send(content)
- }
-}
diff --git a/internal/router/api-v1-tz.go b/internal/router/api-v1-tz.go
deleted file mode 100644
index 4e4e462..0000000
--- a/internal/router/api-v1-tz.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package router
-
-import (
- "log/slog"
- "time"
-
- "github.com/gofiber/fiber/v2"
-)
-
-func Api_V1_TZ() func(c *fiber.Ctx) error {
- return func(c *fiber.Ctx) error {
- timestampString := c.Query("timestamp")
- tz := c.Query("tz")
-
- loc, err := time.LoadLocation(tz)
- if err != nil {
- loc = time.UTC
- slog.Warn("unable to parse a client timezone, defaulting to UTC", slog.String("error", err.Error()), slog.String("tz", tz))
- }
-
- var format string
- var timestamp time.Time
- for _, format = range []string{"2006-01-02 15:04:05 -07:00", time.RFC3339} {
- if timestamp, err = time.Parse(format, timestampString); err == nil {
- break
- }
- }
-
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
-
- if err != nil {
- return c.Status(fiber.StatusBadRequest).SendString("invalid timestamp format")
- }
-
- return c.Status(fiber.StatusOK).SendString(timestamp.In(loc).Format(format))
- }
-}
diff --git a/internal/router/lang-blog-title.go b/internal/router/lang-blog-title.go
deleted file mode 100644
index b7b5d7e..0000000
--- a/internal/router/lang-blog-title.go
+++ /dev/null
@@ -1,79 +0,0 @@
-package router
-
-import (
- "bytes"
- "fmt"
- "html/template"
- "log/slog"
- "slices"
- "strconv"
- "strings"
-
- "github.com/SayaAndy/saya-today-web/internal/b2"
- "github.com/SayaAndy/saya-today-web/internal/frontmatter"
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
- "github.com/yuin/goldmark"
-)
-
-func init() {
- tm.Add("blog-page", "views/layouts/general-page.html", "views/pages/blog-page.html")
-}
-
-func Lang_Blog_Title(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client, md goldmark.Markdown) func(c *fiber.Ctx) error {
- return func(c *fiber.Ctx) error {
- lang := c.Params("lang")
- if !slices.Contains(langs, lang) {
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang))
- }
-
- metadata, parsedMarkdown, err := readBlogPost(md, b2Client, lang+"/"+c.Params("title"))
- if err != nil {
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("failed to find '%s' post", c.Params("title")))
- }
-
- geolocationParts := strings.Split(metadata.Geolocation, " ")
- var x, y, areaError string
- if len(geolocationParts) >= 2 {
- x = geolocationParts[0]
- y = geolocationParts[1]
- }
- if len(geolocationParts) >= 3 {
- areaError = geolocationParts[2]
- }
-
- content, err := tm.Render("blog-page", fiber.Map{
- "Title": metadata.Title,
- "PublishedDate": metadata.PublishedTime.Format("2006-01-02 15:04:05 -07:00"),
- "PublishedYear": strconv.Itoa(metadata.PublishedTime.Year()),
- "ParsedMarkdown": template.HTML(parsedMarkdown),
- "MapLocationX": x,
- "MapLocationY": y,
- "MapLocationAreaMeters": areaError,
- "Lang": lang,
- })
- if err != nil {
- slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/blog/"+c.Params("title")), slog.String("error", err.Error()))
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
- }
-
- return c.Type("html").Send(content)
- }
-}
-
-func readBlogPost(md goldmark.Markdown, b2Client *b2.B2Client, sourceName string) (metadata *frontmatter.Metadata, html string, err error) {
- metadata, markdown, err := b2Client.ReadFrontmatter(sourceName + ".md")
- if err != nil {
- return nil, "", fmt.Errorf("failed to read a frontmatter file: %w", err)
- }
-
- var buf bytes.Buffer
- if err := md.Convert(markdown, &buf); err != nil {
- return nil, "", fmt.Errorf("convert source context from md to html: %w", err)
- }
-
- return metadata, buf.String(), nil
-}
diff --git a/internal/router/lang-blog.go b/internal/router/lang-blog.go
deleted file mode 100644
index 650c4b1..0000000
--- a/internal/router/lang-blog.go
+++ /dev/null
@@ -1,93 +0,0 @@
-package router
-
-import (
- "fmt"
- "log/slog"
- "net/url"
- "regexp"
- "slices"
- "strings"
-
- "github.com/SayaAndy/saya-today-web/internal/b2"
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- tm.Add("blog-catalogue", "views/layouts/general-page.html", "views/pages/blog-catalogue.html")
-}
-
-func Lang_Blog(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client) func(c *fiber.Ctx) error {
- return func(c *fiber.Ctx) error {
- lang := c.Params("lang")
- if !slices.Contains(langs, lang) {
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang))
- }
-
- querySort := c.Query("sort")
- if querySort == "" {
- querySort = "publicationDateDesc"
- }
-
- encodedQuery := c.Request().URI().QueryString()
- re, err := regexp.Compile(`tags\[\]=([\w]+)`)
- if err != nil {
- slog.Warn("failed to generate regex for tags gathering", slog.String("error", err.Error()))
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate regex for tags gathering")
- }
- decodedQuery, err := url.QueryUnescape(string(encodedQuery))
- matches := re.FindAllStringSubmatch(decodedQuery, -1)
-
- queryTags := make([]string, 0, len(matches))
- for _, match := range matches {
- queryTags = append(queryTags, string(match[1]))
- }
-
- pages, err := b2Client.Scan(lang + "/")
- status := fiber.StatusOK
- if err != nil {
- status = fiber.StatusPartialContent
- pages = []*b2.BlogPage{}
- }
-
- tagsMap := make(map[string]int)
- for _, page := range pages {
- slog.Debug("enlist page for catalogue", slog.Any("page", page), slog.String("endpoint", "/"+lang+"/blog"))
- for _, tag := range page.Metadata.Tags {
- tagsMap[tag]++
- }
- }
-
- type Tag struct {
- Name string `json:"Name" yaml:"name"`
- Count int `json:"Count" yaml:"count"`
- }
-
- tagsArray := make([]Tag, 0, len(tagsMap))
- for tag, count := range tagsMap {
- tagsArray = append(tagsArray, Tag{tag, count})
- }
- slices.SortFunc(tagsArray, func(a Tag, b Tag) int {
- return strings.Compare(a.Name, b.Name)
- })
-
- content, err := tm.Render("blog-catalogue", fiber.Map{
- "QuerySort": querySort,
- "QueryTags": strings.Join(queryTags, ","),
- "Tags": tagsArray,
- "Lang": lang,
- "L": l[lang],
- "PublishedYear": "2025",
- "Title": l[lang].BlogSearch.Header,
- })
- if err != nil {
- slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/blog"), slog.String("error", err.Error()))
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
- }
-
- return c.Type("html").Status(status).Send(content)
- }
-}
diff --git a/internal/router/lang-map.go b/internal/router/lang-map.go
deleted file mode 100644
index e94d756..0000000
--- a/internal/router/lang-map.go
+++ /dev/null
@@ -1,85 +0,0 @@
-package router
-
-import (
- "fmt"
- "log/slog"
- "slices"
- "strconv"
- "strings"
-
- "github.com/SayaAndy/saya-today-web/internal/b2"
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- tm.Add("global-map", "views/pages/global-map.html")
-}
-
-func Lang_Map(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client) func(c *fiber.Ctx) error {
- return func(c *fiber.Ctx) error {
- lang := c.Params("lang")
- if !slices.Contains(langs, lang) {
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang))
- }
-
- pages, err := b2Client.Scan(lang + "/")
- status := fiber.StatusOK
- if err != nil {
- status = fiber.StatusPartialContent
- pages = []*b2.BlogPage{}
- }
-
- type MapMarker struct {
- Title string `json:"Title"`
- PageLink string `json:"PageLink"`
- Lat float64 `json:"Lat"`
- Long float64 `json:"Long"`
- AccuracyMeters int64 `json:"AccuracyMeters"`
- Thumbnail string `json:"Thumbnail"`
- }
-
- mapMarkers := make([]*MapMarker, 0, len(pages))
- for _, page := range pages {
- geolocationParts := strings.Split(page.Metadata.Geolocation, " ")
- if len(geolocationParts) < 2 {
- continue
- }
-
- var x, y float64
- var areaError int64
- if len(geolocationParts) >= 2 {
- x, _ = strconv.ParseFloat(geolocationParts[0], 64)
- y, _ = strconv.ParseFloat(geolocationParts[1], 64)
- }
- if len(geolocationParts) >= 3 {
- areaError, _ = strconv.ParseInt(geolocationParts[2], 10, 64)
- }
-
- mapMarkers = append(mapMarkers, &MapMarker{
- Title: page.Metadata.Title,
- PageLink: fmt.Sprintf("/%s/blog/%s", lang, page.FileName),
- Lat: x,
- Long: y,
- AccuracyMeters: areaError,
- Thumbnail: page.Metadata.Thumbnail,
- })
- }
-
- content, err := tm.Render("global-map", fiber.Map{
- "Lang": lang,
- "L": l[lang],
- "MapLocationLat": 45.4507,
- "MapLocationLong": 68.8319,
- "MapMarkers": mapMarkers,
- })
- if err != nil {
- slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/map"), slog.String("error", err.Error()))
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
- }
-
- return c.Type("html").Status(status).Send(content)
- }
-}
diff --git a/internal/router/root.go b/internal/router/root.go
deleted file mode 100644
index f4a0692..0000000
--- a/internal/router/root.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package router
-
-import (
- "log/slog"
-
- "github.com/SayaAndy/saya-today-web/config"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- tm.Add("index", "views/index.html")
-}
-
-func Root(localeCfg []config.AvailableLanguageConfig) func(c *fiber.Ctx) error {
- return func(c *fiber.Ctx) error {
- content, err := tm.Render("index", fiber.Map{
- "AvailableLanguages": localeCfg,
- })
- if err != nil {
- slog.Warn("failed to generate page", slog.String("page", "/"), slog.String("error", err.Error()))
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
- }
-
- return c.Type("html").Send(content)
- }
-}
diff --git a/internal/router/tm.go b/internal/router/tm.go
deleted file mode 100644
index e8095ee..0000000
--- a/internal/router/tm.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package router
-
-import (
- "log/slog"
- "os"
-
- "github.com/SayaAndy/saya-today-web/internal/templatemanager"
-)
-
-var tm = assert(templatemanager.NewTemplateManager())
-
-func assert[T any](t T, err error) T {
- if err != nil {
- slog.Error("fail to initialize template manager", slog.String("error", err.Error()))
- os.Exit(1)
- }
- return t
-}
diff --git a/internal/templatemanager/templatemanager.go b/internal/templatemanager/templatemanager.go
index f89d0c8..5effcb3 100644
--- a/internal/templatemanager/templatemanager.go
+++ b/internal/templatemanager/templatemanager.go
@@ -22,7 +22,7 @@ type TemplateManagerTemplates struct {
Files []string
}
-func NewTemplateManager(templates ...TemplateManagerTemplates) (*TemplateManager, error) {
+func NewTemplateManager(templates []TemplateManagerTemplates) (*TemplateManager, error) {
templateMap := make(map[string]templateManagerRender)
for _, tmplStruct := range templates {
@@ -37,6 +37,7 @@ func NewTemplateManager(templates ...TemplateManagerTemplates) (*TemplateManager
Main: filepath.Base(tmplStruct.Files[0]),
Tmpl: tmpl,
}
+
}
return &TemplateManager{
@@ -54,24 +55,3 @@ func (tm *TemplateManager) Render(name string, data interface{}) ([]byte, error)
err := tmpl.Tmpl.ExecuteTemplate(&buf, tmpl.Main, data)
return buf.Bytes(), err
}
-
-func (tm *TemplateManager) Add(name string, files ...string) error {
- if len(files) == 0 {
- return fmt.Errorf("you can't add template without any files")
- }
-
- tmpl := template.New("").Funcs(template.FuncMap{
- "contains": strings.Contains,
- })
-
- tmpl, err := tmpl.ParseFiles(files...)
- if err != nil {
- return fmt.Errorf("failed to add template into manager: %w", err)
- }
-
- tm.templates[name] = templateManagerRender{
- Main: filepath.Base(files[0]),
- Tmpl: tmpl,
- }
- return nil
-}
diff --git a/locale/localization.en.yaml b/locale/localization.en.yaml
index f5ee6c9..879fce8 100644
--- a/locale/localization.en.yaml
+++ b/locale/localization.en.yaml
@@ -7,5 +7,3 @@ BlogSearch:
ActionDateOrdered: 'Action Date'
PublicationDateOrdered: 'Publication Date'
ChooseAllTags: 'Choose All'
-GlobalMap:
- Header: 'Global Map'
diff --git a/locale/localization.go b/locale/localization.go
index 56b6462..d5c6b48 100644
--- a/locale/localization.go
+++ b/locale/localization.go
@@ -10,7 +10,6 @@ import (
type LocaleConfig struct {
TagsLabel string `yaml:"TagsLabel" json:"TagsLabel"`
BlogSearch BlogSearchConfig `yaml:"BlogSearch" json:"BlogSearch"`
- GlobalMap GlobalMapConfig `yaml:"GlobalMap" json:"GlobalMap"`
}
type BlogSearchConfig struct {
@@ -23,10 +22,6 @@ type BlogSearchConfig struct {
ChooseAllTags string `yaml:"ChooseAllTags" json:"ChooseAllTags"`
}
-type GlobalMapConfig struct {
- Header string `yaml:"Header" json:"Header"`
-}
-
func LoadConfig(path string, config *LocaleConfig) error {
fileBytes, err := os.ReadFile(path)
if err != nil {
diff --git a/locale/localization.ru.yaml b/locale/localization.ru.yaml
index c236d1e..786665d 100644
--- a/locale/localization.ru.yaml
+++ b/locale/localization.ru.yaml
@@ -7,5 +7,3 @@ BlogSearch:
ActionDateOrdered: 'дате действия'
PublicationDateOrdered: 'времени публикации'
ChooseAllTags: 'Выбрать все'
-GlobalMap:
- Header: 'Глобальная карта'
diff --git a/main.go b/main.go
index 566e796..7b8bfd3 100644
--- a/main.go
+++ b/main.go
@@ -1,16 +1,26 @@
package main
import (
+ "bytes"
"flag"
+ "fmt"
+ "html/template"
"log"
"log/slog"
+ "net/url"
"os"
+ "regexp"
+ "slices"
+ "strconv"
+ "strings"
+ "time"
"github.com/SayaAndy/saya-today-web/config"
"github.com/SayaAndy/saya-today-web/internal/b2"
+ "github.com/SayaAndy/saya-today-web/internal/frontmatter"
"github.com/SayaAndy/saya-today-web/internal/lightgallery"
- "github.com/SayaAndy/saya-today-web/internal/router"
"github.com/SayaAndy/saya-today-web/internal/tailwind"
+ "github.com/SayaAndy/saya-today-web/internal/templatemanager"
"github.com/SayaAndy/saya-today-web/locale"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/redirect"
@@ -68,6 +78,17 @@ func main() {
localization[lang.Name] = localeCfg
}
+ tm, err := templatemanager.NewTemplateManager([]templatemanager.TemplateManagerTemplates{
+ {Name: "blog-page", Files: []string{"views/layouts/general-page.html", "views/pages/blog-page.html"}},
+ {Name: "blog-catalogue", Files: []string{"views/layouts/general-page.html", "views/pages/blog-catalogue.html"}},
+ {Name: "index", Files: []string{"views/index.html"}},
+ {Name: "catalogue-blog-cards", Files: []string{"views/partials/catalogue-blog-cards.html"}},
+ })
+ if err != nil {
+ slog.Error("fail to initialize template manager", slog.String("error", err.Error()))
+ os.Exit(1)
+ }
+
app := fiber.New(fiber.Config{})
app.Use(redirect.New(redirect.Config{
@@ -78,14 +99,226 @@ func main() {
StatusCode: 301,
}))
- app.Get("/", router.Root(cfg.AvailableLanguages))
- app.Get("/:lang/map", router.Lang_Map(localization, availableLanguages, b2Client))
- app.Get("/:lang/blog", router.Lang_Blog(localization, availableLanguages, b2Client))
- app.Get("/:lang/blog/:title", router.Lang_Blog_Title(localization, availableLanguages, b2Client, md))
- app.Get("/api/v1/tz", router.Api_V1_TZ())
- app.Get("/api/v1/blog-search", router.Api_V1_BlogSearch(localization, availableLanguages, b2Client))
+ app.Get("/", func(c *fiber.Ctx) error {
+ content, err := tm.Render("index", fiber.Map{
+ "AvailableLanguages": cfg.AvailableLanguages,
+ })
+ if err != nil {
+ slog.Warn("failed to generate page", slog.String("page", "/"), slog.String("error", err.Error()))
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
+ }
+
+ return c.Type("html").Send(content)
+ })
+
+ app.Get("/:lang/blog", func(c *fiber.Ctx) error {
+ lang := c.Params("lang")
+ if !slices.Contains(availableLanguages, lang) {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang))
+ }
+
+ querySort := c.Query("sort")
+ if querySort == "" {
+ querySort = "titleAsc"
+ }
+
+ encodedQuery := c.Request().URI().QueryString()
+ re, err := regexp.Compile(`tags\[\]=([\w]+)`)
+ if err != nil {
+ slog.Warn("failed to generate regex for tags gathering", slog.String("error", err.Error()))
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate regex for tags gathering")
+ }
+ decodedQuery, err := url.QueryUnescape(string(encodedQuery))
+ matches := re.FindAllStringSubmatch(decodedQuery, -1)
+
+ queryTags := make([]string, 0, len(matches))
+ for _, match := range matches {
+ queryTags = append(queryTags, string(match[1]))
+ }
+
+ pages, err := b2Client.Scan(lang + "/")
+ status := fiber.StatusOK
+ if err != nil {
+ status = fiber.StatusPartialContent
+ pages = []*b2.BlogPage{}
+ }
+
+ tagsSet := make(map[string]struct{})
+ for _, page := range pages {
+ slog.Debug("enlist page for catalogue", slog.Any("page", page), slog.String("endpoint", "/"))
+ for _, tag := range page.Metadata.Tags {
+ tagsSet[tag] = struct{}{}
+ }
+ }
+
+ tagsArray := make([]string, 0, len(tagsSet))
+ for tag := range tagsSet {
+ tagsArray = append(tagsArray, tag)
+ }
+ slices.Sort(tagsArray)
+
+ content, err := tm.Render("blog-catalogue", fiber.Map{
+ "QuerySort": querySort,
+ "QueryTags": strings.Join(queryTags, ","),
+ "Tags": tagsArray,
+ "Lang": lang,
+ "L": localization[lang],
+ "PublishedYear": "2025",
+ "Title": localization[lang].BlogSearch.Header,
+ })
+ if err != nil {
+ slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/blog"), slog.String("error", err.Error()))
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
+ }
+
+ return c.Type("html").Status(status).Send(content)
+ })
+
+ app.Get("/:lang/blog/:title", func(c *fiber.Ctx) error {
+ lang := c.Params("lang")
+ if !slices.Contains(availableLanguages, lang) {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang))
+ }
+
+ metadata, parsedMarkdown, err := readBlogPost(lang + "/" + c.Params("title"))
+ if err != nil {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("failed to find '%s' post", c.Params("title")))
+ }
+
+ geolocationParts := strings.Split(metadata.Geolocation, " ")
+ var x, y, areaError string
+ if len(geolocationParts) >= 2 {
+ x = geolocationParts[0]
+ y = geolocationParts[1]
+ }
+ if len(geolocationParts) >= 3 {
+ areaError = geolocationParts[2]
+ }
+
+ content, err := tm.Render("blog-page", fiber.Map{
+ "Title": metadata.Title,
+ "PublishedDate": metadata.PublishedTime.Format("2006-01-02 15:04:05 -07:00"),
+ "PublishedYear": strconv.Itoa(metadata.PublishedTime.Year()),
+ "ParsedMarkdown": template.HTML(parsedMarkdown),
+ "MapLocationX": x,
+ "MapLocationY": y,
+ "MapLocationAreaMeters": areaError,
+ "Lang": lang,
+ })
+ if err != nil {
+ slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/blog/"+c.Params("title")), slog.String("error", err.Error()))
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
+ }
+
+ return c.Type("html").Send(content)
+ })
+
+ app.Get("/api/v1/blog-search", func(c *fiber.Ctx) error {
+ sort := c.Query("sort")
+ lang := c.Query("lang")
+
+ if !slices.Contains(availableLanguages, lang) {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language", lang))
+ }
+
+ pages, err := b2Client.Scan(lang + "/")
+ if err != nil {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString(fmt.Sprintf("failed to scan pages for '%s' lang: %v", lang, err))
+ }
+
+ encodedQuery := c.Request().URI().QueryString()
+ re, err := regexp.Compile(`tags\[\]=([\w]+)`)
+ if err != nil {
+ slog.Warn("failed to generate regex for tags gathering", slog.String("error", err.Error()))
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate regex for tags gathering")
+ }
+ decodedQuery, err := url.QueryUnescape(string(encodedQuery))
+ matches := re.FindAllStringSubmatch(decodedQuery, -1)
+
+ tags := make([]string, 0, len(matches))
+ for _, match := range matches {
+ tags = append(tags, string(match[1]))
+ }
+
+ pageMeta := make([]map[string]string, 0, len(pages))
+ for _, page := range pages {
+ for _, tag := range page.Metadata.Tags {
+ if len(tags) == 0 || slices.Contains(tags, tag) {
+ pageMeta = append(pageMeta, map[string]string{
+ "Link": page.Link,
+ "ArticleLink": "/" + lang + "/blog/" + page.FileName,
+ "Title": page.Metadata.Title,
+ "PublishedTime": page.Metadata.PublishedTime.Format("2006-01-02 15:04:05 -0700"),
+ "ActionDate": page.Metadata.ActionDate,
+ "ShortDescription": page.Metadata.ShortDescription,
+ "Thumbnail": page.Metadata.Thumbnail,
+ "Tags": strings.Join(page.Metadata.Tags, ", "),
+ })
+ break
+ }
+ }
+ }
+
+ slices.SortFunc(pageMeta, func(a, b map[string]string) int {
+ switch sort {
+ case "titleAsc":
+ return strings.Compare(a["Title"], b["Title"])
+ case "titleDesc":
+ return strings.Compare(b["Title"], a["Title"])
+ case "actionDateAsc":
+ return strings.Compare(a["ActionDate"], b["ActionDate"])
+ case "actionDateDesc":
+ return strings.Compare(b["ActionDate"], a["ActionDate"])
+ case "publicationDateAsc":
+ publishedTimeA, _ := time.Parse(time.RFC3339, a["PublishedTime"])
+ publishedTimeB, _ := time.Parse(time.RFC3339, b["PublishedTime"])
+ return publishedTimeA.Compare(publishedTimeB)
+ case "publicationDateDesc":
+ publishedTimeA, _ := time.Parse(time.RFC3339, a["PublishedTime"])
+ publishedTimeB, _ := time.Parse(time.RFC3339, b["PublishedTime"])
+ return publishedTimeB.Compare(publishedTimeA)
+ }
+ return 0
+ })
+
+ content, err := tm.Render("catalogue-blog-cards", fiber.Map{
+ "BlogPages": pageMeta,
+ "L": localization[lang],
+ })
+ if err != nil {
+ slog.Warn("failed to generate div", slog.String("page", "/"+lang+"/blog"), slog.String("error", err.Error()))
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div")
+ }
+
+ return c.Type("html").Send(content)
+ })
app.Static("/", "./static")
log.Fatal(app.Listen(":3000"))
}
+
+func readBlogPost(sourceName string) (metadata *frontmatter.Metadata, html string, err error) {
+ metadata, markdown, err := b2Client.ReadFrontmatter(sourceName + ".md")
+ if err != nil {
+ return nil, "", fmt.Errorf("failed to read a frontmatter file: %w", err)
+ }
+
+ var buf bytes.Buffer
+ if err := md.Convert(markdown, &buf); err != nil {
+ return nil, "", fmt.Errorf("convert source context from md to html: %w", err)
+ }
+
+ return metadata, buf.String(), nil
+}
diff --git a/static/input.css b/static/input.css
index 9f834a6..8776b25 100644
--- a/static/input.css
+++ b/static/input.css
@@ -1,6 +1,17 @@
@import "tailwindcss";
@theme {
+ --color-olive-dark: #4b513a;
+ --color-olive-medium: #8a9d8a;
+ --color-olive-light: #a5b8a5;
+ --color-cream-dark: #ffffdd;
+ --color-cream-light: #fffff0;
+ --color-lettuce-dark: #384a0c;
+ --color-lettuce-medium: #9bb665;
+ --color-lettuce-light: #bbd095;
+ --color-sunny-dark: #f5eee2;
+ --color-sunny-light: #fff93f;
+
--font-spectral: Spectral, serif;
--font-patua: "Patua One", cursive;
}
@@ -14,9 +25,6 @@
--sidebar-text-color: var(--background-dark-color);
--sidebar-stroke-color: var(--main-dark-color);
--secondary-color: var(--main-light-color);
- --leaflet-text-color: var(--main-dark-color);
- --leaflet-link-color: var(--main-medium-color);
- --leaflet-link-hover-color: var(--main-light-color);
--interlocked-hexagons-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23ffffdb%22%2F%3E%3Cpath%20fill%3D%22%23777e63%22%20d%3D%22m33.64%200-.17.25h-33L4.3%206h33.07l4-6zm8.33%200-4.33%206.5h-33l3.83%205.75h33.06L49.7%200zm8.34%200-8.5%2012.75h-33l3.83%205.75H45.7L58.03%200zm8.33%200L45.97%2019h-33l3.83%205.75h33.06L66.36%200zm-8.5%2025.25L33.63%2050l3.86%205.8L54.03%2031h33l-3.83-5.75zm4.16%206.25L37.8%2056.25l3.87%205.8%2016.53-24.8h33l-3.84-5.75zm4.17%206.25L41.97%2062.5l3.86%205.8%2016.54-24.8h33l-3.84-5.75zM62.63%2044l-16.5%2024.75%203.87%205.8%2016.53-24.8h33L95.7%2044zM100%2050.45l-3.87%205.8%203.87%205.81v-11.6zM0%2050.46v11.6L12.5%2080.8l3.86-5.8zm95.83%206.25-3.87%205.8L100%2074.55v-11.6zm-4.17%206.24-3.86%205.8%2012.2%2018.3v-11.6zM0%2062.96v11.6l8.33%2012.49%203.86-5.8zm87.5%206.24L83.63%2075%20100%2099.54V87.96zM50%2075.45%2033.64%20100h7.72l12.5-18.75zm-50%200v11.6l4.16%206.24%203.87-5.8zm54.17%206.25L41.97%20100h7.73l8.33-12.5zm4.17%206.25L50.3%20100h7.73l4.17-6.25zM0%2087.95v11.59l3.86-5.8zm62.5%206.25-3.87%205.8h7.73z%22%2F%3E%3Cpath%20fill%3D%22%23a0a182%22%20d%3D%22M66.8.25%2062.97%206h33l4.03%206.05V.45l-.14-.2zM0%20.45v11.61L12.5%2030.8l3.86-5.8zM62.63%206.5l-3.83%205.75h33l8.2%2012.3v-11.6L95.7%206.5zm-4.16%206.25-3.84%205.75h33L100%2037.06v-11.6l-8.47-12.71zM0%2012.96v11.6l8.33%2012.49%203.86-5.8zM54.3%2019l-3.84%205.75h33L100%2049.55l.01-.01V37.96L87.36%2019zm-4.77%206.25H41.8L25.3%2050l16.66%2025-16.5%2024.75h7.74L49.7%2075%2033.03%2050zm-8.33%200h-7.74L16.96%2050l16.67%2025-16.5%2024.75h7.73L41.36%2075%2024.7%2050Zm-8.34%200h-7.73L8.63%2050%2025.3%2075%208.8%2099.75h7.73L33.03%2075%2016.36%2050zm-16.07%200L.3%2050l16.67%2025L.46%2099.75H8.2L24.7%2075%208.02%2050l16.5-24.75zM0%2025.46v11.6l4.16%206.24%203.87-5.8zm0%2012.5v11.58l3.86-5.79zm66.8%2012.29L50.3%2074.99l3.87%205.81%204.16%206.25%204.17%206.25%204.3%206.45h7.73L58.03%2075l16.5-24.75zm16.06%200h-7.73L58.63%2075l16.5%2024.75h7.73L66.36%2075zm.6%200L66.96%2075l16.5%2024.75h7.74L74.7%2075l16.5-24.75zm16.07%200H91.8L75.3%2075l16.5%2024.75h7.73L83.03%2075z%22%2F%3E%3C%2Fsvg%3E");
--interlocked-hexagons-background-repeat: repeat;
--interlocked-hexagons-background-size: 10vh;
@@ -35,9 +43,6 @@
--sidebar-text-color: var(--main-dark-color);
--sidebar-stroke-color: var(--background-dark-color);
--secondary-color: var(--main-medium-color);
- --leaflet-text-color: var(--main-dark-color);
- --leaflet-link-color: var(--main-medium-color);
- --leaflet-link-hover-color: var(--main-light-color);
--interlocked-hexagons-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23f5ede1%22%2F%3E%3Cpath%20fill%3D%22%239ab663%22%20d%3D%22m33.64%200-.17.25h-33L4.3%206h33.07l4-6zm8.33%200-4.33%206.5h-33l3.83%205.75h33.06L49.7%200zm8.34%200-8.5%2012.75h-33l3.83%205.75H45.7L58.03%200zm8.33%200L45.97%2019h-33l3.83%205.75h33.06L66.36%200zm-8.5%2025.25L33.63%2050l3.86%205.8L54.03%2031h33l-3.83-5.75zm4.16%206.25L37.8%2056.25l3.87%205.8%2016.53-24.8h33l-3.84-5.75zm4.17%206.25L41.97%2062.5l3.86%205.8%2016.54-24.8h33l-3.84-5.75zM62.63%2044l-16.5%2024.75%203.87%205.8%2016.53-24.8h33L95.7%2044zM100%2050.45l-3.87%205.8%203.87%205.81v-11.6zM0%2050.46v11.6L12.5%2080.8l3.86-5.8zm95.83%206.25-3.87%205.8L100%2074.55v-11.6zm-4.17%206.24-3.86%205.8%2012.2%2018.3v-11.6zM0%2062.96v11.6l8.33%2012.49%203.86-5.8zm87.5%206.24L83.63%2075%20100%2099.54V87.96zM50%2075.45%2033.64%20100h7.72l12.5-18.75zm-50%200v11.6l4.16%206.24%203.87-5.8zm54.17%206.25L41.97%20100h7.73l8.33-12.5zm4.17%206.25L50.3%20100h7.73l4.17-6.25zM0%2087.95v11.59l3.86-5.8zm62.5%206.25-3.87%205.8h7.73z%22%2F%3E%3Cpath%20fill%3D%22%23bbd095%22%20d%3D%22M66.8.25%2062.97%206h33l4.03%206.05V.45l-.14-.2zM0%20.45v11.61L12.5%2030.8l3.86-5.8zM62.63%206.5l-3.83%205.75h33l8.2%2012.3v-11.6L95.7%206.5zm-4.16%206.25-3.84%205.75h33L100%2037.06v-11.6l-8.47-12.71zM0%2012.96v11.6l8.33%2012.49%203.86-5.8zM54.3%2019l-3.84%205.75h33L100%2049.55l.01-.01V37.96L87.36%2019zm-4.77%206.25H41.8L25.3%2050l16.66%2025-16.5%2024.75h7.74L49.7%2075%2033.03%2050zm-8.33%200h-7.74L16.96%2050l16.67%2025-16.5%2024.75h7.73L41.36%2075%2024.7%2050Zm-8.34%200h-7.73L8.63%2050%2025.3%2075%208.8%2099.75h7.73L33.03%2075%2016.36%2050zm-16.07%200L.3%2050l16.67%2025L.46%2099.75H8.2L24.7%2075%208.02%2050l16.5-24.75zM0%2025.46v11.6l4.16%206.24%203.87-5.8zm0%2012.5v11.58l3.86-5.79zm66.8%2012.29L50.3%2074.99l3.87%205.81%204.16%206.25%204.17%206.25%204.3%206.45h7.73L58.03%2075l16.5-24.75zm16.06%200h-7.73L58.63%2075l16.5%2024.75h7.73L66.36%2075zm.6%200L66.96%2075l16.5%2024.75h7.74L74.7%2075l16.5-24.75zm16.07%200H91.8L75.3%2075l16.5%2024.75h7.73L83.03%2075z%22%2F%3E%3C%2Fsvg%3E");
--interlocked-hexagons-background-repeat: repeat;
--interlocked-hexagons-background-size: 10vh;
@@ -56,9 +61,6 @@
--sidebar-text-color: var(--main-dark-color);
--sidebar-stroke-color: var(--background-light-color);
--secondary-color: var(--main-light-color);
- --leaflet-text-color: var(--background-dark-color);
- --leaflet-link-color: var(--background-dark-color);
- --leaflet-link-hover-color: var(--background-light-color);
--interlocked-hexagons-background: radial-gradient(33.5% 25% at 100% 82%, rgba(190, 227, 248, 0.2) 0%, rgba(197, 179, 246, 0.13) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(54% 25% at 55% 100%, rgba(190, 227, 248, 0.2) 0%, rgba(197, 179, 246, 0.13) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(37% 19.5% at 20% 100%, rgba(176, 220, 248, 0.3) 0%, rgba(197, 172, 247, 0.2) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(63% 34.5% at 66% 77%, rgba(190, 227, 248, 0.15) 0%, rgba(197, 179, 246, 0.1) 33.33%, rgba(255, 0, 0, 0) 100%), linear-gradient(0deg, #2f5883 0%, #2f4e7a 4.75%, #2e4472 9.5%, #2a3065 19%, #272e57 26.5%, #242b4b 34%, #1f2437 49%, #1c2030 57.75%, #1a1b29 66.5%, #13131b 84%);
--interlocked-hexagons-background-repeat: no-repeat;
--interlocked-hexagons-background-size: 100vw 100vh;
@@ -77,9 +79,6 @@
--sidebar-text-color: var(--main-dark-color);
--sidebar-stroke-color: var(--background-light-color);
--secondary-color: var(--main-light-color);
- --leaflet-text-color: var(--background-dark-color);
- --leaflet-link-color: var(--background-dark-color);
- --leaflet-link-hover-color: var(--background-light-color);
--interlocked-hexagons-background: url('https://f003.backblazeb2.com/file/sayana-static/themes/ram/ram-background.webp');
--interlocked-hexagons-background-repeat: repeat;
--interlocked-hexagons-background-size: auto;
@@ -89,29 +88,6 @@
--squares-and-triangles-background-height-coef: 15;
}
-a {
- color: var(--main-medium-color) !important;
- text-decoration: underline;
-}
-
-a:hover {
- color: var(--main-light-color) !important;
-}
-
-#map-container {
- color: var(--leaflet-text-color) !important;
-}
-
-#map-container a {
- color: var(--leaflet-link-color) !important;
- text-decoration: underline;
-}
-
-#map-container a:hover {
- color: var(--leaflet-link-hover-color) !important;
- text-decoration: underline;
-}
-
.bg-interlocked-hexagons {
background-image: var(--interlocked-hexagons-background);
background-repeat: var(--interlocked-hexagons-background-repeat);
@@ -178,6 +154,10 @@ a:hover {
background-size: calc(var(--squares-and-triangles-background-width-coef) * 1vw) calc(var(--squares-and-triangles-background-height-coef) * 1vw);
}
+body:has(.lg-container:not([class~="lg-inline"])) .desktop-sidebar-custom {
+ display: none !important;
+}
+
.mobile-topbar-custom {
width: 90vw;
height: 8%;
@@ -185,7 +165,7 @@ a:hover {
margin-left: 5vw;
}
-body:has(.lg-container:not([class~="lg-inline"])) #sidebar {
+body:has(.lg-container:not([class~="lg-inline"])) .mobile-topbar-custom {
display: none !important;
}
@@ -237,66 +217,6 @@ body:has(.lg-container:not([class~="lg-inline"])) #sidebar {
-webkit-text-stroke-width: 0.1vw;
}
-.theme-wheel {
- position: fixed;
- width: 15vmax;
- height: 15vmax;
- border-radius: 50%;
- background: var(--background-light-color);
- opacity: 0.8;
- border: 1vmax outset var(--background-dark-color);
- box-shadow: 0 0 5vmax var(--main-light-color);
- opacity: 0;
- visibility: hidden;
- transform: scale(0.5);
- transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
- z-index: 1001;
-}
-
-.theme-wheel.active {
- opacity: 1;
- visibility: visible;
- transform: scale(1);
-}
-
-.theme-icon {
- position: absolute;
- width: 4vmax;
- height: 4vmax;
- border-radius: 50%;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.3s ease;
- border: 0.1vmax inset var(--background-light-color);
- background: var(--interlocked-hexagons-background);
- background-size: contain;
-}
-
-.theme-icon:hover {
- transform: scale(1.2);
-}
-
-.theme-icon.active {
- transform: scale(1.2);
- border: 0.3vmax outset var(--main-light-color);
-}
-
-.custom-svg-marker {
- background: transparent;
- border: none;
-}
-
-.custom-svg-marker svg {
- filter: drop-shadow(2px 2px 2px rgba(0,0,0,0.3));
- transition: transform 0.2s;
-}
-
-.custom-svg-marker:hover svg {
- transform: scale(1.1);
-}
-
.lg-backdrop {
background-color: var(--background-light-color);
opacity: 0.8 !important;
@@ -337,25 +257,14 @@ body:has(.lg-container:not([class~="lg-inline"])) #sidebar {
}
.lg-content {
- top: 2.5vmax !important;
- bottom: calc(10vmin + 5vmax) !important;
-}
-
-.lg-outer[class~="lg-single-item"] .lg-content {
- bottom: 5vmax !important;
-}
-
-.lg-thumb-item {
- width: 10vmin !important;
- height: 10vmin !important;
+ top: 3vmax !important;
}
.lg-container:not([class~="lg-inline"]) .lg-content {
top: 4vmax !important;
}
-.lg-toolbar .lg-icon,
-.lg-content .lg-icon {
+.lg-toolbar .lg-icon {
padding: 0 0.7vmax;
font-size: 1.5vmax !important;
height: 1.5vmax;
@@ -363,20 +272,11 @@ body:has(.lg-container:not([class~="lg-inline"])) #sidebar {
color: var(--main-dark-color);
}
-.lg-container:not([class~="lg-inline"]) .lg-toolbar .lg-icon,
-.lg-container:not([class~="lg-inline"]) .lg-content .lg-icon {
+.lg-container:not([class~="lg-inline"]) .lg-toolbar .lg-icon {
font-size: 3vmax !important;
height: 3vmax;
}
-.lg-prev {
- left: 0 !important;
-}
-
-.lg-next {
- right: 0 !important;
-}
-
.lg-toolbar .lg-icon:hover {
color: var(--main-medium-color);
}
@@ -396,7 +296,7 @@ body:has(.lg-container:not([class~="lg-inline"])) #sidebar {
.inline-gallery-container {
width: 80%;
- height: 70vmin;
+ height: 60vmin;
}
.light-gallery-captions {
@@ -436,11 +336,6 @@ body:has(.lg-container:not([class~="lg-inline"])) #sidebar {
}
}
-body[data-theme~="night"] .leaflet-tile,
-body[data-theme~="ram"] .leaflet-tile {
- filter: invert(1) hue-rotate(190deg) contrast(1.1) brightness(1.2) !important;
-}
-
@media (min-aspect-ratio: 0.8/1) {
.ar-gt-0\.8\:mr-\[1\.2vmin\] {
margin-right: 1.2vmin;
@@ -521,10 +416,6 @@ body[data-theme~="ram"] .leaflet-tile {
height: 10vh;
}
- .ar-lt-0\.8\:h-\[30vh\] {
- height: 30vh;
- }
-
.ar-lt-0\.8\:h-\[90vh\] {
height: 90vh;
}
@@ -533,10 +424,6 @@ body[data-theme~="ram"] .leaflet-tile {
width: 8vh;
}
- .ar-lt-0\.8\:w-\[80\%\] {
- width: 80%;
- }
-
.ar-lt-0\.8\:w-\[90vw\] {
width: 90vw;
}
@@ -549,10 +436,6 @@ body[data-theme~="ram"] .leaflet-tile {
max-height: 80%;
}
- .ar-lt-0\.8\:max-h-\[calc\(100\%-25vh\)\] {
- max-height: calc(100% - 25vh);
- }
-
.ar-lt-0\.8\:text-\[5vh\] {
font-size: 5vh;
}
diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html
index cd19bd0..d5146f8 100644
--- a/views/layouts/general-page.html
+++ b/views/layouts/general-page.html
@@ -12,7 +12,7 @@
<link href="/output.css" rel="stylesheet">
</head>
-<body data-theme="ram" class="font-spectral text-main-dark overflow-hidden bg-interlocked-hexagons h-screen flex flex-row ar-lt-0.8:flex-col">
+<body data-theme="night" class="font-spectral text-main-dark overflow-hidden bg-interlocked-hexagons h-screen flex flex-row ar-lt-0.8:flex-col">
<script>
function switchTheme(theme) {
@@ -29,42 +29,33 @@
document.body.setAttribute('data-theme', theme);
}
- const savedTheme = localStorage.getItem('theme') || 'ram';
+ const savedTheme = localStorage.getItem('theme') || 'night';
switchTheme(savedTheme);
</script>
- <script>
- const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
- </script>
-
- <div id="sidebar" class="bg-sidebar flex sticky flex-col ar-gt-0.8:items-center shadow-2xl z-20 w-[5vw] ar-lt-0.8:w-[90vw] ar-lt-0.4:w-[100vw] h-screen ar-lt-0.8:h-[8vh] ar-lt-0.8:ml-[5vw] ar-lt-0.4:ml-0">
+ <div class="bg-sidebar flex sticky flex-col ar-gt-0.8:items-center shadow-2xl z-20 w-[5vw] ar-lt-0.8:w-[90vw] ar-lt-0.4:w-[100vw] h-screen ar-lt-0.8:h-[8vh] ar-lt-0.8:ml-[5vw] ar-lt-0.4:ml-0">
<div class="logo-custom ar-lt-0.8:hidden text-sidebar font-patua font-extrabold text-center relative z-20">
saya.today
</div>
<div class="text-[3vw] ar-lt-0.8:text-[5vh] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1vw] flex flex-col ar-lt-0.8:flex-row gap-0 relative z-20">
<i onclick="location.href='/';" class="fas fa-home w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
<i onclick="location.href='/{{ .Lang }}/blog';" class="fas fa-search w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
- <i onclick="location.href='/{{ .Lang }}/map';" class="fas fa-map w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
- <i onclick="toggleThemeWheel();" id="theme-button" class="fas fa-swatchbook w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
- </div>
- <div class="theme-wheel" id="theme-wheel">
- <div class="theme-icon" data-theme="olive"></div>
- <div class="theme-icon" data-theme="lettuce"></div>
- <div class="theme-icon" data-theme="night"></div>
- <div class="theme-icon" data-theme="ram"></div>
+ <!-- <i class="fas fa-images w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> -->
+ <i onclick="switchTheme('')" class="fas fa-swatchbook w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
</div>
</div>
<div class="bg-background-dark flex z-10 ar-lt-0.8:w-[90vw] ar-lt-0.4:w-[100vw] h-screen ar-lt-0.8:ml-[5vw] ar-lt-0.4:ml-0">
<div class="pl-[0.5vw] bg-background-dark border-r-[0.4vmin] border-dashed border-main-dark"></div>
- <div class="ar-gt-0.8:min-w-fit ar-gt-0.8:max-w-[100vh] flex flex-col bg-background-dark relative pt-[1.6vmin] ml-[1.6vmin] pr-[1.6vmin]">
- <div {{ if .PublishedDate }} hx-get="/api/v1/tz" hx-vals='js:{timestamp: "{{ .PublishedDate }}", tz: clientTimeZone}' hx-target="#published-date" hx-swap="innerHTML" hx-trigger="load" {{ end }}
- class="flex flex-row mb-[0.4vmin]">
+ <div class="ar-gt-0.8:min-w-fit ar-gt-0.8:max-w-[100vh] flex flex-col bg-background-dark relative mt-[1.6vmin] ml-[1.6vmin] pr-[1.6vmin]">
+ <div class="flex flex-row mb-[0.4vmin]">
<i onclick="location.pathname += location.pathname.endsWith('/') ? '../' : '/../';" class="fas fa-circle-left hover:bg-main-dark hover:bg-opacity-10 cursor-pointer transition-colors duration-300 rounded-md text-[2vmax] mt-auto mb-auto px-[0.8vmin]"></i>
<p class="text-[2vmax] font-spectral italic font-extrabold select-none mt-auto pl-[0.8vmin] pr-[1.2vmin]">//</p>
<p class="text-[2vmax] font-spectral italic text-left mt-auto">{{ .Title }}</p>
- <p id="published-date" class="grow text-[1.5vmax] font-spectral text-secondary text-right mt-auto">{{ .PublishedDate }}</p>
+ {{- if .PublishedDate }}
+ <p class="grow text-[1.5vmax] font-spectral text-secondary text-right mt-auto">{{ .PublishedDate }}</p>
+ {{- end }}
</div>
<hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]">
@@ -85,130 +76,6 @@
<div class="pr-[0.5vw] bg-background-dark shadow-2xl border-l-[0.4vmin] border-dotted border-main-dark"></div>
</div>
- <script src="https://f003.backblazeb2.com/file/sayana-static/libs/htmx/2.0.6/htmx.min.js"></script>
-
- <script>
- const themeWheel = document.getElementById('theme-wheel');
- const themeButton = document.getElementById('theme-button');
- const themeIcons = document.querySelectorAll('.theme-icon');
-
- themeIcons.forEach(icon => {
- if (icon.getAttribute('data-theme') == savedTheme) {
- icon.classList.add('active');
- }
- });
-
- function calculateVmin(percent) {
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
-
- const smallerDimension = Math.min(viewportWidth, viewportHeight);
-
- const vminValue = (smallerDimension / 100) * percent;
-
- return vminValue;
- }
-
- function calculateVmax(percent) {
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
-
- const largerDimension = Math.max(viewportWidth, viewportHeight);
-
- const vmaxValue = (largerDimension / 100) * percent;
-
- return vmaxValue;
- }
-
- function positionThemeIcons() {
- const icons = themeWheel.querySelectorAll('.theme-icon');
- const iconCount = icons.length;
- const vmax = calculateVmax(1);
- const wheelRadius = 7.5*vmax; // Radius of the wheel
- const iconRadius = 4*vmax; // Distance from center to place icons
- const iconSize = 3*vmax; // Half of icon size for centering
-
- icons.forEach((icon, index) => {
- const angle = (index * 360 / iconCount) - 90;
- const angleRad = (angle * Math.PI) / 180;
-
- const x = Math.cos(angleRad) * iconRadius + wheelRadius - iconSize;
- const y = Math.sin(angleRad) * iconRadius + wheelRadius - iconSize;
-
- icon.style.left = x + 'px';
- icon.style.top = y + 'px';
- });
- }
-
- function positionThemeWheel() {
- const buttonRect = themeButton.getBoundingClientRect();
- const buttonCenterX = buttonRect.left + buttonRect.width / 2;
- const buttonCenterY = buttonRect.top + buttonRect.height / 2;
-
- const wheelRadius = calculateVmax(7.5);
- const margin = calculateVmax(1);
-
- let wheelX = buttonCenterX - wheelRadius;
- let wheelY = buttonCenterY - wheelRadius;
-
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
-
- if (wheelX < margin) {
- wheelX = margin;
- } else if (wheelX + (wheelRadius * 2) > viewportWidth - margin) {
- wheelX = viewportWidth - (wheelRadius * 2) - margin;
- }
-
- if (wheelY < margin) {
- wheelY = margin;
- } else if (wheelY + (wheelRadius * 2) > viewportHeight - margin) {
- wheelY = viewportHeight - (wheelRadius * 2) - margin;
- }
-
- themeWheel.style.left = wheelX + 'px';
- themeWheel.style.top = wheelY + 'px';
- }
-
- function hideThemeWheel() {
- themeWheel.classList.remove('active');
- }
-
- function showThemeWheel() {
- positionThemeWheel();
- positionThemeIcons();
- themeWheel.classList.add('active');
- }
-
- function toggleThemeWheel() {
- if (themeWheel.classList.contains('active')) {
- hideThemeWheel();
- } else {
- showThemeWheel();
- }
- }
-
- themeIcons.forEach(icon => {
- icon.addEventListener('click', (e) => {
- e.stopPropagation();
- const theme = icon.getAttribute('data-theme');
- switchTheme(theme);
- themeIcons.forEach(icon => {
- icon.classList.remove('active');
- })
- icon.classList.add('active');
- });
- });
-
- themeWheel.addEventListener('mouseleave', hideThemeWheel);
-
- document.addEventListener('click', (e) => {
- if (!themeWheel.contains(e.target) && e.target !== themeButton) {
- hideThemeWheel();
- }
- });
- </script>
-
{{ block "footer" . }}{{ end }}
</body>
diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html
index 9fa8ea7..6fe1c82 100644
--- a/views/pages/blog-catalogue.html
+++ b/views/pages/blog-catalogue.html
@@ -1,39 +1,39 @@
{{ define "body" }}
-<div class="flex flex-row ar-lt-0.8:flex-col max-h-[calc(100%-7vmax)] ar-lt-0.8:max-h-[calc(100%-25vh)] flex-1 text-[1vmax]">
+<div class="flex flex-row ar-lt-0.8:flex-col flex-1 text-[1vmax]">
<form hx-get="/api/v1/blog-search" hx-vals='{"lang": "{{ .Lang }}"}' hx-target=".blog-cards" hx-swap="innerHTML"
- class="tags-list ar-gt-0.8:min-w-[10vh] ar-gt-0.8:max-w-[20vh] ar-gt-0.8:w-fit ar-lt-0.8:w-[100%] flex shrink-0 flex-col overflow-y-auto ar-gt-0.8:mr-[1.2vmin]">
+ class="tags-list ar-gt-0.8:min-w-[10vh] ar-gt-0.8:max-w-[20vh] ar-gt-0.8:w-fit ar-lt-0.8:w-[100%] flex shrink-0 flex-col ar-gt-0.8:mr-[1.2vmin]">
<fieldset>
<legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.TagsHeader }}</legend>
- <div class="flex flex-col ar-lt-0.8:grid grid-cols-5 grid-flow-row-dense">
- <div class="m-[0.4vmin]">
+ <div class="flex flex-col ar-lt-0.8:grid grid-cols-5 grid-flow-row-dense gap-[5%]">
+ <div>
<label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();"> {{ .L.BlogSearch.ChooseAllTags }}</label>
</div>
{{- range .Tags }}
- <div class="m-[0.4vmin]">
- <label><input type="checkbox" id="tag{{ .Name }}Checkbox" name="tags[]" value="{{ .Name }}" onclick="contextAll();" {{ if contains $.QueryTags .Name }}checked{{ end }}> {{ .Name }} <span class="text-secondary italic">{{ .Count }}</span></label>
+ <div>
+ <label><input type="checkbox" id="tag{{ . }}Checkbox" name="tags[]" value="{{ . }}" onclick="contextAll();" {{ if contains $.QueryTags . }}checked{{ end }}> {{ . }}</label>
</div>
{{- end }}
</div>
</fieldset>
<fieldset class="mt-[0.8vmin]">
<legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.OrderByHeader }}</legend>
- <div class="flex flex-col ar-lt-0.8:grid grid-cols-3 grid-rows-2 grid-flow-col">
- <div class="m-[0.4vmin]">
+ <div class="flex flex-col ar-lt-0.8:grid grid-cols-3 grid-rows-2 grid-flow-col gap-[5%]">
+ <div>
<label><input type="radio" id="sortTitleAsc" name="sort" value="titleAsc" {{ if eq .QuerySort "titleAsc" }}checked{{ end }}> {{ .L.BlogSearch.TitleOrdered }} <i class="fas fa-arrow-down-a-z"></i></label>
</div>
- <div class="m-[0.4vmin]">
+ <div>
<label><input type="radio" id="sortTitleDesc" name="sort" value="titleDesc" {{ if eq .QuerySort "titleDesc" }}checked{{ end }}> {{ .L.BlogSearch.TitleOrdered }} <i class="fas fa-arrow-down-z-a"></i></label>
</div>
- <div class="m-[0.4vmin]">
+ <div>
<label><input type="radio" id="sortActionDateAsc" name="sort" value="actionDateAsc" {{ if eq .QuerySort "actionDateAsc" }}checked{{ end }}> {{ .L.BlogSearch.ActionDateOrdered }} <i class="fas fa-arrow-down-1-9"></i></label>
</div>
- <div class="m-[0.4vmin]">
+ <div>
<label><input type="radio" id="sortActionDateDesc" name="sort" value="actionDateDesc" {{ if eq .QuerySort "actionDateDesc" }}checked{{ end }}> {{ .L.BlogSearch.ActionDateOrdered }} <i class="fas fa-arrow-down-9-1"></i></label>
</div>
- <div class="m-[0.4vmin]">
+ <div>
<label><input type="radio" id="sortPublicationDateAsc" name="sort" value="publicationDateAsc" {{ if eq .QuerySort "publicationDateAsc" }}checked{{ end }}> {{ .L.BlogSearch.PublicationDateOrdered }} <i class="fas fa-arrow-down-1-9"></i></label>
</div>
- <div class="m-[0.4vmin]">
+ <div>
<label><input type="radio" id="sortPublicationDateDesc" name="sort" value="publicationDateDesc" {{ if eq .QuerySort "publicationDateDesc" }}checked{{ end }}> {{ .L.BlogSearch.PublicationDateOrdered }} <i class="fas fa-arrow-down-9-1"></i></label>
</div>
</div>
@@ -49,7 +49,7 @@
<div class="w-[0.5vh] border-r-[0.3vh] border-dotted border-main-dark bg-background-dark"></div>
- <div hx-get="/api/v1/blog-search" hx-vals='js:{lang: "{{ .Lang }}", tz: clientTimeZone}' hx-target="this" hx-swap="innerHTML" hx-trigger="load" hx-include=".tags-list"
+ <div hx-get="/api/v1/blog-search" hx-vals='{"lang": "{{ .Lang }}"}' hx-target="this" hx-swap="innerHTML" hx-trigger="load" hx-include=".tags-list"
class="blog-cards grow flex flex-col overflow-y-auto bg-background-light inset-shadow p-[2.4vmin]">
Loading...
</div>
@@ -57,6 +57,22 @@
{{ end }}
{{ define "footer" }}
+<script src="https://f003.backblazeb2.com/file/sayana-static/libs/htmx/2.0.6/htmx.min.js"></script>
+
+<script>
+ const tagsContainers = document.querySelectorAll('form.tags-list');
+ tagsContainers.forEach(tagsContainer => {
+ const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]');
+ anyChecked = false;
+ checkboxes.forEach(checkbox => {
+ anyChecked = checkbox.checked ? true : anyChecked;
+ });
+ if (!anyChecked) {
+ selectAll();
+ }
+ });
+</script>
+
<script>
function selectAll() {
const tagsContainers = document.querySelectorAll('form.tags-list');
@@ -89,7 +105,7 @@
checkboxes.forEach(checkbox => {
checkbox.checked = true;
});
- const sortRadio = tagsContainer.querySelector('#sortPublicationDateDesc');
+ const sortRadio = tagsContainer.querySelector('#sortTitleAsc');
sortRadio.checked = true;
});
}
@@ -118,41 +134,7 @@
window.history.pushState({}, '', url.toString());
}
- function pickedTagSetHrefParams(tag) {
- const url = new URL(window.location.href);
- url.searchParams.delete('tags[]');
- url.searchParams.set('tags[]', tag);
-
- const tagsContainers = document.querySelectorAll('form.tags-list');
- tagsContainers.forEach(tagsContainer => {
- const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]');
- checkboxes.forEach(checkbox => {
- checkbox.checked = false;
- });
- chosenCheckbox = tagsContainer.querySelector(`#tag${tag}Checkbox`);
- chosenCheckbox.checked = true;
- });
-
- window.history.pushState({}, '', url.toString());
- }
-
document.addEventListener('DOMContentLoaded', contextAll);
</script>
-<script>
- const tagsContainers = document.querySelectorAll('form.tags-list');
- tagsContainers.forEach(tagsContainer => {
- const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]');
- anyChecked = false;
- checkboxes.forEach(checkbox => {
- anyChecked = checkbox.checked ? true : anyChecked;
- });
- if (!anyChecked) {
- const selectAllCheckbox = tagsContainer.querySelector('#tagsAllCheckbox');
- selectAllCheckbox.checked = true;
- selectAll();
- }
- });
-</script>
-
{{ end }}
diff --git a/views/pages/blog-page.html b/views/pages/blog-page.html
index 755744d..5f001d6 100644
--- a/views/pages/blog-page.html
+++ b/views/pages/blog-page.html
@@ -15,11 +15,11 @@
{{ end }}
{{ define "body" }}
-<div class="bg-background-light flex flex-col grow overflow-y-auto inset-shadow p-[2.4vmin] max-h-[calc(100%-7vmax)] ar-lt-0.8:max-h-[calc(100%-25vh)]">
+<div class="bg-background-light flex flex-col grow overflow-y-auto inset-shadow p-[2.4vmin] ar-lt-0.8:max-h-[80%]">
{{ .ParsedMarkdown }}
{{- if .MapLocationX }}
<hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto">
- <div id="map-container" class="relative p-[0.8vmin] flex-none ml-auto mr-auto w-[60%] ar-lt-0.8:w-[80%] h-[40vh] ar-lt-0.8:h-[30vh]"></div>
+ <div id="location-map" class="relative p-[0.8vmin] flex-none ml-auto mr-auto w-[60%] h-[40vh]"></div>
<hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto">
{{- end }}
</div>
@@ -33,13 +33,11 @@
<script src="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.js"></script>
<script>
- var map;
-
function initLocationMap(id, x, y, relativeErrorMeters = 0) {
- map = L.map(id).setView([x, y], 8);
+ var map = L.map(id).setView([x, y], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
- maxZoom: 18,
+ maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
@@ -52,15 +50,11 @@
}).addTo(map);
}
- var marker = L.marker([x, y], {
- title: '{{ .Title }}',
- }).addTo(map)
- .bindPopup('<span><b>{{ .Title }}</b><br><a href="https://www.openstreetmap.org/#map=13/{{ .MapLocationX }}/{{ .MapLocationY }}">{{ .MapLocationX }}, {{ .MapLocationY }}</a></span>')
- .openPopup();
+ var marker = L.marker([x, y]).addTo(map);
}
document.addEventListener('DOMContentLoaded', () => {initLocationMap(
- 'map-container',
+ 'location-map',
{{ .MapLocationX }},
{{ .MapLocationY }},
{{ .MapLocationAreaMeters }}
@@ -68,7 +62,7 @@
window.addEventListener('resize', function() {
setTimeout(() => {
- map.invalidateSize();
+ if (window.location-map) window.location-map.invalidateSize();
}, 100);
});
</script>
diff --git a/views/pages/global-map.html b/views/pages/global-map.html
deleted file mode 100644
index 6185076..0000000
--- a/views/pages/global-map.html
+++ /dev/null
@@ -1,269 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>SAYA TODAY // {{ .L.GlobalMap.Header }}</title>
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
- <link rel="preconnect" href="https://fonts.googleapis.com">
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
- <link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,200;1,300;1,400;1,500;1,600;1,700;1,800&family=Patua+One&display=swap" rel="stylesheet">
- {{ block "header" . }}{{ end }}
- <link href="/output.css" rel="stylesheet">
- <link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.css">
- <link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet.markercluster/1.4.1/MarkerCluster.css">
- <link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css">
-</head>
-
-<body data-theme="ram" class="font-spectral text-main-dark overflow-hidden bg-interlocked-hexagons h-screen flex flex-row ar-lt-0.8:flex-col">
-
- <script>
- function switchTheme(theme) {
- if (theme == "" || typeof theme == "undefined") {
- const currentTheme = document.body.getAttribute('data-theme');
- switch (currentTheme) {
- case "olive": theme = "lettuce"; break;
- case "lettuce": theme = "night"; break;
- case "night": theme = "ram"; break;
- case "ram": theme = "olive"; break;
- }
- }
- localStorage.setItem('theme', theme);
- document.body.setAttribute('data-theme', theme);
- }
-
- const savedTheme = localStorage.getItem('theme') || 'ram';
- switchTheme(savedTheme);
- </script>
-
- <script>
- const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
- </script>
-
- <div id="sidebar" class="bg-sidebar flex sticky flex-col ar-gt-0.8:items-center shadow-2xl z-20 w-[5vw] ar-lt-0.8:w-screen h-screen ar-lt-0.8:h-[8vh]">
- <div class="logo-custom ar-lt-0.8:hidden text-sidebar font-patua font-extrabold text-center relative z-20">
- saya.today
- </div>
- <div class="text-[3vw] ar-lt-0.8:text-[5vh] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1vw] flex flex-col ar-lt-0.8:flex-row gap-0 relative z-20">
- <i onclick="location.href='/';" class="fas fa-home w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
- <i onclick="location.href='/{{ .Lang }}/blog';" class="fas fa-search w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
- <i onclick="location.href='/{{ .Lang }}/map';" class="fas fa-map w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
- <i onclick="toggleThemeWheel();" id="theme-button" class="fas fa-swatchbook w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
- </div>
- <div class="theme-wheel" id="theme-wheel">
- <div class="theme-icon" data-theme="olive"></div>
- <div class="theme-icon" data-theme="lettuce"></div>
- <div class="theme-icon" data-theme="night"></div>
- <div class="theme-icon" data-theme="ram"></div>
- </div>
- </div>
-
- <div id="map-container" class="bg-background-dark flex z-10 w-[95vw] ar-lt-0.8:w-screen h-screen ar-lt-0.8:h-[92vh]"></div>
-
- <script src="https://f003.backblazeb2.com/file/sayana-static/libs/htmx/2.0.6/htmx.min.js"></script>
- <script src="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.js"></script>
- <script src="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js"></script>
-
- <script>
- const themeWheel = document.getElementById('theme-wheel');
- const themeButton = document.getElementById('theme-button');
- const themeIcons = document.querySelectorAll('.theme-icon');
-
- themeIcons.forEach(icon => {
- if (icon.getAttribute('data-theme') == savedTheme) {
- icon.classList.add('active');
- }
- });
-
- function calculateVmax(percent) {
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
-
- const largerDimension = Math.max(viewportWidth, viewportHeight);
-
- const vmaxValue = (largerDimension / 100) * percent;
-
- return vmaxValue;
- }
-
- function positionThemeIcons() {
- const icons = themeWheel.querySelectorAll('.theme-icon');
- const iconCount = icons.length;
- const vmax = calculateVmax(1);
- const wheelRadius = 7.5*vmax; // Radius of the wheel
- const iconRadius = 4*vmax; // Distance from center to place icons
- const iconSize = 3*vmax; // Half of icon size for centering
-
- icons.forEach((icon, index) => {
- const angle = (index * 360 / iconCount) - 90;
- const angleRad = (angle * Math.PI) / 180;
-
- const x = Math.cos(angleRad) * iconRadius + wheelRadius - iconSize;
- const y = Math.sin(angleRad) * iconRadius + wheelRadius - iconSize;
-
- icon.style.left = x + 'px';
- icon.style.top = y + 'px';
- });
- }
-
- function positionThemeWheel() {
- const buttonRect = themeButton.getBoundingClientRect();
- const buttonCenterX = buttonRect.left + buttonRect.width / 2;
- const buttonCenterY = buttonRect.top + buttonRect.height / 2;
-
- const wheelRadius = calculateVmax(7.5);
- const margin = calculateVmax(1);
-
- let wheelX = buttonCenterX - wheelRadius;
- let wheelY = buttonCenterY - wheelRadius;
-
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
-
- if (wheelX < margin) {
- wheelX = margin;
- } else if (wheelX + (wheelRadius * 2) > viewportWidth - margin) {
- wheelX = viewportWidth - (wheelRadius * 2) - margin;
- }
-
- if (wheelY < margin) {
- wheelY = margin;
- } else if (wheelY + (wheelRadius * 2) > viewportHeight - margin) {
- wheelY = viewportHeight - (wheelRadius * 2) - margin;
- }
-
- themeWheel.style.left = wheelX + 'px';
- themeWheel.style.top = wheelY + 'px';
- }
-
- function hideThemeWheel() {
- themeWheel.classList.remove('active');
- }
-
- function showThemeWheel() {
- positionThemeWheel();
- positionThemeIcons();
- themeWheel.classList.add('active');
- }
-
- function toggleThemeWheel() {
- if (themeWheel.classList.contains('active')) {
- hideThemeWheel();
- } else {
- showThemeWheel();
- }
- }
-
- themeIcons.forEach(icon => {
- icon.addEventListener('click', (e) => {
- e.stopPropagation();
- const theme = icon.getAttribute('data-theme');
- switchTheme(theme);
- themeIcons.forEach(icon => {
- icon.classList.remove('active');
- })
- icon.classList.add('active');
- });
- });
-
- themeWheel.addEventListener('mouseleave', hideThemeWheel);
-
- document.addEventListener('click', (e) => {
- if (!themeWheel.contains(e.target) && e.target !== themeButton) {
- hideThemeWheel();
- }
- });
- </script>
-
- <script>
- var map;
- var markers = L.markerClusterGroup();
-
- function createCustomIcon(color, borderColor = "var(--main-light-color)") {
- const svgIcon = `
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
- <path d="M12 1C7.5 1 4 4.5 4 9c0 6.5 8 14 8 14s8-7.5 8-14c0-4.5-3.5-8-8-8zm0 11c-1.5 0-3-1.5-3-3s1.5-3 3-3 3 1.5 3 3-1.5 3-3 3z"
- fill="${color}"
- stroke="${borderColor}"
- stroke-width="1"
- stroke-linejoin="round"/>
- </svg>
- `;
-
- return L.divIcon({
- html: svgIcon,
- className: 'custom-svg-marker',
- iconSize: [32, 32],
- iconAnchor: [16, 32],
- popupAnchor: [0, -32]
- });
- }
-
- function initLocationMap() {
- map = L.map('map-container').setView([{{ .MapLocationLat }}, {{ .MapLocationLong }}], 4);
-
- L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
- maxZoom: 18,
- attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
- }).addTo(map);
-
- map.addLayer(markers);
- }
-
- function stringToColor(str) {
- let hash = 0;
- for (let i = 0; i < str.length; i++) {
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
- }
-
- let color = '#';
- for (let i = 0; i < 3; i++) {
- const value = (hash >> (i * 8)) & 0xFF;
- color += ('00' + value.toString(16)).substr(-2);
- }
-
- return color;
- }
-
- function mapAddMarker(x, y, relativeErrorMeters = 0, title = "", link = "", thumbnail = "") {
- const titleColor = stringToColor(title);
-
- if (relativeErrorMeters != 0) {
- var circle = L.circle([x, y], {
- color: titleColor,
- fillColor: titleColor,
- fillOpacity: 0.15,
- radius: relativeErrorMeters
- }).addTo(map);
- }
-
- var marker = L.marker([x, y], {
- icon: createCustomIcon(titleColor),
- title: title,
- }).bindPopup(`
- <div class="flex flex-row justify-center justify-items-center">
- <img onclick="location.href='${link}';" class="cursor-pointer object-cover rounded-[10%] select-none w-[3vmax] h-[3vmax] mr-[0.5vmax]" src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/${thumbnail}.webp">
- <span><b><a href="${link}">${title}</a></b><br><a href="https://www.openstreetmap.org/#map=13/${x}/${y}">${x}, ${y}</a></span>
- </div>`);
-
- markers.addLayer(marker);
- }
-
- document.addEventListener('DOMContentLoaded', () => {
- initLocationMap();
- {{- range .MapMarkers }}
- mapAddMarker({{ .Lat }}, {{ .Long }}, {{ .AccuracyMeters }}, {{ .Title }}, {{ .PageLink }}, {{ .Thumbnail }});
- {{- end }}
- });
-
- window.addEventListener('resize', function() {
- setTimeout(() => {
- map.invalidateSize();
- }, 100);
- });
- </script>
-
- {{ block "footer" . }}{{ end }}
-
-</body>
-</html>
diff --git a/views/partials/catalogue-blog-card-tags.html b/views/partials/catalogue-blog-card-tags.html
deleted file mode 100644
index 374314a..0000000
--- a/views/partials/catalogue-blog-card-tags.html
+++ /dev/null
@@ -1 +0,0 @@
-{{ range $i, $tag := .Tags }}{{ if ne $i 0 }}, {{ end }}<a hx-trigger="click" hx-get="/api/v1/blog-search" hx-target=".blog-cards" hx-swap="innerHTML" hx-trigger="load" hx-include=".tags-list" class="hx-anchor cursor-pointer" onclick="pickedTagSetHrefParams({{ $tag }})">{{ $tag }}</a>{{ end }} \ No newline at end of file
diff --git a/views/partials/catalogue-blog-cards.html b/views/partials/catalogue-blog-cards.html
index 62e931a..36a6184 100644
--- a/views/partials/catalogue-blog-cards.html
+++ b/views/partials/catalogue-blog-cards.html
@@ -1,18 +1,16 @@
{{- range .BlogPages }}
<hr class="first-of-type:hidden border-t-[0.3vh] my-[0.8vmin] border-dotted border-main-dark w-[80%] mx-auto">
<div class="m-[0.4vmin] flex flex-row justify-center justify-items-center">
- <img onclick="location.href='{{ .ArticleLink }}';" class="cursor-pointer object-cover rounded-[10%] select-none w-[10vmin] h-[10vmin] aspect-square my-[0.4vmin] mr-[0.8vmin]" src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/{{ .Thumbnail }}.webp">
+ <img onclick="location.href='{{ .ArticleLink }}}';" class="cursor-pointer object-cover rounded-[10%] select-none w-[10vmin] h-[10vmin] aspect-square my-[0.4vmin] mr-[0.8vmin]" src="https://f003.backblazeb2.com/file/sayana-photos/thumbnails/{{ .Thumbnail }}.webp">
<div class="grow my-[0.4vmin] mx-[0.8vmin] flex flex-col justify-center">
- <div class="flex flex-row gap-[5%] max-w-[100%]">
- <a href="{{ .ArticleLink }}" class="flex-9/12 grow-0 block text-[1vmax] cursor-pointer">
- <span class="font-extrabold">{{ .Title }}</span>
- <span class="font-extrabold select-none text-secondary">//</span>
- <span class="italic text-secondary">{{ .ActionDate }}</span>
- </a>
- <p class="flex-3/12 grow-0 text-[1vmax] italic text-secondary text-right">{{ .PublishedTime }}</p>
+ <div class="flex flex-row">
+ <a href="{{ .ArticleLink }}" class="block text-[1vmax] font-extrabold">{{ .Title }}</a>
+ <a href="{{ .ArticleLink }}" class="block text-[1vmax] font-extrabold pl-[0.4vmin] pr-[0.4vmin] select-none cursor-pointer text-secondary">//</a>
+ <a href="{{ .ArticleLink }}" class="block text-[1vmax] italic text-secondary">{{ .ActionDate }}</a>
+ <p class="text-[1vmax] italic text-secondary text-right grow">{{ .PublishedTime }}</p>
</div>
<p class="text-[1vmax] text-main-dark">{{ .ShortDescription }}</p>
- <p class="text-[1vmax] text-secondary">{{ $.L.TagsLabel }} {{ template "catalogue-blog-card-tags.html" . }}</p>
+ <p class="text-[1vmax] text-secondary">{{ .L.TagsLabel }} {{ .Tags }}</p>
</div>
</div>
{{- end }} \ No newline at end of file