From a11f1fee50c9ce893b9236a85f5b92a80d54a2b3 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Tue, 6 Jan 2026 13:51:47 -0500 Subject: [PATCH] consolidation --- src/components/Bars.tsx | 23 +-------- src/components/DeletionForm.tsx | 4 +- src/components/LoadingSpinner.tsx | 12 ----- src/components/RecentCommits.tsx | 28 ++--------- src/components/SkeletonLoader.tsx | 12 ----- src/components/blog/CardLinks.tsx | 6 +-- src/components/blog/TextEditor.tsx | 21 ++------ src/components/ui/Button.tsx | 4 +- src/lib/date-utils.ts | 78 ++++++++++++++++++++++++++++++ src/routes/401.tsx | 33 ++++--------- src/routes/contact.tsx | 1 - 11 files changed, 106 insertions(+), 116 deletions(-) delete mode 100644 src/components/LoadingSpinner.tsx 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";