diff --git a/src/components/Bars.tsx b/src/components/Bars.tsx
index 27f7188..005a875 100644
--- a/src/components/Bars.tsx
+++ b/src/components/Bars.tsx
@@ -2,7 +2,7 @@ import { Typewriter } from "./Typewriter";
import { useBars } from "~/context/bars";
import { onMount, createSignal, Show, For, onCleanup } from "solid-js";
import { api } from "~/lib/api";
-import { insertSoftHyphens } from "~/lib/client-utils";
+import { insertSoftHyphens, glitchText } from "~/lib/client-utils";
import GitHub from "./icons/GitHub";
import LinkedIn from "./icons/LinkedIn";
import { RecentCommits } from "./RecentCommits";
@@ -314,26 +314,7 @@ export function LeftBar() {
setGetLostText(originalText);
// Occasional glitch effect after reveal
- glitchInterval = setInterval(() => {
- if (Math.random() > 0.92) {
- let glitched = "";
- for (let i = 0; i < originalText.length; i++) {
- if (Math.random() > 0.75) {
- glitched +=
- glitchChars[
- Math.floor(Math.random() * glitchChars.length)
- ];
- } else {
- glitched += originalText[i];
- }
- }
- setGetLostText(glitched);
-
- setTimeout(() => {
- setGetLostText(originalText);
- }, 80);
- }
- }, 200);
+ glitchInterval = glitchText(originalText, setGetLostText, 200, 80);
return;
}
}
diff --git a/src/components/DeletionForm.tsx b/src/components/DeletionForm.tsx
index 266c70c..8dd0313 100644
--- a/src/components/DeletionForm.tsx
+++ b/src/components/DeletionForm.tsx
@@ -1,6 +1,6 @@
import { createSignal, createEffect, onCleanup, Show } from "solid-js";
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
-import LoadingSpinner from "~/components/LoadingSpinner";
+import { Spinner } from "~/components/Spinner";
import { getClientCookie } from "~/lib/cookies.client";
export default function DeletionForm() {
@@ -133,7 +133,7 @@ export default function DeletionForm() {
} shadow-maroon flex w-36 justify-center rounded py-3 font-light text-white shadow-lg transition-all duration-300 ease-out`}
>
-
+
}
diff --git a/src/components/LoadingSpinner.tsx b/src/components/LoadingSpinner.tsx
deleted file mode 100644
index 5665cc0..0000000
--- a/src/components/LoadingSpinner.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Spinner } from "~/components/Spinner";
-
-export default function LoadingSpinner(props: {
- height: number;
- width: number;
-}) {
- return (
-
-
-
- );
-}
diff --git a/src/components/RecentCommits.tsx b/src/components/RecentCommits.tsx
index 025604e..6a0516a 100644
--- a/src/components/RecentCommits.tsx
+++ b/src/components/RecentCommits.tsx
@@ -1,6 +1,7 @@
import { Component, For, Show } from "solid-js";
import { Typewriter } from "./Typewriter";
import { SkeletonText, SkeletonBox } from "./SkeletonLoader";
+import { formatRelativeTime } from "~/lib/date-utils";
interface Commit {
sha: string;
@@ -16,28 +17,6 @@ export const RecentCommits: Component<{
title: string;
loading?: boolean;
}> = (props) => {
- const formatDate = (dateString: string) => {
- const date = new Date(dateString);
- const now = new Date();
- const diffMs = now.getTime() - date.getTime();
- const diffMins = Math.floor(diffMs / 60000);
- const diffHours = Math.floor(diffMs / 3600000);
- const diffDays = Math.floor(diffMs / 86400000);
-
- if (diffMins < 60) {
- return `${diffMins}m ago`;
- } else if (diffHours < 24) {
- return `${diffHours}h ago`;
- } else if (diffDays < 7) {
- return `${diffDays}d ago`;
- } else {
- return date.toLocaleDateString("en-US", {
- month: "short",
- day: "numeric"
- });
- }
- };
-
return (
{props.title}
@@ -90,7 +69,10 @@ export const RecentCommits: Component<{
- {formatDate(commit.date)}
+ {formatRelativeTime(commit.date, {
+ style: "short",
+ maxDays: 7
+ })}
diff --git a/src/components/SkeletonLoader.tsx b/src/components/SkeletonLoader.tsx
index f877dfb..171e2d4 100644
--- a/src/components/SkeletonLoader.tsx
+++ b/src/components/SkeletonLoader.tsx
@@ -27,15 +27,3 @@ export function SkeletonText(props: SkeletonProps) {
);
}
-
-export function SkeletonCircle(props: SkeletonProps) {
- return (
-
-
-
- );
-}
diff --git a/src/components/blog/CardLinks.tsx b/src/components/blog/CardLinks.tsx
index ed4f3a8..889ca8c 100644
--- a/src/components/blog/CardLinks.tsx
+++ b/src/components/blog/CardLinks.tsx
@@ -1,6 +1,6 @@
import { createSignal, Show } from "solid-js";
import { A } from "@solidjs/router";
-import LoadingSpinner from "~/components/LoadingSpinner";
+import { Spinner } from "~/components/Spinner";
export interface CardLinksProps {
postTitle: string;
@@ -22,7 +22,7 @@ export default function CardLinks(props: CardLinksProps) {
} mx-auto mb-1 flex rounded px-4 py-2 text-base font-light shadow transition-all duration-300 ease-out active:scale-90`}
>
-
+
@@ -34,7 +34,7 @@ export default function CardLinks(props: CardLinksProps) {
} mx-auto flex rounded px-4 py-2 text-base font-light shadow transition-all duration-300 ease-out active:scale-90`}
>
-
+
diff --git a/src/components/blog/TextEditor.tsx b/src/components/blog/TextEditor.tsx
index a683242..a1de8c5 100644
--- a/src/components/blog/TextEditor.tsx
+++ b/src/components/blog/TextEditor.tsx
@@ -9,6 +9,7 @@ import {
} from "solid-js";
import { useSearchParams, useNavigate } from "@solidjs/router";
import { api } from "~/lib/api";
+import { formatRelativeTime } from "~/lib/date-utils";
import { createTiptapEditor } from "solid-tiptap";
import StarterKit from "@tiptap/starter-kit";
import Link from "@tiptap/extension-link";
@@ -1153,21 +1154,6 @@ export default function TextEditor(props: TextEditorProps) {
return new Date(isoString);
};
- const formatRelativeTime = (date: Date): string => {
- const now = new Date();
- const diffMs = now.getTime() - date.getTime();
- const diffSec = Math.floor(diffMs / 1000);
- const diffMin = Math.floor(diffSec / 60);
- const diffHour = Math.floor(diffMin / 60);
- const diffDay = Math.floor(diffHour / 24);
-
- if (diffSec < 60) return `${diffSec} seconds ago`;
- if (diffMin < 60) return `${diffMin} minute${diffMin === 1 ? "" : "s"} ago`;
- if (diffHour < 24)
- return `${diffHour} hour${diffHour === 1 ? "" : "s"} ago`;
- return `${diffDay} day${diffDay === 1 ? "" : "s"} ago`;
- };
-
const restoreHistory = (index: number) => {
const instance = editor();
if (!instance) return;
@@ -4237,7 +4223,10 @@ export default function TextEditor(props: TextEditorProps) {
{isCurrent ? `>${index() + 1}<` : index() + 1}
- {formatRelativeTime(node.timestamp)}
+ {formatRelativeTime(node.timestamp, {
+ style: "long",
+ includeSeconds: true
+ })}
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
index a6afb2f..39da95d 100644
--- a/src/components/ui/Button.tsx
+++ b/src/components/ui/Button.tsx
@@ -1,5 +1,5 @@
import { JSX, splitProps, Show } from "solid-js";
-import LoadingSpinner from "~/components/LoadingSpinner";
+import { Spinner } from "~/components/Spinner";
export interface ButtonProps extends JSX.ButtonHTMLAttributes {
variant?: "primary" | "secondary" | "danger" | "ghost";
@@ -72,7 +72,7 @@ export default function Button(props: ButtonProps) {
class={`${baseClasses} ${variantClasses()} ${sizeClasses()} ${widthClass()} ${local.class || ""}`}
>
-
+
);
diff --git a/src/lib/date-utils.ts b/src/lib/date-utils.ts
index 5abbdc6..43a2194 100644
--- a/src/lib/date-utils.ts
+++ b/src/lib/date-utils.ts
@@ -16,3 +16,81 @@ export function getSQLFormattedDate(): string {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
+
+export interface FormatRelativeTimeOptions {
+ /**
+ * Style of formatting:
+ * - "short": "5m ago", "2h ago", "3d ago"
+ * - "long": "5 minutes ago", "2 hours ago", "3 days ago"
+ */
+ style?: "short" | "long";
+ /**
+ * Include seconds in the output (only for style="long")
+ */
+ includeSeconds?: boolean;
+ /**
+ * For dates older than this many days, return a formatted date instead
+ * If undefined, always returns relative time
+ */
+ maxDays?: number;
+ /**
+ * Locale options for fallback date formatting when maxDays is exceeded
+ */
+ dateFormatOptions?: Intl.DateTimeFormatOptions;
+}
+
+/**
+ * Formats a date as relative time (e.g., "5 minutes ago", "2h ago")
+ * @param date - Date to format (can be Date object or ISO string)
+ * @param options - Formatting options
+ * @returns Formatted relative time string
+ */
+export function formatRelativeTime(
+ date: Date | string,
+ options: FormatRelativeTimeOptions = {}
+): string {
+ const {
+ style = "short",
+ includeSeconds = false,
+ maxDays,
+ dateFormatOptions
+ } = options;
+
+ const dateObj = typeof date === "string" ? new Date(date) : date;
+ const now = new Date();
+ const diffMs = now.getTime() - dateObj.getTime();
+ const diffSec = Math.floor(diffMs / 1000);
+ const diffMin = Math.floor(diffSec / 60);
+ const diffHour = Math.floor(diffMin / 60);
+ const diffDay = Math.floor(diffHour / 24);
+
+ // If maxDays is specified and exceeded, return formatted date
+ if (maxDays !== undefined && diffDay >= maxDays) {
+ return dateObj.toLocaleDateString(
+ "en-US",
+ dateFormatOptions || { month: "short", day: "numeric" }
+ );
+ }
+
+ if (style === "short") {
+ if (diffMin < 60) {
+ return `${diffMin}m ago`;
+ } else if (diffHour < 24) {
+ return `${diffHour}h ago`;
+ } else {
+ return `${diffDay}d ago`;
+ }
+ } else {
+ // style === "long"
+ if (includeSeconds && diffSec < 60) {
+ return `${diffSec} second${diffSec === 1 ? "" : "s"} ago`;
+ }
+ if (diffMin < 60) {
+ return `${diffMin} minute${diffMin === 1 ? "" : "s"} ago`;
+ }
+ if (diffHour < 24) {
+ return `${diffHour} hour${diffHour === 1 ? "" : "s"} ago`;
+ }
+ return `${diffDay} day${diffDay === 1 ? "" : "s"} ago`;
+ }
+}
diff --git a/src/routes/401.tsx b/src/routes/401.tsx
index 5ec1f0c..44b31a2 100644
--- a/src/routes/401.tsx
+++ b/src/routes/401.tsx
@@ -1,38 +1,23 @@
import { PageHead } from "~/components/PageHead";
import { HttpStatusCode } from "@solidjs/start";
import { useNavigate } from "@solidjs/router";
-import { createEffect, createSignal, For } from "solid-js";
+import { createEffect, createSignal, For, onCleanup } from "solid-js";
import { ERROR_PAGE_CONFIG } from "~/config";
+import { glitchText } from "~/lib/client-utils";
export default function Page_401() {
const navigate = useNavigate();
const [glitchText, setGlitchText] = createSignal("401");
createEffect(() => {
- const glitchChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?~`";
- const originalText = "401";
+ const interval = glitchText(
+ "401",
+ setGlitchText,
+ ERROR_PAGE_CONFIG.GLITCH_INTERVAL_MS,
+ ERROR_PAGE_CONFIG.GLITCH_DURATION_MS
+ );
- const glitchInterval = setInterval(() => {
- if (Math.random() > 0.85) {
- let glitched = "";
- for (let i = 0; i < originalText.length; i++) {
- if (Math.random() > 0.7) {
- glitched +=
- glitchChars[Math.floor(Math.random() * glitchChars.length)];
- } else {
- glitched += originalText[i];
- }
- }
- setGlitchText(glitched);
-
- setTimeout(
- () => setGlitchText(originalText),
- ERROR_PAGE_CONFIG.GLITCH_DURATION_MS
- );
- }
- }, ERROR_PAGE_CONFIG.GLITCH_INTERVAL_MS);
-
- return () => clearInterval(glitchInterval);
+ onCleanup(() => clearInterval(interval));
});
const createParticles = () => {
diff --git a/src/routes/contact.tsx b/src/routes/contact.tsx
index 9f94009..0fe3a8d 100644
--- a/src/routes/contact.tsx
+++ b/src/routes/contact.tsx
@@ -12,7 +12,6 @@ import { PageHead } from "~/components/PageHead";
import { api } from "~/lib/api";
import { getClientCookie, setClientCookie } from "~/lib/cookies.client";
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
-import LoadingSpinner from "~/components/LoadingSpinner";
import RevealDropDown from "~/components/RevealDropDown";
import Input from "~/components/ui/Input";
import { Button } from "~/components/ui/Button";