summaryrefslogtreecommitdiffci
diff options
from:
to:
context:
space:
mode:
-rw-r--r--internal/b2/client.go1
-rw-r--r--internal/frontmatter/parser.go1
-rw-r--r--locale/localization.en.yaml2
-rw-r--r--locale/localization.go5
-rw-r--r--locale/localization.ru.yaml2
-rw-r--r--main.go67
-rw-r--r--static/input.css17
-rw-r--r--views/layouts/general-page.html2
-rw-r--r--views/pages/blog-page.html10
-rw-r--r--views/pages/global-map.html241
10 files changed, 328 insertions, 20 deletions
diff --git a/internal/b2/client.go b/internal/b2/client.go
index 5322b84..7b745a8 100644
--- a/internal/b2/client.go
+++ b/internal/b2/client.go
@@ -84,6 +84,7 @@ func (c *B2Client) Scan(prefix string) ([]*BlogPage, error) {
PublishedTime: publishedTime,
Thumbnail: attrs.Info["thumbnail"],
Tags: strings.Split(attrs.Info["tags"], ","),
+ Geolocation: attrs.Info["geolocation"],
},
})
}
diff --git a/internal/frontmatter/parser.go b/internal/frontmatter/parser.go
index 3c00a7f..1a521c0 100644
--- a/internal/frontmatter/parser.go
+++ b/internal/frontmatter/parser.go
@@ -16,7 +16,6 @@ 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/locale/localization.en.yaml b/locale/localization.en.yaml
index 879fce8..f5ee6c9 100644
--- a/locale/localization.en.yaml
+++ b/locale/localization.en.yaml
@@ -7,3 +7,5 @@ BlogSearch:
ActionDateOrdered: 'Action Date'
PublicationDateOrdered: 'Publication Date'
ChooseAllTags: 'Choose All'
+GlobalMap:
+ Header: 'Global Map'
diff --git a/locale/localization.go b/locale/localization.go
index d5c6b48..56b6462 100644
--- a/locale/localization.go
+++ b/locale/localization.go
@@ -10,6 +10,7 @@ import (
type LocaleConfig struct {
TagsLabel string `yaml:"TagsLabel" json:"TagsLabel"`
BlogSearch BlogSearchConfig `yaml:"BlogSearch" json:"BlogSearch"`
+ GlobalMap GlobalMapConfig `yaml:"GlobalMap" json:"GlobalMap"`
}
type BlogSearchConfig struct {
@@ -22,6 +23,10 @@ type BlogSearchConfig struct {
ChooseAllTags string `yaml:"ChooseAllTags" json:"ChooseAllTags"`
}
+type GlobalMapConfig struct {
+ Header string `yaml:"Header" json:"Header"`
+}
+
func LoadConfig(path string, config *LocaleConfig) error {
fileBytes, err := os.ReadFile(path)
if err != nil {
diff --git a/locale/localization.ru.yaml b/locale/localization.ru.yaml
index 786665d..c236d1e 100644
--- a/locale/localization.ru.yaml
+++ b/locale/localization.ru.yaml
@@ -7,3 +7,5 @@ BlogSearch:
ActionDateOrdered: 'дате действия'
PublicationDateOrdered: 'времени публикации'
ChooseAllTags: 'Выбрать все'
+GlobalMap:
+ Header: 'Глобальная карта'
diff --git a/main.go b/main.go
index 9e3f9a4..e07ca6b 100644
--- a/main.go
+++ b/main.go
@@ -83,6 +83,7 @@ func main() {
{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", "views/partials/catalogue-blog-card-tags.html"}},
+ {Name: "global-map", Files: []string{"views/pages/global-map.html"}},
})
if err != nil {
slog.Error("fail to initialize template manager", slog.String("error", err.Error()))
@@ -112,6 +113,72 @@ func main() {
return c.Type("html").Send(content)
})
+ app.Get("/:lang/map", 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))
+ }
+
+ pages, err := b2Client.Scan(lang + "/")
+ status := fiber.StatusOK
+ if err != nil {
+ status = fiber.StatusPartialContent
+ pages = []*b2.BlogPage{}
+ }
+
+ type MapMarker struct {
+ Title string `json:"Title"`
+ PageLink string `json:"PageLink"`
+ Lat float64 `json:"Lat"`
+ Long float64 `json:"Long"`
+ AccuracyMeters int64 `json:"AccuracyMeters"`
+ Thumbnail string `json:"Thumbnail"`
+ }
+
+ mapMarkers := make([]*MapMarker, 0, len(pages))
+ for _, page := range pages {
+ geolocationParts := strings.Split(page.Metadata.Geolocation, " ")
+ if len(geolocationParts) < 2 {
+ continue
+ }
+
+ var x, y float64
+ var areaError int64
+ if len(geolocationParts) >= 2 {
+ x, _ = strconv.ParseFloat(geolocationParts[0], 64)
+ y, _ = strconv.ParseFloat(geolocationParts[1], 64)
+ }
+ if len(geolocationParts) >= 3 {
+ areaError, _ = strconv.ParseInt(geolocationParts[2], 10, 64)
+ }
+
+ mapMarkers = append(mapMarkers, &MapMarker{
+ Title: page.Metadata.Title,
+ PageLink: fmt.Sprintf("/%s/blog/%s", lang, page.FileName),
+ Lat: x,
+ Long: y,
+ AccuracyMeters: areaError,
+ Thumbnail: page.Metadata.Thumbnail,
+ })
+ }
+
+ content, err := tm.Render("global-map", fiber.Map{
+ "Lang": lang,
+ "L": localization[lang],
+ "MapLocationLat": 45.4507,
+ "MapLocationLong": 68.8319,
+ "MapMarkers": mapMarkers,
+ })
+ if err != nil {
+ slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/map"), slog.String("error", err.Error()))
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
+ }
+
+ return c.Type("html").Status(status).Send(content)
+ })
+
app.Get("/:lang/blog", func(c *fiber.Ctx) error {
lang := c.Params("lang")
if !slices.Contains(availableLanguages, lang) {
diff --git a/static/input.css b/static/input.css
index 00ac12f..0d6e786 100644
--- a/static/input.css
+++ b/static/input.css
@@ -1,17 +1,6 @@
@import "tailwindcss";
@theme {
- --color-olive-dark: #4b513a;
- --color-olive-medium: #8a9d8a;
- --color-olive-light: #a5b8a5;
- --color-cream-dark: #ffffdd;
- --color-cream-light: #fffff0;
- --color-lettuce-dark: #384a0c;
- --color-lettuce-medium: #9bb665;
- --color-lettuce-light: #bbd095;
- --color-sunny-dark: #f5eee2;
- --color-sunny-light: #fff93f;
-
--font-spectral: Spectral, serif;
--font-patua: "Patua One", cursive;
}
@@ -109,16 +98,16 @@ a:hover {
color: var(--main-light-color) !important;
}
-#location-map {
+#map-container {
color: var(--leaflet-text-color) !important;
}
-#location-map a {
+#map-container a {
color: var(--leaflet-link-color) !important;
text-decoration: underline;
}
-#location-map a:hover {
+#map-container a:hover {
color: var(--leaflet-link-hover-color) !important;
text-decoration: underline;
}
diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html
index d2e74fa..5918956 100644
--- a/views/layouts/general-page.html
+++ b/views/layouts/general-page.html
@@ -44,7 +44,7 @@
<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="location.href='/{{ .Lang }}/map';" class="fas fa-map w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
<i onclick="toggleThemeWheel();" id="theme-button" class="fas fa-swatchbook w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
</div>
<div class="theme-wheel" id="theme-wheel">
diff --git a/views/pages/blog-page.html b/views/pages/blog-page.html
index 330a1ac..755744d 100644
--- a/views/pages/blog-page.html
+++ b/views/pages/blog-page.html
@@ -19,7 +19,7 @@
{{ .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>
+ <div id="map-container" class="relative p-[0.8vmin] flex-none ml-auto mr-auto w-[60%] ar-lt-0.8:w-[80%] h-[40vh] ar-lt-0.8:h-[30vh]"></div>
<hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto">
{{- end }}
</div>
@@ -33,8 +33,10 @@
<script src="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.js"></script>
<script>
+ var map;
+
function initLocationMap(id, x, y, relativeErrorMeters = 0) {
- var map = L.map(id).setView([x, y], 8);
+ map = L.map(id).setView([x, y], 8);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
@@ -58,7 +60,7 @@
}
document.addEventListener('DOMContentLoaded', () => {initLocationMap(
- 'location-map',
+ 'map-container',
{{ .MapLocationX }},
{{ .MapLocationY }},
{{ .MapLocationAreaMeters }}
@@ -66,7 +68,7 @@
window.addEventListener('resize', function() {
setTimeout(() => {
- if (window.location-map) window.location-map.invalidateSize();
+ map.invalidateSize();
}, 100);
});
</script>
diff --git a/views/pages/global-map.html b/views/pages/global-map.html
new file mode 100644
index 0000000..cbb6c7d
--- /dev/null
+++ b/views/pages/global-map.html
@@ -0,0 +1,241 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>SAYA TODAY // {{ .L.GlobalMap.Header }}</title>
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,200;1,300;1,400;1,500;1,600;1,700;1,800&family=Patua+One&display=swap" rel="stylesheet">
+ {{ block "header" . }}{{ end }}
+ <link href="/output.css" rel="stylesheet">
+ <link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.css">
+</head>
+
+<body data-theme="ram" class="font-spectral text-main-dark overflow-hidden bg-interlocked-hexagons h-screen flex flex-row ar-lt-0.8:flex-col">
+
+ <script>
+ function switchTheme(theme) {
+ if (theme == "" || typeof theme == "undefined") {
+ const currentTheme = document.body.getAttribute('data-theme');
+ switch (currentTheme) {
+ case "olive": theme = "lettuce"; break;
+ case "lettuce": theme = "night"; break;
+ case "night": theme = "ram"; break;
+ case "ram": theme = "olive"; break;
+ }
+ }
+ localStorage.setItem('theme', theme);
+ document.body.setAttribute('data-theme', theme);
+ }
+
+ const savedTheme = localStorage.getItem('theme') || 'ram';
+ switchTheme(savedTheme);
+ </script>
+
+ <script>
+ const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ </script>
+
+ <div id="sidebar" class="bg-sidebar flex sticky flex-col ar-gt-0.8:items-center shadow-2xl z-20 w-[5vw] ar-lt-0.8:w-screen h-screen ar-lt-0.8:h-[8vh]">
+ <div class="logo-custom ar-lt-0.8:hidden text-sidebar font-patua font-extrabold text-center relative z-20">
+ saya.today
+ </div>
+ <div class="text-[3vw] ar-lt-0.8:text-[5vh] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1vw] flex flex-col ar-lt-0.8:flex-row gap-0 relative z-20">
+ <i onclick="location.href='/';" class="fas fa-home w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
+ <i onclick="location.href='/{{ .Lang }}/blog';" class="fas fa-search w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
+ <i onclick="location.href='/{{ .Lang }}/map';" class="fas fa-map w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
+ <i onclick="toggleThemeWheel();" id="theme-button" class="fas fa-swatchbook w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i>
+ </div>
+ <div class="theme-wheel" id="theme-wheel">
+ <div class="theme-icon" data-theme="olive"></div>
+ <div class="theme-icon" data-theme="lettuce"></div>
+ <div class="theme-icon" data-theme="night"></div>
+ <div class="theme-icon" data-theme="ram"></div>
+ </div>
+ </div>
+
+ <div id="map-container" class="bg-background-dark flex z-10 w-[95vw] ar-lt-0.8:w-screen h-screen ar-lt-0.8:h-[92vh]"></div>
+
+ <script src="https://f003.backblazeb2.com/file/sayana-static/libs/htmx/2.0.6/htmx.min.js"></script>
+ <script src="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.js"></script>
+
+ <script>
+ const themeWheel = document.getElementById('theme-wheel');
+ const themeButton = document.getElementById('theme-button');
+ const themeIcons = document.querySelectorAll('.theme-icon');
+
+ themeIcons.forEach(icon => {
+ if (icon.getAttribute('data-theme') == savedTheme) {
+ icon.classList.add('active');
+ }
+ });
+
+ function calculateVmax(percent) {
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+
+ const largerDimension = Math.max(viewportWidth, viewportHeight);
+
+ const vmaxValue = (largerDimension / 100) * percent;
+
+ return vmaxValue;
+ }
+
+ function positionThemeIcons() {
+ const icons = themeWheel.querySelectorAll('.theme-icon');
+ const iconCount = icons.length;
+ const vmax = calculateVmax(1);
+ const wheelRadius = 7.5*vmax; // Radius of the wheel
+ const iconRadius = 4*vmax; // Distance from center to place icons
+ const iconSize = 3*vmax; // Half of icon size for centering
+
+ icons.forEach((icon, index) => {
+ const angle = (index * 360 / iconCount) - 90;
+ const angleRad = (angle * Math.PI) / 180;
+
+ const x = Math.cos(angleRad) * iconRadius + wheelRadius - iconSize;
+ const y = Math.sin(angleRad) * iconRadius + wheelRadius - iconSize;
+
+ icon.style.left = x + 'px';
+ icon.style.top = y + 'px';
+ });
+ }
+
+ function positionThemeWheel() {
+ const buttonRect = themeButton.getBoundingClientRect();
+ const buttonCenterX = buttonRect.left + buttonRect.width / 2;
+ const buttonCenterY = buttonRect.top + buttonRect.height / 2;
+
+ const wheelRadius = calculateVmax(7.5);
+ const margin = calculateVmax(1);
+
+ let wheelX = buttonCenterX - wheelRadius;
+ let wheelY = buttonCenterY - wheelRadius;
+
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+
+ if (wheelX < margin) {
+ wheelX = margin;
+ } else if (wheelX + (wheelRadius * 2) > viewportWidth - margin) {
+ wheelX = viewportWidth - (wheelRadius * 2) - margin;
+ }
+
+ if (wheelY < margin) {
+ wheelY = margin;
+ } else if (wheelY + (wheelRadius * 2) > viewportHeight - margin) {
+ wheelY = viewportHeight - (wheelRadius * 2) - margin;
+ }
+
+ themeWheel.style.left = wheelX + 'px';
+ themeWheel.style.top = wheelY + 'px';
+ }
+
+ function hideThemeWheel() {
+ themeWheel.classList.remove('active');
+ }
+
+ function showThemeWheel() {
+ positionThemeWheel();
+ positionThemeIcons();
+ themeWheel.classList.add('active');
+ }
+
+ function toggleThemeWheel() {
+ if (themeWheel.classList.contains('active')) {
+ hideThemeWheel();
+ } else {
+ showThemeWheel();
+ }
+ }
+
+ themeIcons.forEach(icon => {
+ icon.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const theme = icon.getAttribute('data-theme');
+ switchTheme(theme);
+ themeIcons.forEach(icon => {
+ icon.classList.remove('active');
+ })
+ icon.classList.add('active');
+ });
+ });
+
+ themeWheel.addEventListener('mouseleave', hideThemeWheel);
+
+ document.addEventListener('click', (e) => {
+ if (!themeWheel.contains(e.target) && e.target !== themeButton) {
+ hideThemeWheel();
+ }
+ });
+ </script>
+
+ <script>
+ var map;
+
+ function initLocationMap() {
+ map = L.map('map-container').setView([{{ .MapLocationLat }}, {{ .MapLocationLong }}], 4);
+
+ L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ maxZoom: 18,
+ attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
+ }).addTo(map);
+ }
+
+ function stringToColor(str) {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
+ }
+
+ let color = '#';
+ for (let i = 0; i < 3; i++) {
+ const value = (hash >> (i * 8)) & 0xFF;
+ color += ('00' + value.toString(16)).substr(-2);
+ }
+
+ return color;
+ }
+
+ function mapAddMarker(x, y, relativeErrorMeters = 0, title = "", link = "", thumbnail = "") {
+ const titleColor = stringToColor(title);
+
+ if (relativeErrorMeters != 0) {
+ var circle = L.circle([x, y], {
+ color: titleColor,
+ fillColor: titleColor,
+ fillOpacity: 0.15,
+ radius: relativeErrorMeters
+ }).addTo(map);
+ }
+
+ var marker = L.marker([x, y], {
+ title: title,
+ }).addTo(map)
+ .bindPopup(`
+ <div class="flex flex-row justify-center justify-items-center">
+ <img onclick="location.href='${link}';" class="cursor-pointer object-cover rounded-[10%] select-none w-[3vmax] h-[3vmax] mr-[0.5vmax]" src="https://f003.backblazeb2.com/file/sayana-photos/thumbnails/${thumbnail}.webp">
+ <span><b><a href="${link}">${title}</a></b><br><a href="https://www.openstreetmap.org/#map=13/${x}/${y}">${x}, ${y}</a></span>
+ </div>`);
+ }
+
+ document.addEventListener('DOMContentLoaded', () => {
+ initLocationMap();
+ {{- range .MapMarkers }}
+ mapAddMarker({{ .Lat }}, {{ .Long }}, {{ .AccuracyMeters }}, {{ .Title }}, {{ .PageLink }}, {{ .Thumbnail }});
+ {{- end }}
+ });
+
+ window.addEventListener('resize', function() {
+ setTimeout(() => {
+ map.invalidateSize();
+ }, 100);
+ });
+ </script>
+
+ {{ block "footer" . }}{{ end }}
+
+</body>
+</html>