consolidation
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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]">
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
const glitchInterval = setInterval(() => {
|
ERROR_PAGE_CONFIG.GLITCH_INTERVAL_MS,
|
||||||
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_DURATION_MS
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}, ERROR_PAGE_CONFIG.GLITCH_INTERVAL_MS);
|
|
||||||
|
|
||||||
return () => clearInterval(glitchInterval);
|
onCleanup(() => clearInterval(interval));
|
||||||
});
|
});
|
||||||
|
|
||||||
const createParticles = () => {
|
const createParticles = () => {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user