summaryrefslogtreecommitdiff
path: root/views
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
Diffstat (limited to 'views')
-rw-r--r--views/index.html145
-rw-r--r--views/layouts/general-mail.html105
-rw-r--r--views/layouts/general-page.html408
-rw-r--r--views/messages/new-post.html19
-rw-r--r--views/messages/verify-email.html10
-rw-r--r--views/pages/blog-catalogue.html152
-rw-r--r--views/pages/blog-page.html107
-rw-r--r--views/pages/global-map.html298
-rw-r--r--views/pages/home-page.html122
-rw-r--r--views/pages/language-pick.html16
-rw-r--r--views/pages/unsubscribe-page.html30
-rw-r--r--views/pages/user-page.html205
-rw-r--r--views/partials/blog-page-like-button.html6
-rw-r--r--views/partials/catalogue-blog-card-tags.html1
-rw-r--r--views/partials/catalogue-blog-cards.html33
-rw-r--r--views/partials/general-page-body.html1
-rw-r--r--views/partials/general-page-bottom-embeds.html1
-rw-r--r--views/partials/general-page-footer.html7
-rw-r--r--views/partials/general-page-header.html7
-rw-r--r--views/partials/general-page-top-embeds.html1
-rw-r--r--views/partials/personal-page-status.html12
21 files changed, 1368 insertions, 318 deletions
diff --git a/views/index.html b/views/index.html
deleted file mode 100644
index 1c57bd1..0000000
--- a/views/index.html
+++ /dev/null
@@ -1,145 +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: 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='/blog/{{ .Name }}';" 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-mail.html b/views/layouts/general-mail.html
new file mode 100644
index 0000000..8f42c7a
--- /dev/null
+++ b/views/layouts/general-mail.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<html lang="en">
+ <meta charset="UTF-8">
+ <head>
+ <style>
+@font-face {
+ font-family: 'Philosopher';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/philosopher/v21/vEFV2_5QCwIS4_Dhez5jcWBuT0020Nqf.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+@font-face {
+ font-family: 'Philosopher';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(https://fonts.gstatic.com/s/philosopher/v21/vEFV2_5QCwIS4_Dhez5jcWBqT0020NqfZ7c.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+
+html {
+ font-family: 'Philosopher', sans-serif;
+ --noise-image: url(data:image/png;base64,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);
+}
+
+body {
+ background-color: #e3e6ff;
+}
+
+a {
+ text-decoration: underline;
+ text-shadow: none !important;
+}
+
+.header {
+ width: 100%;
+ padding: 8px 8px 8px 32px;
+ font-size: 48px;
+ font-family: 'Philosopher';
+ color: white;
+ text-shadow: -2px 0 #343b58, 0 2px #343b58, 2px 0 #343b58, 0 -2px #343b58;
+ background: url("data:image/svg+xml,<svg width='160' height='160' xmlns='http://www.w3.org/2000/svg'><defs><pattern id='a' patternUnits='userSpaceOnUse' width='40' height='80' patternTransform='scale(2) rotate(0)'><rect x='0' y='0' width='100%' height='100%' fill='%23e3e6ffff'/><path d='M-10 7.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%238c4351ff' fill='none'/><path d='M-10 27.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%23385f0dff' fill='none'/><path d='M-10 47.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%23006c86ff' fill='none'/><path d='M-10 67.5l20 5 20-5 20 5' stroke-linejoin='round' stroke-linecap='round' stroke-width='4' stroke='%235a3e8eff' fill='none'/></pattern></defs><rect width='800%' height='800%' transform='translate(0,0)' fill='url(%23a)'/></svg>");
+ background-color: #e3e6ff;
+ background-size: contain;
+ background-repeat: repeat;
+}
+
+.content {
+ margin: 0 32px 0 32px;
+ padding: 16px 16px 32px 16px;
+ font-size: 24px;
+ text-shadow: -1px 0 #343b58, 0 1px #343b58, 1px 0 #343b58, 0 -1px #343b58;
+ color: white;
+ background-color: #c3caff;
+ background-image: var(--noise-image);
+}
+
+.footer {
+ margin: 0;
+ padding: 1px 8px 1px 8px;
+ font-size: 16px;
+ color: white;
+}
+
+.darkened {
+ background-color: #9aa5f4;
+ background-image: var(--noise-image);
+}
+
+.outlier {
+ padding-inline: 4px;
+ background-color: #6c6e75;
+ background-image: var(--noise-image);
+}
+
+table, th, td {
+ border: 1px solid black;
+}
+
+td {
+ padding: 5px;
+}
+
+img {
+ width: 200px;
+}
+ </style>
+ </head>
+
+ <body>
+ <div id="header" class="header">
+ saya.today
+ </div>
+
+ <div id="content" class="content" style="background-color: #e3e6ff !important;">
+ {{ block "body" . }}{{ end }}
+ </div>
+
+ <div id="footer" class="footer darkened">
+ {{ block "footer" . }}{{ end }}
+ </div>
+ </body>
+</html>
diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html
index d33aa76..62360ad 100644
--- a/views/layouts/general-page.html
+++ b/views/layouts/general-page.html
@@ -3,18 +3,143 @@
<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 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/glightbox/3.3.0/css/glightbox.min.css">
<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-andika text-main-hard overflow-hidden bg-interlocked-hexagons h-dvh flex flex-row">
<script>
+ let map = null;
+ </script>
+
+ <script>
+ function changeUrl(path, toAppend = false) {
+ const urlParams = new URLSearchParams(window.location.search);
+ const newPathname = location.pathname.endsWith('/') ? location.pathname.slice(0, -1) : location.pathname;
+ const newPath = toAppend ? `${newPathname}${path}` : `${path}`;
+
+ history.pushState({}, '', `${newPath}${urlParams.entries.length == 0 ? '' : '?' + urlParams.toString()}`);
+ window.dispatchEvent(new Event('popstate'));
+ return false;
+ };
+
+ function convertRemToPixels(rem) {
+ return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
+ }
+ </script>
+
+ <script src="https://f003.backblazeb2.com/file/sayana-static/libs/glightbox/3.3.0/js/glightbox.min.js"></script>
+
+ <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');
@@ -22,61 +147,264 @@
case "olive": theme = "lettuce"; break;
case "lettuce": theme = "night"; break;
case "night": theme = "ram"; break;
- case "ram": theme = "olive"; break;
+ case "ram": theme = "lilac"; break;
+ case "lilac": theme = "olive"; break;
}
}
localStorage.setItem('theme', theme);
document.body.setAttribute('data-theme', theme);
+ switchThemeDecor(theme);
}
- const savedTheme = localStorage.getItem('theme') || 'night';
+ const savedTheme = localStorage.getItem('theme') || 'lilac';
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 id="general-page-top-embeds" class="flex"
+ hx-get="/api/v1/general-page/top-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML">
</div>
+ <div id="general-page-top-embeds-trigger" hx-get="/api/v1/general-page/top-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-top-embeds" hx-swap="innerHTML"></div>
+
+ <script>
+ const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ </script>
- <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="grow bg-split-left"></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 class="flex flex-col [@media(max-height:32rem)]:flex-row">
+ <div id="sidebar" class="bg-sidebar flex sticky flex-col z-20 w-dvw xs:w-[90dvw] sm:w-[80dvw] md:w-[75dvw] lg:w-[70dvw] xl:w-[65dvw] 2xl:w-[60dvw] [@media(max-height:32rem)]:w-20 h-20 [@media(max-height:32rem)]:h-dvh shadow-[0_0_1rem_black]">
+ <div class="text-[3rem] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1dvw] flex flex-row [@media(max-height:32rem)]:flex-col gap-0 relative z-20">
+ <a href="./" id="sidebar-back-button" class="w-20 h-20 hidden">
+ <i onclick="return changeUrl('/../', true);" class="fas fa-circle-left flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
+ </a>
+ <a href="/{{ .Lang }}" id="sidebar-home-button" class="w-20 h-20 hidden">
+ <i onclick="return changeUrl('/{{ .Lang }}');" class="fas fa-home flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
+ </a>
+ <a href="/{{ .Lang }}/blog" id="sidebar-blog-button" class="w-20 h-20 hidden">
+ <i onclick="return changeUrl('/{{ .Lang }}/blog');" class="fas fa-search flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
+ </a>
+ <a href="/{{ .Lang }}/map" id="sidebar-map-button" class="w-20 h-20 hidden">
+ <i class="fas fa-map flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
+ </a>
+ <i onclick="toggleThemeWheel();" id="sidebar-theme-button" class="fas fa-swatchbook flex w-20 h-20 content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 rounded-lg"></i>
+ <a href="/{{ .Lang }}/user" id="sidebar-user-button" class="w-20 h-20 hidden">
+ <i onclick="return changeUrl('/{{ .Lang }}/user');" class="fas fa-user flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
+ </a>
+ </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 class="theme-icon" data-theme="lilac"></div>
+ <div class="theme-pin"></div>
</div>
- <hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]">
+ <script>
+ sidebarHomeButton = document.getElementById("sidebar-home-button");
+ sidebarMapButton = document.getElementById("sidebar-map-button");
+ sidebarBlogButton = document.getElementById("sidebar-blog-button");
+ sidebarBackButton = document.getElementById("sidebar-back-button");
+ sidebarUserButton = document.getElementById("sidebar-user-button");
+
+ function updateSidebar() {
+ if (location.pathname == "" || location.pathname == "/") {
+ sidebarHomeButton.classList.add('hidden');
+ sidebarMapButton.classList.add('hidden');
+ sidebarBlogButton.classList.add('hidden');
+ sidebarBackButton.classList.add('hidden');
+ sidebarUserButton.classList.add('hidden');
+ } else {
+ sidebarHomeButton.classList.remove('hidden');
+ sidebarMapButton.classList.remove('hidden');
+ sidebarBlogButton.classList.remove('hidden');
+ sidebarBackButton.classList.remove('hidden');
+ sidebarUserButton.classList.remove('hidden');
+ lang = location.pathname.split("/")[1];
+ sidebarHomeButton.setAttribute('href', `/${lang}`);
+ sidebarHomeButton.firstElementChild.setAttribute('onclick', `return changeUrl('/${lang}', false);`);
+ sidebarMapButton.setAttribute('href', `/${lang}/map`);
+ sidebarBlogButton.setAttribute('href', `/${lang}/blog`);
+ sidebarBlogButton.firstElementChild.setAttribute('onclick', `return changeUrl('/${lang}/blog', false);`);
+ sidebarUserButton.setAttribute('href', `/${lang}/user`);
+ sidebarUserButton.firstElementChild.setAttribute('onclick', `return changeUrl('/${lang}/user', false);`);
+ }
+ }
+ window.addEventListener("popstate", updateSidebar);
+ window.addEventListener("load", updateSidebar, {once: true});
+ </script>
+ </div>
+
+ <div class="bg-background-dark @container/main flex z-10 w-dvw xs:w-[90dvw] sm:w-[80dvw] md:w-[75dvw] lg:w-[70dvw] xl:w-[65dvw] 2xl:w-[60dvw] grow-0 h-[calc(100dvh-5rem)] [@media(max-height:32rem)]:h-dvh shadow-[0_0_1rem_black]">
+ <div class="bg-background-dark border-r-2 border-dashed border-main-hard"></div>
- {{ block "body" . }}{{ end }}
+ <div class="max-h-100% flex flex-col grow bg-background-dark relative pt-4 ml-8 pr-4">
+ <div id="general-page-header" class="max-xs:hidden [@media(max-height:32rem)]:hidden flex flex-col basis-8"
+ hx-get="/api/v1/general-page/header{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML">
+ </div>
+ <div id="general-page-header-trigger" hx-get="/api/v1/general-page/header{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-header" hx-swap="innerHTML"></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">
- 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;">
+ <hr class="max-xs:hidden [@media(max-height:32rem)]:hidden border-t-4 border-dotted border-main-hard mb-2">
+
+ <div id="general-page-body" class="flex flex-col grow overflow-y-auto"
+ hx-get="/api/v1/general-page/body{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML">
+ </div>
+ <div id="general-page-body-trigger" hx-get="/api/v1/general-page/body{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-body" hx-swap="innerHTML"></div>
+
+ <div id="general-page-footer" class="flex flex-row basis-4 my-1"
+ hx-get="/api/v1/general-page/footer{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML">
+ </div>
+ <div id="general-page-footer-trigger" hx-get="/api/v1/general-page/footer{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-footer" hx-swap="innerHTML"></div>
+
+ <hr class="border-t-4 border-dotted border-main-hard mb-2">
</div>
- <hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]">
+ <div class="pr-[0.5dvw] bg-background-dark shadow-2xl border-l-2 border-dotted border-main-hard"></div>
</div>
+ </div>
+
+ <div class="grow bg-split-right"></div>
- <div class="pr-[0.5vw] bg-background-dark shadow-2xl border-l-[0.4vmin] border-dotted border-main-dark"></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/imagesloaded/5.0.0/imagesloaded.pkgd.min.js"></script>
+ <script src="https://f003.backblazeb2.com/file/sayana-static/libs/masonry-layout/4.2.2/masonry.pkgd.min.js"></script>
+
+ <div id="general-page-bottom-embeds" class="flex"
+ hx-get="/api/v1/general-page/bottom-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML">
</div>
+ <div id="general-page-bottom-embeds-trigger" hx-get="/api/v1/general-page/bottom-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-bottom-embeds" hx-swap="innerHTML"></div>
+
+ <script>
+ const themeWheel = document.getElementById('theme-wheel');
+ const themeButton = document.getElementById('sidebar-theme-button');
+ const themeIcons = document.querySelectorAll('.theme-icon');
+ const themeWheelRadius = convertRemToPixels(6);
+
+ 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 sliceAngle = 360 / themeIcons.length;
+
+ themeIcons.forEach((icon, index) => {
+ const startAngle = index * sliceAngle;
+ const endAngle = (index + 1) * sliceAngle;
+
+ const clipPath = createSliceClipPath(startAngle, endAngle, themeWheelRadius);
+ icon.style.clipPath = clipPath;
+ });
+ }
+
+ function createSliceClipPath(startAngle, endAngle, radius) {
+ const centerX = radius;
+ const centerY = radius;
+
+ let points = `${centerX}px ${centerY}px`;
+
+ const segments = Math.max(2, Math.ceil(Math.abs(endAngle - startAngle) / 5));
+ const angleStep = (endAngle - startAngle) / segments;
+
+ for (let i = 0; i <= segments; i++) {
+ const angle = ((startAngle + angleStep * i - 90) * Math.PI) / 180;
+ const x = centerX + Math.cos(angle) * radius;
+ const y = centerY + Math.sin(angle) * radius;
+ points += `, ${x}px ${y}px`;
+ }
+
+ points += `, ${centerX}px ${centerY}px`;
+
+ return `polygon(${points})`;
+ }
+
+ function positionThemeWheel() {
+ const buttonRect = themeButton.getBoundingClientRect();
+ const buttonCenterX = buttonRect.left + buttonRect.width / 2;
+ const buttonCenterY = buttonRect.top + buttonRect.height / 2;
+
+ const margin = 0;
- {{ block "footer" . }}{{ end }}
+ let wheelX = buttonCenterX - themeWheelRadius;
+ let wheelY = buttonCenterY - themeWheelRadius;
+
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+
+ if (wheelX < margin) {
+ wheelX = margin;
+ } else if (wheelX + (themeWheelRadius * 2) > viewportWidth - margin) {
+ wheelX = viewportWidth - (themeWheelRadius * 2) - margin;
+ }
+
+ if (wheelY < margin) {
+ wheelY = margin;
+ } else if (wheelY + (themeWheelRadius * 2) > viewportHeight - margin) {
+ wheelY = viewportHeight - (themeWheelRadius * 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>
</body>
</html>
diff --git a/views/messages/new-post.html b/views/messages/new-post.html
new file mode 100644
index 0000000..f1338f7
--- /dev/null
+++ b/views/messages/new-post.html
@@ -0,0 +1,19 @@
+{{ define "body" }}
+<p>{{ .L.Mail.NewPost.Intro }}</p>
+<table>
+ <tr>
+ <td rowspan="3"><a href="https://{{ .ClientHost }}/{{ .Lang }}/blog/{{ .Post.FileName }}"><img src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/{{ .Post.Metadata.Thumbnail }}.webp"></a></td>
+ <td class="darkened" style="font-size: 24px"><a style="color: #273de1 !important;" href="https://{{ .ClientHost }}/{{ .Lang }}/blog/{{ .Post.FileName }}">{{ .Post.Metadata.Title }}</a></td>
+ </tr>
+ <tr>
+ <td class="darkened" style="font-size: 16px">{{ .Post.Metadata.ShortDescription }}</td>
+ </tr>
+ <tr>
+ <td class="darkened" style="font-size: 16px">{{ .L.Mail.NewPost.CapturedOn }} {{ .Post.Metadata.ActionDate }}</td>
+ </tr>
+</table>
+{{ end }}
+
+{{ define "footer" }}
+<p>{{ .UnsubscribeFooter }}</p>
+{{ end }}
diff --git a/views/messages/verify-email.html b/views/messages/verify-email.html
new file mode 100644
index 0000000..dd6ef33
--- /dev/null
+++ b/views/messages/verify-email.html
@@ -0,0 +1,10 @@
+{{ define "body" }}
+<p>{{ .L.Mail.VerifyEmail.Welcome }}</p>
+<p>{{ .L.Mail.VerifyEmail.Intro }}</p>
+<ol>
+ <li>{{ .L.Mail.VerifyEmail.GotoLink }} <a href="https://{{ .ClientHost }}/{{ .Lang }}/user?email_code={{ .VerificationCode }}" class="outlier">&gt;&gt;&gt;</a></li>
+ <li>{{ .L.Mail.VerifyEmail.InputCode }} <span class="outlier">{{ .VerificationCode }}</span><span></li>
+</ol>
+<p>{{ .L.Mail.VerifyEmail.IfRandom }}</p>
+<a href="https://{{ .ClientHost }}" class="outlier">{{ .ClientHost }}</a>
+{{ end }} \ No newline at end of file
diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html
index 6fe1c82..d209205 100644
--- a/views/pages/blog-catalogue.html
+++ b/views/pages/blog-catalogue.html
@@ -1,77 +1,62 @@
{{ define "body" }}
-<div class="flex flex-row ar-lt-0.8:flex-col flex-1 text-[1vmax]">
+<div class="flex flex-row ar-lt-0.8:flex-col text-base h-full">
<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>
+ class="tags-list ar-gt-0.8:min-w-[10dvh] ar-gt-0.8:max-w-[20dvh] ar-gt-0.8:w-fit ar-lt-0.8:w-full ar-lt-0.8:max-h-[30%] flex shrink-0 flex-col ar-gt-0.8:mr-6 bg-background-light inset-shadow-[0_0_0.4rem_black]">
+ <div class="flex flex-col overflow-y-auto">
+ <fieldset>
+ <legend class="font-bold w-full text-center">{{ .L.BlogSearch.TagsHeader }}</legend>
+ <div class="flex flex-col ar-lt-0.8:grid grid-cols-3 xs:grid-cols-4 sm:grid-cols-5 grid-flow-row-dense">
+ <div class="m-1">
+ <label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();"> {{ .L.BlogSearch.ChooseAllTags }}</label>
+ </div>
+ {{- range .Tags }}
+ <div class="m-1">
+ <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>
- {{- range .Tags }}
- <div>
- <label><input type="checkbox" id="tag{{ . }}Checkbox" name="tags[]" value="{{ . }}" onclick="contextAll();" {{ if contains $.QueryTags . }}checked{{ end }}> {{ . }}</label>
+ </fieldset>
+ <fieldset class="mt-1">
+ <legend class="font-bold w-full 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-1">
+ <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-1">
+ <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-1">
+ <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-1">
+ <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-1">
+ <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-1">
+ <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>
- {{- 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>
+ </fieldset>
+ </div>
+ <hr class="border-t-2 border-dotted border-main-hard my-0.5">
+ <div class="flex flex-row bg-background-dark my-2 p-0.5 rounded-1 justify-items-center">
+ <i onclick="defaultSearchParams(); cleanHrefParams();" class="fas fa-rotate-left flex-1 text-center text-2xl hover:bg-background-light cursor-pointer transition-colors duration-300"></i>
+ <div class="w-1 bg-background-light 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>
+ <i onclick="setHrefParams();" class="fas fa-magnifying-glass text-center text-2xl hover:bg-background-light 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 class="w-[0.5dvh] border-r-2 border-dotted border-main-hard 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]">
+ <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-[0_0_0.4rem_black] p-6">
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() {
@@ -105,7 +90,7 @@
checkboxes.forEach(checkbox => {
checkbox.checked = true;
});
- const sortRadio = tagsContainer.querySelector('#sortTitleAsc');
+ const sortRadio = tagsContainer.querySelector('#sortPublicationDateDesc');
sortRadio.checked = true;
});
}
@@ -113,6 +98,7 @@
function cleanHrefParams() {
const url = new URL(window.location.href);
url.search = '';
+
window.history.pushState({}, '', url.toString());
}
@@ -134,7 +120,45 @@
window.history.pushState({}, '', url.toString());
}
- document.addEventListener('DOMContentLoaded', contextAll);
-</script>
+ 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('htmx:afterRequest', (e) => {
+ if (e.detail.xhr.status == 404 || e.detail.successful != true) {
+ return console.error(e);
+ }
+ if (e.detail.target.id == 'general-page-body') {
+ contextAll();
+
+ 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();
+ }
+ });
+ }
+ }, {once: true});
+</script>
{{ end }}
diff --git a/views/pages/blog-page.html b/views/pages/blog-page.html
index 5f001d6..7170ad8 100644
--- a/views/pages/blog-page.html
+++ b/views/pages/blog-page.html
@@ -1,70 +1,61 @@
-{{ 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%]">
+<div class="bg-background-light flex flex-col inset-shadow-[0_0_0.4rem_black] px-8 py-4 overflow-y-auto">
+ <p class="max-xs:block [@media(max-height:32rem)]:block hidden font-andika text-2xl font-bold italic text-main-hard tracking-[.0125rem] mb-1">{{ .Title }}</p>
+ <p class="max-xs:block [@media(max-height:32rem)]:block hidden grow text-lg font-andika text-secondary">{{ .L.Metadata.Published }}: {{ .PublishedDate }}</p>
+ <p class="max-xs:block [@media(max-height:32rem)]:block hidden grow text-lg font-andika text-secondary">{{ .L.Metadata.Action }}: {{ .ActionDate }}</p>
+ <hr class="max-xs:block [@media(max-height:32rem)]:block hidden border-t-2 border-dotted border-main-hard mt-1 mb-2 w-full ml-auto mr-auto">
{{ .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 }}
+ <hr class="border-t-3 border-dotted border-main-hard mt-1 mb-2 w-[80%] ml-auto mr-auto">
+ <div id="map-container" class="relative p-1 flex-none ml-auto mr-auto w-[60%] ar-lt-0.8:w-[80%] h-[40dvh] ar-lt-0.8:h-[30dvh]"></div>
+ <hr class="border-t-3 border-dotted border-main-hard mt-1 mb-2 w-[80%] ml-auto mr-auto">
-{{ 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) {
+ map = L.map(id).setView([x, y], 8);
-<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: 18,
+ attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
+ }).addTo(map);
- L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
- maxZoom: 19,
- attribution: '&copy; <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);
+ }
- 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();
}
- var marker = L.marker([x, y]).addTo(map);
- }
+ if (map) {
+ map.remove();
+ map = null;
+ }
- document.addEventListener('DOMContentLoaded', () => {initLocationMap(
- 'location-map',
- {{ .MapLocationX }},
- {{ .MapLocationY }},
- {{ .MapLocationAreaMeters }}
- )});
+ setTimeout(() => initLocationMap(
+ 'map-container',
+ {{ .MapLocationX }},
+ {{ .MapLocationY }},
+ {{ .MapLocationAreaMeters }}
+ ), 1000);
- window.addEventListener('resize', function() {
- setTimeout(() => {
- if (window.location-map) window.location-map.invalidateSize();
- }, 100);
- });
-</script>
+ var lightbox = GLightbox({
+ selector: '.glightbox',
+ moreLength: 0
+ });
+ </script>
{{- end }}
-{{ end }} \ No newline at end of file
+</div>
+{{ end }}
+
+{{ define "footer" }}
+<div hx-get="/api/v1/like" hx-target="this" hx-swap="outerHTML" hx-trigger="load"></div>
+{{ end }}
diff --git a/views/pages/global-map.html b/views/pages/global-map.html
new file mode 100644
index 0000000..306f442
--- /dev/null
+++ b/views/pages/global-map.html
@@ -0,0 +1,298 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>{{ .L.GlobalMap.Header }} // SAYA TODAY</title>
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">
+ {{ 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-andika text-main-hard overflow-hidden bg-interlocked-hexagons h-dvh">
+
+ <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 = "lilac"; break;
+ case "lilac": theme = "olive"; break;
+ }
+ }
+ localStorage.setItem('theme', theme);
+ document.body.setAttribute('data-theme', theme);
+ }
+
+ const savedTheme = localStorage.getItem('theme') || 'lilac';
+ switchTheme(savedTheme);
+ </script>
+
+ <script>
+ const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+ function convertRemToPixels(rem) {
+ return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
+ }
+ </script>
+
+ <div id="sidebar" class="bg-sidebar block fixed shadow-[0_0_0.4rem_black] z-1001 w-[5rem] h-auto">
+ <div class="text-[3rem] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1dvw] flex flex-col gap-0 relative z-1001">
+ <a href="./" class="w-[5rem] h-[5rem]">
+ <i class="fas fa-circle-left flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
+ </a>
+ <a href="/{{ .Lang }}" class="w-[5rem] h-[5rem]">
+ <i class="fas fa-home flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
+ </a>
+ <a href="/{{ .Lang }}/blog" class="w-[5rem] h-[5rem]">
+ <i class="fas fa-search flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
+ </a>
+ <a href="/{{ .Lang }}/map" class="w-[5rem] h-[5rem]">
+ <i class="fas fa-map flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
+ </a>
+ <i onclick="toggleThemeWheel();" id="theme-button" class="fas fa-swatchbook flex w-[5rem] h-[5rem] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 rounded-lg"></i>
+ <a href="/{{ .Lang }}/user" id="sidebar-user-button" class="w-[5rem] h-[5rem]">
+ <i onclick="return changeUrl('/{{ .Lang }}/user');" class="fas fa-user flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
+ </a>
+ </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 class="theme-icon" data-theme="lilac"></div>
+ <div class="theme-pin"></div>
+ </div>
+ </div>
+
+ <div id="map-container" class="bg-background-dark flex z-10 w-full h-full"></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');
+ const themeWheelRadius = convertRemToPixels(6);
+
+ 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 sliceAngle = 360 / themeIcons.length;
+
+ themeIcons.forEach((icon, index) => {
+ const startAngle = index * sliceAngle;
+ const endAngle = (index + 1) * sliceAngle;
+
+ const clipPath = createSliceClipPath(startAngle, endAngle, themeWheelRadius);
+ icon.style.clipPath = clipPath;
+ });
+ }
+
+ function createSliceClipPath(startAngle, endAngle, radius) {
+ const centerX = radius;
+ const centerY = radius;
+
+ let points = `${centerX}px ${centerY}px`;
+
+ const segments = Math.max(2, Math.ceil(Math.abs(endAngle - startAngle) / 5));
+ const angleStep = (endAngle - startAngle) / segments;
+
+ for (let i = 0; i <= segments; i++) {
+ const angle = ((startAngle + angleStep * i - 90) * Math.PI) / 180;
+ const x = centerX + Math.cos(angle) * radius;
+ const y = centerY + Math.sin(angle) * radius;
+ points += `, ${x}px ${y}px`;
+ }
+
+ points += `, ${centerX}px ${centerY}px`;
+
+ return `polygon(${points})`;
+ }
+
+ function positionThemeWheel() {
+ const buttonRect = themeButton.getBoundingClientRect();
+ const buttonCenterX = buttonRect.left + buttonRect.width / 2;
+ const buttonCenterY = buttonRect.top + buttonRect.height / 2;
+
+ const margin = 0;
+
+ let wheelX = buttonCenterX - themeWheelRadius;
+ let wheelY = buttonCenterY - themeWheelRadius;
+
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+
+ if (wheelX < margin) {
+ wheelX = margin;
+ } else if (wheelX + (themeWheelRadius * 2) > viewportWidth - margin) {
+ wheelX = viewportWidth - (themeWheelRadius * 2) - margin;
+ }
+
+ if (wheelY < margin) {
+ wheelY = margin;
+ } else if (wheelY + (themeWheelRadius * 2) > viewportHeight - margin) {
+ wheelY = viewportHeight - (themeWheelRadius * 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-soft-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: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
+ zoomControl: false
+ }).addTo(map);
+
+ L.control.zoom({
+ position: 'topright'
+ }).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-12 h-12 mr-1" 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/pages/home-page.html b/views/pages/home-page.html
new file mode 100644
index 0000000..11d0f7b
--- /dev/null
+++ b/views/pages/home-page.html
@@ -0,0 +1,122 @@
+{{ define "body" }}
+<div class="bg-background-light flex flex-row ar-lt-0.6:flex-col w-full h-full inset-shadow-[0_0_0.4rem_black] overflow-y-auto text-base/[2] tracking-wide">
+ <div class="ar-gt-0.6:flex-3/5 justify-center content-start p-8">
+ <div class="flex flex-col justify-start items-start bg-background-dark mb-8 shadow-[0.4rem_0.4rem_0.4rem_black]">
+ <p class="text-3xl/[2] sm:text-4xl/[2] lg:text-5xl/[2] tracking-widest uppercase w-full font-andika italic text-left
+ border-l-4 border-solid border-(--background-dark-color)
+ bg-gradient-to-r from-(--background-dark-color) to-(--background-light-color)">{{ .L.HomePage.Hymn1 }}</p>
+ <hr class="w-full border-t-2 border-dotted border-main-hard mb-1">
+ <p class="text-xl/[2] sm:text-2xl/[2] lg:text-3xl/[2] -indent-8 ml-12 p-1">{{ .L.HomePage.Hymn2 }}</p>
+ <p class="text-xl/[2] sm:text-2xl/[2] lg:text-3xl/[2] -indent-8 ml-12 p-1">{{ .L.HomePage.Hymn3 }}</p>
+ <p class="text-xl/[2] sm:text-2xl/[2] lg:text-3xl/[2] -indent-8 ml-12 p-1">{{ .L.HomePage.Hymn4 }}</p>
+ </div>
+
+ <div class="flex flex-col justify-center items-center">
+ <p class="text-xl sm:text-2xl lg:text-3xl p-1 w-full font-andika italic text-left
+ border-l-1.5 border-solid border-(--background-dark-color)
+ bg-gradient-to-r from-(--background-dark-color) to-(--background-light-color)">{{ .L.HomePage.DidYouKnowThat }}</p>
+ <hr class="w-full border-t-2 border-dotted border-main-hard mb-1">
+ <ul class="w-full text-left list-[circle]">
+ <li class="ml-4 mb-1">{{ index .FunFacts 0 }}</li>
+ <li class="ml-4 mb-1">{{ index .FunFacts 1 }}</li>
+ <li class="ml-4">{{ index .FunFacts 2 }}</li>
+ </ul>
+ </div>
+ </div>
+
+ <div class="ar-gt-0.6:flex-2/5 justify-center content-start p-8">
+ <div class="flex flex-col justify-center items-start mb-4">
+ <p class="text-xl sm:text-2xl lg:text-3xl p-1 w-full font-andika italic text-left
+ border-l-1.5 border-solid border-(--background-dark-color)
+ bg-gradient-to-r from-(--background-dark-color) to-(--background-light-color)">{{ .L.HomePage.Contacts }}</p>
+ <hr class="w-full border-t-2 border-dotted border-main-hard mb-1">
+ <a href="https://t.me/EarlInisMona">
+ <i class="fa-brands fa-telegram w-4 h-4 mr-1"></i>
+ <span class="font-bold">Telegram:</span>
+ <span>@EarlInisMona</span>
+ </a>
+ <a href="https://discord.com/users/356057270951346176">
+ <i class="fa-brands fa-discord w-4 h-4 mr-1"></i>
+ <span class="font-bold">Discord:</span>
+ <span>montferrat</span>
+ </a>
+ <a href="mailto:saya.andy@posteo.com">
+ <i class="fas fa-envelope w-4 h-4 mr-1"></i>
+ <span class="font-bold">E-mail:</span>
+ <span>saya.andy@posteo.com</span>
+ </a>
+ </div>
+
+ <div class="flex relative aspect-square justify-center content-center mb-4">
+ {{- range $_ := iterate .FilledHeartCount }}
+ <div class="home-page-heart z-50">♥</div>
+ {{- end }}
+ {{- range $_ := iterate .OutlineHeartCount }}
+ <div class="home-page-heart z-50">♡</div>
+ {{- end }}
+ <img src="https://f003.backblazeb2.com/file/sayana-static/home-page-gifs/{{ .GifName }}"
+ class="home-page-gif z-49 -rotate-12 max-w-[70%] max-h-[70%] border-4 border-(--background-dark-color) border-inset">
+ </div>
+
+ <script>
+ function shuffleHearts() {
+ vmin = calculateVmin(1);
+ document.querySelectorAll('.home-page-heart').forEach((e) => {
+ let top, left;
+ do {
+ top = Math.floor(rand() * 90) + 5;
+ left = Math.floor(rand() * 90) + 5;
+ } while (top > 30 && top < 70 && left > 30 && left < 70);
+ const opacity = rand() * 0.7 + 0.2;
+ const size = rand() * 3.2 + 0.8;
+ e.style["top"] = `calc(${top}% - ${size}vmin / 2)`;
+ e.style["left"] = `calc(${left}% - ${size}vmin / 2)`;
+ e.style["opacity"] = `${opacity}`;
+ e.style["font-size"] = `${size}vmin`;
+ });
+ }
+
+ let shuffleHeartsResizeTimeout;
+ function shuffleHeartsResizeCb() {
+ clearTimeout(shuffleHeartsResizeTimeout);
+ shuffleHeartsResizeTimeout = setTimeout(shuffleHearts, 300);
+ }
+ window.addEventListener('resize', shuffleHeartsResizeCb);
+
+ document.addEventListener('popout', (e) => {
+ window.removeEventListener('resize', shuffleHeartsResizeCb);
+ shuffleHeartsResizeCb = null;
+ });
+ </script>
+
+ <script>
+ document.addEventListener('htmx:afterRequest', (e) => {
+ if (e.detail.xhr.status == 404 || e.detail.successful != true) {
+ return console.error(e);
+ }
+ if (e.detail.target.id == 'general-page-body') {
+ shuffleHearts();
+ }
+ }, {once: true});
+ </script>
+
+ <div class="flex flex-col justify-center items-center">
+ <p class="text-xl sm:text-2xl lg:text-3xl p-1 w-full font-andika italic text-left
+ border-l-1.5 border-solid border-(--background-dark-color)
+ bg-gradient-to-r from-(--background-dark-color) to-(--background-light-color)">{{ .L.HomePage.SidebarDescription }}</p>
+ <hr class="w-full border-t-2 border-dotted border-main-hard mb-1">
+ <div class="w-full grid grid-cols-[1fr_4fr] content-center items-center
+ [&>*:nth-child(4n+2)]:bg-(--background-medium-color)/50 [&>*:nth-child(4n+3)]:bg-(--background-medium-color)/50" style="container-type: inline-size;">
+ <i class="fas fa-home w-full h-full text-[10cqi] content-center text-center text-main-hard"></i>
+ <p class="p-1">{{ .L.HomePage.HomePageDescription }}</p>
+ <i class="fas fa-search w-full h-full text-[10cqi] content-center text-center text-main-hard"></i>
+ <p class="p-1">{{ .L.HomePage.BlogSearchDescription }}</p>
+ <i class="fas fa-map w-full h-full text-[10cqi] content-center text-center text-main-hard"></i>
+ <p class="p-1">{{ .L.HomePage.MarkerMapDescription }}</p>
+ <i class="fas fa-swatchbook w-full h-full text-[10cqi] content-center text-center text-main-hard"></i>
+ <p class="p-1">{{ .L.HomePage.ThemeSwitchDescription }}</p>
+ </div>
+ </div>
+ </div>
+</div>
+{{ end }}
diff --git a/views/pages/language-pick.html b/views/pages/language-pick.html
new file mode 100644
index 0000000..5a9d229
--- /dev/null
+++ b/views/pages/language-pick.html
@@ -0,0 +1,16 @@
+{{ define "body" }}
+<div class="flex flex-col justify-center text-3xl h-[100%]">
+ {{- range .AvailableLanguages }}
+ <hr class="border-t-2 border-dotted border-main-hard">
+ <div class="nth-[2n+1_of_div]:bg-(--background-medium-color)/50 nth-[2n_of_div]:bg-(--background-light-color)/50 p-2.5 flex flex-row justify-center justify-items-center">
+ <a href="/{{ .Name }}" class="w-12 h-12 basis-12 shrink-0 mr-4">
+ <img onclick="return changeUrl('{{ .Name }}');" class="w-full h-full aspect-square cursor-pointer object-cover rounded-md select-none" src="{{ .Flag }}">
+ </a>
+ <a href="/{{ .Name }}" onclick="return changeUrl('{{ .Name }}');" class="cursor-pointer">
+ {{ .Alt }}
+ </a>
+ </div>
+ {{- end }}
+ <hr class="border-t-2 border-dotted border-main-hard">
+</div>
+{{ end }}
diff --git a/views/pages/unsubscribe-page.html b/views/pages/unsubscribe-page.html
new file mode 100644
index 0000000..3876818
--- /dev/null
+++ b/views/pages/unsubscribe-page.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>{{ .L.UnsubscribePage.Header }} // SAYA TODAY</title>
+<style>
+ html {
+ --noise-image: url(data:image/png;base64,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);
+ }
+</style>
+</head>
+
+<body style="width: 100dvw; height: 100dvh; display: flex; flex-direction: column; position: fixed">
+
+ <div style='flex: 3 0 0; background-image: var(--noise-image); background-color: rgba({{ .StatusColor }}, 0.1)'></div>
+
+ <div style="flex: 1 0 0; background-image: var(--noise-image); align-content: center;">
+ <p style="position: relative; width: 100%; margin-inline: auto; text-align: center; font-size: 2rem;">{{ .StatusEmoji }}</p>
+ </div>
+
+ <div style="flex: 1 0 0; background-image: var(--noise-image); align-content: center;">
+ <p style="position: relative; width: 100%; margin-inline: auto; text-align: center; font-size: 1.5rem;">{{ .StatusText }}</p>
+ </div>
+
+ <div style='flex: 3 0 0; background-image: var(--noise-image); background-color: rgba({{ .StatusColor }}, 0.2)'></div>
+
+</body>
+
+</html>
diff --git a/views/pages/user-page.html b/views/pages/user-page.html
new file mode 100644
index 0000000..7d39825
--- /dev/null
+++ b/views/pages/user-page.html
@@ -0,0 +1,205 @@
+{{ define "body" }}
+<div class="bg-background-light flex flex-col inset-shadow-[0_0_0.4rem_black] px-8 py-4">
+ <form id="email-form" class="w-full mb-2 flex flex-col relative crossable"
+ hx-get="/api/v1/email/is-in-verification" hx-target="#email-message" hx-swap="outerHTML" hx-trigger="load" hx-indicator="this">
+ <div class="flex flex-row w-full mb-4 z-21">
+ <div class="flex flex-col w-full sm:flex-3/5 grow-0">
+ <label class="block text-main-medium font-bold mb-1 z-22">
+ {{ .L.UserProfile.EmailHeader }}
+ </label>
+ <input class="bg-(--background-medium-color) appearance-none border-(--background-medium-color) rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-[0_0_0.4rem_black] focus:outline-none focus:bg-(--background-light-color) focus:text-main-hard z-22"
+ id="email-input" name="email" type="text" value="{{ .Email }}" placeholder="{{ .L.UserProfile.TypeEmail }}" maxlength="64"
+ data-validated-email="{{ .Email }}" onchange="checkIfValidated(this);">
+ </div>
+ </div>
+ <button class="w-max mx-auto bg-(--main-hard-color) hover:bg-(--main-medium-color) active:bg-(--main-soft-color) active:inset-shadow-[0.2em_0.2em_0.2rem_black] transition-colors transition-300 text-(--background-light-color) font-bold py-2 px-4 rounded z-22"
+ hx-post="/api/v1/email/send-verification-code" hx-target="#email-message" hx-swap="outerHTML" hx-indicator="#email-form">
+ {{ .L.UserProfile.SendCodeButton }}
+ </button>
+ </form>
+ <div id="email-message" class="bg-background-dark text-main-soft z-26 w-full inset-shadow-[0_0_0.4rem_black] py-2 px-4 text-center"></div>
+
+ <hr class="my-2 border-t-4 border-dotted border-main-hard">
+
+ <form id="email-verification-form" class="w-full mb-2 flex flex-col relative hidden crossable"
+ {{ if .EmailCode }}hx-post="/api/v1/email/verify" hx-target="#verification-message" hx-swap="outerHTML" hx-trigger="load" hx-indicator="this"{{ end }}>
+ <div class="flex flex-row w-full mb-4 z-21">
+ <div class="flex flex-col w-full sm:flex-2/5 grow-0">
+ <label class="block text-main-medium font-bold ml-1 z-22">
+ {{ .L.UserProfile.VerificationCodeHeader }}
+ </label>
+ <input class="bg-(--background-medium-color) appearance-none border-(--background-medium-color) rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-[0_0_0.4rem_black] focus:outline-none focus:bg-(--background-light-color) focus:text-main-hard z-22"
+ name="email_code" type="text" value="{{ .EmailCode }}" placeholder="0123456789ABCDEF" maxlength="16" autocapitalize="characters">
+ </div>
+ </div>
+ <button class="w-max mx-auto bg-(--main-hard-color) hover:bg-(--main-medium-color) active:bg-(--main-soft-color) active:inset-shadow-[0.2em_0.2em_0.2rem_black] transition-colors transition-300 text-(--background-light-color) font-bold py-2 px-4 rounded z-22"
+ hx-post="/api/v1/email/verify" hx-target="#verification-message" hx-swap="outerHTML" hx-indicator="#email-verification-form">
+ {{ .L.UserProfile.VerifyButton }}
+ </button>
+ </form>
+ <div id="verification-message" class="bg-background-dark text-main-soft z-26 w-full inset-shadow-[0_0_0.4rem_black] py-2 px-4 text-center"></div>
+</div>
+
+<hr class="my-4 border-t-4 border-dotted border-main-hard">
+
+<div class="bg-background-light flex flex-col inset-shadow-[0_0_0.4rem_black] px-8 py-4">
+ <form id="subs-form" class="w-full mb-2 flex flex-col relative crossable">
+ <div class="flex flex-col w-full mb-4 z-21">
+ <label class="block text-main-medium font-bold mb-1 z-22">
+ {{ .L.UserProfile.SubscriptionHeader }}
+ </label>
+ <div class="flex flex-col sm:flex-row w-full">
+ <div class="m-1 w-1/3">
+ <label><input class="mr-1" type="radio" id="tagsNone" name="tags" value="none" {{ if eq .TagsPicked "none" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ .L.UserProfile.TagsNone }}</label>
+ </div>
+ <div class="m-1 w-1/3">
+ <label><input class="mr-1" type="radio" id="tagsAll" name="tags" value="all" {{ if eq .TagsPicked "all" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ .L.UserProfile.TagsAll }}</label>
+ </div>
+ <div class="m-1 w-1/3">
+ <label><input class="mr-1" type="radio" id="tagsSpecific" name="tags" value="specific" {{ if eq .TagsPicked "specific" }}checked{{ end }} onclick="toggleTagsPick(this);">{{ .L.UserProfile.TagsSpecific }}</label>
+ </div>
+ </div>
+ <div id="pick-tags-form" class="flex flex-col w-full relative crossable">
+ <input type="text" class="bg-(--background-medium-color) appearance-none border-(--background-medium-color) rounded ml-6 mr-4 py-2 px-4 text-main-medium leading-tight inset-shadow-[0_0_0.4rem_black] focus:outline-none focus:bg-(--background-light-color) focus:text-main-hard z-22"
+ list="existing-tags" onchange="addTag(this.value);" onkeydown="if (event.keyCode === 13) {addTag(this.value); this.value=''; return false;}">
+ <datalist id="existing-tags">
+ {{- range .ExistingTags }}
+ <option value="{{ .Name }}">
+ {{- end }}
+ </datalist>
+ <div id="tags-picked-list" class="bg-background-dark flex flex-wrap inset-shadow-[0_0_0.4rem_black] p-2 m-4 w-full">
+ {{- range .TagsPickedList }}
+ <div id="tagPicked{{ . }}" class="bg-main-hard text-background-light m-2 flex flex-row">
+ <p class="py-1 px-2">{{ . }}</p>
+ <p class="py-1 px-2 bg-main-soft cursor-pointer" onclick="htmx.remove(htmx.find('#tagPicked{{ . }}'));">✘</p>
+ <input type="hidden" name="tags_picked" value="{{ . }}">
+ </div>
+ {{- end }}
+ </div>
+ </div>
+ <button class="w-max mx-auto bg-(--main-hard-color) hover:bg-(--main-medium-color) active:bg-(--main-soft-color) active:inset-shadow-[0.2em_0.2em_0.2rem_black] transition-colors transition-300 text-(--background-light-color) font-bold py-2 px-4 rounded z-22"
+ hx-put="/api/v1/subs" hx-target="#subs-message" hx-swap="outerHTML" hx-indicator="#subs-form">
+ {{ .L.UserProfile.SaveButton }}
+ </button>
+ </div>
+ </form>
+ <div id="subs-message" class="bg-background-dark text-main-soft z-26 w-full inset-shadow-[0_0_0.4rem_black] py-2 px-4 text-center"></div>
+</div>
+
+<script>
+htmx.on("htmx:beforeSwap", (e) => {
+ if (e.detail.xhr.status === 422) {
+ e.detail.shouldSwap = true;
+ e.detail.isError = false;
+ }
+});
+
+var formAllowTimeout;
+var verificationHideTimeout;
+
+var url = new URL(window.location.href);
+url.searchParams.delete('email_code');
+window.history.pushState({}, '', url.toString());
+
+htmx.on("htmx:afterSwap", (e) => {
+ emailMessage = htmx.find("#email-message");
+ verificationMessage = htmx.find("#verification-message");
+ emailForm = htmx.find("#email-form");
+ emailInput = htmx.find("#email-input");
+ emailVerificationForm = htmx.find("#email-verification-form");
+ subsForm = htmx.find("#subs-form");
+ tryAgainTimeDiff = parseInt(emailMessage.dataset.strikedEndTime || "0") - new Date().getTime();
+ verificationWaitingTimeDiff = parseInt(emailMessage.dataset.codeExpiryTime || "0") - new Date().getTime();
+
+ if (tryAgainTimeDiff > 0) {
+ emailForm.classList.add("disabled");
+ formAllowTimeout = setTimeout(() => {
+ emailForm.classList.remove("disabled");
+ emailMessage.classList.add("hidden");
+ }, tryAgainTimeDiff);
+ } else {
+ emailForm.classList.remove("disabled");
+ }
+
+ if (verificationWaitingTimeDiff > 0) {
+ verificationHideTimeout = setTimeout(() => emailVerificationForm.classList.add("hidden"), verificationWaitingTimeDiff);
+ emailVerificationForm.classList.remove("hidden");
+ } else {
+ emailVerificationForm.classList.add("hidden");
+ }
+
+ if ((verificationMessage.dataset.hideVerificationPanel || "false") == "true") {
+ emailVerificationForm.classList.add("hidden");
+ }
+
+ if (emailInput.dataset.validatedEmail != "") {
+ subsForm.classList.remove("disabled");
+ } else {
+ subsForm.classList.add("disabled");
+ }
+});
+
+function checkIfValidated(elem) {
+ if ((elem.dataset.validatedEmail != "") && (elem.value == elem.dataset.validatedEmail)) {
+ elem.classList.add("!bg-green-300/20");
+ } else {
+ elem.classList.remove("!bg-green-300/20");
+ }
+}
+
+checkIfValidated(htmx.find("#email-input"));
+
+function toggleTagsPick(elem) {
+ const pickTagsForm = htmx.find("#pick-tags-form");
+ if (elem.value === "specific" && elem.checked) {
+ pickTagsForm.classList.remove("disabled");
+ } else {
+ pickTagsForm.classList.add("disabled");
+ }
+}
+
+toggleTagsPick(htmx.find("#tagsSpecific"));
+
+function addTag(name) {
+ name = name.trim();
+ const subsMessage = htmx.find("#subs-message");
+ const existingTags = htmx.findAll(htmx.find("#existing-tags"), "option");
+ found = [...existingTags].find(opt => opt.value===name);
+ if (!found) {
+ subsMessage.classList.remove("hidden");
+ subsMessage.innerHTML = "{{ .L.UserProfile.TagDoesNotExist }}".replace("{}", name);
+ return;
+ }
+
+ const tagsPickedList = htmx.find("#tags-picked-list");
+ const subscribedTags = htmx.findAll(tagsPickedList, "div input");
+ found = [...subscribedTags].find(opt => opt.value===name);
+ if (found) {
+ subsMessage.classList.remove("hidden");
+ subsMessage.innerHTML = "{{ .L.UserProfile.TagAlreadyAdded }}".replace("{}", name);
+ return;
+ }
+
+ tagsPickedList.innerHTML += `
+ <div id="tagPicked${name}" class="bg-main-hard text-background-light m-2 flex flex-row">
+ <p class="py-1 px-2">${name}</p>
+ <p class="py-1 px-2 bg-main-soft cursor-pointer" onclick="htmx.remove(htmx.find('#tagPicked${name}'));">✘</p>
+ <input type="hidden" name="tags_picked" value="${name}">
+ </div>
+ `;
+}
+
+htmx.on("htmx:configRequest", (e) => {
+ const params = e.detail.parameters;
+
+ if (Array.isArray(params.tags_picked)) {
+ tags_join = params.tags_picked[0] || "";
+ for (i = 1; i < params.tags_picked.length; i++) {
+ tags_join += ",";
+ tags_join += params.tags_picked[i];
+ }
+ params.tags_picked = tags_join;
+ }
+})
+</script>
+{{ end }} \ No newline at end of file
diff --git a/views/partials/blog-page-like-button.html b/views/partials/blog-page-like-button.html
new file mode 100644
index 0000000..8d555da
--- /dev/null
+++ b/views/partials/blog-page-like-button.html
@@ -0,0 +1,6 @@
+<button
+ hx-put="/api/v1/like" hx-vals='{"like": {{ not .Liked }}}' hx-target="this" hx-swap="outerHTML" hx-trigger="click"
+ class="text-sm/[0.9] shrink-0 font-andika text-left px-1 py-0.5 {{ if .Liked }}bg-main-soft hover:bg-main-medium text-background-dark border-inset{{ else }}bg-main-hard hover:bg-main-medium text-background-light border-outset{{ end }} cursor-pointer border-2 border-background-dark">
+ <i class="fas fa-thumbs-up w-3 h-3"></i>
+ <span class="italic ml-0.5 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
index 7cb48f0..b50f419 100644
--- a/views/partials/catalogue-blog-cards.html
+++ b/views/partials/catalogue-blog-cards.html
@@ -1,16 +1,27 @@
{{- 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>
+<hr class="first-of-type:hidden my-1 border-t-2 border-dotted border-main-hard">
+<div class="m-1 flex flex-row justify-center justify-items-center">
+ <a href="{{ .ArticleLink }}" class="w-24 h-24 basis-24 shrink-0 my-2.5 mr-2">
+ <img onclick="return changeUrl('{{ .ArticleLink }}');" class="w-full h-full aspect-square cursor-pointer object-cover rounded-lg select-none" src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/{{ .Thumbnail }}.webp">
+ </a>
+ <div class="grow my-0.5 mx-1 flex flex-col justify-center">
+ <div class="flex flex-row gap-2 max-w-full">
+ <a href="{{ .ArticleLink }}" onclick="return changeUrl('{{ .ArticleLink }}');" class="grow text-base cursor-pointer">
+ <span class="font-extrabold">{{ .Title }}</span>
+ <span class="font-extrabold select-none text-secondary">//</span>
+ <span class="italic text-secondary">{{ .ActionDate }}</span>
+ </a>
+ <p class="basis-56 text-base 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-base text-main-hard">{{ .ShortDescription }}</p>
+ <p class="text-base text-secondary italic">
+ <i class="fas fa-thumbs-up w-4 h-4"></i>
+ <span>{{ .LikeCount }}</span>
+ <span class="font-extrabold select-none">//</span>
+ <i class="fas fa-eye w-4 h-4"></i>
+ <span>{{ .ViewCount }}</span>
+ </p>
+ <p class="text-base text-secondary">{{ $.L.TagsLabel }} {{ template "catalogue-blog-card-tags.html" . }}</p>
</div>
</div>
{{- end }} \ No newline at end of file
diff --git a/views/partials/general-page-body.html b/views/partials/general-page-body.html
new file mode 100644
index 0000000..d697c14
--- /dev/null
+++ b/views/partials/general-page-body.html
@@ -0,0 +1 @@
+{{ block "body" . }}{{ end }} \ No newline at end of file
diff --git a/views/partials/general-page-bottom-embeds.html b/views/partials/general-page-bottom-embeds.html
new file mode 100644
index 0000000..4c36b26
--- /dev/null
+++ b/views/partials/general-page-bottom-embeds.html
@@ -0,0 +1 @@
+{{ block "bottom-embeds" . }}{{ end }} \ No newline at end of file
diff --git a/views/partials/general-page-footer.html b/views/partials/general-page-footer.html
new file mode 100644
index 0000000..9b91bf0
--- /dev/null
+++ b/views/partials/general-page-footer.html
@@ -0,0 +1,7 @@
+{{ block "footer" . }}{{ end }}
+<p class="grow content-center text-base/[1] font-andika italic text-right text-secondary mr-1">
+ 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-4 max-h-4 self-center" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="CC">
+<img class="max-w-4 max-h-4 self-center" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="BY">
+<img class="max-w-4 max-h-4 self-center" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="SA"> \ No newline at end of file
diff --git a/views/partials/general-page-header.html b/views/partials/general-page-header.html
new file mode 100644
index 0000000..90180de
--- /dev/null
+++ b/views/partials/general-page-header.html
@@ -0,0 +1,7 @@
+<title>{{ .Title }} // SAYA TODAY</title>
+<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-2">
+ <p class="text-4xl font-andika font-light italic text-shadow-[0_2px_2px_var(--main-soft-color)] text-left mt-auto">{{ .Title }}</p>
+ <p id="published-date" class="grow text-2xl font-andika italic text-secondary text-right mt-auto">{{ .PublishedDate }}</p>
+</div>
+{{ block "header" . }}{{ end }}
diff --git a/views/partials/general-page-top-embeds.html b/views/partials/general-page-top-embeds.html
new file mode 100644
index 0000000..1407b50
--- /dev/null
+++ b/views/partials/general-page-top-embeds.html
@@ -0,0 +1 @@
+{{ block "top-embeds" . }}{{ end }} \ No newline at end of file
diff --git a/views/partials/personal-page-status.html b/views/partials/personal-page-status.html
new file mode 100644
index 0000000..a4e47c2
--- /dev/null
+++ b/views/partials/personal-page-status.html
@@ -0,0 +1,12 @@
+<div id="{{ .StatusId }}" class='z-26 w-full inset-shadow-[0_0_0.4rem_black]
+ {{if eq .Status "OK"}}bg-green-700 text-green-100
+ {{else if eq .Status "Failed"}}bg-red-700 text-red-100
+ {{else}}bg-background-dark text-main-soft{{end}}
+ {{if eq .Message ""}}hidden{{end}}
+ py-2 px-4 text-center'
+ {{- range $_, $val := .DataAttributes }}
+ {{ $val }}
+ {{- end }}
+ >
+ {{ .Message }}
+</div> \ No newline at end of file