consolidation

This commit is contained in:
Michael Freno
2026-01-06 13:51:47 -05:00
parent 133800f2e3
commit a11f1fee50
11 changed files with 106 additions and 116 deletions

View File

@@ -2,7 +2,7 @@ import { Typewriter } from "./Typewriter";
import { useBars } from "~/context/bars"; import { useBars } from "~/context/bars";
import { onMount, createSignal, Show, For, onCleanup } from "solid-js"; import { onMount, createSignal, Show, For, onCleanup } from "solid-js";
import { api } from "~/lib/api"; import { api } from "~/lib/api";
import { insertSoftHyphens } from "~/lib/client-utils"; import { insertSoftHyphens, glitchText } from "~/lib/client-utils";
import GitHub from "./icons/GitHub"; import GitHub from "./icons/GitHub";
import LinkedIn from "./icons/LinkedIn"; import LinkedIn from "./icons/LinkedIn";
import { RecentCommits } from "./RecentCommits"; import { RecentCommits } from "./RecentCommits";
@@ -314,26 +314,7 @@ export function LeftBar() {
setGetLostText(originalText); setGetLostText(originalText);
// Occasional glitch effect after reveal // Occasional glitch effect after reveal
glitchInterval = setInterval(() => { glitchInterval = glitchText(originalText, setGetLostText, 200, 80);
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);
return; return;
} }
} }

View File

@@ -1,6 +1,6 @@
import { createSignal, createEffect, onCleanup, Show } from "solid-js"; import { createSignal, createEffect, onCleanup, Show } from "solid-js";
import CountdownCircleTimer from "~/components/CountdownCircleTimer"; import CountdownCircleTimer from "~/components/CountdownCircleTimer";
import LoadingSpinner from "~/components/LoadingSpinner"; import { Spinner } from "~/components/Spinner";
import { getClientCookie } from "~/lib/cookies.client"; import { getClientCookie } from "~/lib/cookies.client";
export default function DeletionForm() { 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`} } shadow-maroon flex w-36 justify-center rounded py-3 font-light text-white shadow-lg transition-all duration-300 ease-out`}
> >
<Show when={loading()} fallback="Send Deletion Request"> <Show when={loading()} fallback="Send Deletion Request">
<LoadingSpinner height={24} width={24} /> <Spinner size={24} />
</Show> </Show>
</button> </button>
} }

View File

@@ -1,12 +0,0 @@
import { Spinner } from "~/components/Spinner";
export default function LoadingSpinner(props: {
height: number;
width: number;
}) {
return (
<div class="flex w-full justify-center">
<Spinner size={props.height} />
</div>
);
}

View File

