Diffstat (limited to 'views')
| -rw-r--r-- | views/index.html | 145 | ||||
| -rw-r--r-- | views/layouts/general-page.html | 321 | ||||
| -rw-r--r-- | views/pages/blog-catalogue.html | 161 | ||||
| -rw-r--r-- | views/pages/blog-page.html | 92 | ||||
| -rw-r--r-- | views/pages/global-map.html | 267 | ||||
| -rw-r--r-- | views/partials/blog-page-like-button.html | 7 | ||||
| -rw-r--r-- | views/partials/catalogue-blog-card-tags.html | 1 | ||||
| -rw-r--r-- | views/partials/catalogue-blog-cards.html | 21 |
8 files changed, 1015 insertions, 0 deletions
diff --git a/views/index.html b/views/index.html new file mode 100644 index 0000000..2b8c937 --- /dev/null +++ b/views/index.html @@ -0,0 +1,145 @@ +<!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: 40vh; + height: 30vh; + } + + .menu-inner-body { + width: 85%; + } + + .flex-adaptive { + flex-direction: row; + } + + .language-icon { + max-width: calc(80% / {{ .AvailableLanguages | len }}); + max-height: 80%; + } + + @media (max-aspect-ratio: 0.8/1) { + .menu-outer-body { + border-radius: 5%; + } + + .menu-outer-body { + width: min(40vh, 100vw); + height: 40vh; + border-radius: 0; + } + + .language-icon { + max-width: 40%; + max-height: calc(80% / {{ .AvailableLanguages | len }}); + } + + .menu-inner-body { + width: 92.5%; + } + + .flex-adaptive { + flex-direction: column; + } + } + </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-[0.8vmin]">saya.today</p> + <div class="menu-inner-body grow bg-background-light flex flex-adaptive mx-auto opacity-100"> + {{- range .AvailableLanguages }} + <img onclick="location.href='/{{ .Name }}/blog';" class="object-scale-down cursor-pointer flex-1 rounded-[20%] language-icon mx-auto my-auto relative" src="{{ .Flag }}" alt="{{ .Alt }}"> + {{- end }} + </div> + <p class="font-spectral mx-auto text-[1.5vmax] text-secondary mt-[0.8vmin] mb-[0.8vmin]">Choose your language</p> + </div> + + <script> + 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/general-page.html b/views/layouts/general-page.html new file mode 100644 index 0000000..a7cf66e --- /dev/null +++ b/views/layouts/general-page.html @@ -0,0 +1,321 @@ +<!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 "top-embeds" . }}{{ end }} + <link href="/output.css" rel="stylesheet"> +</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 sfc32(a, b, c, d) { + return function() { + a |= 0; b |= 0; c |= 0; d |= 0; + let t = (a + b | 0) + d | 0; + d = d + 1 | 0; + a = b ^ b >>> 9; + b = c + (c << 3) | 0; + c = (c << 21 | c >>> 11); + c = c + t | 0; + return (t >>> 0) / 4294967296; + } + } + + function cyrb128(str) { + let h1 = 1779033703, h2 = 3144134277, + h3 = 1013904242, h4 = 2773480762; + for (let i = 0, k; i < str.length; i++) { + k = str.charCodeAt(i); + h1 = h2 ^ Math.imul(h1 ^ k, 597399067); + h2 = h3 ^ Math.imul(h2 ^ k, 2869860233); + h3 = h4 ^ Math.imul(h3 ^ k, 951274213); + h4 = h1 ^ Math.imul(h4 ^ k, 2716044179); + } + h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067); + h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233); + h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213); + h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179); + h1 ^= (h2 ^ h3 ^ h4), h2 ^= h1, h3 ^= h1, h4 ^= h1; + return [h1>>>0, h2>>>0, h3>>>0, h4>>>0]; + } + + var seed = cyrb128("{{ .Title }}"); + var rand = sfc32(seed[0], seed[1], seed[2], seed[3]); + + function calculateVmin(percent) { + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + const smallerDimension = Math.min(viewportWidth, viewportHeight); + + const vminValue = (smallerDimension / 100) * percent; + + return vminValue; + } + + function switchThemeDecor(theme) { + document.querySelectorAll('.theme-decor').forEach((e) => { + e.remove(); + }); + + if (theme == "" || typeof theme == "undefined") { + theme = document.body.getAttribute('data-theme'); + } + + vmin = calculateVmin(1); + var clWidth = document.documentElement.clientWidth; + var clHeight = document.documentElement.clientHeight; + + switch (theme) { + case "olive": break; + case "lettuce": break; + case "night": + for (i = 0; i < 25; i++) { + let star = document.createElement("div"); + star.append("*"); + star.classList.add("night-star", "theme-decor", "z-0", "absolute"); + const x = Math.floor(rand() * clWidth); + const y = Math.floor(rand() * clHeight * 0.8); + const rot = rand() * 90; + const animDuration = (rand() * 10) + 3; + const sz = Math.floor(rand() * 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.body.append(star); + } + break; + case "ram": + const frameCenters = new Array(4); + for (i = 1; i <= 4; i++) { + let frame = document.createElement("img"); + frame.src = `https://f003.backblazeb2.com/file/sayana-static/themes/ram/ram-frame${i}.webp`; + frame.classList.add("ram-frame", "theme-decor", "z-0", "absolute"); + let x, y; + outerLoop: + while (true) { + x = Math.floor(rand() * clWidth - 20*vmin); + y = Math.floor(rand() * clHeight - 20*vmin); + for (j = 0; j < i-1; j++) { + const xd = frameCenters[j].x - x; + const yd = frameCenters[j].y - y; + if (Math.sqrt(xd*xd + yd*yd) < 30*vmin) { + continue outerLoop; + } + } + break; + } + frameCenters[i-1] = {x: x, y: y}; + const rot = rand() * 90 - 45; + frame.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rot}deg)`; + document.body.append(frame); + } + break; + } + } + + 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); + switchThemeDecor(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-[90vw] ar-lt-0.4:w-[100vw] h-screen ar-lt-0.8:h-[8vh] ar-lt-0.8:ml-[5vw] ar-lt-0.4:ml-0"> + <div class="logo-custom ar-lt-0.8:hidden text-sidebar font-patua font-extrabold text-center relative z-20"> + saya.today + </div> + <div class="text-[3vw] ar-lt-0.8:text-[5vh] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1vw] flex flex-col ar-lt-0.8:flex-row gap-0 relative z-20"> + <i onclick="location.href='/';" class="fas fa-home w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> + <i onclick="location.href='/{{ .Lang }}/blog';" class="fas fa-search w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> + <i 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 class="bg-background-dark flex z-10 ar-lt-0.8:w-[90vw] ar-lt-0.4:w-[100vw] h-screen ar-lt-0.8:ml-[5vw] ar-lt-0.4:ml-0"> + <div class="pl-[0.5vw] bg-background-dark border-r-[0.4vmin] border-dashed border-main-dark"></div> + + <div class="ar-gt-0.8:min-w-fit ar-gt-0.8:max-w-[100vh] flex flex-col bg-background-dark relative pt-[1.6vmin] ml-[1.6vmin] pr-[1.6vmin]"> + <div {{ 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> + <p id="published-date" class="grow text-[1.5vmax] font-spectral text-secondary text-right mt-auto">{{ .PublishedDate }}</p> + </div> + {{ block "header" . }}{{ end }} + <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]"> + {{ block "footer" . }}{{ end }} + <p class="grow content-center text-[1vmax]/[1] 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 class="max-w-[1vmax] max-h-[1vmax] self-center" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="CC"> + <img class="max-w-[1vmax] max-h-[1vmax] self-center" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="BY"> + <img class="max-w-[1vmax] max-h-[1vmax] self-center" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="SA"> + </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> + + <script src="https://f003.backblazeb2.com/file/sayana-static/libs/htmx/2.0.6/htmx.min.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> + let decorResizeTimeout; + window.addEventListener('resize', function() { + clearTimeout(decorResizeTimeout); + decorResizeTimeout = setTimeout(switchThemeDecor, 300); + }); + </script> + + {{ block "bottom-embeds" . }}{{ end }} + +</body> +</html> diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html new file mode 100644 index 0000000..1d0a2de --- /dev/null +++ b/views/pages/blog-catalogue.html @@ -0,0 +1,161 @@ +{{ define "body" }} +<div class="flex flex-row ar-lt-0.8:flex-col max-h-[calc(100%-7vmax)] ar-lt-0.8:max-h-[calc(100%-25vh)] flex-1 text-[1vmax]"> + <form hx-get="/api/v1/blog-search" hx-vals='{"lang": "{{ .Lang }}"}' hx-target=".blog-cards" hx-swap="innerHTML" + class="tags-list ar-gt-0.8:min-w-[10vh] ar-gt-0.8:max-w-[20vh] ar-gt-0.8:w-fit ar-lt-0.8:w-[100%] ar-lt-0.8:max-h-[25%] flex shrink-0 flex-col ar-gt-0.8:mr-[1.2vmin]"> + <div class="flex flex-col overflow-y-auto"> + <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"> + <div class="m-[0.4vmin]"> + <label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();"> {{ .L.BlogSearch.ChooseAllTags }}</label> + </div> + {{- range .Tags }} + <div class="m-[0.4vmin]"> + <label><input type="checkbox" id="tag{{ .Name }}Checkbox" name="tags[]" value="{{ .Name }}" onclick="contextAll();" {{ if contains $.QueryTags .Name }}checked{{ end }}> {{ .Name }} <span class="text-secondary italic">{{ .Count }}</span></label> + </div> + {{- end }} + </div> + </fieldset> + <fieldset class="mt-[0.8vmin]"> + <legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.OrderByHeader }}</legend> + <div class="flex flex-col ar-lt-0.8:grid grid-cols-3 grid-rows-2 grid-flow-col"> + <div class="m-[0.4vmin]"> + <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 class="m-[0.4vmin]"> + <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 class="m-[0.4vmin]"> + <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 class="m-[0.4vmin]"> + <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 class="m-[0.4vmin]"> + <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 class="m-[0.4vmin]"> + <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> + <hr class="border-t-[0.2vmax] border-dotted border-main-dark my-[0.4vmin]"> + <div class="flex flex-row grow 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='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> +</div> +{{ end }} + +{{ define "bottom-embeds" }} +<script> + function selectAll() { + const tagsContainers = document.querySelectorAll('form.tags-list'); + tagsContainers.forEach(tagsContainer => { + const selectAllCheckbox = tagsContainer.querySelector('#tagsAllCheckbox'); + const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]:not([id="tagsAllCheckbox"])'); + checkboxes.forEach(checkbox => { + checkbox.checked = selectAllCheckbox.checked; + }); + }); + } + + function contextAll() { + const tagsContainers = document.querySelectorAll('form.tags-list'); + tagsContainers.forEach(tagsContainer => { + const selectAllCheckbox = tagsContainer.querySelector('#tagsAllCheckbox'); + const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]:not([id="tagsAllCheckbox"])'); + var areAllChecked = true; + checkboxes.forEach(checkbox => { + if (!checkbox.checked) areAllChecked = false; + }); + selectAllCheckbox.checked = areAllChecked; + }); + } + + function defaultSearchParams() { + const tagsContainers = document.querySelectorAll('form.tags-list'); + tagsContainers.forEach(tagsContainer => { + const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]'); + checkboxes.forEach(checkbox => { + checkbox.checked = true; + }); + const sortRadio = tagsContainer.querySelector('#sortPublicationDateDesc'); + sortRadio.checked = true; + }); + } + + function cleanHrefParams() { + const url = new URL(window.location.href); + url.search = ''; + window.history.pushState({}, '', url.toString()); + } + + function setHrefParams() { + const url = new URL(window.location.href); + url.search = ''; + + const tagsContainers = document.querySelectorAll('form.tags-list'); + tagsContainers.forEach(tagsContainer => { + const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]'); + checkboxes.forEach(checkbox => { + if (checkbox.checked) + url.searchParams.append('tags[]', checkbox.value); + }); + const sortRadio = tagsContainer.querySelector('input[type="radio"]:checked'); + url.searchParams.set('sort', sortRadio.value); + }); + + window.history.pushState({}, '', url.toString()); + } + + 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> + +<script> + const tagsContainers = document.querySelectorAll('form.tags-list'); + tagsContainers.forEach(tagsContainer => { + const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]'); + anyChecked = false; + checkboxes.forEach(checkbox => { + anyChecked = checkbox.checked ? true : anyChecked; + }); + if (!anyChecked) { + const selectAllCheckbox = tagsContainer.querySelector('#tagsAllCheckbox'); + selectAllCheckbox.checked = true; + selectAll(); + } + }); +</script> + +{{ end }} diff --git a/views/pages/blog-page.html b/views/pages/blog-page.html new file mode 100644 index 0000000..65ee31d --- /dev/null +++ b/views/pages/blog-page.html @@ -0,0 +1,92 @@ +{{ define "top-embeds" }} +<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> + +<script> + const galleryMap = new Map(); +</script> +{{ end }} + +{{ define "body" }} +<div class="bg-background-light flex flex-col grow overflow-y-auto inset-shadow p-[2.4vmin] max-h-[calc(100%-7vmax)] ar-lt-0.8:max-h-[calc(100%-25vh)]"> + {{ .ParsedMarkdown }} +{{- if .MapLocationX }} + <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto"> + <div id="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> +{{ end }} + +{{ define "footer" }} +<div hx-get="/api/v1/like" hx-target="this" hx-swap="outerHTML" hx-trigger="load"></div> +{{ end }} + +{{ define "bottom-embeds" }} +<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> + var map; + + function initLocationMap(id, x, y, relativeErrorMeters = 0) { + map = L.map(id).setView([x, y], 8); + + L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 18, + 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], { + 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( + 'map-container', + {{ .MapLocationX }}, + {{ .MapLocationY }}, + {{ .MapLocationAreaMeters }} + )}); + + let galleryResizeTimeout; + window.addEventListener('resize', function() { + clearTimeout(galleryResizeTimeout); + galleryResizeTimeout = setTimeout(() => { + map.invalidateSize(); + var oldGalleryMap = new Map(galleryMap); + oldGalleryMap.forEach((createFunc, g, map) => { + galleryMap.delete(g); + g.destroy(); + createFunc(); + }); + }, 300); + }); +</script> +{{- end }} +{{ end }}
\ No newline at end of file diff --git a/views/pages/global-map.html b/views/pages/global-map.html new file mode 100644 index 0000000..569166f --- /dev/null +++ b/views/pages/global-map.html @@ -0,0 +1,267 @@ +<!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"> + <link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet.markercluster/1.4.1/MarkerCluster.css"> + <link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.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 src="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.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; + var markers = L.markerClusterGroup(); + + function createCustomIcon(color, borderColor = "var(--main-light-color)") { + const svgIcon = ` + <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> + <path d="M12 1C7.5 1 4 4.5 4 9c0 6.5 8 14 8 14s8-7.5 8-14c0-4.5-3.5-8-8-8zm0 11c-1.5 0-3-1.5-3-3s1.5-3 3-3 3 1.5 3 3-1.5 3-3 3z" + fill="${color}" + stroke="${borderColor}" + stroke-width="1" + stroke-linejoin="round"/> + </svg> + `; + + return L.divIcon({ + html: svgIcon, + className: 'custom-svg-marker', + iconSize: [32, 32], + iconAnchor: [16, 32], + popupAnchor: [0, -32] + }); + } + + function initLocationMap() { + map = L.map('map-container').setView([{{ .MapLocationLat }}, {{ .MapLocationLong }}], 4); + + L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 18, + attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' + }).addTo(map); + + map.addLayer(markers); + } + + 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], { + icon: createCustomIcon(titleColor), + title: title, + }).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/webp-320p/${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>`); + + markers.addLayer(marker); + } + + document.addEventListener('DOMContentLoaded', () => { + initLocationMap(); + {{- range .MapMarkers }} + mapAddMarker({{ .Lat }}, {{ .Long }}, {{ .AccuracyMeters }}, {{ .Title }}, {{ .PageLink }}, {{ .Thumbnail }}); + {{- end }} + }); + + window.addEventListener('resize', function() { + setTimeout(() => { + map.invalidateSize(); + }, 100); + }); + </script> + +</body> +</html> diff --git a/views/partials/blog-page-like-button.html b/views/partials/blog-page-like-button.html new file mode 100644 index 0000000..afc3027 --- /dev/null +++ b/views/partials/blog-page-like-button.html @@ -0,0 +1,7 @@ +<button + hx-put="/api/v1/like" hx-vals='{"like": {{ not .Liked }}}' hx-target="this" hx-swap="outerHTML" hx-trigger="click" + class="text-[0.8vmax]/[0.9] font-spectral text-left px-[0.4vmax] py-[0.2vmax] {{ if .Liked }}bg-main-light hover:bg-main-medium text-background-dark border-inset{{ else }}bg-main-dark hover:bg-main-medium text-background-light border-outset{{ end }} cursor-pointer border-[0.2vmax] border-background-dark"> + <i class="fas fa-thumbs-up w-[0.8vmax] h-[0.8vmax] mr-[0.4vmax]"></i> + <span>{{ .L.LikeButton }}</span> + <span class="italic ml-[0.2vmax] text-background-light">({{ .LikedCount }})</span> +</button>
\ No newline at end of file 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 new file mode 100644 index 0000000..35c7ad7 --- /dev/null +++ b/views/partials/catalogue-blog-cards.html @@ -0,0 +1,21 @@ +{{- range .BlogPages }} +<hr class="first-of-type:hidden border-t-[0.3vh] my-[0.8vmin] border-dotted border-main-dark w-[80%] mx-auto"> +<div class="m-[0.4vmin] flex flex-row justify-center justify-items-center"> + <img onclick="location.href='{{ .ArticleLink }}';" class="cursor-pointer object-cover rounded-[10%] select-none w-[10vmin] h-[10vmin] aspect-square my-[0.4vmin] mr-[0.8vmin]" src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/{{ .Thumbnail }}.webp"> + <div class="grow my-[0.4vmin] mx-[0.8vmin] flex flex-col justify-center"> + <div class="flex flex-row gap-[5%] max-w-[100%]"> + <a href="{{ .ArticleLink }}" class="flex-9/12 grow-0 block text-[1vmax] cursor-pointer"> + <span class="font-extrabold">{{ .Title }}</span> + <span class="font-extrabold select-none text-secondary">//</span> + <span class="italic text-secondary">{{ .ActionDate }}</span> + <span class="font-extrabold select-none text-secondary">//</span> + <i class="fas fa-thumbs-up w-[1vmax] h-[1vmax]"></i> + <span class="italic text-secondary">{{ .LikeCount }}</span> + </a> + <p class="flex-3/12 grow-0 text-[1vmax] italic text-secondary text-right">{{ .PublishedTime }}</p> + </div> + <p class="text-[1vmax] text-main-dark">{{ .ShortDescription }}</p> + <p class="text-[1vmax] text-secondary">{{ $.L.TagsLabel }} {{ template "catalogue-blog-card-tags.html" . }}</p> + </div> +</div> +{{- end }}
\ No newline at end of file |