| -rw-r--r-- | Dockerfile | 2 | ||||
| -rw-r--r-- | internal/frontmatter/parser.go | 1 | ||||
| -rw-r--r-- | internal/lightgallery/block.go | 11 | ||||
| -rw-r--r-- | internal/lightgallery/html_renderer.go | 30 | ||||
| -rw-r--r-- | internal/lightgallery/parser.go | 35 | ||||
| -rw-r--r-- | main.go | 66 | ||||
| -rwxr-xr-x | saya-today-web | bin | 0 -> 19752119 bytes | |||
| -rw-r--r-- | static/input.css | 58 | ||||
| -rw-r--r-- | views/layouts/general-page.html | 19 | ||||
| -rw-r--r-- | views/pages/blog-catalogue.html | 26 | ||||
| -rw-r--r-- | views/pages/blog-page.html | 14 | ||||
| -rw-r--r-- | views/partials/catalogue-blog-card-tags.html | 1 | ||||
| -rw-r--r-- | views/partials/catalogue-blog-cards.html | 2 |
13 files changed, 201 insertions, 64 deletions
@@ -16,4 +16,6 @@ COPY --from=build-stage /builddir/views /app/views COPY --from=build-stage /builddir/locale/*.yaml /app/locale/ COPY --from=build-stage /builddir/config/config.yaml /app/config.yaml +RUN apk add --no-cache tzdata + ENTRYPOINT [ "/app/sayana-demo", "-c", "/app/config.yaml" ] diff --git a/internal/frontmatter/parser.go b/internal/frontmatter/parser.go index 1a521c0..3c00a7f 100644 --- a/internal/frontmatter/parser.go +++ b/internal/frontmatter/parser.go @@ -16,6 +16,7 @@ type Metadata struct { Thumbnail string `yaml:"thumbnail"` Tags []string `yaml:"tags"` Geolocation string `yaml:"geolocation"` + Timezone string `yaml:"timezone"` } func ParseFrontmatter(content []byte) (metadata *Metadata, markdown []byte, err error) { diff --git a/internal/lightgallery/block.go b/internal/lightgallery/block.go index dade2ea..8eb7c9e 100644 --- a/internal/lightgallery/block.go +++ b/internal/lightgallery/block.go @@ -1,16 +1,21 @@ package lightgallery -import "github.com/yuin/goldmark/ast" +import ( + "time" + + "github.com/yuin/goldmark/ast" +) // LightGalleryBlock represents a light gallery block in the AST type LightGalleryBlock struct { ast.BaseBlock - Images []LightGalleryImage + Images []LightGalleryImage + Location *time.Location } type LightGalleryImage struct { URL string - Caption string + Caption []byte } var KindLightGalleryBlock = ast.NewNodeKind("LightGalleryBlock") diff --git a/internal/lightgallery/html_renderer.go b/internal/lightgallery/html_renderer.go index e611f0b..850cd95 100644 --- a/internal/lightgallery/html_renderer.go +++ b/internal/lightgallery/html_renderer.go @@ -1,11 +1,13 @@ package lightgallery import ( + "bytes" "fmt" "math/rand" "strings" "time" + "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/renderer/html" @@ -14,11 +16,18 @@ import ( type LightGalleryHTMLRenderer struct { html.Config + md goldmark.Markdown } func NewLightGalleryHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { r := &LightGalleryHTMLRenderer{ Config: html.NewConfig(), + md: goldmark.New( + goldmark.WithRendererOptions( + html.WithHardWraps(), + html.WithXHTML(), + ), + ), } for _, opt := range opts { opt.SetHTMLOption(&r.Config) @@ -53,7 +62,17 @@ func (r *LightGalleryHTMLRenderer) renderLightGallery(w util.BufWriter, source [ imageUrlWithoutExt := strings.Join(imageUrlSegments[:len(imageUrlSegments)-1], ".") imageNameParts := strings.Split(imageUrlWithoutExt, "-") + var captionBuf bytes.Buffer + captionHTML := img.Caption + if err := r.md.Convert(img.Caption, &captionBuf); err == nil { + captionHTML = captionBuf.Bytes() + captionHTML = bytes.TrimPrefix(captionHTML, []byte("<p>")) + captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>\n")) + captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>")) + } + dayDate, _ := time.Parse("20060102 150405", imageNameParts[len(imageNameParts)-2]+" "+imageNameParts[len(imageNameParts)-1]) + dayDate = dayDate.In(gallery.Location) dynamicElements = append(dynamicElements, fmt.Sprintf(`{ src: "https://f003.backblazeb2.com/file/sayana-photos/full/%s", @@ -70,7 +89,7 @@ func (r *LightGalleryHTMLRenderer) renderLightGallery(w util.BufWriter, source [ <p class="grow !text-[1vmax]/[0.9] text-left font-spectral text-main-dark">%s</p> <p class="!text-[1vmax]/[0.9] text-right font-spectral text-secondary">%s</p> </div>`+"`"+` - }`, img.URL, img.URL, img.Caption, imageUrlWithoutExt, imageUrlWithoutExt, img.Caption, dayDate.Format("2006-01-02 15:04:05 -07:00"))) + }`, img.URL, img.URL, util.EscapeHTML(captionHTML), imageUrlWithoutExt, imageUrlWithoutExt, captionHTML, dayDate.Format("2006-01-02 15:04:05 -07:00"))) } w.WriteString(fmt.Sprintf(` @@ -117,15 +136,6 @@ window.addEventListener("resize", () => { return ast.WalkContinue, nil } -func escapeHTML(s string) string { - s = strings.ReplaceAll(s, "&", "&") - s = strings.ReplaceAll(s, "<", "<") - s = strings.ReplaceAll(s, ">", ">") - s = strings.ReplaceAll(s, "\"", """) - s = strings.ReplaceAll(s, "'", "'") - return s -} - func generateDivId(length int) string { const charset = "abcdefghijklmnopqrstuvwxyz" seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) // Seed with current time diff --git a/internal/lightgallery/parser.go b/internal/lightgallery/parser.go index 0ce209c..daec797 100644 --- a/internal/lightgallery/parser.go +++ b/internal/lightgallery/parser.go @@ -2,7 +2,9 @@ package lightgallery import ( "bytes" - "strings" + "log/slog" + "regexp" + "time" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" @@ -22,43 +24,52 @@ func (p *LightGalleryParser) Trigger() []byte { func (p *LightGalleryParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { line, _ := reader.PeekLine() - if !bytes.HasPrefix(line, []byte("{Gallery}")) { + if !bytes.HasPrefix(line, []byte("{Gallery:")) { return nil, parser.NoChildren } + r := regexp.MustCompile(`^\{Gallery:([A-Za-z0-9\+\-/]+)\}$`) + trimmed := bytes.TrimSpace(line) - if !bytes.Equal(trimmed, []byte("{Gallery}")) { + parts := r.FindSubmatch(trimmed) + if len(parts) < 2 { + slog.Warn("invalid gallery header format", slog.String("line", string(trimmed)), slog.Int("submatch_count", len(parts))) return nil, parser.NoChildren } - return &LightGalleryBlock{}, parser.NoChildren + loc, err := time.LoadLocation(string(parts[1])) + if err != nil { + slog.Warn("invalid location specified for a gallery", slog.String("error", err.Error()), slog.String("line", string(trimmed)), slog.String("location", string(parts[1]))) + return nil, parser.NoChildren + } + + return &LightGalleryBlock{Location: loc}, parser.NoChildren } func (p *LightGalleryParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { line, segment := reader.PeekLine() if len(line) == 0 || segment.Len() == 0 { - return parser.Close + return parser.Continue | parser.NoChildren } trimmed := bytes.TrimSpace(line) - if bytes.Equal(trimmed, []byte("{Gallery}")) { + if bytes.Equal(trimmed, []byte("{Gallery}")) || bytes.Equal(trimmed, []byte("{/Gallery}")) { reader.AdvanceLine() return parser.Close } gallery := node.(*LightGalleryBlock) - lineStr := string(trimmed) - parts := strings.SplitN(lineStr, "|", 2) - url := strings.TrimSpace(parts[0]) - caption := "" + parts := bytes.SplitN(trimmed, []byte{'|'}, 2) + url := bytes.TrimSpace(parts[0]) + caption := make([]byte, 0) if len(parts) > 1 { - caption = strings.TrimSpace(parts[1]) + caption = bytes.TrimSpace(parts[1]) } gallery.Images = append(gallery.Images, LightGalleryImage{ - URL: url, + URL: string(url), Caption: caption, }) @@ -82,7 +82,7 @@ func main() { {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"}}, + {Name: "catalogue-blog-cards", Files: []string{"views/partials/catalogue-blog-cards.html", "views/partials/catalogue-blog-card-tags.html"}}, }) if err != nil { slog.Error("fail to initialize template manager", slog.String("error", err.Error())) @@ -121,7 +121,7 @@ func main() { querySort := c.Query("sort") if querySort == "" { - querySort = "titleAsc" + querySort = "publicationDateDesc" } encodedQuery := c.Request().URI().QueryString() @@ -148,7 +148,7 @@ func main() { tagsMap := make(map[string]int) for _, page := range pages { - slog.Debug("enlist page for catalogue", slog.Any("page", page), slog.String("endpoint", "/")) + slog.Debug("enlist page for catalogue", slog.Any("page", page), slog.String("endpoint", "/"+lang+"/blog")) for _, tag := range page.Metadata.Tags { tagsMap[tag]++ } @@ -227,15 +227,49 @@ func main() { return c.Type("html").Send(content) }) + app.Get("/api/v1/tz", func(c *fiber.Ctx) error { + timestampString := c.Query("timestamp") + tz := c.Query("tz") + + loc, err := time.LoadLocation(tz) + if err != nil { + loc = time.UTC + slog.Warn("unable to parse a client timezone, defaulting to UTC", slog.String("error", err.Error()), slog.String("tz", tz)) + } + + var format string + var timestamp time.Time + for _, format = range []string{"2006-01-02 15:04:05 -07:00", time.RFC3339} { + if timestamp, err = time.Parse(format, timestampString); err == nil { + break + } + } + + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + + if err != nil { + return c.Status(fiber.StatusBadRequest).SendString("invalid timestamp format") + } + + return c.Status(fiber.StatusOK).SendString(timestamp.In(loc).Format(format)) + }) + app.Get("/api/v1/blog-search", func(c *fiber.Ctx) error { sort := c.Query("sort") lang := c.Query("lang") + tz := c.Query("tz") 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)) } + loc, err := time.LoadLocation(tz) + if err != nil { + loc = time.UTC + slog.Warn("unable to parse a client timezone, defaulting to UTC", slog.String("error", err.Error()), slog.String("tz", tz)) + } + pages, err := b2Client.Scan(lang + "/") if err != nil { c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) @@ -257,42 +291,42 @@ func main() { tags = append(tags, string(match[1])) } - pageMeta := make([]map[string]string, 0, len(pages)) + pageMeta := make([]fiber.Map, 0, len(pages)) for _, page := range pages { for _, tag := range page.Metadata.Tags { if len(tags) == 0 || slices.Contains(tags, tag) { - pageMeta = append(pageMeta, map[string]string{ + pageMeta = append(pageMeta, fiber.Map{ "Link": page.Link, "ArticleLink": "/" + lang + "/blog/" + page.FileName, "Title": page.Metadata.Title, - "PublishedTime": page.Metadata.PublishedTime.Format("2006-01-02 15:04:05 -0700"), + "PublishedTime": page.Metadata.PublishedTime.In(loc).Format("2006-01-02 15:04:05 -07:00"), "ActionDate": page.Metadata.ActionDate, "ShortDescription": page.Metadata.ShortDescription, "Thumbnail": page.Metadata.Thumbnail, - "Tags": strings.Join(page.Metadata.Tags, ", "), + "Tags": page.Metadata.Tags, }) break } } } - slices.SortFunc(pageMeta, func(a, b map[string]string) int { + slices.SortFunc(pageMeta, func(a, b fiber.Map) int { switch sort { case "titleAsc": - return strings.Compare(a["Title"], b["Title"]) + return strings.Compare(a["Title"].(string), b["Title"].(string)) case "titleDesc": - return strings.Compare(b["Title"], a["Title"]) + return strings.Compare(b["Title"].(string), a["Title"].(string)) case "actionDateAsc": - return strings.Compare(a["ActionDate"], b["ActionDate"]) + return strings.Compare(a["ActionDate"].(string), b["ActionDate"].(string)) case "actionDateDesc": - return strings.Compare(b["ActionDate"], a["ActionDate"]) + return strings.Compare(b["ActionDate"].(string), a["ActionDate"].(string)) case "publicationDateAsc": - publishedTimeA, _ := time.Parse(time.RFC3339, a["PublishedTime"]) - publishedTimeB, _ := time.Parse(time.RFC3339, b["PublishedTime"]) + publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -07:00", a["PublishedTime"].(string)) + publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", b["PublishedTime"].(string)) return publishedTimeA.Compare(publishedTimeB) case "publicationDateDesc": - publishedTimeA, _ := time.Parse(time.RFC3339, a["PublishedTime"]) - publishedTimeB, _ := time.Parse(time.RFC3339, b["PublishedTime"]) + publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -07:00", a["PublishedTime"].(string)) + publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", b["PublishedTime"].(string)) return publishedTimeB.Compare(publishedTimeA) } return 0 diff --git a/saya-today-web b/saya-today-web Binary files differnew file mode 100755 index 0000000..b6522e8 --- /dev/null +++ b/saya-today-web diff --git a/static/input.css b/static/input.css index 8776b25..131faa9 100644 --- a/static/input.css +++ b/static/input.css @@ -25,6 +25,9 @@ --sidebar-text-color: var(--background-dark-color); --sidebar-stroke-color: var(--main-dark-color); --secondary-color: var(--main-light-color); + --leaflet-text-color: var(--main-dark-color); + --leaflet-link-color: var(--main-medium-color); + --leaflet-link-hover-color: var(--main-light-color); --interlocked-hexagons-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23ffffdb%22%2F%3E%3Cpath%20fill%3D%22%23777e63%22%20d%3D%22m33.64%200-.17.25h-33L4.3%206h33.07l4-6zm8.33%200-4.33%206.5h-33l3.83%205.75h33.06L49.7%200zm8.34%200-8.5%2012.75h-33l3.83%205.75H45.7L58.03%200zm8.33%200L45.97%2019h-33l3.83%205.75h33.06L66.36%200zm-8.5%2025.25L33.63%2050l3.86%205.8L54.03%2031h33l-3.83-5.75zm4.16%206.25L37.8%2056.25l3.87%205.8%2016.53-24.8h33l-3.84-5.75zm4.17%206.25L41.97%2062.5l3.86%205.8%2016.54-24.8h33l-3.84-5.75zM62.63%2044l-16.5%2024.75%203.87%205.8%2016.53-24.8h33L95.7%2044zM100%2050.45l-3.87%205.8%203.87%205.81v-11.6zM0%2050.46v11.6L12.5%2080.8l3.86-5.8zm95.83%206.25-3.87%205.8L100%2074.55v-11.6zm-4.17%206.24-3.86%205.8%2012.2%2018.3v-11.6zM0%2062.96v11.6l8.33%2012.49%203.86-5.8zm87.5%206.24L83.63%2075%20100%2099.54V87.96zM50%2075.45%2033.64%20100h7.72l12.5-18.75zm-50%200v11.6l4.16%206.24%203.87-5.8zm54.17%206.25L41.97%20100h7.73l8.33-12.5zm4.17%206.25L50.3%20100h7.73l4.17-6.25zM0%2087.95v11.59l3.86-5.8zm62.5%206.25-3.87%205.8h7.73z%22%2F%3E%3Cpath%20fill%3D%22%23a0a182%22%20d%3D%22M66.8.25%2062.97%206h33l4.03%206.05V.45l-.14-.2zM0%20.45v11.61L12.5%2030.8l3.86-5.8zM62.63%206.5l-3.83%205.75h33l8.2%2012.3v-11.6L95.7%206.5zm-4.16%206.25-3.84%205.75h33L100%2037.06v-11.6l-8.47-12.71zM0%2012.96v11.6l8.33%2012.49%203.86-5.8zM54.3%2019l-3.84%205.75h33L100%2049.55l.01-.01V37.96L87.36%2019zm-4.77%206.25H41.8L25.3%2050l16.66%2025-16.5%2024.75h7.74L49.7%2075%2033.03%2050zm-8.33%200h-7.74L16.96%2050l16.67%2025-16.5%2024.75h7.73L41.36%2075%2024.7%2050Zm-8.34%200h-7.73L8.63%2050%2025.3%2075%208.8%2099.75h7.73L33.03%2075%2016.36%2050zm-16.07%200L.3%2050l16.67%2025L.46%2099.75H8.2L24.7%2075%208.02%2050l16.5-24.75zM0%2025.46v11.6l4.16%206.24%203.87-5.8zm0%2012.5v11.58l3.86-5.79zm66.8%2012.29L50.3%2074.99l3.87%205.81%204.16%206.25%204.17%206.25%204.3%206.45h7.73L58.03%2075l16.5-24.75zm16.06%200h-7.73L58.63%2075l16.5%2024.75h7.73L66.36%2075zm.6%200L66.96%2075l16.5%2024.75h7.74L74.7%2075l16.5-24.75zm16.07%200H91.8L75.3%2075l16.5%2024.75h7.73L83.03%2075z%22%2F%3E%3C%2Fsvg%3E"); --interlocked-hexagons-background-repeat: repeat; --interlocked-hexagons-background-size: 10vh; @@ -43,6 +46,9 @@ --sidebar-text-color: var(--main-dark-color); --sidebar-stroke-color: var(--background-dark-color); --secondary-color: var(--main-medium-color); + --leaflet-text-color: var(--main-dark-color); + --leaflet-link-color: var(--main-medium-color); + --leaflet-link-hover-color: var(--main-light-color); --interlocked-hexagons-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23f5ede1%22%2F%3E%3Cpath%20fill%3D%22%239ab663%22%20d%3D%22m33.64%200-.17.25h-33L4.3%206h33.07l4-6zm8.33%200-4.33%206.5h-33l3.83%205.75h33.06L49.7%200zm8.34%200-8.5%2012.75h-33l3.83%205.75H45.7L58.03%200zm8.33%200L45.97%2019h-33l3.83%205.75h33.06L66.36%200zm-8.5%2025.25L33.63%2050l3.86%205.8L54.03%2031h33l-3.83-5.75zm4.16%206.25L37.8%2056.25l3.87%205.8%2016.53-24.8h33l-3.84-5.75zm4.17%206.25L41.97%2062.5l3.86%205.8%2016.54-24.8h33l-3.84-5.75zM62.63%2044l-16.5%2024.75%203.87%205.8%2016.53-24.8h33L95.7%2044zM100%2050.45l-3.87%205.8%203.87%205.81v-11.6zM0%2050.46v11.6L12.5%2080.8l3.86-5.8zm95.83%206.25-3.87%205.8L100%2074.55v-11.6zm-4.17%206.24-3.86%205.8%2012.2%2018.3v-11.6zM0%2062.96v11.6l8.33%2012.49%203.86-5.8zm87.5%206.24L83.63%2075%20100%2099.54V87.96zM50%2075.45%2033.64%20100h7.72l12.5-18.75zm-50%200v11.6l4.16%206.24%203.87-5.8zm54.17%206.25L41.97%20100h7.73l8.33-12.5zm4.17%206.25L50.3%20100h7.73l4.17-6.25zM0%2087.95v11.59l3.86-5.8zm62.5%206.25-3.87%205.8h7.73z%22%2F%3E%3Cpath%20fill%3D%22%23bbd095%22%20d%3D%22M66.8.25%2062.97%206h33l4.03%206.05V.45l-.14-.2zM0%20.45v11.61L12.5%2030.8l3.86-5.8zM62.63%206.5l-3.83%205.75h33l8.2%2012.3v-11.6L95.7%206.5zm-4.16%206.25-3.84%205.75h33L100%2037.06v-11.6l-8.47-12.71zM0%2012.96v11.6l8.33%2012.49%203.86-5.8zM54.3%2019l-3.84%205.75h33L100%2049.55l.01-.01V37.96L87.36%2019zm-4.77%206.25H41.8L25.3%2050l16.66%2025-16.5%2024.75h7.74L49.7%2075%2033.03%2050zm-8.33%200h-7.74L16.96%2050l16.67%2025-16.5%2024.75h7.73L41.36%2075%2024.7%2050Zm-8.34%200h-7.73L8.63%2050%2025.3%2075%208.8%2099.75h7.73L33.03%2075%2016.36%2050zm-16.07%200L.3%2050l16.67%2025L.46%2099.75H8.2L24.7%2075%208.02%2050l16.5-24.75zM0%2025.46v11.6l4.16%206.24%203.87-5.8zm0%2012.5v11.58l3.86-5.79zm66.8%2012.29L50.3%2074.99l3.87%205.81%204.16%206.25%204.17%206.25%204.3%206.45h7.73L58.03%2075l16.5-24.75zm16.06%200h-7.73L58.63%2075l16.5%2024.75h7.73L66.36%2075zm.6%200L66.96%2075l16.5%2024.75h7.74L74.7%2075l16.5-24.75zm16.07%200H91.8L75.3%2075l16.5%2024.75h7.73L83.03%2075z%22%2F%3E%3C%2Fsvg%3E"); --interlocked-hexagons-background-repeat: repeat; --interlocked-hexagons-background-size: 10vh; @@ -61,6 +67,9 @@ --sidebar-text-color: var(--main-dark-color); --sidebar-stroke-color: var(--background-light-color); --secondary-color: var(--main-light-color); + --leaflet-text-color: var(--background-dark-color); + --leaflet-link-color: var(--background-dark-color); + --leaflet-link-hover-color: var(--background-light-color); --interlocked-hexagons-background: radial-gradient(33.5% 25% at 100% 82%, rgba(190, 227, 248, 0.2) 0%, rgba(197, 179, 246, 0.13) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(54% 25% at 55% 100%, rgba(190, 227, 248, 0.2) 0%, rgba(197, 179, 246, 0.13) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(37% 19.5% at 20% 100%, rgba(176, 220, 248, 0.3) 0%, rgba(197, 172, 247, 0.2) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(63% 34.5% at 66% 77%, rgba(190, 227, 248, 0.15) 0%, rgba(197, 179, 246, 0.1) 33.33%, rgba(255, 0, 0, 0) 100%), linear-gradient(0deg, #2f5883 0%, #2f4e7a 4.75%, #2e4472 9.5%, #2a3065 19%, #272e57 26.5%, #242b4b 34%, #1f2437 49%, #1c2030 57.75%, #1a1b29 66.5%, #13131b 84%); --interlocked-hexagons-background-repeat: no-repeat; --interlocked-hexagons-background-size: 100vw 100vh; @@ -79,6 +88,9 @@ --sidebar-text-color: var(--main-dark-color); --sidebar-stroke-color: var(--background-light-color); --secondary-color: var(--main-light-color); + --leaflet-text-color: var(--background-dark-color); + --leaflet-link-color: var(--background-dark-color); + --leaflet-link-hover-color: var(--background-light-color); --interlocked-hexagons-background: url('https://f003.backblazeb2.com/file/sayana-static/themes/ram/ram-background.webp'); --interlocked-hexagons-background-repeat: repeat; --interlocked-hexagons-background-size: auto; @@ -88,6 +100,29 @@ --squares-and-triangles-background-height-coef: 15; } +a { + color: var(--main-medium-color) !important; + text-decoration: underline; +} + +a:hover { + color: var(--main-light-color) !important; +} + +#location-map { + color: var(--leaflet-text-color) !important; +} + +#location-map a { + color: var(--leaflet-link-color) !important; + text-decoration: underline; +} + +#location-map a:hover { + color: var(--leaflet-link-hover-color) !important; + text-decoration: underline; +} + .bg-interlocked-hexagons { background-image: var(--interlocked-hexagons-background); background-repeat: var(--interlocked-hexagons-background-repeat); @@ -154,10 +189,6 @@ background-size: calc(var(--squares-and-triangles-background-width-coef) * 1vw) calc(var(--squares-and-triangles-background-height-coef) * 1vw); } -body:has(.lg-container:not([class~="lg-inline"])) .desktop-sidebar-custom { - display: none !important; -} - .mobile-topbar-custom { width: 90vw; height: 8%; @@ -165,7 +196,7 @@ body:has(.lg-container:not([class~="lg-inline"])) .desktop-sidebar-custom { margin-left: 5vw; } -body:has(.lg-container:not([class~="lg-inline"])) .mobile-topbar-custom { +body:has(.lg-container:not([class~="lg-inline"])) #sidebar { display: none !important; } @@ -336,6 +367,11 @@ body:has(.lg-container:not([class~="lg-inline"])) .mobile-topbar-custom { } } +body[data-theme~="night"] .leaflet-tile, +body[data-theme~="ram"] .leaflet-tile { + filter: invert(1) hue-rotate(190deg) contrast(1.1) brightness(1.2) !important; +} + @media (min-aspect-ratio: 0.8/1) { .ar-gt-0\.8\:mr-\[1\.2vmin\] { margin-right: 1.2vmin; @@ -416,6 +452,10 @@ body:has(.lg-container:not([class~="lg-inline"])) .mobile-topbar-custom { height: 10vh; } + .ar-lt-0\.8\:h-\[30vh\] { + height: 30vh; + } + .ar-lt-0\.8\:h-\[90vh\] { height: 90vh; } @@ -424,6 +464,10 @@ body:has(.lg-container:not([class~="lg-inline"])) .mobile-topbar-custom { width: 8vh; } + .ar-lt-0\.8\:w-\[80\%\] { + width: 80%; + } + .ar-lt-0\.8\:w-\[90vw\] { width: 90vw; } @@ -436,6 +480,10 @@ body:has(.lg-container:not([class~="lg-inline"])) .mobile-topbar-custom { max-height: 80%; } + .ar-lt-0\.8\:max-h-\[calc\(100\%-25vh\)\] { + max-height: calc(100% - 25vh); + } + .ar-lt-0\.8\:text-\[5vh\] { font-size: 5vh; } diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html index 121a5cd..46ef2e6 100644 --- a/views/layouts/general-page.html +++ b/views/layouts/general-page.html @@ -12,7 +12,7 @@ <link href="/output.css" rel="stylesheet"> </head> -<body data-theme="night" class="font-spectral text-main-dark overflow-hidden bg-interlocked-hexagons h-screen flex flex-row ar-lt-0.8:flex-col"> +<body data-theme="ram" class="font-spectral text-main-dark overflow-hidden bg-interlocked-hexagons h-screen flex flex-row ar-lt-0.8:flex-col"> <script> function switchTheme(theme) { @@ -29,11 +29,15 @@ document.body.setAttribute('data-theme', theme); } - const savedTheme = localStorage.getItem('theme') || 'night'; + const savedTheme = localStorage.getItem('theme') || 'ram'; 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"> + <script> + const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + </script> + + <div id="sidebar" class="bg-sidebar flex sticky flex-col ar-gt-0.8:items-center shadow-2xl z-20 w-[5vw] ar-lt-0.8:w-[90vw] ar-lt-0.4:w-[100vw] h-screen ar-lt-0.8:h-[8vh] ar-lt-0.8:ml-[5vw] ar-lt-0.4:ml-0"> <div class="logo-custom ar-lt-0.8:hidden text-sidebar font-patua font-extrabold text-center relative z-20"> saya.today </div> @@ -49,13 +53,12 @@ <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]"> + <div {{ if .PublishedDate }} hx-get="/api/v1/tz" hx-vals='js:{timestamp: "{{ .PublishedDate }}", tz: clientTimeZone}' hx-target="#published-date" hx-swap="innerHTML" hx-trigger="load" {{ end }} + class="flex flex-row mb-[0.4vmin]"> <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 }} + <p id="published-date" 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]"> @@ -76,6 +79,8 @@ <div class="pr-[0.5vw] bg-background-dark shadow-2xl border-l-[0.4vmin] border-dotted border-main-dark"></div> </div> + <script src="https://f003.backblazeb2.com/file/sayana-static/libs/htmx/2.0.6/htmx.min.js"></script> + {{ block "footer" . }}{{ end }} </body> diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html index 4cda19d..2b248c3 100644 --- a/views/pages/blog-catalogue.html +++ b/views/pages/blog-catalogue.html @@ -1,5 +1,5 @@ {{ 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]"> +<div class="flex flex-row ar-lt-0.8:flex-col max-h-[calc(100%-7vmax)] ar-lt-0.8:max-h-[calc(100%-25vh)] flex-1 text-[1vmax]"> <form hx-get="/api/v1/blog-search" hx-vals='{"lang": "{{ .Lang }}"}' hx-target=".blog-cards" hx-swap="innerHTML" class="tags-list ar-gt-0.8:min-w-[10vh] ar-gt-0.8:max-w-[20vh] ar-gt-0.8:w-fit ar-lt-0.8:w-[100%] flex shrink-0 flex-col overflow-y-auto ar-gt-0.8:mr-[1.2vmin]"> <fieldset> @@ -49,7 +49,7 @@ <div class="w-[0.5vh] border-r-[0.3vh] border-dotted border-main-dark bg-background-dark"></div> - <div hx-get="/api/v1/blog-search" hx-vals='{"lang": "{{ .Lang }}"}' hx-target="this" hx-swap="innerHTML" hx-trigger="load" hx-include=".tags-list" + <div hx-get="/api/v1/blog-search" hx-vals='js:{lang: "{{ .Lang }}", tz: clientTimeZone}' hx-target="this" hx-swap="innerHTML" hx-trigger="load" hx-include=".tags-list" class="blog-cards grow flex flex-col overflow-y-auto bg-background-light inset-shadow p-[2.4vmin]"> Loading... </div> @@ -57,8 +57,6 @@ {{ 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'); @@ -91,7 +89,7 @@ checkboxes.forEach(checkbox => { checkbox.checked = true; }); - const sortRadio = tagsContainer.querySelector('#sortTitleAsc'); + const sortRadio = tagsContainer.querySelector('#sortPublicationDateDesc'); sortRadio.checked = true; }); } @@ -120,6 +118,24 @@ window.history.pushState({}, '', url.toString()); } + function pickedTagSetHrefParams(tag) { + const url = new URL(window.location.href); + url.searchParams.delete('tags[]'); + url.searchParams.set('tags[]', tag); + + const tagsContainers = document.querySelectorAll('form.tags-list'); + tagsContainers.forEach(tagsContainer => { + const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]'); + checkboxes.forEach(checkbox => { + checkbox.checked = false; + }); + chosenCheckbox = tagsContainer.querySelector(`#tag${tag}Checkbox`); + chosenCheckbox.checked = true; + }); + + window.history.pushState({}, '', url.toString()); + } + document.addEventListener('DOMContentLoaded', contextAll); </script> diff --git a/views/pages/blog-page.html b/views/pages/blog-page.html index 5f001d6..330a1ac 100644 --- a/views/pages/blog-page.html +++ b/views/pages/blog-page.html @@ -15,11 +15,11 @@ {{ end }} {{ define "body" }} -<div class="bg-background-light flex flex-col grow overflow-y-auto inset-shadow p-[2.4vmin] ar-lt-0.8:max-h-[80%]"> +<div class="bg-background-light flex flex-col grow overflow-y-auto inset-shadow p-[2.4vmin] max-h-[calc(100%-7vmax)] ar-lt-0.8:max-h-[calc(100%-25vh)]"> {{ .ParsedMarkdown }} {{- if .MapLocationX }} <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto"> - <div id="location-map" class="relative p-[0.8vmin] flex-none ml-auto mr-auto w-[60%] h-[40vh]"></div> + <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> @@ -34,10 +34,10 @@ <script> function initLocationMap(id, x, y, relativeErrorMeters = 0) { - var map = L.map(id).setView([x, y], 13); + var map = L.map(id).setView([x, y], 8); L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, + maxZoom: 18, attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' }).addTo(map); @@ -50,7 +50,11 @@ }).addTo(map); } - var marker = L.marker([x, y]).addTo(map); + var marker = L.marker([x, y], { + title: '{{ .Title }}', + }).addTo(map) + .bindPopup('<span><b>{{ .Title }}</b><br><a href="https://www.openstreetmap.org/#map=13/{{ .MapLocationX }}/{{ .MapLocationY }}">{{ .MapLocationX }}, {{ .MapLocationY }}</a></span>') + .openPopup(); } document.addEventListener('DOMContentLoaded', () => {initLocationMap( diff --git a/views/partials/catalogue-blog-card-tags.html b/views/partials/catalogue-blog-card-tags.html new file mode 100644 index 0000000..374314a --- /dev/null +++ b/views/partials/catalogue-blog-card-tags.html @@ -0,0 +1 @@ +{{ range $i, $tag := .Tags }}{{ if ne $i 0 }}, {{ end }}<a hx-trigger="click" hx-get="/api/v1/blog-search" hx-target=".blog-cards" hx-swap="innerHTML" hx-trigger="load" hx-include=".tags-list" class="hx-anchor cursor-pointer" onclick="pickedTagSetHrefParams({{ $tag }})">{{ $tag }}</a>{{ end }}
\ No newline at end of file diff --git a/views/partials/catalogue-blog-cards.html b/views/partials/catalogue-blog-cards.html index a9e4010..406ef9b 100644 --- a/views/partials/catalogue-blog-cards.html +++ b/views/partials/catalogue-blog-cards.html @@ -12,7 +12,7 @@ <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> + <p class="text-[1vmax] text-secondary">{{ $.L.TagsLabel }} {{ template "catalogue-blog-card-tags.html" . }}</p> </div> </div> {{- end }}
\ No newline at end of file |