Rename StatefulDemo, move socket handling to useSocket, Emboldened skeleton

This commit is contained in:
2024-12-23 16:14:11 -06:00
parent 9a54104bdd
commit 9315fbd985
5 changed files with 99 additions and 61 deletions

View File

@@ -1,10 +1,10 @@
import Badge from "@/components/Badge";
import Emboldened from "@/components/Emboldened";
import useSocket from "@/components/useSocket";
import { cn, plural, type ClassValue } from "@/util";
import { useRef, useState } from "preact/hooks";
import { useEffect } from "preact/hooks";
type StatefulDemoProps = {
type DemoProps = {
class?: ClassValue;
};
@@ -13,51 +13,16 @@ type SessionData = {
downloads: string[];
};
const StatefulDemo = ({ class: className }: StatefulDemoProps) => {
useEffect(() => {
const socket = new WebSocket(
(window.location.protocol === "https:" ? "wss://" : "ws://") +
(import.meta.env.DEV != undefined
? "localhost:5800"
: window.location.host) +
"/ws"
);
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type == undefined)
throw new Error("Received message without type");
switch (data.type) {
case "session":
setSession(data.session);
break;
default:
console.warn("Received unknown message type", data.type);
}
};
socket.onclose = () => {
console.log("WebSocket connection closed");
};
return () => {
socket.close();
};
}, []);
const Demo = ({ class: className }: DemoProps) => {
const { id, downloads } = useSocket();
// TODO: Toasts
const randomBits = (bits: number) =>
Math.floor(Math.random() * 2 ** bits)
.toString(16)
.padStart(bits / 4, "0")
.toUpperCase();
const [session, setSession] = useState<SessionData | null>({
id: "0×" + randomBits(32),
downloads: Array.from({ length: 7 }).map(() => "0×" + randomBits(16)),
});
const [highlightedIndex, setHighlightedIndex] = useState<number | null>(null);
const highlightedTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -81,19 +46,18 @@ const StatefulDemo = ({ class: className }: StatefulDemoProps) => {
browser. Each download gets a unique identifier bound to the user
session.
<br />
{session != null ? (
<>
Your session is{" "}
<Emboldened copyable={true}>{session.id}</Emboldened>. You have{" "}
<Emboldened className="text-teal-400 font-inter">
{session.downloads.length}
</Emboldened>{" "}
known {plural("download", session.downloads.length)}.
</>
) : null}
Your session is{" "}
<Emboldened skeletonWidth="0x12345678" copyable={true}>
{id}
</Emboldened>
. You have{" "}
<Emboldened className="text-teal-400 font-inter">
{downloads?.length ?? null}
</Emboldened>{" "}
known {plural("download", downloads?.length ?? 0)}.
</p>
<div class="flex flex-wrap justify-center gap-y-2.5">
{session?.downloads.map((download, i) => (
{downloads?.map((download, i) => (
<Badge
className={cn(
"transition-colors border hover:border-zinc-500 duration-100 ease-in border-transparent",
@@ -111,6 +75,7 @@ const StatefulDemo = ({ class: className }: StatefulDemoProps) => {
{download}
</Badge>
))}
<Badge>download</Badge>
</div>
<div class="mt-4 p-2 bg-zinc-900/90 rounded-md border border-zinc-700">
<p class="my-0">
@@ -123,4 +88,4 @@ const StatefulDemo = ({ class: className }: StatefulDemoProps) => {
);
};
export default StatefulDemo;
export default Demo;

View File

@@ -1,29 +1,38 @@
import { cn, type ClassValue } from "@/util";
type EmboldenedProps = {
children: string | number;
children: string | number | null;
skeletonWidth?: string;
className?: ClassValue;
copyable?: boolean;
};
const Emboldened = ({ children, copyable, className }: EmboldenedProps) => {
const Emboldened = ({
children,
skeletonWidth,
copyable,
className,
}: EmboldenedProps) => {
function copyToClipboard() {
// Copy to clipboard
navigator.clipboard.writeText(children.toString());
if (children != null) navigator.clipboard.writeText(children.toString());
}
return (
<span
onClick={copyable ? copyToClipboard : undefined}
onClick={copyable && children != null ? copyToClipboard : undefined}
className={cn(
className,
"bg-zinc-900/40 rounded border border-zinc-700 py-0.5 px-1 font-mono text-teal-400",
{
"cursor-pointer": copyable,
"cursor-pointer": copyable && children,
}
)}
>
{children}
{children ?? (
<span class="animate-pulse bg-teal-800 max-h-1 overflow-hidden select-none text-transparent">
{skeletonWidth ?? "?"}
</span>
)}
</span>
);
};

View File

@@ -0,0 +1,63 @@
import { useEffect, useState } from "preact/hooks";
interface Download {
token: number;
filename: string;
last_used: string;
download_time: string;
}
interface UseSocketResult {
sessionId: string | null;
downloads: Download[] | null;
deleteDownload: (id: string) => void;
}
function useSocket(): UseSocketResult {
const [sessionId, setSessionId] = useState<string | null>(null);
const [sessionDownloads, setSessionDownloads] = useState<Download[] | null>(
null
);
function deleteDownload() {}
useEffect(() => {
const socket = new WebSocket(
(window.location.protocol === "https:" ? "wss://" : "ws://") +
(import.meta.env.DEV != undefined
? "localhost:5800"
: window.location.host) +
"/ws"
);
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type == undefined)
throw new Error("Received message without type");
switch (data.type) {
case "state":
const downloads = data.downloads as Download[];
setSessionId(data.session);
setSessionDownloads(downloads);
break;
default:
console.warn("Received unknown message type", data.type);
}
};
socket.onclose = () => {
console.log("WebSocket connection closed");
};
return () => {
// Close the socket when the component is unmounted
socket.close();
};
}, []);
return { sessionId, sessionDownloads, deleteDownload };
}
export default useSocket;

View File

@@ -1,6 +1,6 @@
---
import Base from "@/layouts/Base.astro";
import StatefulDemo from "@/components/StatefulDemo.tsx";
import Demo from "@/components/Demo";
---
<Base>
@@ -43,7 +43,7 @@ import StatefulDemo from "@/components/StatefulDemo.tsx";
<span class="px-3 text-2xl font-bebas tracking-wide">Demo</span>
<hr class="w-32 h-px border-0 bg-zinc-600 my-0" />
</div>
<StatefulDemo client:load />
<Demo client:load />
</div>
</div>
</div>