diff options
| author | 2025-08-11 16:52:48 +0700 | |
|---|---|---|
| committer | 2025-08-11 16:52:48 +0700 | |
| commit | ea1f83df820baa14dcd904d581ab2bb0976b172d (patch) | |
| tree | 61322010ecdd59534cc1d02ff1c29d815d07152c | |
| parent | 15356da2c3e3544d757591a2f6e68367bf2d137f (diff) | |
| download | web-0.5.0.tar.gz web-0.5.0.zip | |
feat: redo blog catalogue with same layout as blog-pagev0.5.0
feat: implement localization
feat: filters and sort in blog catalogue
feat: one-page search
feat: ram theme
refactor: generate blog catalogue and blog page out of one layout
refactor: have one layout for both mobile & desktop (aspect ratio dependable)
refactor: use own template manager to use templates separately
| -rw-r--r-- | config/config.go | 16 | ||||
| -rw-r--r-- | config/config.yaml | 17 | ||||
| -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 | 211 | ||||
| -rw-r--r-- | static/input.css | 150 | ||||
| -rw-r--r-- | views/layouts/blog-main.html | 147 | ||||
| -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 | 140 | ||||
| -rw-r--r-- | views/pages/blog-page.html | 70 | ||||
| -rw-r--r-- | views/partials/catalogue-blog-cards.html | 16 |
14 files changed, 747 insertions, 421 deletions
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/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,19 +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/fiber/v2/middleware/redirect" - "github.com/gofiber/template/html/v2" "github.com/yuin/goldmark" "github.com/yuin/goldmark/parser" gmhtml "github.com/yuin/goldmark/renderer/html" @@ -41,6 +45,7 @@ var ( 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() { @@ -63,15 +68,28 @@ func main() { os.Exit(1) } - for _, lang := range cfg.BlogPages.AvailableLanguages { + 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 } - engine := html.New("./views", ".html") - - 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{ @@ -81,9 +99,16 @@ func main() { })) app.Get("/", func(c *fiber.Ctx) error { - return c.Status(fiber.StatusOK).Render("index", fiber.Map{ - "AvailableLanguages": cfg.BlogPages.AvailableLanguages, + 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("/blog/:lang", func(c *fiber.Ctx) error { @@ -93,6 +118,26 @@ func main() { 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 { @@ -100,24 +145,36 @@ func main() { pages = []*b2.BlogPage{} } - pageMeta := make([]map[string]string, 0, len(pages)) + tagsSet := make(map[string]struct{}) 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 { + tagsSet[tag] = struct{}{} + } + } + + tagsArray := make([]string, 0, len(tagsSet)) + for tag := range tagsSet { + tagsArray = append(tagsArray, tag) } + slices.Sort(tagsArray) - return c.Status(status).Render("layouts/blog-main", fiber.Map{ + 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", "/blog/"+lang), 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 { @@ -127,7 +184,7 @@ func main() { return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) } - metadata, parsedMarkdownDesktop, parsedMarkdownMobile, err := readBlogPost(lang + "/" + c.Params("title")) + 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"))) @@ -143,16 +200,106 @@ 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", "/blog/"+lang+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, + "Title": page.Metadata.Title, + "PublishedTime": page.Metadata.PublishedTime.Format("2006-01-02 15:04:05 -0700"), + "ActionDate": page.Metadata.ActionDate, + "ShortDescription": page.Metadata.ShortDescription, + "Thumbnail": page.Metadata.Thumbnail, + "Tags": strings.Join(page.Metadata.Tags, ", "), + }) + break + } + } + } + + slices.SortFunc(pageMeta, func(a, b map[string]string) int { + switch sort { + case "titleAsc": + return strings.Compare(a["Title"], b["Title"]) + case "titleDesc": + return strings.Compare(b["Title"], a["Title"]) + case "actionDateAsc": + return strings.Compare(a["ActionDate"], b["ActionDate"]) + case "actionDateDesc": + return strings.Compare(b["ActionDate"], a["ActionDate"]) + case "publicationDateAsc": + publishedTimeA, _ := time.Parse(time.RFC3339, a["PublishedTime"]) + publishedTimeB, _ := time.Parse(time.RFC3339, b["PublishedTime"]) + return publishedTimeA.Compare(publishedTimeB) + case "publicationDateDesc": + publishedTimeA, _ := time.Parse(time.RFC3339, a["PublishedTime"]) + publishedTimeB, _ := time.Parse(time.RFC3339, b["PublishedTime"]) + return publishedTimeB.Compare(publishedTimeA) + } + return 0 }) + + content, err := tm.Render("catalogue-blog-cards", fiber.Map{ + "BlogPages": pageMeta, + "L": localization[lang], + }) + if err != nil { + slog.Warn("failed to generate div", slog.String("page", "/blog/"+lang), 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") @@ -160,20 +307,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..8776b25 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,65 @@ 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; } - .border-right-custom { - width: 1.5%; - border-left: 0.5vw dotted var(--main-dark-color); + .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-\[90vh\] { + height: 90vh; + } + + .ar-lt-0\.8\:w-\[8vh\] { + width: 8vh; + } + + .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/layouts/blog-main.html b/views/layouts/blog-main.html deleted file mode 100644 index 1b8e149..0000000 --- a/views/layouts/blog-main.html +++ /dev/null @@ -1,147 +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> - <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"> - <link href="/output.css" rel="stylesheet"> - <style> - .menu-outer-body { - border-radius: 10%; - width: 60vw; - height: 40vh; - } - - .menu-inner-body { - width: 85%; - } - - @media (max-aspect-ratio: 1.2/1) { - .menu-outer-body { - border-radius: 5%; - } - - .menu-inner-body { - width: 92.5%; - } - } - - @media (max-aspect-ratio: 0.8/1) { - .menu-outer-body { - width: 100vw; - height: 80vh; - border-radius: 0; - } - - .menu-inner-body { - width: 100%; - } - } - </style> -</head> - -<body data-theme="night" class="font-spectral text-main-dark text-[1.5vmax] overflow-hidden bg-interlocked-hexagons h-screen flex flex-col"> - - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - <div class="absolute star z-0">*</div> - - <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 = 'night'; - switchTheme(savedTheme); - </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> - {{- end }} - </div> - </div> - - <script> - function generateStars() { - var clWidth = document.documentElement.clientWidth; - var clHeight = document.documentElement.clientHeight; - - stars = document.getElementsByClassName("star"); - for (let starIndex = 0; starIndex < stars.length; starIndex++) { - const star = stars[starIndex]; - const x = Math.floor(Math.random() * clWidth); - const y = Math.floor(Math.random() * clHeight * 0.8); - const rot = Math.random() * 90; - const animDuration = (Math.random() * 10) + 3; - const sz = Math.floor(Math.random() * 8) + 12; - star.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rot}deg)`; - star.style.animationDuration = `${animDuration}s`; - star.style.fontSize = `${sz}px`; - } - } - - document.addEventListener('DOMContentLoaded', generateStars); - - let generateStarsTimer; - window.addEventListener("resize", () => { - clearTimeout(generateStarsTimer); - generateStarsTimer = setTimeout(() => { - generateStars(); - }, 1000); - }); - </script> -</body> -</html> diff --git a/views/layouts/blog-page.html b/views/layouts/blog-page.html deleted file mode 100644 index 46fe9f4..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..d33aa76 --- /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='/blog/{{ .Lang }}';" 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 mt-[1.6vmin] ml-[1.6vmin] pr-[1.6vmin]"> + <div class="flex flex-row mb-[0.4vmin]"> + <i onclick="location.pathname+='/../';" 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..6fe1c82 --- /dev/null +++ b/views/pages/blog-catalogue.html @@ -0,0 +1,140 @@ +{{ define "body" }} +<div class="flex flex-row ar-lt-0.8:flex-col flex-1 text-[1vmax]"> + <form hx-get="/api/v1/blog-search" hx-vals='{"lang": "{{ .Lang }}"}' hx-target=".blog-cards" hx-swap="innerHTML" + class="tags-list ar-gt-0.8:min-w-[10vh] ar-gt-0.8:max-w-[20vh] ar-gt-0.8:w-fit ar-lt-0.8:w-[100%] flex shrink-0 flex-col 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{{ . }}Checkbox" name="tags[]" value="{{ . }}" onclick="contextAll();" {{ if contains $.QueryTags . }}checked{{ end }}> {{ . }}</label> + </div> + {{- end }} + </div> + </fieldset> + <fieldset class="mt-[0.8vmin]"> + <legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.OrderByHeader }}</legend> + <div class="flex flex-col ar-lt-0.8:grid grid-cols-3 grid-rows-2 grid-flow-col 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> + const tagsContainers = document.querySelectorAll('form.tags-list'); + tagsContainers.forEach(tagsContainer => { + const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]'); + anyChecked = false; + checkboxes.forEach(checkbox => { + anyChecked = checkbox.checked ? true : anyChecked; + }); + if (!anyChecked) { + selectAll(); + } + }); +</script> + +<script> + function selectAll() { + const tagsContainers = document.querySelectorAll('form.tags-list'); + 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> + +{{ end }} diff --git a/views/pages/blog-page.html b/views/pages/blog-page.html new file mode 100644 index 0000000..5f001d6 --- /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%] 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> +{{ 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..7cb48f0 --- /dev/null +++ b/views/partials/catalogue-blog-cards.html @@ -0,0 +1,16 @@ +{{- range .BlogPages }} +<hr class="first-of-type:hidden border-t-[0.3vh] my-[0.8vmin] border-dotted border-main-dark w-[80%] mx-auto"> +<div class="m-[0.4vmin] flex flex-row justify-center justify-items-center"> + <img onclick="location.href='/blog/{{ .Link }}';" 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"> + <a href="/blog/{{ .Link }}" class="block text-[1vmax] font-extrabold">{{ .Title }}</a> + <a href="/blog/{{ .Link }}" class="block text-[1vmax] font-extrabold pl-[0.4vmin] pr-[0.4vmin] select-none cursor-pointer text-secondary">//</a> + <a href="/blog/{{ .Link }}" class="block text-[1vmax] italic text-secondary">{{ .ActionDate }}</a> + <p class="text-[1vmax] italic text-secondary text-right grow">{{ .PublishedTime }}</p> + </div> + <p class="text-[1vmax] text-main-dark">{{ .ShortDescription }}</p> + <p class="text-[1vmax] text-secondary">{{ .L.TagsLabel }} {{ .Tags }}</p> + </div> +</div> +{{- end }}
\ No newline at end of file |