summaryrefslogtreecommitdiff
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build-and-deploy.yml11
-rw-r--r--.gitignore4
-rw-r--r--Dockerfile3
-rw-r--r--config/config.go16
-rw-r--r--config/config.yaml17
-rw-r--r--deploy/inventory.yml14
-rw-r--r--deploy/playbook.yml28
-rw-r--r--internal/b2/client.go12
-rw-r--r--internal/frontmatter/parser.go1
-rw-r--r--internal/lightgallery/block.go11
-rw-r--r--internal/lightgallery/html_renderer.go30
-rw-r--r--internal/lightgallery/parser.go35
-rw-r--r--internal/templatemanager/templatemanager.go57
-rw-r--r--locale/localization.en.yaml9
-rw-r--r--locale/localization.go50
-rw-r--r--locale/localization.ru.yaml9
-rw-r--r--main.go254
-rwxr-xr-xsaya-today-webbin0 -> 19752119 bytes
-rw-r--r--static/input.css204
-rw-r--r--views/index.html60
-rw-r--r--views/layouts/blog-page.html194
-rw-r--r--views/layouts/general-page.html82
-rw-r--r--views/pages/blog-catalogue.html142
-rw-r--r--views/pages/blog-page.html74
-rw-r--r--views/partials/catalogue-blog-cards.html18
25 files changed, 966 insertions, 369 deletions
diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml
index e3bacd2..ac5fa1f 100644
--- a/.github/workflows/build-and-deploy.yml
+++ b/.github/workflows/build-and-deploy.yml
@@ -61,7 +61,7 @@ jobs:
- name: Test SSH connection
run: |
- ssh -o ConnectTimeout=10 -i ~/.ssh/id_rsa svc_github@pl.saya.today "echo 'SSH connection successful'"
+ ssh -o ConnectTimeout=10 -i ~/.ssh/id_ed25519 svc_github@pl.saya.today "echo 'SSH connection successful'"
- name: Install Ansible
shell: bash
@@ -73,10 +73,5 @@ jobs:
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.tag=${{ github.ref_name }}
+ 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_tag=${{ github.ref_name }}
diff --git a/.gitignore b/.gitignore
index 5e9bfe0..b5ae410 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,7 +36,9 @@ config*.json
/main
/sayana-demo
-/static/input.css
+/static/output.css
/.vscode
.envrc
+
+/.ansible
diff --git a/Dockerfile b/Dockerfile
index d23c3eb..45d3e8e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,6 +13,9 @@ WORKDIR /app
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.yaml
+RUN apk add --no-cache tzdata
+
ENTRYPOINT [ "/app/sayana-demo", "-c", "/app/config.yaml" ]
diff --git a/config/config.go b/config/config.go
index 67c0c4d..8c0321e 100644
--- a/config/config.go
+++ b/config/config.go
@@ -9,13 +9,14 @@ import (
)
type Config struct {
- LogLevel slog.Level `json:"LogLevel" yaml:"logLevel" validate:"required"`
- BlogPages BlogPagesConfig `json:"BlogPages" yaml:"blogPages" validate:"required"`
+ LogLevel slog.Level `json:"LogLevel" yaml:"logLevel" validate:"required"`
+ BlogPages BlogPagesConfig `json:"BlogPages" yaml:"blogPages" validate:"required"`
+ LocalePath string `json:"LocalePath" yaml:"localePath" validate:"required,filepath"`
+ AvailableLanguages []AvailableLanguageConfig `json:"AvailableLanguages" yaml:"availableLanguages" validate:"required"`
}
type BlogPagesConfig struct {
- Storage StorageConfig `json:"Storage" yaml:"storage" validate:"required"`
- AvailableLanguages []AvailableLanguageConfig `json:"AvailableLanguages" yaml:"availableLanguages" validate:"required"`
+ Storage StorageConfig `json:"Storage" yaml:"storage" validate:"required"`
}
type StorageConfig struct {
@@ -32,9 +33,10 @@ type B2Config struct {
}
type AvailableLanguageConfig struct {
- Name string `json:"Name" yaml:"name" validate:"required"`
- Alt string `json:"Alt" yaml:"alt"`
- Flag string `json:"Flag" yaml:"flag" validate:"url"`
+ Name string `json:"Name" yaml:"name" validate:"required"`
+ Alt string `json:"Alt" yaml:"alt"`
+ Flag string `json:"Flag" yaml:"flag" validate:"url"`
+ LocFile string `json:"LocFile" yaml:"locFile" validate:"required,filepath"`
}
func LoadConfig(path string, config *Config) error {
diff --git a/config/config.yaml b/config/config.yaml
index 908bfe2..dac7b9a 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -8,10 +8,13 @@ blogPages:
prefix: ''
keyID: '${B2_KEY_ID}'
applicationKey: '${B2_APPLICATION_KEY}'
- availableLanguages:
- - name: ru
- alt: Русский
- flag: https://f003.backblazeb2.com/file/sayana-static/flags/ru.svg
- - name: en
- alt: English
- flag: https://f003.backblazeb2.com/file/sayana-static/flags/gb.svg
+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/deploy/inventory.yml b/deploy/inventory.yml
index 5c88365..3c1220d 100644
--- a/deploy/inventory.yml
+++ b/deploy/inventory.yml
@@ -3,15 +3,13 @@ all:
prod:
hosts:
pl.saya.today:
- saya_today_web:
- name: sayana-web
- hostname: sayana-web
- ipv4_address: 172.16.0.18
+ saya_today_web_name: sayana-web
+ saya_today_web_hostname: sayana-web
+ saya_today_web_ipv4_address: 172.16.0.18
stage:
hosts:
pl.saya.today:
- saya_today_web:
- name: sayana-demo
- hostname: sayana-demo
- ipv4_address: 172.16.0.17
+ 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 c126f23..773bb61 100644
--- a/deploy/playbook.yml
+++ b/deploy/playbook.yml
@@ -4,18 +4,18 @@
remote_user: svc_github
tasks:
- - name: Deploy saya-today-web Docker Container
- community.docker.docker_container:
- name: "{{ saya_today_web.name }}"
- hostname: "{{ saya_today_web.hostname }}"
- image: "ghcr.io/sayaandy/saya-today-web:{{ saya_today_web.tag }}"
- env:
- B2_KEY_ID: "{{ saya_today_web.b2.key_id }}"
- B2_APPLICATION_KEY: "{{ saya_today_web.b2.application_key }}"
- network_mode: caddy
- networks:
- - name: caddy
- ipv4_address: "{{ saya_today_web.ipv4_address }}"
- restart_policy: unless-stopped
- no_log: true
+ - name: Deploy saya-today-web Docker Container
+ community.docker.docker_container:
+ name: "{{ saya_today_web_name }}"
+ hostname: "{{ saya_today_web_hostname }}"
+ image: "ghcr.io/sayaandy/saya-today-web:{{ saya_today_web_tag }}"
+ env:
+ B2_KEY_ID: "{{ saya_today_web_b2_key_id }}"
+ B2_APPLICATION_KEY: "{{ saya_today_web_b2_application_key }}"
+ network_mode: caddy
+ networks:
+ - name: caddy
+ ipv4_address: "{{ saya_today_web_ipv4_address }}"
+ restart_policy: unless-stopped
+ no_log: true
...
diff --git a/internal/b2/client.go b/internal/b2/client.go
index f2b6cad..5322b84 100644
--- a/internal/b2/client.go
+++ b/internal/b2/client.go
@@ -33,13 +33,14 @@ func NewB2Client(cfg *config.B2Config) (*B2Client, error) {
type BlogPage struct {
Link string
+ FileName string
Metadata *frontmatter.Metadata
}
-func (c *B2Client) Scan() ([]*BlogPage, error) {
+func (c *B2Client) Scan(prefix string) ([]*BlogPage, error) {
filePaths := []*BlogPage{}
- iter := c.bucket.List(context.Background(), b2.ListPrefix(c.prefix))
+ iter := c.bucket.List(context.Background(), b2.ListPrefix(c.prefix+prefix))
for iter.Next() {
obj := iter.Object()
@@ -69,10 +70,13 @@ func (c *B2Client) Scan() ([]*BlogPage, error) {
return nil, fmt.Errorf("failed to parse published time metadata field: %w", err)
}
- linkParts := strings.Split(obj.Name(), ".")
+ linkParts := strings.Split(obj.Name(), "/")
+ nameParts := strings.Split(linkParts[len(linkParts)-1], ".")
+ fileName := strings.Join(nameParts[:len(linkParts)-1], ".")
filePaths = append(filePaths, &BlogPage{
- Link: strings.Join(linkParts[:len(linkParts)-1], "."),
+ Link: obj.Name(),
+ FileName: fileName,
Metadata: &frontmatter.Metadata{
Title: attrs.Info["title"],
ShortDescription: attrs.Info["short-description"],
diff --git a/internal/frontmatter/parser.go b/internal/frontmatter/parser.go
index 1a521c0..3c00a7f 100644
--- a/internal/frontmatter/parser.go
+++ b/internal/frontmatter/parser.go
@@ -16,6 +16,7 @@ type Metadata struct {
Thumbnail string `yaml:"thumbnail"`
Tags []string `yaml:"tags"`
Geolocation string `yaml:"geolocation"`
+ Timezone string `yaml:"timezone"`
}
func ParseFrontmatter(content []byte) (metadata *Metadata, markdown []byte, err error) {
diff --git a/internal/lightgallery/block.go b/internal/lightgallery/block.go
index dade2ea..8eb7c9e 100644
--- a/internal/lightgallery/block.go
+++ b/internal/lightgallery/block.go
@@ -1,16 +1,21 @@
package lightgallery
-import "github.com/yuin/goldmark/ast"
+import (
+ "time"
+
+ "github.com/yuin/goldmark/ast"
+)
// LightGalleryBlock represents a light gallery block in the AST
type LightGalleryBlock struct {
ast.BaseBlock
- Images []LightGalleryImage
+ Images []LightGalleryImage
+ Location *time.Location
}
type LightGalleryImage struct {
URL string
- Caption string
+ Caption []byte
}
var KindLightGalleryBlock = ast.NewNodeKind("LightGalleryBlock")
diff --git a/internal/lightgallery/html_renderer.go b/internal/lightgallery/html_renderer.go
index e611f0b..850cd95 100644
--- a/internal/lightgallery/html_renderer.go
+++ b/internal/lightgallery/html_renderer.go
@@ -1,11 +1,13 @@
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"
@@ -14,11 +16,18 @@ 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)
@@ -53,7 +62,17 @@ 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",
@@ -70,7 +89,7 @@ func (r *LightGalleryHTMLRenderer) renderLightGallery(w util.BufWriter, source [
<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, img.Caption, imageUrlWithoutExt, imageUrlWithoutExt, img.Caption, dayDate.Format("2006-01-02 15:04:05 -07:00")))
+ }`, img.URL, img.URL, util.EscapeHTML(captionHTML), imageUrlWithoutExt, imageUrlWithoutExt, captionHTML, dayDate.Format("2006-01-02 15:04:05 -07:00")))
}
w.WriteString(fmt.Sprintf(`
@@ -117,15 +136,6 @@ window.addEventListener("resize", () => {
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 0ce209c..daec797 100644
--- a/internal/lightgallery/parser.go
+++ b/internal/lightgallery/parser.go
@@ -2,7 +2,9 @@ package lightgallery
import (
"bytes"
- "strings"
+ "log/slog"
+ "regexp"
+ "time"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
@@ -22,43 +24,52 @@ 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)
- if !bytes.Equal(trimmed, []byte("{Gallery}")) {
+ 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)))
return nil, parser.NoChildren
}
- return &LightGalleryBlock{}, 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
}
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.Close
+ return parser.Continue | parser.NoChildren
}
trimmed := bytes.TrimSpace(line)
- if bytes.Equal(trimmed, []byte("{Gallery}")) {
+ if bytes.Equal(trimmed, []byte("{Gallery}")) || bytes.Equal(trimmed, []byte("{/Gallery}")) {
reader.AdvanceLine()
return parser.Close
}
gallery := node.(*LightGalleryBlock)
- lineStr := string(trimmed)
- parts := strings.SplitN(lineStr, "|", 2)
- url := strings.TrimSpace(parts[0])
- caption := ""
+ parts := bytes.SplitN(trimmed, []byte{'|'}, 2)
+ url := bytes.TrimSpace(parts[0])
+ caption := make([]byte, 0)
if len(parts) > 1 {
- caption = strings.TrimSpace(parts[1])
+ caption = bytes.TrimSpace(parts[1])
}
gallery.Images = append(gallery.Images, LightGalleryImage{
- URL: url,
+ URL: string(url),
Caption: caption,
})
diff --git a/internal/templatemanager/templatemanager.go b/internal/templatemanager/templatemanager.go
new file mode 100644
index 0000000..5effcb3
--- /dev/null
+++ b/internal/templatemanager/templatemanager.go
@@ -0,0 +1,57 @@
+package templatemanager
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "path/filepath"
+ "strings"
+)
+
+type TemplateManager struct {
+ templates map[string]templateManagerRender
+}
+
+type templateManagerRender struct {
+ Main string
+ Tmpl *template.Template
+}
+
+type TemplateManagerTemplates struct {
+ Name string
+ Files []string
+}
+
+func NewTemplateManager(templates []TemplateManagerTemplates) (*TemplateManager, error) {
+ templateMap := make(map[string]templateManagerRender)
+
+ for _, tmplStruct := range templates {
+ tmpl := template.New("").Funcs(template.FuncMap{
+ "contains": strings.Contains,
+ })
+ tmpl, err := tmpl.ParseFiles(tmplStruct.Files...)
+ if err != nil {
+ return nil, err
+ }
+ templateMap[tmplStruct.Name] = templateManagerRender{
+ Main: filepath.Base(tmplStruct.Files[0]),
+ Tmpl: tmpl,
+ }
+
+ }
+
+ return &TemplateManager{
+ templates: templateMap,
+ }, nil
+}
+
+func (tm *TemplateManager) Render(name string, data interface{}) ([]byte, error) {
+ tmpl, exists := tm.templates[name]
+ if !exists {
+ return nil, fmt.Errorf("template %s is not found", name)
+ }
+
+ var buf bytes.Buffer
+ err := tmpl.Tmpl.ExecuteTemplate(&buf, tmpl.Main, data)
+ return buf.Bytes(), err
+}
diff --git a/locale/localization.en.yaml b/locale/localization.en.yaml
new file mode 100644
index 0000000..879fce8
--- /dev/null
+++ b/locale/localization.en.yaml
@@ -0,0 +1,9 @@
+TagsLabel: 'Tags:'
+BlogSearch:
+ Header: 'Blog Search'
+ TagsHeader: 'Tags'
+ OrderByHeader: 'Order by...'
+ TitleOrdered: 'Title'
+ ActionDateOrdered: 'Action Date'
+ PublicationDateOrdered: 'Publication Date'
+ ChooseAllTags: 'Choose All'
diff --git a/locale/localization.go b/locale/localization.go
new file mode 100644
index 0000000..d5c6b48
--- /dev/null
+++ b/locale/localization.go
@@ -0,0 +1,50 @@
+package locale
+
+import (
+ "os"
+
+ "github.com/go-playground/validator/v10"
+ "gopkg.in/yaml.v3"
+)
+
+type LocaleConfig struct {
+ TagsLabel string `yaml:"TagsLabel" json:"TagsLabel"`
+ BlogSearch BlogSearchConfig `yaml:"BlogSearch" json:"BlogSearch"`
+}
+
+type BlogSearchConfig struct {
+ Header string `yaml:"Header" json:"Header"`
+ TagsHeader string `yaml:"TagsHeader" json:"TagsHeader"`
+ OrderByHeader string `yaml:"OrderByHeader" json:"OrderByHeader"`
+ TitleOrdered string `yaml:"TitleOrdered" json:"TitleOrdered"`
+ ActionDateOrdered string `yaml:"ActionDateOrdered" json:"ActionDateOrdered"`
+ PublicationDateOrdered string `yaml:"PublicationDateOrdered" json:"PublicationDateOrdered"`
+ ChooseAllTags string `yaml:"ChooseAllTags" json:"ChooseAllTags"`
+}
+
+func LoadConfig(path string, config *LocaleConfig) error {
+ fileBytes, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+
+ if err = yaml.Unmarshal(fileBytes, config); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func InitConfig(path string) (*LocaleConfig, error) {
+ config := &LocaleConfig{}
+ if err := LoadConfig(path, config); err != nil {
+ return nil, err
+ }
+
+ validate := validator.New(validator.WithRequiredStructEnabled())
+ if err := validate.Struct(config); err != nil {
+ return nil, err
+ }
+
+ return config, nil
+}
diff --git a/locale/localization.ru.yaml b/locale/localization.ru.yaml
new file mode 100644
index 0000000..786665d
--- /dev/null
+++ b/locale/localization.ru.yaml
@@ -0,0 +1,9 @@
+TagsLabel: 'Тэги:'
+BlogSearch:
+ Header: 'Поиск по блогу'
+ TagsHeader: 'Тэги'
+ OrderByHeader: 'Упорядочить по...'
+ TitleOrdered: 'названию'
+ ActionDateOrdered: 'дате действия'
+ PublicationDateOrdered: 'времени публикации'
+ ChooseAllTags: 'Выбрать все'
diff --git a/main.go b/main.go
index 447329b..4dc9619 100644
--- a/main.go
+++ b/main.go
@@ -7,17 +7,23 @@ import (
"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/tailwind"
+ "github.com/SayaAndy/saya-today-web/internal/templatemanager"
+ "github.com/SayaAndy/saya-today-web/locale"
"github.com/gofiber/fiber/v2"
- "github.com/gofiber/template/html/v2"
+ "github.com/gofiber/fiber/v2/middleware/redirect"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/parser"
gmhtml "github.com/yuin/goldmark/renderer/html"
@@ -36,8 +42,10 @@ var (
gmhtml.WithXHTML(),
),
)
- b2Client *b2.B2Client
- configPath = flag.String("c", "config.yaml", "Path to the configuration file (in YAML format)")
+ b2Client *b2.B2Client
+ configPath = flag.String("c", "config.yaml", "Path to the configuration file (in YAML format)")
+ availableLanguages = make([]string, 0)
+ localization = make(map[string]*locale.LocaleConfig)
)
func main() {
@@ -60,42 +68,131 @@ func main() {
os.Exit(1)
}
- engine := html.New("./views", ".html")
+ for _, lang := range cfg.AvailableLanguages {
+ localeCfg, err := locale.InitConfig(cfg.LocalePath + lang.LocFile)
+ if err != nil {
+ slog.Warn("fail to initialize a locale", slog.String("locale", lang.Name), slog.String("error", err.Error()))
+ continue
+ }
+ availableLanguages = append(availableLanguages, lang.Name)
+ localization[lang.Name] = localeCfg
+ }
- app := fiber.New(fiber.Config{
- Views: engine,
+ 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{
+ Rules: map[string]string{
+ "/ru": "/",
+ "/en": "/",
+ },
+ StatusCode: 301,
+ }))
app.Get("/", func(c *fiber.Ctx) error {
- pages, err := b2Client.Scan()
+ 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 = "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{}
}
- pageMeta := make([]map[string]string, 0, len(pages))
+ tagsMap := make(map[string]int)
for _, page := range pages {
- slog.Debug("enlist page for catalogue", slog.Any("page", page), slog.String("endpoint", "/"))
- pageMeta = append(pageMeta, map[string]string{
- "Link": page.Link,
- "Title": page.Metadata.Title,
- "PublishedTime": page.Metadata.PublishedTime.Format("2006-01-02 15:04:05 -0700"),
- "ActionDate": page.Metadata.ActionDate,
- "ShortDescription": page.Metadata.ShortDescription,
- "Thumbnail": page.Metadata.Thumbnail,
- "Tags": strings.Join(page.Metadata.Tags, ", "),
- })
+ slog.Debug("enlist page for catalogue", slog.Any("page", page), slog.String("endpoint", "/"+lang+"/blog"))
+ for _, tag := range page.Metadata.Tags {
+ tagsMap[tag]++
+ }
}
- return c.Status(status).Render("index", fiber.Map{
+ 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": localization[lang],
"PublishedYear": "2025",
- "BlogPages": pageMeta,
+ "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("/blog/:lang/:title", func(c *fiber.Ctx) error {
- metadata, parsedMarkdownDesktop, parsedMarkdownMobile, err := readBlogPost(c.Params("lang") + "/" + c.Params("title"))
+ 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")))
@@ -111,16 +208,107 @@ func main() {
areaError = geolocationParts[2]
}
- return c.Render("layouts/blog-page", fiber.Map{
+ 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()),
- "ParsedMarkdownDesktop": template.HTML(parsedMarkdownDesktop),
- "ParsedMarkdownMobile": template.HTML(parsedMarkdownMobile),
+ "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 -07:00"),
+ "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("2006-01-02 15:04:05 -07:00", a["PublishedTime"])
+ publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", b["PublishedTime"])
+ return publishedTimeA.Compare(publishedTimeB)
+ case "publicationDateDesc":
+ publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -07:00", a["PublishedTime"])
+ publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", 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")
@@ -128,20 +316,16 @@ func main() {
log.Fatal(app.Listen(":3000"))
}
-func readBlogPost(sourceName string) (metadata *frontmatter.Metadata, desktopBody string, mobileBody string, err error) {
+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)
+ return nil, "", fmt.Errorf("failed to read a frontmatter file: %w", err)
}
- var bufDesktop bytes.Buffer
- if err := md.Convert(markdown, &bufDesktop); err != nil {
- return nil, "", "", fmt.Errorf("convert source context from md to html (desktop version): %w", err)
- }
- var bufMobile bytes.Buffer
- if err := md.Convert(markdown, &bufMobile); err != nil {
- return nil, "", "", fmt.Errorf("convert source context from md to html (mobile version): %w", err)
+ 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, bufDesktop.String(), bufMobile.String(), nil
+ return metadata, buf.String(), nil
}
diff --git a/saya-today-web b/saya-today-web
new file mode 100755
index 0000000..b6522e8
--- /dev/null
+++ b/saya-today-web
Binary files differ
diff --git a/static/input.css b/static/input.css
index edecc19..131faa9 100644
--- a/static/input.css
+++ b/static/input.css
@@ -25,12 +25,16 @@
--sidebar-text-color: var(--background-dark-color);
--sidebar-stroke-color: var(--main-dark-color);
--secondary-color: var(--main-light-color);
+ --leaflet-text-color: var(--main-dark-color);
+ --leaflet-link-color: var(--main-medium-color);
+ --leaflet-link-hover-color: var(--main-light-color);
--interlocked-hexagons-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23ffffdb%22%2F%3E%3Cpath%20fill%3D%22%23777e63%22%20d%3D%22m33.64%200-.17.25h-33L4.3%206h33.07l4-6zm8.33%200-4.33%206.5h-33l3.83%205.75h33.06L49.7%200zm8.34%200-8.5%2012.75h-33l3.83%205.75H45.7L58.03%200zm8.33%200L45.97%2019h-33l3.83%205.75h33.06L66.36%200zm-8.5%2025.25L33.63%2050l3.86%205.8L54.03%2031h33l-3.83-5.75zm4.16%206.25L37.8%2056.25l3.87%205.8%2016.53-24.8h33l-3.84-5.75zm4.17%206.25L41.97%2062.5l3.86%205.8%2016.54-24.8h33l-3.84-5.75zM62.63%2044l-16.5%2024.75%203.87%205.8%2016.53-24.8h33L95.7%2044zM100%2050.45l-3.87%205.8%203.87%205.81v-11.6zM0%2050.46v11.6L12.5%2080.8l3.86-5.8zm95.83%206.25-3.87%205.8L100%2074.55v-11.6zm-4.17%206.24-3.86%205.8%2012.2%2018.3v-11.6zM0%2062.96v11.6l8.33%2012.49%203.86-5.8zm87.5%206.24L83.63%2075%20100%2099.54V87.96zM50%2075.45%2033.64%20100h7.72l12.5-18.75zm-50%200v11.6l4.16%206.24%203.87-5.8zm54.17%206.25L41.97%20100h7.73l8.33-12.5zm4.17%206.25L50.3%20100h7.73l4.17-6.25zM0%2087.95v11.59l3.86-5.8zm62.5%206.25-3.87%205.8h7.73z%22%2F%3E%3Cpath%20fill%3D%22%23a0a182%22%20d%3D%22M66.8.25%2062.97%206h33l4.03%206.05V.45l-.14-.2zM0%20.45v11.61L12.5%2030.8l3.86-5.8zM62.63%206.5l-3.83%205.75h33l8.2%2012.3v-11.6L95.7%206.5zm-4.16%206.25-3.84%205.75h33L100%2037.06v-11.6l-8.47-12.71zM0%2012.96v11.6l8.33%2012.49%203.86-5.8zM54.3%2019l-3.84%205.75h33L100%2049.55l.01-.01V37.96L87.36%2019zm-4.77%206.25H41.8L25.3%2050l16.66%2025-16.5%2024.75h7.74L49.7%2075%2033.03%2050zm-8.33%200h-7.74L16.96%2050l16.67%2025-16.5%2024.75h7.73L41.36%2075%2024.7%2050Zm-8.34%200h-7.73L8.63%2050%2025.3%2075%208.8%2099.75h7.73L33.03%2075%2016.36%2050zm-16.07%200L.3%2050l16.67%2025L.46%2099.75H8.2L24.7%2075%208.02%2050l16.5-24.75zM0%2025.46v11.6l4.16%206.24%203.87-5.8zm0%2012.5v11.58l3.86-5.79zm66.8%2012.29L50.3%2074.99l3.87%205.81%204.16%206.25%204.17%206.25%204.3%206.45h7.73L58.03%2075l16.5-24.75zm16.06%200h-7.73L58.63%2075l16.5%2024.75h7.73L66.36%2075zm.6%200L66.96%2075l16.5%2024.75h7.74L74.7%2075l16.5-24.75zm16.07%200H91.8L75.3%2075l16.5%2024.75h7.73L83.03%2075z%22%2F%3E%3C%2Fsvg%3E");
--interlocked-hexagons-background-repeat: repeat;
--interlocked-hexagons-background-size: 10vh;
--squares-and-triangles-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%234a5139%22%2F%3E%3Cpath%20fill%3D%22%23b2b285%22%20d%3D%22M0%200v6.77L6.72%200zm8.91%200L0%208.96v3.86L12.83%200zm6.11%200L0%2015.06v3.87L18.9%200zm6.11%200L0%2021.12v3.92L25%200zM25%200l25%2024.99v-3.87L28.87%200zm25%2024.99%2025%2024.98L100%2025%2075%200zm-50%20.05v3.87l21.13%2021.12H25l-25-25zm25%2024.99h3.87L50%2028.9v-3.87zm0%200L0%2075l25%2025%2025-24.99zM50%2075l25-24.98h-3.87L50%2071.14v3.87zm0%200v3.87L71.13%20100H75L50%2075.01zm25%2025h3.87L100%2078.88v-3.87zm25-24.99v-3.87L78.87%2050.03H75L100%2075zM31.1%200%2050%2018.93v-3.87L34.98%200h-3.87zm6.07%200L50%2012.82V8.96L41.04%200zm6.1%200L50%206.77V0h-6.72zM25%202.19%202.14%2025.04H25zm0%2022.85v22.8l22.81-22.8zM75%203.92l21.13%2021.12L75%2046.16%2053.87%2025.04zm0%202.19L56.06%2025.04l18.89%2018.88%2018.94-18.88L75%206.1zm0%203.92%2015.07%2015L75%2040.06%2059.98%2025.04zm0%202.18L62.17%2025.04%2075%2037.86l12.83-12.82L75%2012.2zm0%203.82%208.96%209L75%2033.95%2066.04%2025zm0%202.24-6.72%206.77L75%2031.76l6.77-6.72h-.05zM0%2031.09v3.87l15.02%2015.07h3.87zm50%200L31.1%2050.03h3.88L50%2034.96V31.1zM0%2037.2v3.87l8.91%208.96h3.92zm50%200L37.17%2050.03h3.87L50%2041.07zM0%2043.3v6.73h6.72zm50%200-6.72%206.73H50zm0%206.73v6.71l6.72-6.71zm8.96%200L50%2058.93v3.92l12.83-12.82zm6.06%200L50%2065.03v3.88l18.94-18.88zm16.09%200L100%2068.9v-3.87l-15.02-15zm6.11%200L100%2062.85v-3.92l-8.91-8.9zm6.06%200%206.72%206.71v-6.71zM75%2052.2%2052.19%2075H75zM75%2075v22.8L97.81%2075zM25%2053.9l21.13%2021.12L25%2096.13%203.87%2075.01zm0%202.2L6.1%2074.95l18.95%2018.88%2018.9-18.88L25%2056.08zm0%203.9%2015.02%2015.01L25%2090.03%209.98%2075zm0%202.19L12.17%2075%2025%2087.84%2037.83%2075%2025%2062.2zm0%203.87L33.96%2075%2025%2083.97%2016.04%2075zm0%202.23-6.72%206.72L25%2081.73l6.72-6.72L25%2068.3zm25%2012.83v3.87L65.02%20100h3.87zm50%200L81.1%20100h3.88L100%2084.99zm-50%206.06v3.86l8.96%208.96h3.87zm50%200L87.22%20100h3.87l8.91-8.96zm-50%206.1V100h6.72zm50%200L93.28%20100H100z%22%2F%3E%3C%2Fsvg%3E");
--squares-and-triangles-background-repeat: repeat;
- --squares-and-triangles-background-size: 10;
+ --squares-and-triangles-background-width-coef: 10;
+ --squares-and-triangles-background-height-coef: 10;
}
[data-theme="lettuce"] {
@@ -42,12 +46,16 @@
--sidebar-text-color: var(--main-dark-color);
--sidebar-stroke-color: var(--background-dark-color);
--secondary-color: var(--main-medium-color);
+ --leaflet-text-color: var(--main-dark-color);
+ --leaflet-link-color: var(--main-medium-color);
+ --leaflet-link-hover-color: var(--main-light-color);
--interlocked-hexagons-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23f5ede1%22%2F%3E%3Cpath%20fill%3D%22%239ab663%22%20d%3D%22m33.64%200-.17.25h-33L4.3%206h33.07l4-6zm8.33%200-4.33%206.5h-33l3.83%205.75h33.06L49.7%200zm8.34%200-8.5%2012.75h-33l3.83%205.75H45.7L58.03%200zm8.33%200L45.97%2019h-33l3.83%205.75h33.06L66.36%200zm-8.5%2025.25L33.63%2050l3.86%205.8L54.03%2031h33l-3.83-5.75zm4.16%206.25L37.8%2056.25l3.87%205.8%2016.53-24.8h33l-3.84-5.75zm4.17%206.25L41.97%2062.5l3.86%205.8%2016.54-24.8h33l-3.84-5.75zM62.63%2044l-16.5%2024.75%203.87%205.8%2016.53-24.8h33L95.7%2044zM100%2050.45l-3.87%205.8%203.87%205.81v-11.6zM0%2050.46v11.6L12.5%2080.8l3.86-5.8zm95.83%206.25-3.87%205.8L100%2074.55v-11.6zm-4.17%206.24-3.86%205.8%2012.2%2018.3v-11.6zM0%2062.96v11.6l8.33%2012.49%203.86-5.8zm87.5%206.24L83.63%2075%20100%2099.54V87.96zM50%2075.45%2033.64%20100h7.72l12.5-18.75zm-50%200v11.6l4.16%206.24%203.87-5.8zm54.17%206.25L41.97%20100h7.73l8.33-12.5zm4.17%206.25L50.3%20100h7.73l4.17-6.25zM0%2087.95v11.59l3.86-5.8zm62.5%206.25-3.87%205.8h7.73z%22%2F%3E%3Cpath%20fill%3D%22%23bbd095%22%20d%3D%22M66.8.25%2062.97%206h33l4.03%206.05V.45l-.14-.2zM0%20.45v11.61L12.5%2030.8l3.86-5.8zM62.63%206.5l-3.83%205.75h33l8.2%2012.3v-11.6L95.7%206.5zm-4.16%206.25-3.84%205.75h33L100%2037.06v-11.6l-8.47-12.71zM0%2012.96v11.6l8.33%2012.49%203.86-5.8zM54.3%2019l-3.84%205.75h33L100%2049.55l.01-.01V37.96L87.36%2019zm-4.77%206.25H41.8L25.3%2050l16.66%2025-16.5%2024.75h7.74L49.7%2075%2033.03%2050zm-8.33%200h-7.74L16.96%2050l16.67%2025-16.5%2024.75h7.73L41.36%2075%2024.7%2050Zm-8.34%200h-7.73L8.63%2050%2025.3%2075%208.8%2099.75h7.73L33.03%2075%2016.36%2050zm-16.07%200L.3%2050l16.67%2025L.46%2099.75H8.2L24.7%2075%208.02%2050l16.5-24.75zM0%2025.46v11.6l4.16%206.24%203.87-5.8zm0%2012.5v11.58l3.86-5.79zm66.8%2012.29L50.3%2074.99l3.87%205.81%204.16%206.25%204.17%206.25%204.3%206.45h7.73L58.03%2075l16.5-24.75zm16.06%200h-7.73L58.63%2075l16.5%2024.75h7.73L66.36%2075zm.6%200L66.96%2075l16.5%2024.75h7.74L74.7%2075l16.5-24.75zm16.07%200H91.8L75.3%2075l16.5%2024.75h7.73L83.03%2075z%22%2F%3E%3C%2Fsvg%3E");
--interlocked-hexagons-background-repeat: repeat;
--interlocked-hexagons-background-size: 10vh;
--squares-and-triangles-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23bbd095%22%2F%3E%3Cpath%20fill%3D%22%23f5ede1%22%20d%3D%22M0%200v6.77L6.72%200zm8.91%200L0%208.96v3.86L12.83%200zm6.11%200L0%2015.06v3.87L18.9%200zm6.11%200L0%2021.12v3.92L25%200zM25%200l25%2024.99v-3.87L28.87%200zm25%2024.99%2025%2024.98L100%2025%2075%200zm-50%20.05v3.87l21.13%2021.12H25l-25-25zm25%2024.99h3.87L50%2028.9v-3.87zm0%200L0%2075l25%2025%2025-24.99zM50%2075l25-24.98h-3.87L50%2071.14v3.87zm0%200v3.87L71.13%20100H75L50%2075.01zm25%2025h3.87L100%2078.88v-3.87zm25-24.99v-3.87L78.87%2050.03H75L100%2075zM31.1%200%2050%2018.93v-3.87L34.98%200h-3.87zm6.07%200L50%2012.82V8.96L41.04%200zm6.1%200L50%206.77V0h-6.72zM25%202.19%202.14%2025.04H25zm0%2022.85v22.8l22.81-22.8zM75%203.92l21.13%2021.12L75%2046.16%2053.87%2025.04zm0%202.19L56.06%2025.04l18.89%2018.88%2018.94-18.88L75%206.1zm0%203.92%2015.07%2015L75%2040.06%2059.98%2025.04zm0%202.18L62.17%2025.04%2075%2037.86l12.83-12.82L75%2012.2zm0%203.82%208.96%209L75%2033.95%2066.04%2025zm0%202.24-6.72%206.77L75%2031.76l6.77-6.72h-.05zM0%2031.09v3.87l15.02%2015.07h3.87zm50%200L31.1%2050.03h3.88L50%2034.96V31.1zM0%2037.2v3.87l8.91%208.96h3.92zm50%200L37.17%2050.03h3.87L50%2041.07zM0%2043.3v6.73h6.72zm50%200-6.72%206.73H50zm0%206.73v6.71l6.72-6.71zm8.96%200L50%2058.93v3.92l12.83-12.82zm6.06%200L50%2065.03v3.88l18.94-18.88zm16.09%200L100%2068.9v-3.87l-15.02-15zm6.11%200L100%2062.85v-3.92l-8.91-8.9zm6.06%200%206.72%206.71v-6.71zM75%2052.2%2052.19%2075H75zM75%2075v22.8L97.81%2075zM25%2053.9l21.13%2021.12L25%2096.13%203.87%2075.01zm0%202.2L6.1%2074.95l18.95%2018.88%2018.9-18.88L25%2056.08zm0%203.9%2015.02%2015.01L25%2090.03%209.98%2075zm0%202.19L12.17%2075%2025%2087.84%2037.83%2075%2025%2062.2zm0%203.87L33.96%2075%2025%2083.97%2016.04%2075zm0%202.23-6.72%206.72L25%2081.73l6.72-6.72L25%2068.3zm25%2012.83v3.87L65.02%20100h3.87zm50%200L81.1%20100h3.88L100%2084.99zm-50%206.06v3.86l8.96%208.96h3.87zm50%200L87.22%20100h3.87l8.91-8.96zm-50%206.1V100h6.72zm50%200L93.28%20100H100z%22%2F%3E%3C%2Fsvg%3E");
--squares-and-triangles-background-repeat: repeat;
- --squares-and-triangles-background-size: 10;
+ --squares-and-triangles-background-width-coef: 10;
+ --squares-and-triangles-background-height-coef: 10;
}
[data-theme="night"] {
@@ -59,12 +67,60 @@
--sidebar-text-color: var(--main-dark-color);
--sidebar-stroke-color: var(--background-light-color);
--secondary-color: var(--main-light-color);
+ --leaflet-text-color: var(--background-dark-color);
+ --leaflet-link-color: var(--background-dark-color);
+ --leaflet-link-hover-color: var(--background-light-color);
--interlocked-hexagons-background: radial-gradient(33.5% 25% at 100% 82%, rgba(190, 227, 248, 0.2) 0%, rgba(197, 179, 246, 0.13) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(54% 25% at 55% 100%, rgba(190, 227, 248, 0.2) 0%, rgba(197, 179, 246, 0.13) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(37% 19.5% at 20% 100%, rgba(176, 220, 248, 0.3) 0%, rgba(197, 172, 247, 0.2) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(63% 34.5% at 66% 77%, rgba(190, 227, 248, 0.15) 0%, rgba(197, 179, 246, 0.1) 33.33%, rgba(255, 0, 0, 0) 100%), linear-gradient(0deg, #2f5883 0%, #2f4e7a 4.75%, #2e4472 9.5%, #2a3065 19%, #272e57 26.5%, #242b4b 34%, #1f2437 49%, #1c2030 57.75%, #1a1b29 66.5%, #13131b 84%);
--interlocked-hexagons-background-repeat: no-repeat;
--interlocked-hexagons-background-size: 100vw 100vh;
--squares-and-triangles-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2260%22%20height%3D%2260%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%2313131b%22%2F%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23363645%22%20stroke-width%3D%22.5%22%20d%3D%22M10%2050H0m60-40H50m0%2040v10M10%200v10m21%2030v20m0-60v20m9%2010h20M0%2030h20m0%2010h20V20H20v10zM10%2010h40v40H10zM0%200h60v60H0z%22%2F%3E%3C%2Fsvg%3E");
--squares-and-triangles-background-repeat: repeat;
- --squares-and-triangles-background-size: 5;
+ --squares-and-triangles-background-width-coef: 5;
+ --squares-and-triangles-background-height-coef: 5;
+}
+
+[data-theme="ram"] {
+ --main-dark-color: #fefad8;
+ --main-medium-color: #f6e4c5;
+ --main-light-color: #e8d4b8;
+ --background-dark-color: #413b34;
+ --background-light-color: #6d6258;
+ --sidebar-text-color: var(--main-dark-color);
+ --sidebar-stroke-color: var(--background-light-color);
+ --secondary-color: var(--main-light-color);
+ --leaflet-text-color: var(--background-dark-color);
+ --leaflet-link-color: var(--background-dark-color);
+ --leaflet-link-hover-color: var(--background-light-color);
+ --interlocked-hexagons-background: url('https://f003.backblazeb2.com/file/sayana-static/themes/ram/ram-background.webp');
+ --interlocked-hexagons-background-repeat: repeat;
+ --interlocked-hexagons-background-size: auto;
+ --squares-and-triangles-background: url('https://f003.backblazeb2.com/file/sayana-static/themes/ram/ram-sidebar.webp');
+ --squares-and-triangles-background-repeat: repeat;
+ --squares-and-triangles-background-width-coef: 5;
+ --squares-and-triangles-background-height-coef: 15;
+}
+
+a {
+ color: var(--main-medium-color) !important;
+ text-decoration: underline;
+}
+
+a:hover {
+ color: var(--main-light-color) !important;
+}
+
+#location-map {
+ color: var(--leaflet-text-color) !important;
+}
+
+#location-map a {
+ color: var(--leaflet-link-color) !important;
+ text-decoration: underline;
+}
+
+#location-map a:hover {
+ color: var(--leaflet-link-hover-color) !important;
+ text-decoration: underline;
}
.bg-interlocked-hexagons {
@@ -127,8 +183,10 @@
background-size: calc(var(--squares-and-triangles-background-size) * 1vw);
}
-body:has(.lg-container:not([class~="lg-inline"])) .desktop-sidebar-custom {
- display: none !important;
+.bg-sidebar {
+ background-image: var(--squares-and-triangles-background);
+ background-repeat: var(--squares-and-triangles-background-repeat);
+ background-size: calc(var(--squares-and-triangles-background-width-coef) * 1vw) calc(var(--squares-and-triangles-background-height-coef) * 1vw);
}
.mobile-topbar-custom {
@@ -138,7 +196,7 @@ body:has(.lg-container:not([class~="lg-inline"])) .desktop-sidebar-custom {
margin-left: 5vw;
}
-body:has(.lg-container:not([class~="lg-inline"])) .mobile-topbar-custom {
+body:has(.lg-container:not([class~="lg-inline"])) #sidebar {
display: none !important;
}
@@ -165,10 +223,6 @@ body:has(.lg-container:not([class~="lg-inline"])) .mobile-topbar-custom {
font-size: 4vw;
}
-.content-square {
- width: 100vh;
-}
-
.border-left-custom {
width: 0.5vw;
border-right: 0.3vh dashed var(--main-dark-color);
@@ -186,12 +240,12 @@ body:has(.lg-container:not([class~="lg-inline"])) .mobile-topbar-custom {
inset 0vh -0.5vh rgba(75, 81, 58, 0.5);
}
-.desktop {
- display: flex;
+.text-stroke-\(--sidebar-stroke-color\) {
+ -webkit-text-stroke-color: var(--sidebar-stroke-color);
}
-.mobile {
- display: none;
+.text-stroke-\[0\.1vw\] {
+ -webkit-text-stroke-width: 0.1vw;
}
.lg-backdrop {
@@ -313,25 +367,48 @@ body:has(.lg-container:not([class~="lg-inline"])) .mobile-topbar-custom {
}
}
-/* Mobile responsive styles */
-@media (max-aspect-ratio: 1.1/1) {
- .desktop {
- display: none;
+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;
}
- .mobile {
- display: flex;
+ .ar-gt-0\.8\:w-fit {
+ width: fit-content;
}
- .inline-gallery-container {
- width: 100%;
+ .ar-gt-0\.8\:min-w-fit {
+ min-width: fit-content;
}
- .desktop-sidebar-custom {
- width: 90%;
- margin-left: 5vw;
- height: 10vh;
- background-size: 20vh;
+ .ar-gt-0\.8\:min-w-\[10vh\] {
+ min-width: 10vh;
+ }
+
+ .ar-gt-0\.8\:max-w-\[20vh\] {
+ max-width: 20vh;
+ }
+
+ .ar-gt-0\.8\:max-w-\[100vh\] {
+ max-width: 100vh;
+ }
+
+ .ar-gt-0\.8\:items-center {
+ align-items: center;
+ }
+}
+
+@media (max-aspect-ratio: 0.8/1) {
+ .bg-sidebar {
+ background-size: calc(var(--squares-and-triangles-background-width-coef) * 1.6vh) calc(var(--squares-and-triangles-background-height-coef) * 1.6vh);
+ }
+
+ .inline-gallery-container {
+ width: 100%;
}
.logo-custom {
@@ -355,20 +432,77 @@ body:has(.lg-container:not([class~="lg-inline"])) .mobile-topbar-custom {
background-size: 10vh;
}
- .border-left-custom {
- width: 1%;
- border-right: 0.3vw dashed var(--main-dark-color);
+ .ar-lt-0\.8\:grid {
+ display: grid;
+ }
+
+ .ar-lt-0\.8\:flex-col {
+ flex-direction: column;
+ }
+
+ .ar-lt-0\.8\:flex-row {
+ flex-direction: row;
+ }
+
+ .ar-lt-0\.8\:h-\[8vh\] {
+ height: 8vh;
+ }
+
+ .ar-lt-0\.8\:h-\[10vh\] {
+ height: 10vh;
+ }
+
+ .ar-lt-0\.8\:h-\[30vh\] {
+ height: 30vh;
+ }
+
+ .ar-lt-0\.8\:h-\[90vh\] {
+ height: 90vh;
+ }
+
+ .ar-lt-0\.8\:w-\[8vh\] {
+ width: 8vh;
}
- .border-right-custom {
- width: 1.5%;
- border-left: 0.5vw dotted var(--main-dark-color);
+ .ar-lt-0\.8\:w-\[80\%\] {
+ width: 80%;
+ }
+
+ .ar-lt-0\.8\:w-\[90vw\] {
+ width: 90vw;
+ }
+
+ .ar-lt-0\.8\:w-screen {
+ width: 100vw;
+ }
+
+ .ar-lt-0\.8\:max-h-\[80\%\] {
+ max-height: 80%;
+ }
+
+ .ar-lt-0\.8\:max-h-\[calc\(100\%-25vh\)\] {
+ max-height: calc(100% - 25vh);
+ }
+
+ .ar-lt-0\.8\:text-\[5vh\] {
+ font-size: 5vh;
+ }
+
+ .ar-lt-0\.8\:ml-\[5vw\] {
+ margin-left: 5vw;
+ }
+
+ .ar-lt-0\.8\:hidden {
+ display: none;
}
}
@media (max-aspect-ratio: 0.4/1) {
- .desktop-sidebar-custom {
- width: 100%;
+ .ar-lt-0\.4\:w-\[100vw\] {
+ width: 100vw;
+ }
+
+ .ar-lt-0\.4\:ml-0 {
margin-left: 0;
}
}
diff --git a/views/index.html b/views/index.html
index 731def6..2b8c937 100644
--- a/views/index.html
+++ b/views/index.html
@@ -12,33 +12,45 @@
<style>
.menu-outer-body {
border-radius: 10%;
- width: 60vw;
- height: 40vh;
+ width: 40vh;
+ height: 30vh;
}
.menu-inner-body {
width: 85%;
}
- @media (max-aspect-ratio: 1.2/1) {
- .menu-outer-body {
- border-radius: 5%;
- }
+ .flex-adaptive {
+ flex-direction: row;
+ }
- .menu-inner-body {
- width: 92.5%;
- }
+ .language-icon {
+ max-width: calc(80% / {{ .AvailableLanguages | len }});
+ max-height: 80%;
}
@media (max-aspect-ratio: 0.8/1) {
.menu-outer-body {
- width: 100vw;
- height: 80vh;
+ border-radius: 5%;
+ }
+
+ .menu-outer-body {
+ width: min(40vh, 100vw);
+ height: 40vh;
border-radius: 0;
}
+ .language-icon {
+ max-width: 40%;
+ max-height: calc(80% / {{ .AvailableLanguages | len }});
+ }
+
.menu-inner-body {
- width: 100%;
+ width: 92.5%;
+ }
+
+ .flex-adaptive {
+ flex-direction: column;
}
}
</style>
@@ -91,27 +103,13 @@
</script>
<div class="menu-outer-body p-4 z-1 bg-background-dark flex flex-col mx-auto my-auto opacity-70">
- <p class="font-patua mx-auto text-[2.5vmax] mt-[0.8vmin] mb-[1.6vmin]">saya.today</p>
- <div class="menu-inner-body grow bg-background-light flex flex-col mx-auto mb-[1.6vmin] overflow-y-auto opacity-100">
- {{- range .BlogPages }}
- <div class="m-2 flex flex-row justify-center justify-items-center gap-4">
- <img onclick="location.href='/blog/{{ .Link }}';" style="flex-grow: 0;" class="flex-1 cursor-pointer object-cover rounded-4xl select-none w-[4vmax] h-[4vmax]" src="https://f003.backblazeb2.com/file/sayana-photos/thumbnails/{{ .Thumbnail }}.webp">
- <div class="flex-5 justify-self-center flex-col border-r-1 border-dashed border-opacity-50 border-main-medium">
- <a href="/blog/{{ .Link }}" class="block font-extrabold">{{ .Title }}</a>
- <a href="/blog/{{ .Link }}" class="block italic text-[1vmax] text-secondary">{{ .ActionDate }}</a>
- <p>{{ .ShortDescription }}</p>
- </div>
- <div class="flex-3 justify-self-center flex flex-col text-right">
- <p class="grow text-[1vmax]">
- Тэги: {{ .Tags }}
- </p>
- <p class="italic text-secondary text-[1vmax]">
- {{ .PublishedTime }}
- </p>
- </div>
- </div>
+ <p class="font-patua mx-auto text-[2.5vmax] mt-[0.8vmin] mb-[0.8vmin]">saya.today</p>
+ <div class="menu-inner-body grow bg-background-light flex flex-adaptive mx-auto opacity-100">
+ {{- range .AvailableLanguages }}
+ <img onclick="location.href='/{{ .Name }}/blog';" class="object-scale-down cursor-pointer flex-1 rounded-[20%] language-icon mx-auto my-auto relative" src="{{ .Flag }}" alt="{{ .Alt }}">
{{- end }}
</div>
+ <p class="font-spectral mx-auto text-[1.5vmax] text-secondary mt-[0.8vmin] mb-[0.8vmin]">Choose your language</p>
</div>
<script>
diff --git a/views/layouts/blog-page.html b/views/layouts/blog-page.html
deleted file mode 100644
index 08a6f93..0000000
--- a/views/layouts/blog-page.html
+++ /dev/null
@@ -1,194 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>SAYA TODAY // {{ .Title }}</title>
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lightgallery.min.css">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lg-zoom.min.css">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lg-thumbnail.min.css">
-{{- if .MapLocationX }}
- <link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.css">
-{{- end }}
- <link rel="preconnect" href="https://fonts.googleapis.com">
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
- <link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,200;1,300;1,400;1,500;1,600;1,700;1,800&family=Patua+One&display=swap" rel="stylesheet">
- <link href="/output.css" rel="stylesheet">
-
- <style>
- .map-container .leaflet-container {
- width: 100% !important;
- height: 100% !important;
- }
- </style>
-</head>
-
-<body data-theme="night" class="font-spectral text-main-dark overflow-hidden bg-interlocked-hexagons h-screen flex">
-
- <script>
- function switchTheme(theme) {
- if (theme == "" || typeof theme == "undefined") {
- const currentTheme = document.body.getAttribute('data-theme');
- switch (currentTheme) {
- case "olive": theme = "lettuce"; break;
- case "lettuce": theme = "night"; break;
- case "night": theme = "olive"; break;
- }
- }
- localStorage.setItem('theme', theme);
- document.body.setAttribute('data-theme', theme);
- }
-
- const savedTheme = localStorage.getItem('theme') || 'night';
- switchTheme(savedTheme);
- </script>
-
- <div class="desktop-sidebar-custom sticky bg-squares-and-triangles bg-repeat flex-col items-center shadow-2xl z-20 h-screen desktop">
- <div class="logo-custom text-sidebar font-patua font-extrabold text-center relative z-20">
- saya.today
- </div>
- <div class="flex flex-col gap-0 relative z-20">
- <i onclick="location.href='/';" class="fas fa-home nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i>
- <i class="fas fa-search nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i>
- <i class="fas fa-images nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i>
- <i onclick="switchTheme('')" class="fas fa-swatchbook nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i>
- </div>
- </div>
-
- <div class="bg-background-dark z-10 h-screen desktop">
- <div class="border-left-custom bg-background-dark"></div>
-
- <div class="content-square flex flex-col bg-background-dark relative mt-[1.6vmin] mx-[1.6vmin]">
- <div class="flex flex-row mb-[0.4vmin]">
- <i onclick="location.href='/';" class="fas fa-circle-left hover:bg-main-dark hover:bg-opacity-10 cursor-pointer transition-colors duration-300 rounded-md text-[2vmax] mt-auto mb-auto px-[0.8vmin]"></i>
- <p class="text-[2vmax] font-spectral italic font-extrabold select-none mt-auto pl-[0.8vmin] pr-[1.2vmin]">//</p>
- <p class="text-[2vmax] font-spectral italic text-left mt-auto">{{ .Title }}</p>
- <p class="grow text-[1.5vmax] font-spectral text-secondary text-right mt-auto">{{ .PublishedDate }}</p>
- </div>
- <hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]">
-
- <div class="bg-background-light flex flex-col flex-1 overflow-y-auto inset-shadow p-[2.4vmin]">
- {{ .ParsedMarkdownDesktop }}
- {{- if .MapLocationX }}
- <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto">
- <div id="location-map-desktop" class="relative p-[0.8vmin] flex-none ml-auto mr-auto w-[60%] h-[40vh]"></div>
- <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto">
- {{- end }}
- </div>
-
- <div class="flex flex-row mt-[0.8vmin] mb-[0.4vmin]">
- <p class="grow text-[0.8vmax]/[0.9] font-spectral italic text-right text-secondary mr-2">
- <a href="">{{ .Title }}</a> © {{ .PublishedYear }} by <a href="https://t.me/EarlInisMona">Saya Andy</a> is licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>
- </p>
- <img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;">
- <img src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;">
- <img src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;">
- </div>
-
- <hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]">
- </div>
-
- <div class="border-right-custom bg-background-dark shadow-2xl"></div>
- </div>
-
- <div class="mobile-topbar-custom fixed bg-squares-and-triangles bg-repeat flex-row items-center shadow-2xl w-[90vw] z-20 mobile">
- <div class="flex flex-row gap-0 relative z-20">
- <i onclick="location.href='/';" class="fas fa-home nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i>
- <i class="fas fa-search nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i>
- <i class="fas fa-images nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i>
- <i onclick="switchTheme('')" class="fas fa-swatchbook nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i>
- </div>
- <div class="logo-custom text-background-dark font-patua font-extrabold text-center relative grow z-20">
- demo.saya.today
- </div>
- </div>
-
- <div class="bg-background-dark flex-row z-10 w-[90vw] h-[92vh] mobile ml-[5vw] mt-[8vh]">
- <div class="border-left-custom bg-background-dark"></div>
-
- <div class="content-square flex flex-col bg-background-dark relative mt-[1.6vmin] mx-[1.6vmin]">
- <div class="flex flex-row mb-[0.4vmin]">
- <i onclick="location.href='/';" class="fas fa-circle-left hover:bg-main-dark hover:bg-opacity-10 cursor-pointer transition-colors duration-300 rounded-md text-[2vmax] mt-auto mb-auto px-[0.8vmin]"></i>
- <p class="text-[2vmax] font-spectral italic font-extrabold select-none mt-auto pl-[0.8vmin] pr-[1.2vmin]">//</p>
- <p class="text-[2vmax] font-spectral italic text-left mt-auto">{{ .Title }}</p>
- <p class="grow text-[1.5vmax] font-spectral text-secondary text-right mt-auto">{{ .PublishedDate }}</p>
- </div>
- <hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]">
-
- <div class="bg-background-light flex flex-col overflow-y-auto inset-shadow p-[2.4vmin] max-h-[80%]">
- {{ .ParsedMarkdownMobile }}
- {{- if .MapLocationX }}
- <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto">
- <div id="location-map-mobile" class="relative p-[0.8vmin] flex-none ml-auto mr-auto w-[100%] h-[40vh]"></div>
- <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto">
- {{- end }}
- </div>
-
- <div class="flex flex-row mt-[0.8vmin] mb-[0.4vmin]">
- <p class="grow text-[0.8vmax]/[0.9] font-spectral italic text-right text-secondary mr-2">
- <a href="">{{ .Title }}</a> © {{ .PublishedYear }} by <a href="https://t.me/EarlInisMona">Saya Andy</a> is licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>
- </p>
- <img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;">
- <img src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;">
- <img src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;">
- </div>
-
- <hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]">
- </div>
-
- <div class="border-right-custom bg-background-dark shadow-2xl"></div>
- </div>
-
- <script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/lightgallery.min.js"></script>
- <script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/plugins/lg-zoom.min.js"></script>
- <script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/plugins/lg-thumbnail.min.js"></script>
-{{- if .MapLocationX }}
- <script src="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.js"></script>
-
- <script>
- function initLocationMap(id, x, y, relativeErrorMeters = 0) {
- var map = L.map(id).setView([x, y], 13);
-
- L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
- maxZoom: 19,
- attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
- }).addTo(map);
-
- if (relativeErrorMeters != 0) {
- var circle = L.circle([x, y], {
- color: '#8a9d8a',
- fillColor: '#8a9d8a',
- fillOpacity: 0.15,
- radius: relativeErrorMeters
- }).addTo(map);
- }
-
- var marker = L.marker([x, y]).addTo(map);
- }
-
- document.addEventListener('DOMContentLoaded', () => {initLocationMap(
- 'location-map-desktop',
- {{ .MapLocationX }},
- {{ .MapLocationY }},
- {{ .MapLocationAreaMeters }}
- )});
-
- document.addEventListener('DOMContentLoaded', () => {initLocationMap(
- 'location-map-mobile',
- {{ .MapLocationX }},
- {{ .MapLocationY }},
- {{ .MapLocationAreaMeters }}
- )});
-
- window.addEventListener('resize', function() {
- setTimeout(() => {
- if (window.location-map-desktop) window.location-map-desktop.invalidateSize();
- if (window.location-map-mobile) window.location-map-mobile.invalidateSize();
- }, 100);
- });
- </script>
-{{- end }}
-
-</body>
-</html>
diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html
new file mode 100644
index 0000000..eab7cd1
--- /dev/null
+++ b/views/layouts/general-page.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>SAYA TODAY // {{ .Title }}</title>
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,200;1,300;1,400;1,500;1,600;1,700;1,800&family=Patua+One&display=swap" rel="stylesheet">
+ {{ block "header" . }}{{ end }}
+ <link href="/output.css" rel="stylesheet">
+</head>
+
+<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) {
+ 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') || 'night';
+ switchTheme(savedTheme);
+ </script>
+
+ <div id="sidebar" class="bg-sidebar flex sticky flex-col ar-gt-0.8:items-center shadow-2xl z-20 w-[5vw] ar-lt-0.8:w-[90vw] ar-lt-0.4:w-[100vw] h-screen ar-lt-0.8:h-[8vh] ar-lt-0.8:ml-[5vw] ar-lt-0.4:ml-0">
+ <div class="logo-custom ar-lt-0.8:hidden text-sidebar font-patua font-extrabold text-center relative z-20">
+ saya.today
+ </div>
+ <div class="text-[3vw] ar-lt-0.8:text-[5vh] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1vw] flex flex-col ar-lt-0.8:flex-row gap-0 relative z-20">
+ <i onclick="location.href='/';" class="fas fa-home w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
+ <i onclick="location.href='/{{ .Lang }}/blog';" class="fas fa-search w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
+ <!-- <i 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 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>
+ {{- 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]">
+
+ {{ block "body" . }}{{ end }}
+
+ <div class="flex flex-row mt-[0.8vmin] mb-[0.4vmin]">
+ <p class="grow text-[0.8vmax]/[0.9] font-spectral italic text-right text-secondary mr-2">
+ All the photos on <a href="https://saya.today/">saya.today</a> are licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a> by <a href="https://t.me/EarlInisMona">Saya Andy</a> © {{ .PublishedYear }}
+ </p>
+ <img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;">
+ <img src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;">
+ <img src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" style="max-width:1.2vh; max-height:1.2vh;">
+ </div>
+
+ <hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]">
+ </div>
+
+ <div class="pr-[0.5vw] bg-background-dark shadow-2xl border-l-[0.4vmin] border-dotted border-main-dark"></div>
+ </div>
+
+ {{ block "footer" . }}{{ end }}
+
+</body>
+</html>
diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html
new file mode 100644
index 0000000..6c0f629
--- /dev/null
+++ b/views/pages/blog-catalogue.html
@@ -0,0 +1,142 @@
+{{ define "body" }}
+<div class="flex flex-row ar-lt-0.8:flex-col max-h-[calc(100%-7vmax)] ar-lt-0.8:max-h-[calc(100%-25vh)] flex-1 text-[1vmax]">
+ <form hx-get="/api/v1/blog-search" hx-vals='{"lang": "{{ .Lang }}"}' hx-target=".blog-cards" hx-swap="innerHTML"
+ class="tags-list ar-gt-0.8:min-w-[10vh] ar-gt-0.8:max-w-[20vh] ar-gt-0.8:w-fit ar-lt-0.8:w-[100%] flex shrink-0 flex-col overflow-y-auto 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 gap-[5%]">
+ <div>
+ <label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();"> {{ .L.BlogSearch.ChooseAllTags }}</label>
+ </div>
+ {{- range .Tags }}
+ <div>
+ <label><input type="checkbox" id="tag{{ .Name }}Checkbox" name="tags[]" value="{{ .Name }}" onclick="contextAll();" {{ if contains $.QueryTags .Name }}checked{{ end }}> {{ .Name }} <span class="text-secondary italic">{{ .Count }}</span></label>
+ </div>
+ {{- end }}
+ </div>
+ </fieldset>
+ <fieldset class="mt-[0.8vmin]">
+ <legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.OrderByHeader }}</legend>
+ <div class="flex flex-col ar-lt-0.8:grid grid-cols-3 grid-rows-2 grid-flow-col 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>
+ <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>
+ <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>
+ <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>
+ <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>
+ <label><input type="radio" id="sortPublicationDateDesc" name="sort" value="publicationDateDesc" {{ if eq .QuerySort "publicationDateDesc" }}checked{{ end }}> {{ .L.BlogSearch.PublicationDateOrdered }} <i class="fas fa-arrow-down-9-1"></i></label>
+ </div>
+ </div>
+ </fieldset>
+ <div class="flex flex-row bg-background-light my-[2vmin] p-[0.4vmin] rounded-[0.4vmin] justify-items-center">
+ <i onclick="defaultSearchParams(); cleanHrefParams();" class="fas fa-rotate-left flex-1 text-center text-[1.5vmax] hover:bg-background-dark cursor-pointer transition-colors duration-300"></i>
+ <div class="w-[0.5vmin] bg-background-dark grow-0"></div>
+ <button class="flex-1" type="submit">
+ <i onclick="setHrefParams();" class="fas fa-magnifying-glass text-center text-[1.5vmax] hover:bg-background-dark cursor-pointer transition-colors duration-300"></i>
+ </button>
+ </div>
+ </form>
+
+ <div class="w-[0.5vh] border-r-[0.3vh] border-dotted border-main-dark bg-background-dark"></div>
+
+ <div hx-get="/api/v1/blog-search" hx-vals='{"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>
+</div>
+{{ end }}
+
+{{ define "footer" }}
+<script src="https://f003.backblazeb2.com/file/sayana-static/libs/htmx/2.0.6/htmx.min.js"></script>
+
+<script>
+ function selectAll() {
+ const tagsContainers = document.querySelectorAll('form.tags-list');
+ tagsContainers.forEach(tagsContainer => {
+ const selectAllCheckbox = tagsContainer.querySelector('#tagsAllCheckbox');
+ const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]:not([id="tagsAllCheckbox"])');
+ checkboxes.forEach(checkbox => {
+ checkbox.checked = selectAllCheckbox.checked;
+ });
+ });
+ }
+
+ function contextAll() {
+ const tagsContainers = document.querySelectorAll('form.tags-list');
+ tagsContainers.forEach(tagsContainer => {
+ const selectAllCheckbox = tagsContainer.querySelector('#tagsAllCheckbox');
+ const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]:not([id="tagsAllCheckbox"])');
+ var areAllChecked = true;
+ checkboxes.forEach(checkbox => {
+ if (!checkbox.checked) areAllChecked = false;
+ });
+ selectAllCheckbox.checked = areAllChecked;
+ });
+ }
+
+ function defaultSearchParams() {
+ const tagsContainers = document.querySelectorAll('form.tags-list');
+ tagsContainers.forEach(tagsContainer => {
+ const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]');
+ checkboxes.forEach(checkbox => {
+ checkbox.checked = true;
+ });
+ const sortRadio = tagsContainer.querySelector('#sortPublicationDateDesc');
+ sortRadio.checked = true;
+ });
+ }
+
+ function cleanHrefParams() {
+ const url = new URL(window.location.href);
+ url.search = '';
+ window.history.pushState({}, '', url.toString());
+ }
+
+ function setHrefParams() {
+ const url = new URL(window.location.href);
+ url.search = '';
+
+ const tagsContainers = document.querySelectorAll('form.tags-list');
+ tagsContainers.forEach(tagsContainer => {
+ const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]');
+ checkboxes.forEach(checkbox => {
+ if (checkbox.checked)
+ url.searchParams.append('tags[]', checkbox.value);
+ });
+ const sortRadio = tagsContainer.querySelector('input[type="radio"]:checked');
+ url.searchParams.set('sort', sortRadio.value);
+ });
+
+ window.history.pushState({}, '', url.toString());
+ }
+
+ 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
new file mode 100644
index 0000000..330a1ac
--- /dev/null
+++ b/views/pages/blog-page.html
@@ -0,0 +1,74 @@
+{{ define "header" }}
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lightgallery.min.css">
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lg-zoom.min.css">
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lg-thumbnail.min.css">
+{{- if .MapLocationX }}
+<link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.css">
+{{- end }}
+
+<style>
+ .map-container .leaflet-container {
+ width: 100% !important;
+ height: 100% !important;
+ }
+</style>
+{{ end }}
+
+{{ define "body" }}
+<div class="bg-background-light flex flex-col grow overflow-y-auto inset-shadow p-[2.4vmin] max-h-[calc(100%-7vmax)] ar-lt-0.8:max-h-[calc(100%-25vh)]">
+ {{ .ParsedMarkdown }}
+{{- if .MapLocationX }}
+ <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto">
+ <div id="location-map" class="relative p-[0.8vmin] flex-none ml-auto mr-auto w-[60%] ar-lt-0.8:w-[80%] h-[40vh] ar-lt-0.8:h-[30vh]"></div>
+ <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto">
+{{- end }}
+</div>
+{{ end }}
+
+{{ define "footer" }}
+<script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/lightgallery.min.js"></script>
+<script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/plugins/lg-zoom.min.js"></script>
+<script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/plugins/lg-thumbnail.min.js"></script>
+{{- if .MapLocationX }}
+<script src="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.js"></script>
+
+<script>
+ function initLocationMap(id, x, y, relativeErrorMeters = 0) {
+ var map = L.map(id).setView([x, y], 8);
+
+ 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);
+
+ if (relativeErrorMeters != 0) {
+ var circle = L.circle([x, y], {
+ color: '#8a9d8a',
+ fillColor: '#8a9d8a',
+ fillOpacity: 0.15,
+ radius: relativeErrorMeters
+ }).addTo(map);
+ }
+
+ var marker = L.marker([x, y], {
+ title: '{{ .Title }}',
+ }).addTo(map)
+ .bindPopup('<span><b>{{ .Title }}</b><br><a href="https://www.openstreetmap.org/#map=13/{{ .MapLocationX }}/{{ .MapLocationY }}">{{ .MapLocationX }}, {{ .MapLocationY }}</a></span>')
+ .openPopup();
+ }
+
+ document.addEventListener('DOMContentLoaded', () => {initLocationMap(
+ 'location-map',
+ {{ .MapLocationX }},
+ {{ .MapLocationY }},
+ {{ .MapLocationAreaMeters }}
+ )});
+
+ window.addEventListener('resize', function() {
+ setTimeout(() => {
+ if (window.location-map) window.location-map.invalidateSize();
+ }, 100);
+ });
+</script>
+{{- end }}
+{{ end }} \ No newline at end of file
diff --git a/views/partials/catalogue-blog-cards.html b/views/partials/catalogue-blog-cards.html
new file mode 100644
index 0000000..a9e4010
--- /dev/null
+++ b/views/partials/catalogue-blog-cards.html
@@ -0,0 +1,18 @@
+{{- 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/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>
+ <p class="text-[1vmax] text-main-dark">{{ .ShortDescription }}</p>
+ <p class="text-[1vmax] text-secondary">{{ .L.TagsLabel }} {{ .Tags }}</p>
+ </div>
+</div>
+{{- end }} \ No newline at end of file