Prechádzať zdrojové kódy

Refine site metadata and desktop behavior

simo 2 dní pred
rodič
commit
238005c8ba

+ 11 - 9
.kb-context.md

@@ -1,9 +1,11 @@
-<!-- KB managed: ~/knowledge-base/projects/simo2/context.md -->
-<!-- Always edit THIS file for project context. Do NOT edit the repo's CLAUDE.md. -->
-<!-- KB:REFS
-  - site-structure-and-window-system.md (134 lines)
-    kb_read: projects/simo2/site-structure-and-window-system.md
-  Use kb_search or kb_read to access these docs.
--->
-
-# simo2
+# Legacy simo2 KB context
+
+This file is no longer canonical.
+
+Use:
+
+```md
+@~/knowledge-base/projects/simo2/context.md
+```
+
+The repo's `CLAUDE.md` already points there. Do not add new durable project notes here.

+ 3 - 4
astro.config.mjs

@@ -1,12 +1,11 @@
 import { defineConfig } from "astro/config";
 import { defineConfig } from "astro/config";
 import mdx from "@astrojs/mdx";
 import mdx from "@astrojs/mdx";
 
 
-import sitemap from "@astrojs/sitemap";
-
 // https://astro.build/config
 // https://astro.build/config
 export default defineConfig({
 export default defineConfig({
 	site: "https://simo.ng",
 	site: "https://simo.ng",
-	// Sitemap is currently crashing in this repo's build (reduce of undefined).
-	// Keep it disabled until the underlying integration/config is fixed.
+	// @astrojs/sitemap currently crashes against this Astro 6 build output
+	// (reduce of undefined in astro:build:done), so sitemap.xml is generated
+	// by src/pages/sitemap.xml.ts instead.
 	integrations: [mdx()],
 	integrations: [mdx()],
 });
 });

+ 9 - 9
data/spotify-last-known.json

@@ -1,11 +1,11 @@
 {
 {
-  "title": "Under Pressure - Remastered 2011",
-  "artist": "Queen, David Bowie",
-  "album": "Hot Space (Deluxe Remastered Version)",
-  "albumArtUrl": "https://i.scdn.co/image/ab67616d0000b273a1e05e1048e2cf2737adf742",
-  "trackUrl": "https://open.spotify.com/track/11IzgLRXV7Cgek3tEgGgjw",
-  "durationMs": 248440,
-  "stoppedAtMs": 1776291222996,
-  "stoppedProgressMs": 19546,
-  "lastUpdatedAtMs": 1776291216786
+  "title": "Last Train At 25 O'clock",
+  "artist": "Lamp",
+  "album": "For Lovers",
+  "albumArtUrl": "https://i.scdn.co/image/ab67616d0000b27358ebd661a51fcf2968db3ea1",
+  "trackUrl": "https://open.spotify.com/track/3VPBPBZKxQu3bqeuzz8gRm",
+  "durationMs": 263840,
+  "stoppedAtMs": 1777575992861,
+  "stoppedProgressMs": null,
+  "lastUpdatedAtMs": 1777534606824
 }
 }

BIN
public/favicon.ico


+ 10 - 8
public/favicon.svg