@@ -1,6 +1,7 @@
import { Component, For, Show } from "solid-js"; import { Component, For, Show } from "solid-js";
import { Typewriter } from "./Typewriter"; import { Typewriter } from "./Typewriter";
import { SkeletonText, SkeletonBox } from "./SkeletonLoader"; import { SkeletonText, SkeletonBox } from "./SkeletonLoader";
import { formatRelativeTime } from "~/lib/date-utils";
interface Commit { interface Commit {
sha: string; sha: string;
@@ -16,28 +17,6 @@ export const RecentCommits: Component<{
title: string; title: string;
loading?: boolean; loading?: boolean;
}> = (props) => { }> = (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 ( return (
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<h3 class="text-subtext0 text-sm font-semibold">{props.title}</h3> <h3 class="text-subtext0 text-sm font-semibold">{props.title}</h3>
@@ -90,7 +69,10 @@ export const RecentCommits: Component<{
</span> </span>
</div> </div>
<span class="text-subtext1 shrink-0 text-[10px]"> <span class="text-subtext1 shrink-0 text-[10px]">
{formatDate(commit.date)} {formatRelativeTime(commit.date, {
style: "short",
maxDays: 7
})}
</span> </span>
<div class="flex min-w-0 items-center gap-2 overflow-hidden"> <div class="flex min-w-0 items-center gap-2 overflow-hidden">
<span class="bg-surface1 shrink-0 rounded px-1.5 py-0.5 font-mono text-[10px]"> <span class="bg-surface1 shrink-0 rounded px-1.5 py-0.5 font-mono text-[10px]">

View File

@@ -27,15 +27,3 @@ export function SkeletonText(props: SkeletonProps) {
</div> </div>
); );
} }
export function SkeletonCircle(props: SkeletonProps) {
return (
<div
class={`bg-surface0 flex items-center justify-center rounded-full ${props.class || ""}`}
aria-label="Loading..."
role="status"
>
<Spinner size="md" />
</div>
);
}

View File

@@ -1,6 +1,6 @@
import { createSignal, Show } from "solid-js"; import { createSignal, Show } from "solid-js";
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import LoadingSpinner from "~/components/LoadingSpinner"; import { Spinner } from "~/components/Spinner";
export interface CardLinksProps { export interface CardLinksProps {
postTitle: string; 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`} } mx-auto mb-1 flex rounded px-4 py-2 text-base font-light shadow transition-all duration-300 ease-out active:scale-90`}
> >
<Show when={readLoading()} fallback="Read"> <Show when={readLoading()} fallback="Read">
<LoadingSpinner height={24} width={24} /> <Spinner size={24} />
</Show> </Show>
</A> </A>
<Show when={props.privilegeLevel === "admin"}> <Show when={props.privilegeLevel === "admin"}>
@@ -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`} } mx-auto flex rounded px-4 py-2 text-base font-light shadow transition-all duration-300 ease-out active:scale-90`}
> >
<Show when={editLoading()} fallback="Edit"> <Show when={editLoading()} fallback="Edit">
<LoadingSpinner height={24} width={24} /> <Spinner size={24} />
</Show> </Show>
</A> </A>
</Show> </Show>

View File

@@ -9,6 +9,7 @@ import {
} from "solid-js"; } from "solid-js";
import { useSearchParams, useNavigate } from "@solidjs/router"; import { useSearchParams, useNavigate } from "@solidjs/router";
import { api } from "~/lib/api"; import { api } from "~/lib/api";
import { formatRelativeTime } from "~/lib/date-utils";
import { createTiptapEditor } from "solid-tiptap"; import { createTiptapEditor } from "solid-tiptap";
import StarterKit from "@tiptap/starter-kit"; import StarterKit from "@tiptap/starter-kit";
import Link from "@tiptap/extension-link"; import Link from "@tiptap/extension-link";
@@ -1153,21 +1154,6 @@ export default function TextEditor(props: TextEditorProps) {
return new Date(isoString); 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 restoreHistory = (index: number) => {
const instance = editor(); const instance = editor();
if (!instance) return; if (!instance) return;
@@ -4237,7 +4223,10 @@ export default function TextEditor(props: TextEditorProps) {
{isCurrent ? `>${index() + 1}<` : index() + 1} {isCurrent ? `>${index() + 1}<` : index() + 1}
</span> </span>
<span class="text-text text-sm"> <span class="text-text text-sm">
{formatRelativeTime(node.timestamp)} {formatRelativeTime(node.timestamp, {
style: "long",
includeSeconds: true
})}
</span> </span>
</div> </div>
<Show when={isCurrent}> <Show when={isCurrent}>

View File

@@ -1,5 +1,5 @@
import { JSX, splitProps, Show } from "solid-js"; import { JSX, splitProps, Show } from "solid-js";
import LoadingSpinner from "~/components/LoadingSpinner"; import { Spinner } from "~/components/Spinner";
export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> { export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "danger" | "ghost"; variant?: "primary" | "secondary" | "danger" | "ghost";
@@ -72,7 +72,7 @@ export default function Button(props: ButtonProps) {
class={`${baseClasses} ${variantClasses()} ${sizeClasses()} ${widthClass()} ${local.class || ""}`} class={`${baseClasses} ${variantClasses()} ${sizeClasses()} ${widthClass()} ${local.class || ""}`}
> >
<Show when={local.loading} fallback={local.children}> <Show when={local.loading} fallback={local.children}>
<LoadingSpinner height={24} width={24} /> <Spinner size={24} />
</Show> </Show>
</button> </button>
); );

View File

@@ -16,3 +16,81 @@ export function getSQLFormattedDate(): string {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; 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`;
}
}

View File

@@ -1,38 +1,23 @@
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { HttpStatusCode } from "@solidjs/start"; import { HttpStatusCode } from "@solidjs/start";
import { useNavigate } from "@solidjs/router"; 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 { ERROR_PAGE_CONFIG } from "~/config";
import { glitchText } from "~/lib/client-utils";
export default function Page_401() { export default function Page_401() {
const navigate = useNavigate(); const navigate = useNavigate();
const [glitchText, setGlitchText] = createSignal("401"); const [glitchText, setGlitchText] = createSignal("401");
createEffect(() => { createEffect(() => {
const glitchChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?~`"; const interval = glitchText(
const originalText = "401"; "401",
setGlitchText,
ERROR_PAGE_CONFIG.GLITCH_INTERVAL_MS,
ERROR_PAGE_CONFIG.GLITCH_DURATION_MS
);
const glitchInterval = setInterval(() => { onCleanup(() => clearInterval(interval));
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);
}); });
const createParticles = () => { const createParticles = () => {

View File

@@ -12,7 +12,6 @@ import { PageHead } from "~/components/PageHead";
import { api } from "~/lib/api"; import { api } from "~/lib/api";
import { getClientCookie, setClientCookie } from "~/lib/cookies.client"; import { getClientCookie, setClientCookie } from "~/lib/cookies.client";
import CountdownCircleTimer from "~/components/CountdownCircleTimer"; import CountdownCircleTimer from "~/components/CountdownCircleTimer";
import LoadingSpinner from "~/components/LoadingSpinner";
import RevealDropDown from "~/components/RevealDropDown"; import RevealDropDown from "~/components/RevealDropDown";
import Input from "~/components/ui/Input"; import Input from "~/components/ui/Input";
import { Button } from "~/components/ui/Button"; import { Button } from "~/components/ui/Button";