| -rw-r--r-- | .github/workflows/build-and-deploy.yml | 11 | ||||
| -rw-r--r-- | .gitignore | 4 | ||||
| -rw-r--r-- | Dockerfile | 1 | ||||
| -rw-r--r-- | config/config.go | 16 | ||||
| -rw-r--r-- | config/config.yaml | 17 | ||||
| -rw-r--r-- | deploy/inventory.yml | 14 | ||||
| -rw-r--r-- | deploy/playbook.yml | 28 | ||||
| -rw-r--r-- | internal/b2/client.go | 12 | ||||
| -rw-r--r-- | internal/templatemanager/templatemanager.go | 57 | ||||
| -rw-r--r-- | locale/localization.en.yaml | 9 | ||||
| -rw-r--r-- | locale/localization.go | 50 | ||||
| -rw-r--r-- | locale/localization.ru.yaml | 9 | ||||
| -rw-r--r-- | main.go | 252 | ||||
| -rw-r--r-- | static/input.css | 158 | ||||
| -rw-r--r-- | views/index.html | 60 | ||||
| -rw-r--r-- | views/layouts/blog-page.html | 194 | ||||
| -rw-r--r-- | views/layouts/general-page.html | 82 | ||||
| -rw-r--r-- | views/pages/blog-catalogue.html | 142 | ||||
| -rw-r--r-- | views/pages/blog-page.html | 70 | ||||
| -rw-r--r-- | views/partials/catalogue-blog-cards.html | 18 |
20 files changed, 864 insertions, 340 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 }} @@ -36,7 +36,9 @@ config*.json /main /sayana-demo -/static/input.css +/static/output.css /.vscode .envrc + +/.ansible @@ -13,6 +13,7 @@ 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 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/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: 'Выбрать все' @@ -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 = "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{} } - 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, ", "), - }) + 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 -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("2006-01-02 15:04:05 -0700", a["PublishedTime"]) + publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -0700", b["PublishedTime"]) + return publishedTimeA.Compare(publishedTimeB) + case "publicationDateDesc": + publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -0700", a["PublishedTime"]) + publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -0700", 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/static/input.css b/static/input.css index edecc19..69cfa91 100644 --- a/static/input.css +++ b/static/input.css @@ -30,7 +30,8 @@ --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"] { @@ -47,7 +48,8 @@ --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"] { @@ -64,7 +66,26 @@ --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); + --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; } .bg-interlocked-hexagons { @@ -127,6 +148,12 @@ background-size: calc(var(--squares-and-triangles-background-size) * 1vw); } +.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); +} + body:has(.lg-container:not([class~="lg-inline"])) .desktop-sidebar-custom { display: none !important; } @@ -165,10 +192,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 +209,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 +336,43 @@ body:has(.lg-container:not([class~="lg-inline"])) .mobile-topbar-custom { } } -/* Mobile responsive styles */ -@media (max-aspect-ratio: 1.1/1) { - .desktop { - display: none; +@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 +396,73 @@ 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; } - .border-right-custom { - width: 1.5%; - border-left: 0.5vw dotted var(--main-dark-color); + .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; + } + + .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\: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: '© <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..121a5cd --- /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 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..4cda19d --- /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%-6vmax)] ar-lt-0.8:max-h-[80%] 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('#sortTitleAsc'); + 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..5f52c2f --- /dev/null +++ b/views/pages/blog-page.html @@ -0,0 +1,70 @@ +{{ 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] 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="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], 13); + + L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, + attribution: '© <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', + {{ .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 |