@@ -1,9 +1,11 @@
-<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
-    <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
-    <style>
-        path { fill: #000; }
-        @media (prefers-color-scheme: dark) {
-            path { fill: #FFF; }
-        }
-    </style>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" role="img" aria-label="simo.ng favicon">
+  <rect width="64" height="64" fill="#008080"/>
+  <rect x="8" y="10" width="48" height="42" fill="#c0c0c0"/>
+  <path fill="#ffffff" d="M8 10h48v3H11v39H8z"/>
+  <path fill="#808080" d="M11 49h45v3H8v-3z"/>
+  <path fill="#000000" d="M53 13h3v39h-3zM8 52h48v3H8z"/>
+  <rect x="12" y="14" width="40" height="8" fill="#000080"/>
+  <path fill="#ffffff" d="M16 16h18v2H16z"/>
+  <path fill="#000000" d="M45 16h4v4h-4zM46 17h2v2h-2z"/>
+  <path fill="#000080" d="M20 34c0-5 4-8 10-8 4 0 8 1 11 3l-3 5c-3-2-6-3-9-3-2 0-3 1-3 2 0 4 16 2 16 11 0 5-4 8-11 8-5 0-10-2-13-5l4-5c3 3 7 4 10 4 2 0 4-1 4-2 0-4-16-2-16-10z"/>
 </svg>
 </svg>

+ 8 - 1
public/oembed.json

@@ -1,5 +1,12 @@
 {
 {
+	"version": "1.0",
+	"provider_name": "simo.ng",
+	"provider_url": "https://simo.ng",
 	"author_name": "simo.ng",
 	"author_name": "simo.ng",
 	"author_url": "https://simo.ng",
 	"author_url": "https://simo.ng",
-	"type": "photo"
+	"type": "photo",
+	"url": "https://simo.ng/smaller.gif",
+	"width": 96,
+	"height": 96,
+	"title": "Simo"
 }
 }

+ 3 - 3
src/components/aboutme.astro

@@ -16,11 +16,11 @@ title="About Me" >
         readonly
         readonly
         id="text201"
         id="text201"
         style="font-size: 22px; resize: none; padding: 10px; font-family: inherit; background-color: #fff; border: 1px solid #ccc; box-sizing: border-box;"
         style="font-size: 22px; resize: none; padding: 10px; font-family: inherit; background-color: #fff; border: 1px solid #ccc; box-sizing: border-box;"
-        >I'm Simon, a software developer studying computer science @ Widener university
+        >I'm Simon, a software developer studying computer science @ Widener University
 
 
-I am flexable with language choice. Development is a universal language, and I frequntly expand my stack knowledge through experience in different projects.
+I am flexible with language choice. Development is a universal language, and I frequently expand my stack knowledge through experience in different projects.
 
 
-When it's up to me, I usually chose to write in TypeScript; sometimes GoLang if the project calls for it's usecase.
+When it's up to me, I usually choose to write in TypeScript; sometimes Golang if the project calls for its use case.
         </textarea>
         </textarea>
     </div>
     </div>
   </div>
   </div>

+ 17 - 4
src/components/app.astro

@@ -2,13 +2,20 @@
 let app = Astro.props.item;
 let app = Astro.props.item;
 
 
 let itemName = app.name.replace(" ", "");
 let itemName = app.name.replace(" ", "");
-
-
+const actionLabel = app.redirectUrl ? `Open ${app.name} link` : `Open ${app.name} window`;
 ---
 ---
 
 
-<a id={itemName} data-id={app.name} href={app.link}>
+<a
+	id={itemName}
+	data-id={app.name}
+	data-window-id={app.windowId}
+	data-redirect-url={app.redirectUrl}
+	href={app.redirectUrl ?? "#"}
+	role="button"
+	aria-label={actionLabel}
+>
 	<div class="h-100 m-4">
 	<div class="h-100 m-4">
-		<img src={app.logo} alt={app.name} class="w-auto h-16 mx-auto my-2" />
+		<img src={app.logo} alt="" aria-hidden="true" class="w-auto h-16 mx-auto my-2" />
 		<h3 class="font-sans text-white text-center">{app.name}</h3>
 		<h3 class="font-sans text-white text-center">{app.name}</h3>
 	</div>
 	</div>
 </a>
 </a>
@@ -23,6 +30,12 @@ let itemName = app.name.replace(" ", "");
 	a {
 	a {
 		cursor: pointer;
 		cursor: pointer;
 		user-select: none;
 		user-select: none;
+		display: block;
+	}
+
+	a:focus-visible {
+		outline: 1px dotted #fff;
+		outline-offset: -2px;
 	}
 	}
 
 
 	.link {
 	.link {

+ 2 - 1
src/components/githubgraph.astro

@@ -11,7 +11,8 @@
 	</div>
 	</div>
 	<div class="window-body" style="padding: 10px;">
 	<div class="window-body" style="padding: 10px;">
 		<iframe
 		<iframe
-			src="https://github-readme-stats.vercel.app/api/top-langs/?username=S1monlol&theme=github_dark"
+			data-src="https://github-readme-stats.vercel.app/api/top-langs/?username=S1monlol&theme=github_dark"
+			loading="lazy"
 			style="width: 100%; height: 300px; border: none; image-rendering: pixelated; image-rendering: -moz-crisp-edges; image-rendering: crisp-edges;"
 			style="width: 100%; height: 300px; border: none; image-rendering: pixelated; image-rendering: -moz-crisp-edges; image-rendering: crisp-edges;"
 			frameborder="0"
 			frameborder="0"
 		></iframe>
 		></iframe>

+ 71 - 24
src/components/nowplaying.astro

@@ -231,11 +231,20 @@ const layout = {
     elTime.textContent = `${fmtTime(progressMs)} / ${fmtTime(durationMs)}`;
     elTime.textContent = `${fmtTime(progressMs)} / ${fmtTime(durationMs)}`;
   }
   }
 
 
+  function numberFrom(...values) {
+    for (const value of values) {
+      if (typeof value === "number" && Number.isFinite(value)) return value;
+    }
+    return null;
+  }
+
   function startTicking() {
   function startTicking() {
     if (tickTimer) return;
     if (tickTimer) return;
     tickTimer = setInterval(() => {
     tickTimer = setInterval(() => {
       if (!lastIsPlaying) return;
       if (!lastIsPlaying) return;
       if (typeof lastProgressMs !== "number" || typeof lastDurationMs !== "number") return;
       if (typeof lastProgressMs !== "number" || typeof lastDurationMs !== "number") return;
+      // lastSyncAt is the server timestamp (best-effort) for when progressMs was sampled.
+      // If we used Date.now() here, client clock skew could make the bar jump.
       const elapsed = Date.now() - lastSyncAt;
       const elapsed = Date.now() - lastSyncAt;
       const est = lastProgressMs + elapsed;
       const est = lastProgressMs + elapsed;
       setProgress(est, lastDurationMs);
       setProgress(est, lastDurationMs);
@@ -257,16 +266,31 @@ const layout = {
     elArt.style.backgroundImage = `url(${url})`;
     elArt.style.backgroundImage = `url(${url})`;
   }
   }
 
 
+  function normalizeResponse(data) {
+    if (!data || typeof data !== "object") return data;
+
+    // The external service returns the last-known shape directly. While music is
+    // playing, that shape has no stoppedAtMs and may expose progressMs directly.
+    if ("title" in data || "lastUpdatedAtMs" in data) {
+      const isPlaying =
+        typeof data.isPlaying === "boolean"
+          ? data.isPlaying
+          : Boolean(data.title) && data.stoppedAtMs == null;
+
+      return isPlaying
+        ? { ...data, isPlaying }
+        : { isPlaying: false, lastKnown: data };
+    }
+
+    return data;
+  }
+
   async function refresh() {
   async function refresh() {
     try {
     try {
       const res = await fetch(apiUrl, { cache: "no-store" });
       const res = await fetch(apiUrl, { cache: "no-store" });
       const data = await res.json();
       const data = await res.json();
 
 
-      // External service returns the last-known state directly.
-      const normalized =
-        data && typeof data === "object" && ("title" in data || "lastUpdatedAtMs" in data)
-          ? { isPlaying: false, lastKnown: data }
-          : data;
+      const normalized = normalizeResponse(data);
 
 
       if (!normalized || normalized.isPlaying === false || !normalized.title) {
       if (!normalized || normalized.isPlaying === false || !normalized.title) {
         // If we just transitioned from playing -> not playing, capture stop time.
         // If we just transitioned from playing -> not playing, capture stop time.
@@ -296,46 +320,69 @@ const layout = {
         return;
         return;
       }
       }
 
 
-      elTitle.textContent = data.title;
-      elArtist.textContent = data.artist || "";
-      elAlbum.textContent = data.album || "";
-      setArt(data.albumArtUrl);
+      elTitle.textContent = normalized.title;
+      elArtist.textContent = normalized.artist || "";
+      elAlbum.textContent = normalized.album || "";
+      setArt(normalized.albumArtUrl);
 
 
       // Update the in-memory "last known" track (server persists it).
       // Update the in-memory "last known" track (server persists it).
       lastKnown = {
       lastKnown = {
-        title: data.title ?? null,
-        artist: data.artist ?? null,
-        album: data.album ?? null,
-        albumArtUrl: data.albumArtUrl ?? null,
-        trackUrl: data.trackUrl ?? null,
-        durationMs: typeof data.durationMs === "number" ? data.durationMs : null,
+        title: normalized.title ?? null,
+        artist: normalized.artist ?? null,
+        album: normalized.album ?? null,
+        albumArtUrl: normalized.albumArtUrl ?? null,
+        trackUrl: normalized.trackUrl ?? null,
+        durationMs: typeof normalized.durationMs === "number" ? normalized.durationMs : null,
         stoppedAtMs: null,
         stoppedAtMs: null,
         stoppedProgressMs: null,
         stoppedProgressMs: null,
         lastUpdatedAtMs: Date.now(),
         lastUpdatedAtMs: Date.now(),
       };
       };
 
 
-      const trackKey = `${data.title}::${data.artist}::${data.album}`;
-      const durationMs = typeof data.durationMs === "number" ? data.durationMs : null;
-      const progressMs = typeof data.progressMs === "number" ? data.progressMs : null;
-      const isPlaying = Boolean(data.isPlaying);
-
-      if (lastTrackKey && trackKey !== lastTrackKey) {
+      const trackKey = `${normalized.title}::${normalized.artist}::${normalized.album}`;
+      const durationMs = numberFrom(normalized.durationMs, normalized.duration_ms);
+      const sampledProgressMs = numberFrom(
+        normalized.progressMs,
+        normalized.progress_ms,
+        normalized.stoppedProgressMs,
+        normalized.stopped_progress_ms,
+      );
+      const startedAtMs = numberFrom(normalized.startedAtMs, normalized.started_at_ms);
+      const isPlaying = Boolean(normalized.isPlaying);
+      const sameTrack = lastTrackKey === trackKey;
+
+      if (lastTrackKey && !sameTrack) {
         lastProgressMs = null;
         lastProgressMs = null;
         lastSyncAt = 0;
         lastSyncAt = 0;
       }
       }
 
 
+      const syncAt =
+        numberFrom(normalized.progressUpdatedAtMs, normalized.lastUpdatedAtMs, normalized.last_updated_at_ms) ??
+        Date.now();
+      let progressMs =
+        isPlaying && startedAtMs != null
+          ? Date.now() - startedAtMs
+          : sampledProgressMs ??
+            (isPlaying && sameTrack && typeof lastProgressMs === "number"
+              ? lastProgressMs + Math.max(0, Date.now() - lastSyncAt)
+              : null);
+      // Some older last-known responses only expose that a track is playing, not
+      // exact progress. In that case, animate from page load instead of blanking.
+      if (progressMs == null && isPlaying) progressMs = 0;
+
       lastTrackKey = trackKey;
       lastTrackKey = trackKey;
       lastDurationMs = durationMs;
       lastDurationMs = durationMs;
       lastProgressMs = progressMs;
       lastProgressMs = progressMs;
       lastIsPlaying = isPlaying;
       lastIsPlaying = isPlaying;
-      lastSyncAt = Date.now();
+      // Prefer the explicit track start time when the last-known service sends
+      // it; otherwise use the timestamp for when progress was sampled.
+      lastSyncAt = startedAtMs != null || sampledProgressMs == null ? Date.now() : syncAt;
 
 
       setProgress(progressMs, durationMs);
       setProgress(progressMs, durationMs);
 
 
-      if (isPlaying) startTicking();
+      if (isPlaying && progressMs != null) startTicking();
       else stopTicking();
       else stopTicking();
 
 
-      lastTrackUrl = data.trackUrl || null;
+      lastTrackUrl = normalized.trackUrl || null;
       if (btnOpen instanceof HTMLButtonElement) btnOpen.disabled = !lastTrackUrl;
       if (btnOpen instanceof HTMLButtonElement) btnOpen.disabled = !lastTrackUrl;
       if (elHint) elHint.textContent = "";
       if (elHint) elHint.textContent = "";
     } catch (e) {
     } catch (e) {

+ 13 - 3
src/components/projects.astro

@@ -98,6 +98,11 @@ let projects = Astro.props.projects;
 			window.open(url, '_blank', 'noopener,noreferrer');
 			window.open(url, '_blank', 'noopener,noreferrer');
 		}
 		}
 
 
+		function isWindowVisible() {
+			const windowEl = container.closest('.window');
+			return !windowEl || window.getComputedStyle(windowEl).display !== 'none';
+		}
+
 		function setSelected(projectId) {
 		function setSelected(projectId) {
 			const p = projects[projectId];
 			const p = projects[projectId];
 			if (!p) return;
 			if (!p) return;
@@ -112,24 +117,29 @@ let projects = Astro.props.projects;
 			const openUrl = p.previewUrl;
 			const openUrl = p.previewUrl;
 			if (openBtn) {
 			if (openBtn) {
 				openBtn.disabled = !openUrl;
 				openBtn.disabled = !openUrl;
-				openBtn.onclick = () => openRedirect(openUrl);
+				openBtn.dataset.url = openUrl || '';
 			}
 			}
 			if (repoBtn) {
 			if (repoBtn) {
 				repoBtn.disabled = !p.repoUrl;
 				repoBtn.disabled = !p.repoUrl;
-				repoBtn.onclick = () => openRedirect(p.repoUrl);
+				repoBtn.dataset.url = p.repoUrl || '';
 			}
 			}
 
 
 			if (frameWrap && frame) {
 			if (frameWrap && frame) {
 				if (p.previewUrl) {
 				if (p.previewUrl) {
 					frameWrap.hidden = false;
 					frameWrap.hidden = false;
-					frame.src = p.previewUrl;
+					frame.dataset.src = p.previewUrl;
+					if (isWindowVisible()) frame.src = p.previewUrl;
+					else frame.removeAttribute('src');
 				} else {
 				} else {
 					frameWrap.hidden = true;
 					frameWrap.hidden = true;
 					frame.removeAttribute('src');
 					frame.removeAttribute('src');
+					delete frame.dataset.src;
 				}
 				}
 			}
 			}
 		}
 		}
 
 
+		openBtn?.addEventListener('click', () => openRedirect(openBtn.dataset.url));
+		repoBtn?.addEventListener('click', () => openRedirect(repoBtn.dataset.url));
 
 
 		// Prevent the homepage window-spawning click handler from running.
 		// Prevent the homepage window-spawning click handler from running.
 		container.querySelectorAll('.project').forEach((pbtn) => {
 		container.querySelectorAll('.project').forEach((pbtn) => {

+ 1 - 1
src/components/projects/glance.astro

@@ -16,7 +16,7 @@ mobile: { height: '300px' },
         A customizable dashboard optimized for E-Ink Displays
         A customizable dashboard optimized for E-Ink Displays
     </h2>
     </h2>
 
 
-    <iframe style="height: 100%; width: 100%; display: none;" class="desktop-only" src="https://glance.simo.ng/app"></iframe>
+    <iframe style="height: 100%; width: 100%; display: none;" class="desktop-only" data-src="https://glance.simo.ng/app" loading="lazy"></iframe>
     <style>
     <style>
       @media (min-width: 768px) {
       @media (min-width: 768px) {
         .desktop-only {
         .desktop-only {

+ 1 - 1
src/components/projects/old/imdb.astro

@@ -13,7 +13,7 @@ import Item from '../item.astro';
       One of my first projects, this is an alternative frontend for IMDB that uses TMDB to fetch information about movies.
       One of my first projects, this is an alternative frontend for IMDB that uses TMDB to fetch information about movies.
     </h2>
     </h2>
 
 
-    <iframe src="https://imdb.simo.ng/?search=invincible" width="800" height="400" frameborder="0" allowfullscreen="">
+    <iframe data-src="https://imdb.simo.ng/?search=invincible" loading="lazy" width="800" height="400" frameborder="0" allowfullscreen="">
     </iframe>
     </iframe>
   </div>
   </div>
 </Item>
 </Item>

+ 2 - 2
src/components/projects/summize.astro

@@ -3,7 +3,7 @@ import Item from './item.astro';
 import summizeExample from "../../assets/summizeExample.png";
 import summizeExample from "../../assets/summizeExample.png";
 ---
 ---
 
 
-<Item projectName="summize" title="Summize - A Youtube Video Summarizer" layout={{
+<Item projectName="summize" title="Summize - A YouTube Video Summarizer" layout={{
   mobile: { top: '40%' },
   mobile: { top: '40%' },
   desktop: { width: '70%', height: '45%' }
   desktop: { width: '70%', height: '45%' }
 }}>
 }}>
@@ -11,7 +11,7 @@ import summizeExample from "../../assets/summizeExample.png";
     id="text207"
     id="text207"
     style="font-size: 22px; resize: none; padding: 10px; height:90%; overflow-y: auto; padding-top: 0px;"
     style="font-size: 22px; resize: none; padding: 10px; height:90%; overflow-y: auto; padding-top: 0px;"
   >
   >
-    <a href="https://github.com/S1monlol/summize"><h1 style="width: fit-content;"><i><u><b>Summize - A Youtube Video Summarizer</b></u></i></h1></a>
+    <a href="https://github.com/S1monlol/summize"><h1 style="width: fit-content;"><i><u><b>Summize - A YouTube Video Summarizer</b></u></i></h1></a>
     <img src={summizeExample.src}>
     <img src={summizeExample.src}>
     <h2>
     <h2>
       Summize is a browser extension that summarizes YouTube videos using OpenAI's GPT-3.5 language model.
       Summize is a browser extension that summarizes YouTube videos using OpenAI's GPT-3.5 language model.

+ 1 - 1
src/components/ram.astro

@@ -12,7 +12,7 @@ const layout = {
 		<iframe
 		<iframe
 			id="ram-frame"
 			id="ram-frame"
 			title="RAM"
 			title="RAM"
-			src="/ram/embed"
+			data-src="/ram/embed"
 			style="width: 100%; height: 100%; border: 0; outline: 0; background: white;"
 			style="width: 100%; height: 100%; border: 0; outline: 0; background: white;"
 		></iframe>
 		></iframe>
 	</div>
 	</div>

+ 1 - 1
src/content/blogs/video-Ge3rG2h2nRY.md

@@ -1,7 +1,7 @@
 ---
 ---
 title: "Your projects don't have to matter"
 title: "Your projects don't have to matter"
 pubDate: 2025-11-04
 pubDate: 2025-11-04
-views: 17725
+views: 17765
 description: "YouTube video"
 description: "YouTube video"
 ---
 ---
 
 

+ 1 - 1
src/content/blogs/video-i_qSU-GiBeo.md

@@ -1,7 +1,7 @@
 ---
 ---
 title: "You Need To Stop Rationalizing Irrational Things"
 title: "You Need To Stop Rationalizing Irrational Things"
 pubDate: 2026-03-14
 pubDate: 2026-03-14
-views: 287
+views: 288
 description: "YouTube video"
 description: "YouTube video"
 ---
 ---
 
 

+ 41 - 5
src/layouts/Layout.astro

@@ -4,6 +4,11 @@ interface Props {
 }
 }
 
 
 const { title } = Astro.props;
 const { title } = Astro.props;
+const siteTitle = "Simo";
+const description = "Simon's personal website";
+const canonicalUrl = "https://simo.ng/";
+const socialImage = "https://simo.ng/smaller.gif";
+
 import { ViewTransitions } from "astro:transitions";
 import { ViewTransitions } from "astro:transitions";
 import "../styles/98.css"
 import "../styles/98.css"
 ---
 ---
@@ -13,13 +18,32 @@ import "../styles/98.css"
 	<head>
 	<head>
 		<meta charset="UTF-8" />
 		<meta charset="UTF-8" />
 		<meta name="viewport" content="width=device-width, initial-scale=1" />
 		<meta name="viewport" content="width=device-width, initial-scale=1" />
-		<meta content="https://simo.ng/smaller.gif" name="og:image">
-		<meta content="#ff0504" name="theme-color">
+		<meta name="description" content={description} />
+		<meta name="theme-color" content="#008080" />
+		<link rel="canonical" href={canonicalUrl} />
+
+		<meta property="og:type" content="website" />
+		<meta property="og:url" content={canonicalUrl} />
+		<meta property="og:title" content={siteTitle} />
+		<meta property="og:description" content={description} />
+		<meta property="og:image" content={socialImage} />
+		<meta property="og:image:secure_url" content={socialImage} />
+		<meta property="og:image:type" content="image/gif" />
+		<meta property="og:image:width" content="96" />
+		<meta property="og:image:height" content="96" />
+		<meta property="og:image:alt" content="Simon's character avatar" />
+		<meta name="twitter:card" content="summary" />
+		<meta name="twitter:title" content={siteTitle} />
+		<meta name="twitter:description" content={description} />
+		<meta name="twitter:image" content={socialImage} />
+		<meta name="twitter:image:alt" content="Simon's character avatar" />
+
 		<link type="application/json+oembed" href="https://simo.ng/oembed.json" />
 		<link type="application/json+oembed" href="https://simo.ng/oembed.json" />
-		<link rel="icon" type="image/svg+xml" href="/favicon.gif" />
+		<link rel="icon" href="/smaller.gif" type="image/gif" sizes="96x96" />
+		<link rel="shortcut icon" href="/smaller.gif" type="image/gif" />
 		<!-- <link rel="stylesheet" href="https://unpkg.com/98.css@0.1.16" /> -->
 		<!-- <link rel="stylesheet" href="https://unpkg.com/98.css@0.1.16" /> -->
 		<meta name="generator" content={Astro.generator} />
 		<meta name="generator" content={Astro.generator} />
-		<title>{title}</title>
+		<title>{siteTitle}</title>
 	</head>
 	</head>
 	<body>
 	<body>
 		<slot />
 		<slot />
@@ -55,10 +79,22 @@ import "../styles/98.css"
 		resize: none;
 		resize: none;
 
 
 		overscroll-behavior-y: contain;
 		overscroll-behavior-y: contain;
-		touch-action: none;
+		touch-action: auto;
 	}
 	}
 
 
 	.title-bar {
 	.title-bar {
 		user-select: none;
 		user-select: none;
+		touch-action: none;
+	}
+
+	@media (prefers-reduced-motion: reduce) {
+		*,
+		*::before,
+		*::after {
+			animation-duration: 0.01ms !important;
+			animation-iteration-count: 1 !important;
+			scroll-behavior: auto !important;
+			transition-duration: 0.01ms !important;
+		}
 	}
 	}
 </style>
 </style>

+ 0 - 79
src/lib/spotifyLastKnown.server.ts

@@ -1,79 +0,0 @@
-import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
-import { dirname, resolve } from "node:path";
-
-export type SpotifyLastKnown = {
-	// Track identity
-	title: string | null;
-	artist: string | null;
-	album: string | null;
-	albumArtUrl: string | null;
-	trackUrl: string | null;
-
-	// Playback metadata
-	durationMs: number | null;
-	stoppedAtMs: number | null;
-	stoppedProgressMs: number | null;
-
-	// When the server last successfully synced this track from Spotify.
-	lastUpdatedAtMs: number | null;
-};
-
-const DEFAULT_STATE: SpotifyLastKnown = {
-	title: null,
-	artist: null,
-	album: null,
-	albumArtUrl: null,
-	trackUrl: null,
-	durationMs: null,
-	stoppedAtMs: null,
-	stoppedProgressMs: null,
-	lastUpdatedAtMs: null,
-};
-
-function coerceState(v: unknown): SpotifyLastKnown {
-	if (!v || typeof v !== "object") return { ...DEFAULT_STATE };
-	const o = v as Record<string, unknown>;
-	return {
-		title: typeof o.title === "string" ? o.title : null,
-		artist: typeof o.artist === "string" ? o.artist : null,
-		album: typeof o.album === "string" ? o.album : null,
-		albumArtUrl: typeof o.albumArtUrl === "string" ? o.albumArtUrl : null,
-		trackUrl: typeof o.trackUrl === "string" ? o.trackUrl : null,
-		durationMs: typeof o.durationMs === "number" ? o.durationMs : null,
-		stoppedAtMs: typeof o.stoppedAtMs === "number" ? o.stoppedAtMs : null,
-		stoppedProgressMs:
-			typeof o.stoppedProgressMs === "number" ? o.stoppedProgressMs : null,
-		lastUpdatedAtMs:
-			typeof o.lastUpdatedAtMs === "number" ? o.lastUpdatedAtMs : null,
-	};
-}
-
-function stateFilePath(): string {
-	// Workspace-local file for a single Node server.
-	// If you want to move it, set SPOTIFY_LAST_KNOWN_PATH.
-	const p = process.env.SPOTIFY_LAST_KNOWN_PATH;
-	return resolve(
-		process.cwd(),
-		p && p.trim() ? p : "data/spotify-last-known.json",
-	);
-}
-
-export async function readSpotifyLastKnown(): Promise<SpotifyLastKnown> {
-	const path = stateFilePath();
-	try {
-		const text = await readFile(path, "utf8");
-		return coerceState(JSON.parse(text));
-	} catch {
-		return { ...DEFAULT_STATE };
-	}
-}
-
-export async function writeSpotifyLastKnown(
-	next: SpotifyLastKnown,
-): Promise<void> {
-	const path = stateFilePath();
-	await mkdir(dirname(path), { recursive: true });
-	const tmp = `${path}.tmp`;
-	await writeFile(tmp, JSON.stringify(next, null, 2) + "\n", "utf8");
-	await rename(tmp, path);
-}

+ 0 - 111
src/pages/api/spotify/callback.json.ts

@@ -1,111 +0,0 @@
-export const prerender = true;
-
-const TOKEN_ENDPOINT = "https://accounts.spotify.com/api/token";
-
-function requiredEnv(name: string): string {
-	const v = import.meta.env[name];
-	if (!v) throw new Error(`Missing env var: ${name}`);
-	return v;
-}
-
-export async function GET({ request }: { request: Request }) {
-	try {
-		const reqUrl = new URL(request.url);
-		const code = reqUrl.searchParams.get("code");
-
-		if (!code) {
-			return new Response(
-				[
-					"Missing ?code",
-					"",
-					"If your browser shows ?code=..., but this endpoint doesn't receive it,",
-					"something in front of Astro is stripping the query string.",
-					"",
-					`request.url: ${request.url}`,
-				].join("\n"),
-				{
-					status: 400,
-					headers: { "Content-Type": "text/plain; charset=utf-8" },
-				},
-			);
-		}
-
-		const client_id = requiredEnv("SPOTIFY_CLIENT_ID");
-		const client_secret = requiredEnv("SPOTIFY_CLIENT_SECRET");
-		const redirect_uri =
-			import.meta.env.SPOTIFY_REDIRECT_URI ??
-			`${reqUrl.origin}/api/spotify/callback.json`;
-
-		const basic = Buffer.from(`${client_id}:${client_secret}`).toString(
-			"base64",
-		);
-
-		const res = await fetch(TOKEN_ENDPOINT, {
-			method: "POST",
-			headers: {
-				Authorization: `Basic ${basic}`,
-				"Content-Type": "application/x-www-form-urlencoded",
-			},
-			body: new URLSearchParams({
-				grant_type: "authorization_code",
-				code,
-				redirect_uri,
-			}),
-		});
-
-		const text = await res.text();
-		if (!res.ok) {
-			return new Response(
-				`Spotify token exchange failed (${res.status}).\n\n${text}`,
-				{
-					status: 500,
-					headers: { "Content-Type": "text/plain; charset=utf-8" },
-				},
-			);
-		}
-
-		const data = JSON.parse(text) as {
-			refresh_token?: string;
-			access_token?: string;
-			expires_in?: number;
-			scope?: string;
-			token_type?: string;
-		};
-
-		const refresh = data.refresh_token;
-		if (!refresh) {
-			return new Response(
-				[
-					"No refresh_token returned.",
-					"",
-					"This usually means you already authorized this app before.",
-					"Go to https://www.spotify.com/account/apps/ and remove access, then try again.",
-					"",
-					"Raw response:",
-					text,
-				].join("\n"),
-				{
-					status: 500,
-					headers: { "Content-Type": "text/plain; charset=utf-8" },
-				},
-			);
-		}
-
-		return new Response(
-			[
-				"Refresh token generated successfully.",
-				"",
-				`SPOTIFY_REFRESH_TOKEN=${refresh}`,
-				"",
-				"Put that into your .env (server-side).",
-			].join("\n"),
-			{ status: 200, headers: { "Content-Type": "text/plain; charset=utf-8" } },
-		);
-	} catch (err) {
-		const message = err instanceof Error ? err.message : String(err);
-		return new Response(message, {
-			status: 500,
-			headers: { "Content-Type": "text/plain; charset=utf-8" },
-		});
-	}
-}

+ 0 - 103
src/pages/api/spotify/callback.ts

@@ -1,103 +0,0 @@
-// These endpoints are only used in dev / when deployed with an SSR adapter.
-// Marking them prerenderable keeps `astro build` working for static deploys.
-export const prerender = true;
-
-const TOKEN_ENDPOINT = "https://accounts.spotify.com/api/token";
-
-function requiredEnv(name: string): string {
-	const v = import.meta.env[name];
-	if (!v) throw new Error(`Missing env var: ${name}`);
-	return v;
-}
-
-export async function GET({ request, url }: { request: Request; url: URL }) {
-	try {
-		const reqUrl = new URL(request.url);
-		const code =
-			reqUrl.searchParams.get("code") ?? url.searchParams.get("code");
-		if (!code) {
-			return new Response(
-				[
-					"Missing ?code",
-					"",
-					`request.url: ${request.url}`,
-					`astro.url: ${url.toString()}`,
-				].join("\n"),
-				{
-					status: 400,
-					headers: { "Content-Type": "text/plain; charset=utf-8" },
-				},
-			);
-		}
-
-		const client_id = requiredEnv("SPOTIFY_CLIENT_ID");
-		const client_secret = requiredEnv("SPOTIFY_CLIENT_SECRET");
-		const redirect_uri =
-			import.meta.env.SPOTIFY_REDIRECT_URI ??
-			`${reqUrl.origin}/api/spotify/callback`;
-
-		const basic = Buffer.from(`${client_id}:${client_secret}`).toString(
-			"base64",
-		);
-
-		const res = await fetch(TOKEN_ENDPOINT, {
-			method: "POST",
-			headers: {
-				Authorization: `Basic ${basic}`,
-				"Content-Type": "application/x-www-form-urlencoded",
-			},
-			body: new URLSearchParams({
-				grant_type: "authorization_code",
-				code,
-				redirect_uri,
-			}),
-		});
-
-		const text = await res.text();
-		if (!res.ok) {
-			return new Response(
-				`Spotify token exchange failed (${res.status}).\n\n${text}`,
-				{
-					status: 500,
-					headers: { "Content-Type": "text/plain; charset=utf-8" },
-				},
-			);
-		}
-
-		const data = JSON.parse(text) as {
-			access_token?: string;
-			refresh_token?: string;
-			scope?: string;
-			expires_in?: number;
-			token_type?: string;
-		};
-
-		const refresh = data.refresh_token;
-		if (!refresh) {
-			return new Response(
-				`No refresh_token returned.\n\nThis usually means you already authorized this app before.\nGo to https://www.spotify.com/account/apps/ and remove access, then try again.\n\nRaw response:\n${text}`,
-				{
-					status: 500,
-					headers: { "Content-Type": "text/plain; charset=utf-8" },
-				},
-			);
-		}
-
-		return new Response(
-			[
-				"Refresh token generated successfully.",
-				"",
-				`SPOTIFY_REFRESH_TOKEN=${refresh}`,
-				"",
-				"Put that into your .env (server-side).",
-			].join("\n"),
-			{ status: 200, headers: { "Content-Type": "text/plain; charset=utf-8" } },
-		);
-	} catch (err) {
-		const message = err instanceof Error ? err.message : String(err);
-		return new Response(message, {
-			status: 500,
-			headers: { "Content-Type": "text/plain; charset=utf-8" },
-		});
-	}
-}

+ 0 - 26
src/pages/api/spotify/login.ts

@@ -1,26 +0,0 @@
-export const prerender = true;
-
-export async function GET({ url }: { url: URL }) {
-	const client_id = import.meta.env.SPOTIFY_CLIENT_ID;
-	if (!client_id) {
-		return new Response("Missing SPOTIFY_CLIENT_ID", { status: 500 });
-	}
-
-	// Allow overriding in case you use a different host/port locally.
-	const redirect_uri =
-		import.meta.env.SPOTIFY_REDIRECT_URI ??
-		`${url.origin}/api/spotify/callback.json`;
-
-	const scopes = [
-		"user-read-currently-playing",
-		"user-read-playback-state",
-	].join(" ");
-
-	const authUrl = new URL("https://accounts.spotify.com/authorize");
-	authUrl.searchParams.set("client_id", client_id);
-	authUrl.searchParams.set("response_type", "code");
-	authUrl.searchParams.set("redirect_uri", redirect_uri);
-	authUrl.searchParams.set("scope", scopes);
-
-	return Response.redirect(authUrl.toString(), 302);
-}

+ 0 - 224
src/pages/api/spotify/now-playing.json.ts

@@ -1,224 +0,0 @@
-export const prerender = true;
-
-import {
-	readSpotifyLastKnown,
-	writeSpotifyLastKnown,
-	type SpotifyLastKnown,
-} from "../../../lib/spotifyLastKnown.server";
-
-const NOW_PLAYING_ENDPOINT =
-	"https://api.spotify.com/v1/me/player/currently-playing";
-const TOKEN_ENDPOINT = "https://accounts.spotify.com/api/token";
-
-type SpotifyNowPlaying = {
-	is_playing?: boolean;
-	progress_ms?: number;
-	item?: {
-		name?: string;
-		duration_ms?: number;
-		artists?: { name?: string }[];
-		album?: {
-			name?: string;
-			images?: { url: string; height?: number; width?: number }[];
-		};
-		external_urls?: { spotify?: string };
-	};
-};
-
-function requiredEnv(name: string): string {
-	const v = import.meta.env[name];
-	if (!v) throw new Error(`Missing env var: ${name}`);
-	return v;
-}
-
-let cachedAccessToken: { token: string; expiresAtMs: number } | null = null;
-
-async function getAccessToken(): Promise<string> {
-	const now = Date.now();
-	if (
-		cachedAccessToken &&
-		cachedAccessAccessTokenStillValid(cachedAccessToken, now)
-	) {
-		return cachedAccessToken.token;
-	}
-
-	const client_id = requiredEnv("SPOTIFY_CLIENT_ID");
-	const client_secret = requiredEnv("SPOTIFY_CLIENT_SECRET");
-	const refresh_token = requiredEnv("SPOTIFY_REFRESH_TOKEN");
-
-	const basic = Buffer.from(`${client_id}:${client_secret}`).toString("base64");
-
-	const res = await fetch(TOKEN_ENDPOINT, {
-		method: "POST",
-		headers: {
-			Authorization: `Basic ${basic}`,
-			"Content-Type": "application/x-www-form-urlencoded",
-		},
-		body: new URLSearchParams({
-			grant_type: "refresh_token",
-			refresh_token,
-		}),
-	});
-
-	if (!res.ok) {
-		const text = await res.text().catch(() => "");
-		throw new Error(`Spotify token error: ${res.status} ${text}`);
-	}
-
-	const data = (await res.json()) as {
-		access_token: string;
-		expires_in: number;
-	};
-	// expires_in is seconds; subtract a small skew so we refresh early.
-	cachedAccessToken = {
-		token: data.access_token,
-		expiresAtMs: Date.now() + data.expires_in * 1000 - 30_000,
-	};
-	return cachedAccessToken.token;
-}
-
-function cachedAccessAccessTokenStillValid(
-	cached: { token: string; expiresAtMs: number },
-	nowMs: number,
-): boolean {
-	return Boolean(cached.token) && cached.expiresAtMs > nowMs;
-}
-
-export async function GET() {
-	try {
-		const prev = await readSpotifyLastKnown();
-		const nowMs = Date.now();
-
-		const accessToken = await getAccessToken();
-
-		const res = await fetch(NOW_PLAYING_ENDPOINT, {
-			headers: {
-				Authorization: `Bearer ${accessToken}`,
-			},
-		});
-
-		// 204 No Content means nothing is currently playing
-		if (res.status === 204) {
-			// Capture the stop time once (first time we observe "not playing").
-			// Do NOT overwrite lastUpdatedAtMs here: that should represent the last
-			// time we successfully synced while it was playing.
-			if (prev.title && !prev.stoppedAtMs) {
-				await writeSpotifyLastKnown({
-					...prev,
-					stoppedAtMs: nowMs,
-				});
-			}
-			const lastKnown = await readSpotifyLastKnown();
-			return new Response(JSON.stringify({ isPlaying: false, lastKnown }), {
-				status: 200,
-				headers: {
-					"Content-Type": "application/json; charset=utf-8",
-					"Cache-Control": "no-store",
-				},
-			});
-		}
-
-		if (res.status === 401) {
-			// Token might be expired; clear cache and let client retry on next poll.
-			cachedAccessToken = null;
-			return new Response(
-				JSON.stringify({
-					isPlaying: false,
-					error: "unauthorized",
-					lastKnown: prev,
-				}),
-				{
-					status: 200,
-					headers: {
-						"Content-Type": "application/json; charset=utf-8",
-						"Cache-Control": "no-store",
-					},
-				},
-			);
-		}
-
-		if (!res.ok) {
-			const text = await res.text().catch(() => "");
-			return new Response(
-				JSON.stringify({
-					isPlaying: false,
-					error: `spotify_${res.status}`,
-					detail: text,
-					lastKnown: prev,
-				}),
-				{
-					status: 200,
-					headers: {
-						"Content-Type": "application/json; charset=utf-8",
-						"Cache-Control": "no-store",
-					},
-				},
-			);
-		}
-
-		const data = (await res.json()) as SpotifyNowPlaying;
-		const item = data.item;
-
-		const payload = {
-			isPlaying: Boolean(data.is_playing),
-			title: item?.name ?? null,
-			artist:
-				item?.artists
-					?.map((a) => a?.name)
-					.filter(Boolean)
-					.join(", ") ?? null,
-			album: item?.album?.name ?? null,
-			albumArtUrl: item?.album?.images?.[0]?.url ?? null,
-			progressMs: data.progress_ms ?? null,
-			durationMs: item?.duration_ms ?? null,
-			trackUrl: item?.external_urls?.spotify ?? null,
-		};
-
-		// Persist last-known track server-side.
-		const nextLastKnown: SpotifyLastKnown = {
-			title: payload.title,
-			artist: payload.artist,
-			album: payload.album,
-			albumArtUrl: payload.albumArtUrl,
-			trackUrl: payload.trackUrl,
-			durationMs:
-				typeof payload.durationMs === "number" ? payload.durationMs : null,
-			stoppedAtMs: payload.isPlaying ? null : (prev.stoppedAtMs ?? nowMs),
-			stoppedProgressMs:
-				payload.isPlaying === false
-					? typeof payload.progressMs === "number"
-						? payload.progressMs
-						: prev.stoppedProgressMs
-					: null,
-			// Only update the "last updated" timestamp while playing.
-			// When playback stops, we want the UI to show when it *stopped being updated*.
-			lastUpdatedAtMs: payload.isPlaying ? nowMs : prev.lastUpdatedAtMs,
-		};
-		await writeSpotifyLastKnown(nextLastKnown);
-
-		return new Response(JSON.stringify(payload), {
-			status: 200,
-			headers: {
-				"Content-Type": "application/json; charset=utf-8",
-				"Cache-Control": "no-store",
-			},
-		});
-	} catch (err) {
-		const message = err instanceof Error ? err.message : String(err);
-		return new Response(
-			JSON.stringify({
-				isPlaying: false,
-				error: "server_error",
-				detail: message,
-				lastKnown: await readSpotifyLastKnown().catch(() => null),
-			}),
-			{
-				status: 200,
-				headers: {
-					"Content-Type": "application/json; charset=utf-8",
-					"Cache-Control": "no-store",
-				},
-			},
-		);
-	}
-}

+ 170 - 209
src/pages/index.astro

@@ -32,186 +32,49 @@ import Glance from "../components/projects/glance.astro";
 
 
 import ramIcon from "../assets/apps/blog.png";
 import ramIcon from "../assets/apps/blog.png";
 
 
-function dragElement(elmnt) {
-	var pos1 = 0,
-		pos2 = 0,
-		pos3 = 0,
-		pos4 = 0;
-
-	if (document.getElementById(elmnt.id + "header")) {
-		document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
-		document.getElementById(elmnt.id + "header").ontouchstart = dragMouseDown;
-	} else {
-		elmnt.onmousedown = dragMouseDown;
-		elmnt.ontouchstart = dragMouseDown;
-	}
-
-	function dragMouseDown(e) {
-		// e.preventDefault();
-		raiseUpSpec(elmnt.id);
-		if (e.type == "touchstart") {
-			pos3 = e.touches[0].clientX;
-			pos4 = e.touches[0].clientY;
-		} else {
-			pos3 = e.clientX;
-			pos4 = e.clientY;
-		}
-		document.onmouseup = closeDragElement;
-		document.ontouchend = closeDragElement;
-		document.onmousemove = elementDrag;
-		document.ontouchmove = elementDrag;
-	}
-
-	function elementDrag(e) {
-		// e.preventDefault();
-		if (e.type == "touchmove") {
-			pos1 = pos3 - e.touches[0].clientX;
-			pos2 = pos4 - e.touches[0].clientY;
-			pos3 = e.touches[0].clientX;
-			pos4 = e.touches[0].clientY;
-		} else {
-			pos1 = pos3 - e.clientX;
-			pos2 = pos4 - e.clientY;
-			pos3 = e.clientX;
-			pos4 = e.clientY;
-		}
-
-		elmnt.style.top = elmnt.offsetTop - pos2 + "px";
-		elmnt.style.left = elmnt.offsetLeft - pos1 + "px";
-	}
-
-	function closeDragElement() {
-		document.onmouseup = null;
-		document.ontouchend = null;
-		document.onmousemove = null;
-		document.ontouchmove = null;
-	}
-}
-
-/** Raise up window with given ID */
-function raiseUpSpec(id: string) {
-	console.log("raising ", id);
-	let els = document.getElementsByClassName("window");
-
-	let elArray = Array.from(els as HTMLCollectionOf<HTMLElement>);
-
-	// find the highest z-index
-	let highestIndex = 0;
-
-	for (let element in elArray) {
-		let zindex = parseInt(elArray[element].style.zIndex);
-		if (zindex > highestIndex) {
-			highestIndex = zindex;
-		}
-	}
-
-	// set the highest z-index plus one
-	document.getElementById(id).style.zIndex = (highestIndex + 1).toString();
-}
-
-function updateRedirectMessage() {
-	const redirectElement = document.getElementById("Redirect");
-
-	const redirectUrl = redirectElement.dataset.redirecturl; // Access the data attribute
-
-	const messageElement = redirectElement.querySelector("h2");
-	messageElement.innerHTML = `You are about to be redirected to <a class="redirUrl" style="  cursor: pointer;
-    color: blue;
-    text-decoration: underline; " href="${redirectUrl}"">${redirectUrl.replace("https://", "")}</a>`;
-
-	const buttonElement = redirectElement.querySelector("#okButton");
-	// open redirect url
-	buttonElement.setAttribute(
-		"onclick",
-		`window.location.href = "${redirectUrl}"`
-	);
-}
-
-let apps: app[] = [
+let apps = [
 	{
 	{
 		name: "About Me",
 		name: "About Me",
 		logo: me.src,
 		logo: me.src,
-		onclick: () => {
-			raiseUpSpec("About Me");
-			document.getElementById("About Me").style.display = "block";
-		},
+		windowId: "About Me",
 		row: 1,
 		row: 1,
 	},
 	},
-	// {
-	// 	name: "Blog",
-	// 	logo: blog.src,
-	// 	onclick: () => {
-	// 		raiseUpSpec("Redirect");
-	// 		document.getElementById("Redirect").style.display = "block";
-	// 		document.getElementById("Redirect").dataset.redirecturl =
-	// 			"https://blog.simo.ng/";
-	// 		updateRedirectMessage();
-	// 	},
-	// 	row: 1,
-	// },
 	{
 	{
 		name: "Projects",
 		name: "Projects",
 		logo: projects.src,
 		logo: projects.src,
-		onclick: () => {
-			raiseUpSpec("Projects ");
-			document.getElementById("Projects ").style.display = "block";
-		},
+		windowId: "Projects ",
 		row: 1,
 		row: 1,
 	},
 	},
-	// {
-	// 	name: "Past Work",
-	// 	logo: pastwork.src,
-	// 	row: 1,
-	// },
 	{
 	{
 		name: "Past Work",
 		name: "Past Work",
 		logo: pastwork.src,
 		logo: pastwork.src,
-		onclick: () => {
-			raiseUpSpec("Past Work");
-			document.getElementById("Past Work").style.display = "block";
-		},
+		windowId: "Past Work",
 		row: 1,
 		row: 1,
 	},
 	},
 	{
 	{
 		name: "Git",
 		name: "Git",
 		logo: github.src,
 		logo: github.src,
-		onclick: () => {
-			// ignore
-			raiseUpSpec("Redirect");
-			document.getElementById("Redirect").style.display = "block";
-			document.getElementById("Redirect").dataset.redirecturl =
-				"https://git.simo.ng/simo";
-			updateRedirectMessage();
-		},
+		redirectUrl: "https://git.simo.ng/simo",
 		row: 2,
 		row: 2,
 	},
 	},
 	{
 	{
 		name: "RAM",
 		name: "RAM",
 		logo: ramIcon.src,
 		logo: ramIcon.src,
-		onclick: () => {
-			raiseUpSpec("ram");
-			document.getElementById("ram").style.display = "block";
-		},
+		windowId: "ram",
 		row: 2,
 		row: 2,
 	},
 	},
 	{
 	{
 		name: "Contact",
 		name: "Contact",
 		logo: contact.src,
 		logo: contact.src,
+		windowId: "contact",
 		row: 2,
 		row: 2,
-		onclick: () => {
-			raiseUpSpec("contact");
-			document.getElementById("contact").style.display = "block";
-		},
 	},
 	},
 	{
 	{
 		name: "PGP Key",
 		name: "PGP Key",
 		logo: pgp.src,
 		logo: pgp.src,
+		windowId: "pgp",
+		fullscreen: true,
 		row: 1,
 		row: 1,
-		onclick: () => {
-			raiseUpSpec("pgp");
-			document.getElementById("pgp").classList.add("fullscreen");
-			document.getElementById("pgp").style.display = "block";
-		},
 	},
 	},
 ];
 ];
 
 
@@ -233,20 +96,6 @@ let projectsArr: project[] = [
 const row1Items = apps.filter((item) => item.row === 1);
 const row1Items = apps.filter((item) => item.row === 1);
 const row2Items = apps.filter((item) => item.row === 2);
 const row2Items = apps.filter((item) => item.row === 2);
 
 
-let clientFunctions: Function[] | String[] = [
-	raiseUpSpec,
-	updateRedirectMessage,
-	dragElement,
-	updateRedirectMessage,
-];
-
-clientFunctions = clientFunctions.map((item) => String(item));
-
-apps = apps.map((item) => {
-	item.onclick = String(item.onclick);
-
-	return item;
-});
 ---
 ---
 
 
 <Layout title="Simo">
 <Layout title="Simo">
@@ -273,7 +122,7 @@ apps = apps.map((item) => {
 		<iframe
 		<iframe
 			id="SimoSearch"
 			id="SimoSearch"
 			title="SimoSearch"
 			title="SimoSearch"
-			src="https://search.simo.ng/#embed"
+			data-src="https://search.simo.ng/#embed"
 			loading="lazy"
 			loading="lazy"
 			referrerpolicy="no-referrer"
 			referrerpolicy="no-referrer"
 			allowfullscreen
 			allowfullscreen
@@ -301,7 +150,7 @@ apps = apps.map((item) => {
 	<Taskbar />
 	<Taskbar />
 </Layout>
 </Layout>
 
 
-<script define:vars={{ apps, projectsArr, clientFunctions }}>
+<script define:vars={{ apps, projectsArr }}>
 	let dino = `
 	let dino = `
 	                	██████████████
 	                	██████████████
                       ████░░████████████
                       ████░░████████████
@@ -327,8 +176,126 @@ apps = apps.map((item) => {
 
 
 	console.log(dino);
 	console.log(dino);
 
 
-	for (func in clientFunctions) {
-		eval(clientFunctions[func]);
+	const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+
+	function getWindow(id) {
+		return document.getElementById(id);
+	}
+
+	function raiseUpSpec(id) {
+		const target = getWindow(id);
+		if (!target) return;
+		const windows = Array.from(document.getElementsByClassName('window'));
+		const highestIndex = windows.reduce((highest, element) => {
+			const zindex = parseInt(element.style.zIndex || '0', 10);
+			return Number.isFinite(zindex) && zindex > highest ? zindex : highest;
+		}, 0);
+		target.style.zIndex = String(highestIndex + 1);
+	}
+
+	function loadDeferredIframes(root = document) {
+		const load = (iframe) => {
+			if (!(iframe instanceof HTMLIFrameElement)) return;
+			const src = iframe.dataset.src;
+			if (src && !iframe.src) iframe.src = src;
+		};
+
+		load(root);
+		if ('querySelectorAll' in root) {
+			root.querySelectorAll('iframe[data-src]:not([src])').forEach(load);
+		}
+	}
+
+	function showWindow(id, options = {}) {
+		const windowEl = getWindow(id);
+		if (!windowEl) return;
+		if (options.fullscreen) windowEl.classList.add('fullscreen');
+		windowEl.style.display = 'block';
+		loadDeferredIframes(windowEl);
+		raiseUpSpec(id);
+		const focusTarget = windowEl.querySelector('button, a, input, textarea, select, [tabindex]:not([tabindex="-1"])');
+		if (focusTarget instanceof HTMLElement) focusTarget.focus({ preventScroll: true });
+	}
+
+	function openRedirect(redirectUrl) {
+		const redirectElement = getWindow('Redirect') || getWindow('redirect');
+		if (!redirectElement || !redirectUrl) return;
+		redirectElement.dataset.redirecturl = redirectUrl;
+		updateRedirectMessage();
+		showWindow(redirectElement.id);
+	}
+
+	function updateRedirectMessage() {
+		const redirectElement = getWindow('Redirect') || getWindow('redirect');
+		if (!redirectElement) return;
+		const redirectUrl = redirectElement.dataset.redirecturl;
+		if (!redirectUrl) return;
+
+		const messageElement = redirectElement.querySelector('h2');
+		if (messageElement) {
+			messageElement.textContent = 'You are about to be redirected to ';
+			const link = document.createElement('a');
+			link.className = 'redirUrl';
+			link.href = redirectUrl;
+			link.textContent = redirectUrl.replace(/^https?:\/\//, '');
+			link.style.cssText = 'cursor: pointer; color: blue; text-decoration: underline;';
+			messageElement.appendChild(link);
+		}
+
+		const buttonElement = redirectElement.querySelector('#okButton');
+		if (buttonElement && !buttonElement.dataset.redirectBound) {
+			buttonElement.dataset.redirectBound = 'true';
+			buttonElement.addEventListener('click', () => {
+				const currentUrl = redirectElement.dataset.redirecturl;
+				if (currentUrl) window.location.href = currentUrl;
+			});
+		}
+	}
+
+	function dragElement(elmnt) {
+		let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
+		const header = document.getElementById(elmnt.id + 'header') || elmnt;
+		header.addEventListener('mousedown', dragMouseDown);
+		header.addEventListener('touchstart', dragMouseDown, { passive: true });
+
+		function dragMouseDown(e) {
+			raiseUpSpec(elmnt.id);
+			if (e.type === 'touchstart') {
+				pos3 = e.touches[0].clientX;
+				pos4 = e.touches[0].clientY;
+			} else {
+				pos3 = e.clientX;
+				pos4 = e.clientY;
+			}
+			document.addEventListener('mouseup', closeDragElement);
+			document.addEventListener('touchend', closeDragElement);
+			document.addEventListener('mousemove', elementDrag);
+			document.addEventListener('touchmove', elementDrag, { passive: false });
+		}
+
+		function elementDrag(e) {
+			if (e.cancelable) e.preventDefault();
+			if (e.type === 'touchmove') {
+				pos1 = pos3 - e.touches[0].clientX;
+				pos2 = pos4 - e.touches[0].clientY;
+				pos3 = e.touches[0].clientX;
+				pos4 = e.touches[0].clientY;
+			} else {
+				pos1 = pos3 - e.clientX;
+				pos2 = pos4 - e.clientY;
+				pos3 = e.clientX;
+				pos4 = e.clientY;
+			}
+			elmnt.style.top = elmnt.offsetTop - pos2 + 'px';
+			elmnt.style.left = elmnt.offsetLeft - pos1 + 'px';
+		}
+
+		function closeDragElement() {
+			document.removeEventListener('mouseup', closeDragElement);
+			document.removeEventListener('touchend', closeDragElement);
+			document.removeEventListener('mousemove', elementDrag);
+			document.removeEventListener('touchmove', elementDrag);
+		}
 	}
 	}
 
 
 	Array.from(document.getElementsByClassName("project")).forEach((project) => {
 	Array.from(document.getElementsByClassName("project")).forEach((project) => {
@@ -343,28 +310,22 @@ apps = apps.map((item) => {
 		let id = found.id;
 		let id = found.id;
 
 
 		if (id == "pgpcord") {
 		if (id == "pgpcord") {
-			project.onclick = () => {
+			project.addEventListener("click", () => {
 				setTimeout(() => {
 				setTimeout(() => {
-					raiseUpSpec("redirect");
-					document.getElementById("redirect").style.display = "block";
-					document.getElementById("redirect").dataset.redirecturl =
-						"https://blog.simo.ng/articles/WIP/pgpcord";
-
-					updateRedirectMessage();
+					openRedirect("https://blog.simo.ng/articles/WIP/pgpcord");
 				}, 1);
 				}, 1);
-			};
+			});
 
 
 			return;
 			return;
 		}
 		}
-		project.onclick = () => {
+		project.addEventListener("click", () => {
 			setTimeout(() => {
 			setTimeout(() => {
 				console.log(id);
 				console.log(id);
 				const el = document.getElementsByClassName(id)[0];
 				const el = document.getElementsByClassName(id)[0];
 				if (!el) return;
 				if (!el) return;
-				el.style.display = "block";
-				raiseUpSpec(el.id);
+				showWindow(el.id);
 			}, 1);
 			}, 1);
-		};
+		});
 	});
 	});
 
 
 	Array.from(document.getElementsByClassName("window")).forEach((window) => {
 	Array.from(document.getElementsByClassName("window")).forEach((window) => {
@@ -379,23 +340,12 @@ apps = apps.map((item) => {
 			raiseUpSpec(window.id);
 			raiseUpSpec(window.id);
 		});
 		});
 
 
-		Array.from(document.querySelectorAll(".projectwindow a")).forEach(
-			(link) => {
-				link.addEventListener("click", (e) => {
-					document.getElementById("redirect").style.display = "block";
-					document.getElementById("redirect").dataset.redirecturl = link.href;
-					updateRedirectMessage();
-
-					e.preventDefault();
-
-					setTimeout(() => {
-						raiseUpSpec("redirect");
-					}, 1);
-
-					return false;
-				});
-			}
-		);
+		Array.from(document.querySelectorAll(".projectwindow a")).forEach((link) => {
+			link.addEventListener("click", (e) => {
+				e.preventDefault();
+				openRedirect(link.href);
+			});
+		});
 
 
 		const titleButtons = Array.from(
 		const titleButtons = Array.from(
 			window.querySelectorAll('.title-bar-controls button')
 			window.querySelectorAll('.title-bar-controls button')
@@ -436,17 +386,22 @@ apps = apps.map((item) => {
 	});
 	});
 
 
 	apps.forEach((app) => {
 	apps.forEach((app) => {
-		let itemName = app.name.replace(" ", "");
-
-		// console.log(itemName)
-
-		if (app.onclick == "undefined") {
-			return;
-		}
+		const itemName = app.name.replace(' ', '');
+		const launcher = document.getElementById(itemName);
+		if (!launcher) return;
+
+		const activate = (event) => {
+			event.preventDefault();
+			if (app.redirectUrl) {
+				openRedirect(app.redirectUrl);
+				return;
+			}
+			if (app.windowId) showWindow(app.windowId, { fullscreen: Boolean(app.fullscreen) });
+		};
 
 
-		document.getElementById(itemName).addEventListener("click", () => {
-			const appFunc = eval(app.onclick);
-			appFunc();
+		launcher.addEventListener('click', activate);
+		launcher.addEventListener('keydown', (event) => {
+			if (event.key === 'Enter' || event.key === ' ') activate(event);
 		});
 		});
 	});
 	});
 
 
@@ -462,16 +417,18 @@ apps = apps.map((item) => {
 		const translateX = buttonRect.left - windowRect.left;
 		const translateX = buttonRect.left - windowRect.left;
 		const translateY = buttonRect.top - windowRect.top;
 		const translateY = buttonRect.top - windowRect.top;
 
 
-		window.style.transition = "all 0.3s ease-out";
-		window.style.transform = `translate(${translateX}px, ${translateY}px) scale(0.1)`;
-		window.style.opacity = "0";
+		if (!prefersReducedMotion) {
+			window.style.transition = "all 0.3s ease-out";
+			window.style.transform = `translate(${translateX}px, ${translateY}px) scale(0.1)`;
+			window.style.opacity = "0";
+		}
 
 
 		setTimeout(() => {
 		setTimeout(() => {
 			window.style.display = "none";
 			window.style.display = "none";
 			window.style.transition = "";
 			window.style.transition = "";
 			window.style.transform = "";
 			window.style.transform = "";
 			window.style.opacity = "";
 			window.style.opacity = "";
-		}, 300);
+		}, prefersReducedMotion ? 0 : 300);
 	}
 	}
 
 
 	// Expose a tiny API for legacy components that wire their own controls.
 	// Expose a tiny API for legacy components that wire their own controls.
@@ -488,14 +445,17 @@ apps = apps.map((item) => {
 		const translateX = buttonRect.left - windowRect.left;
 		const translateX = buttonRect.left - windowRect.left;
 		const translateY = buttonRect.top - windowRect.top;
 		const translateY = buttonRect.top - windowRect.top;
 
 
-		window.style.transform = `translate(${translateX}px, ${translateY}px) scale(0.1)`;
-		window.style.opacity = "0";
+		if (!prefersReducedMotion) {
+			window.style.transform = `translate(${translateX}px, ${translateY}px) scale(0.1)`;
+			window.style.opacity = "0";
+		}
 		window.style.display = "block";
 		window.style.display = "block";
 
 
 		removeFromTaskbar(windowId);
 		removeFromTaskbar(windowId);
 		raiseUpSpec(windowId);
 		raiseUpSpec(windowId);
 
 
 		requestAnimationFrame(() => {
 		requestAnimationFrame(() => {
+			if (prefersReducedMotion) return;
 			window.style.transition = "all 0.3s ease-out";
 			window.style.transition = "all 0.3s ease-out";
 			window.style.transform = "translate(0, 0) scale(1)";
 			window.style.transform = "translate(0, 0) scale(1)";
 			window.style.opacity = "1";
 			window.style.opacity = "1";
@@ -587,6 +547,7 @@ apps = apps.map((item) => {
 		startButton.addEventListener('click', () => {
 		startButton.addEventListener('click', () => {
 			const w = document.getElementById('SimoSearch');
 			const w = document.getElementById('SimoSearch');
 			if (!w) return;
 			if (!w) return;
+			loadDeferredIframes(w);
 			w.style.display = 'block';
 			w.style.display = 'block';
 			w.style.zIndex = '9999';
 			w.style.zIndex = '9999';
 		});
 		});

+ 1 - 35
src/pages/ram/embed.astro

@@ -46,44 +46,10 @@ const posts = allPosts
 
 
 	<footer class="ram-embed__status" aria-label="Status">
 	<footer class="ram-embed__status" aria-label="Status">
 		<span>{posts.length} object{posts.length === 1 ? "" : "s"}</span>
 		<span>{posts.length} object{posts.length === 1 ? "" : "s"}</span>
-		<span class="ram-embed__status-hint">Tip: click an icon to open</span>
+		<span class="ram-embed__status-hint">Tip: single-click an icon to open</span>
 	</footer>
 	</footer>
 </div>
 </div>
 
 
-<script>
-	// Lightweight icon selection (single-click highlights, double-click navigates).
-	const root = document.querySelector(".ram-embed");
-	if (root) {
-		const icons = Array.from(root.querySelectorAll(".ram-embed__icon"));
-		let selected = null;
-
-		function setSelected(el) {
-			if (selected) selected.removeAttribute("data-selected");
-			selected = el;
-			if (selected) selected.setAttribute("data-selected", "true");
-		}
-
-		for (const icon of icons) {
-			icon.addEventListener("click", (e: MouseEvent) => {
-				// Allow normal navigation on modified clicks.
-				if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
-				e.preventDefault();
-				setSelected(icon);
-			});
-
-			icon.addEventListener("dblclick", () => {
-				window.location.href = icon.getAttribute("href") || "/ram/embed";
-			});
-		}
-
-		root.addEventListener("click", (e) => {
-			if (!(e.target instanceof Element)) return;
-			if (e.target.closest(".ram-embed__icon")) return;
-			setSelected(null);
-		});
-	}
-</script>
-
 <style>
 <style>
 	:global(html, body) {
 	:global(html, body) {
 		height: 100%;
 		height: 100%;

+ 1 - 1
src/pages/ram/embed/[slug].astro

@@ -92,7 +92,7 @@ const { Content } = await render(entry);
 	}
 	}
 
 
 	.ram-embed-post__article {
 	.ram-embed-post__article {
-		/* Let embedded content (eg YouTube iframes) use full available width. */
+		/* Let embedded content (e.g. YouTube iframes) use full available width. */
 		padding: 0;
 		padding: 0;
 		max-width: none;
 		max-width: none;
 		margin: 0;
 		margin: 0;

+ 14 - 0
src/pages/robots.txt.ts

@@ -0,0 +1,14 @@
+export const prerender = true;
+
+export function GET() {
+	return new Response([
+		"User-agent: *",
+		"Allow: /",
+		"Sitemap: https://simo.ng/sitemap.xml",
+		"",
+	].join("\n"), {
+		headers: {
+			"Content-Type": "text/plain; charset=utf-8",
+		},
+	});
+}

+ 32 - 0
src/pages/sitemap.xml.ts

@@ -0,0 +1,32 @@
+import { getCollection } from "astro:content";
+
+export const prerender = true;
+
+const site = "https://simo.ng";
+
+function urlEntry(path: string, date?: Date) {
+	const loc = new URL(path, site).toString();
+	const lastmod = date ? `\n\t\t<lastmod>${date.toISOString()}</lastmod>` : "";
+	return `\n\t<url>\n\t\t<loc>${loc}</loc>${lastmod}\n\t</url>`;
+}
+
+export async function GET() {
+	const posts = await getCollection("blogs");
+	const visiblePosts = posts.filter((post) => (import.meta.env.PROD ? !post.data.draft : true));
+
+	const urls = [
+		urlEntry("/"),
+		urlEntry("/ram/"),
+		urlEntry("/ram/embed/"),
+		...visiblePosts.flatMap((post) => [
+			urlEntry(`/ram/${post.id}/`, post.data.pubDate),
+			urlEntry(`/ram/embed/${post.id}/`, post.data.pubDate),
+		]),
+	];
+
+	return new Response(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls.join("")}\n</urlset>\n`, {
+		headers: {
+			"Content-Type": "application/xml; charset=utf-8",
+		},
+	});
+}

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
src/styles/98.css


+ 0 - 10
temp

@@ -1,10 +0,0 @@
-CODE="AQAlhlxxKq6CziG_CWA8TXjjOL9J8v-D8ijr-9o7jPSKWqjL169mJgntvGApUsmaGGTAilY1FeyeSFBzit_IIorMicFH6FDLL2bQlh-PSpnR5KF7fa-6fxvVV0IJrO5EuvlnLmjFzQeDY_5wN4zd2eHFvM1U1r0cj4d-0K8B4Rl5IJWTaUjlvKsLf_ykONtiupYhQW8buXgkKefUaKiH-l9z-Y5u_HFhZqfiqnsP-L-unA6Zl4rFOTYe7tCdSG2rmnQdMH4"
-REDIRECT_URI="http://127.0.0.1:4321/api/spotify/callback"
-CLIENT_SECRET="3ebc996495774e27a8fe279e89e1c2cd"
-CLIENT_ID="86b57478283e4fa08dec17a50a307a8c"
-curl -s -X POST "https://accounts.spotify.com/api/token" \
-  -H "Authorization: Basic $(printf "%s:%s" "$CLIENT_ID" "$CLIENT_SECRET" | base64 -w 0)" \
-  -H "Content-Type: application/x-www-form-urlencoded" \
-  --data-urlencode "grant_type=authorization_code" \
-  --data-urlencode "code=$CODE" \
-  --data-urlencode "redirect_uri=$REDIRECT_URI"

+ 0 - 13
temp.js

@@ -1,13 +0,0 @@
-const client_id = "86b57478283e4fa08dec17a50a307a8c";
-const redirect_uri = "http://127.0.0.1:4321/api/spotify/callback.json";
-const scopes = "user-read-currently-playing user-read-playback-state";
-
-const authUrl = new URL("https://accounts.spotify.com/authorize");
-authUrl.searchParams.append("client_id", client_id);
-authUrl.searchParams.append("response_type", "code");
-authUrl.searchParams.append("redirect_uri", redirect_uri);
-authUrl.searchParams.append("scope", scopes);
-
-// Redirect user to this URL
-console.log("Visit this URL to authorize:", authUrl.toString());
-

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov