chore: add formatting
This commit is contained in:
+1
-3
@@ -6,9 +6,7 @@ import { usePublicDrawing } from "./hooks/usePublicDrawing";
|
||||
import { useDrawingSession } from "./hooks/useDrawingSession";
|
||||
import { useThemeTokenSync } from "./hooks/useThemeTokenSync";
|
||||
|
||||
type AppRoute =
|
||||
| { type: "private" }
|
||||
| { type: "public"; slug: string };
|
||||
type AppRoute = { type: "private" } | { type: "public"; slug: string };
|
||||
|
||||
function routeFromPath(pathname = window.location.pathname): AppRoute {
|
||||
if (!pathname.startsWith("/p/")) {
|
||||
|
||||
@@ -22,9 +22,30 @@ type DrawingSidebarProps = {
|
||||
function DrawerIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect x="3.5" y="5" width="17" height="14" rx="2.5" fill="none" stroke="currentColor" strokeWidth="1.8" />
|
||||
<path d="M9 5v14" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||
<path d="M5.75 9h1.5M5.75 12h1.5M5.75 15h1.5" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
|
||||
<rect
|
||||
x="3.5"
|
||||
y="5"
|
||||
width="17"
|
||||
height="14"
|
||||
rx="2.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
/>
|
||||
<path
|
||||
d="M9 5v14"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.75 9h1.5M5.75 12h1.5M5.75 15h1.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -62,23 +83,35 @@ export function DrawingSidebar({
|
||||
onDisablePublication,
|
||||
}: DrawingSidebarProps) {
|
||||
const publicPath = publication.enabled ? `/p/${publication.slug}` : null;
|
||||
const publicationStatus = publication.enabled ? `Published at ${publicPath}` : "Not published";
|
||||
const publicationStatus = publication.enabled
|
||||
? `Published at ${publicPath}`
|
||||
: "Not published";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={open ? "sidebar-backdrop sidebar-backdrop-open" : "sidebar-backdrop"}
|
||||
className={
|
||||
open ? "sidebar-backdrop sidebar-backdrop-open" : "sidebar-backdrop"
|
||||
}
|
||||
onClick={onClose}
|
||||
aria-hidden={!open}
|
||||
/>
|
||||
<aside className={open ? "sidebar sidebar-open" : "sidebar"} aria-hidden={!open}>
|
||||
<aside
|
||||
className={open ? "sidebar sidebar-open" : "sidebar"}
|
||||
aria-hidden={!open}
|
||||
>
|
||||
<div className="sidebar-header">
|
||||
<h1>excali-box</h1>
|
||||
<div className="sidebar-actions">
|
||||
<button type="button" className="primary-button" onClick={onCreate}>
|
||||
New
|
||||
</button>
|
||||
<button type="button" className="icon-button" onClick={onClose} aria-label="Close drawings">
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={onClose}
|
||||
aria-label="Close drawings"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
@@ -87,7 +120,11 @@ export function DrawingSidebar({
|
||||
{drawings.map((drawing) => (
|
||||
<div
|
||||
key={drawing.id}
|
||||
className={drawing.id === activeId ? "drawing-item drawing-item-active" : "drawing-item"}
|
||||
className={
|
||||
drawing.id === activeId
|
||||
? "drawing-item drawing-item-active"
|
||||
: "drawing-item"
|
||||
}
|
||||
>
|
||||
{drawing.id === activeId ? (
|
||||
<div className="drawing-link drawing-link-active">
|
||||
@@ -113,20 +150,30 @@ export function DrawingSidebar({
|
||||
</button>
|
||||
</div>
|
||||
<div className="publication-panel">
|
||||
<label className="publication-label" htmlFor="publication-slug">
|
||||
<label
|
||||
className="publication-label"
|
||||
htmlFor="publication-slug"
|
||||
>
|
||||
Public name
|
||||
</label>
|
||||
<input
|
||||
id="publication-slug"
|
||||
className="title-input publication-input"
|
||||
value={publicationSlug}
|
||||
onChange={(event) => onPublicationSlugChange(event.target.value)}
|
||||
onChange={(event) =>
|
||||
onPublicationSlugChange(event.target.value)
|
||||
}
|
||||
spellCheck={false}
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
/>
|
||||
<div className="publication-actions">
|
||||
<button type="button" className="secondary-button" onClick={onPublish} disabled={publicationBusy}>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={onPublish}
|
||||
disabled={publicationBusy}
|
||||
>
|
||||
{publication.enabled ? "Update link" : "Publish"}
|
||||
</button>
|
||||
<button
|
||||
@@ -140,7 +187,12 @@ export function DrawingSidebar({
|
||||
</div>
|
||||
<div className="publication-status">
|
||||
{publication.enabled ? (
|
||||
<a href={publicPath ?? "#"} target="_blank" rel="noreferrer" className="publication-link">
|
||||
<a
|
||||
href={publicPath ?? "#"}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="publication-link"
|
||||
>
|
||||
{publicationStatus}
|
||||
</a>
|
||||
) : (
|
||||
@@ -150,7 +202,11 @@ export function DrawingSidebar({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button type="button" className="drawing-link" onClick={() => onSelect(drawing.id)}>
|
||||
<button
|
||||
type="button"
|
||||
className="drawing-link"
|
||||
onClick={() => onSelect(drawing.id)}
|
||||
>
|
||||
<span className="drawing-title">{drawing.title}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { DEFAULT_TITLE, type DrawingMeta, type DrawingPublication, type ScenePayload } from "../../core/shared";
|
||||
import {
|
||||
DEFAULT_TITLE,
|
||||
type DrawingMeta,
|
||||
type DrawingPublication,
|
||||
type ScenePayload,
|
||||
} from "../../core/shared";
|
||||
|
||||
type DrawingResponse = DrawingMeta & ScenePayload;
|
||||
type PublicationResponse = DrawingPublication;
|
||||
@@ -24,7 +29,10 @@ function sortDrawings(drawings: DrawingMeta[]): DrawingMeta[] {
|
||||
return [...drawings].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
||||
}
|
||||
|
||||
function replaceMeta(drawings: DrawingMeta[], drawing: DrawingMeta): DrawingMeta[] {
|
||||
function replaceMeta(
|
||||
drawings: DrawingMeta[],
|
||||
drawing: DrawingMeta,
|
||||
): DrawingMeta[] {
|
||||
const filtered = drawings.filter((item) => item.id !== drawing.id);
|
||||
return sortDrawings([drawing, ...filtered]);
|
||||
}
|
||||
@@ -50,8 +58,14 @@ function fetchDrawingList() {
|
||||
return requestJson<DrawingMeta[]>("/api/drawings", { method: "GET" });
|
||||
}
|
||||
|
||||
function isConflictError(error: unknown): error is RequestError & { body: ApiErrorBody & { drawing: DrawingMeta } } {
|
||||
return error instanceof RequestError && error.status === 409 && error.body.drawing !== undefined;
|
||||
function isConflictError(
|
||||
error: unknown,
|
||||
): error is RequestError & { body: ApiErrorBody & { drawing: DrawingMeta } } {
|
||||
return (
|
||||
error instanceof RequestError &&
|
||||
error.status === 409 &&
|
||||
error.body.drawing !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
function sceneFromDrawing(drawing: DrawingResponse): ScenePayload {
|
||||
@@ -71,7 +85,9 @@ export function useDrawingSession() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
||||
const [editorReloadNonce, setEditorReloadNonce] = useState(0);
|
||||
const [publication, setPublication] = useState<DrawingPublication>({ enabled: false });
|
||||
const [publication, setPublication] = useState<DrawingPublication>({
|
||||
enabled: false,
|
||||
});
|
||||
const [publicationSlug, setPublicationSlugState] = useState("");
|
||||
const [publicationBusy, setPublicationBusy] = useState(false);
|
||||
|
||||
@@ -93,17 +109,23 @@ export function useDrawingSession() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const showToast = useCallback((message: string | null, timeoutMs?: number) => {
|
||||
if (toastTimeoutRef.current !== null) {
|
||||
clearTimeout(toastTimeoutRef.current);
|
||||
toastTimeoutRef.current = null;
|
||||
}
|
||||
const showToast = useCallback(
|
||||
(message: string | null, timeoutMs?: number) => {
|
||||
if (toastTimeoutRef.current !== null) {
|
||||
clearTimeout(toastTimeoutRef.current);
|
||||
toastTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
setToastMessage(message);
|
||||
if (message !== null && timeoutMs !== undefined) {
|
||||
toastTimeoutRef.current = window.setTimeout(() => setToastMessage(null), timeoutMs);
|
||||
}
|
||||
}, []);
|
||||
setToastMessage(message);
|
||||
if (message !== null && timeoutMs !== undefined) {
|
||||
toastTimeoutRef.current = window.setTimeout(
|
||||
() => setToastMessage(null),
|
||||
timeoutMs,
|
||||
);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const refreshList = useCallback(async () => {
|
||||
const list = await fetchDrawingList();
|
||||
@@ -111,17 +133,20 @@ export function useDrawingSession() {
|
||||
return list;
|
||||
}, []);
|
||||
|
||||
const applyPublication = useCallback((nextPublication: DrawingPublication) => {
|
||||
publicationEnabledRef.current = nextPublication.enabled;
|
||||
setPublication(nextPublication);
|
||||
const applyPublication = useCallback(
|
||||
(nextPublication: DrawingPublication) => {
|
||||
publicationEnabledRef.current = nextPublication.enabled;
|
||||
setPublication(nextPublication);
|
||||
|
||||
if (nextPublication.enabled) {
|
||||
setPublicationSlugState(nextPublication.slug);
|
||||
return;
|
||||
}
|
||||
if (nextPublication.enabled) {
|
||||
setPublicationSlugState(nextPublication.slug);
|
||||
return;
|
||||
}
|
||||
|
||||
setPublicationSlugState("");
|
||||
}, []);
|
||||
setPublicationSlugState("");
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const loadDrawing = useCallback(
|
||||
async (drawingId: string) => {
|
||||
@@ -132,8 +157,13 @@ export function useDrawingSession() {
|
||||
try {
|
||||
const [list, drawing, nextPublication] = await Promise.all([
|
||||
requestJson<DrawingMeta[]>("/api/drawings", { method: "GET" }),
|
||||
requestJson<DrawingResponse>(`/api/drawings/${drawingId}`, { method: "GET" }),
|
||||
requestJson<PublicationResponse>(`/api/drawings/${drawingId}/publication`, { method: "GET" }),
|
||||
requestJson<DrawingResponse>(`/api/drawings/${drawingId}`, {
|
||||
method: "GET",
|
||||
}),
|
||||
requestJson<PublicationResponse>(
|
||||
`/api/drawings/${drawingId}/publication`,
|
||||
{ method: "GET" },
|
||||
),
|
||||
]);
|
||||
|
||||
if (version !== loadVersionRef.current) {
|
||||
@@ -159,7 +189,11 @@ export function useDrawingSession() {
|
||||
return;
|
||||
}
|
||||
|
||||
setError(loadError instanceof Error ? loadError.message : "Failed to load drawing");
|
||||
setError(
|
||||
loadError instanceof Error
|
||||
? loadError.message
|
||||
: "Failed to load drawing",
|
||||
);
|
||||
} finally {
|
||||
if (version === loadVersionRef.current) {
|
||||
setLoading(false);
|
||||
@@ -183,10 +217,13 @@ export function useDrawingSession() {
|
||||
showToast("Saving...");
|
||||
}
|
||||
|
||||
const promise = requestJson<UpdateResponse>(`/api/drawings/${drawingId}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ ...nextScene, expectedRevision }),
|
||||
})
|
||||
const promise = requestJson<UpdateResponse>(
|
||||
`/api/drawings/${drawingId}`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ ...nextScene, expectedRevision }),
|
||||
},
|
||||
)
|
||||
.then((body) => {
|
||||
setDrawings((current) => replaceMeta(current, body.drawing));
|
||||
if (currentIdRef.current === drawingId) {
|
||||
@@ -200,14 +237,18 @@ export function useDrawingSession() {
|
||||
if (isConflictError(saveError)) {
|
||||
clearPendingSaveTimeout();
|
||||
setError(null);
|
||||
setDrawings((current) => replaceMeta(current, saveError.body.drawing));
|
||||
setDrawings((current) =>
|
||||
replaceMeta(current, saveError.body.drawing),
|
||||
);
|
||||
if (isManual) {
|
||||
showToast("Reloading latest...");
|
||||
}
|
||||
return loadDrawing(drawingId);
|
||||
}
|
||||
|
||||
setError(saveError instanceof Error ? saveError.message : "Save failed");
|
||||
setError(
|
||||
saveError instanceof Error ? saveError.message : "Save failed",
|
||||
);
|
||||
if (isManual) {
|
||||
showToast("Save failed", 2000);
|
||||
}
|
||||
@@ -257,7 +298,10 @@ export function useDrawingSession() {
|
||||
}, [saveScene]);
|
||||
|
||||
const navigateToDrawing = useCallback(
|
||||
async (drawingId: string, options?: { replace?: boolean; flush?: boolean }) => {
|
||||
async (
|
||||
drawingId: string,
|
||||
options?: { replace?: boolean; flush?: boolean },
|
||||
) => {
|
||||
const replace = options?.replace ?? false;
|
||||
const flush = options?.flush ?? true;
|
||||
|
||||
@@ -285,7 +329,9 @@ export function useDrawingSession() {
|
||||
|
||||
const createDrawing = useCallback(async () => {
|
||||
await flushPendingSave();
|
||||
const created = await requestJson<DrawingResponse>("/api/drawings", { method: "POST" });
|
||||
const created = await requestJson<DrawingResponse>("/api/drawings", {
|
||||
method: "POST",
|
||||
});
|
||||
await navigateToDrawing(created.id, { flush: false });
|
||||
}, [flushPendingSave, navigateToDrawing]);
|
||||
|
||||
@@ -299,12 +345,18 @@ export function useDrawingSession() {
|
||||
await flushPendingSave();
|
||||
}
|
||||
|
||||
const response = await requestJson<{ ok: true; nextId: string | null }>(`/api/drawings/${drawingId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await requestJson<{ ok: true; nextId: string | null }>(
|
||||
`/api/drawings/${drawingId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
|
||||
if (drawingId === currentIdRef.current && response.nextId) {
|
||||
await navigateToDrawing(response.nextId, { replace: true, flush: false });
|
||||
await navigateToDrawing(response.nextId, {
|
||||
replace: true,
|
||||
flush: false,
|
||||
});
|
||||
} else {
|
||||
await refreshList();
|
||||
}
|
||||
@@ -324,13 +376,16 @@ export function useDrawingSession() {
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await requestJson<UpdateResponse>(`/api/drawings/${drawingId}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
title: currentTitleRef.current,
|
||||
expectedRevision: loadedRevisionRef.current,
|
||||
}),
|
||||
});
|
||||
const body = await requestJson<UpdateResponse>(
|
||||
`/api/drawings/${drawingId}`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
title: currentTitleRef.current,
|
||||
expectedRevision: loadedRevisionRef.current,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
currentTitleRef.current = body.drawing.title;
|
||||
loadedRevisionRef.current = body.drawing.revision;
|
||||
@@ -339,12 +394,16 @@ export function useDrawingSession() {
|
||||
} catch (renameError) {
|
||||
if (isConflictError(renameError)) {
|
||||
clearPendingSaveTimeout();
|
||||
setDrawings((current) => replaceMeta(current, renameError.body.drawing));
|
||||
setDrawings((current) =>
|
||||
replaceMeta(current, renameError.body.drawing),
|
||||
);
|
||||
await loadDrawing(drawingId);
|
||||
return;
|
||||
}
|
||||
|
||||
setError(renameError instanceof Error ? renameError.message : "Rename failed");
|
||||
setError(
|
||||
renameError instanceof Error ? renameError.message : "Rename failed",
|
||||
);
|
||||
}
|
||||
}, [clearPendingSaveTimeout, loadDrawing]);
|
||||
|
||||
@@ -391,18 +450,23 @@ export function useDrawingSession() {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = await requestJson<PublicationResponse>(`/api/drawings/${currentDrawingId}/publication`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
slug: publicationSlug,
|
||||
}),
|
||||
});
|
||||
const body = await requestJson<PublicationResponse>(
|
||||
`/api/drawings/${currentDrawingId}/publication`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
slug: publicationSlug,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
applyPublication(body);
|
||||
showToast("Published", 2000);
|
||||
} catch (publishError) {
|
||||
setError(publishError instanceof Error ? publishError.message : "Publish failed");
|
||||
setError(
|
||||
publishError instanceof Error ? publishError.message : "Publish failed",
|
||||
);
|
||||
showToast("Publish failed", 2000);
|
||||
} finally {
|
||||
setPublicationBusy(false);
|
||||
@@ -420,15 +484,20 @@ export function useDrawingSession() {
|
||||
showToast("Disabling...");
|
||||
|
||||
try {
|
||||
const body = await requestJson<PublicationResponse>(`/api/drawings/${drawingId}/publication`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ enabled: false }),
|
||||
});
|
||||
const body = await requestJson<PublicationResponse>(
|
||||
`/api/drawings/${drawingId}/publication`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ enabled: false }),
|
||||
},
|
||||
);
|
||||
|
||||
applyPublication(body);
|
||||
showToast("Publication disabled", 2000);
|
||||
} catch (disableError) {
|
||||
setError(disableError instanceof Error ? disableError.message : "Disable failed");
|
||||
setError(
|
||||
disableError instanceof Error ? disableError.message : "Disable failed",
|
||||
);
|
||||
showToast("Disable failed", 2000);
|
||||
} finally {
|
||||
setPublicationBusy(false);
|
||||
@@ -476,9 +545,15 @@ export function useDrawingSession() {
|
||||
const list = await refreshList();
|
||||
const drawingId = currentIdRef.current;
|
||||
const loadedRevision = loadedRevisionRef.current;
|
||||
const serverDrawing = drawingId ? list.find((drawing) => drawing.id === drawingId) : null;
|
||||
const serverDrawing = drawingId
|
||||
? list.find((drawing) => drawing.id === drawingId)
|
||||
: null;
|
||||
|
||||
if (serverDrawing && loadedRevision !== null && serverDrawing.revision > loadedRevision) {
|
||||
if (
|
||||
serverDrawing &&
|
||||
loadedRevision !== null &&
|
||||
serverDrawing.revision > loadedRevision
|
||||
) {
|
||||
clearPendingSaveTimeout();
|
||||
await loadDrawing(serverDrawing.id);
|
||||
}
|
||||
|
||||
@@ -17,11 +17,16 @@ class RequestError extends Error {
|
||||
}
|
||||
|
||||
async function requestPublicDrawing(slug: string): Promise<PublicDrawing> {
|
||||
const response = await fetch(`/api/public/drawings/${encodeURIComponent(slug)}`);
|
||||
const response = await fetch(
|
||||
`/api/public/drawings/${encodeURIComponent(slug)}`,
|
||||
);
|
||||
const body = (await response.json()) as PublicDrawing & { error?: string };
|
||||
|
||||
if (!response.ok) {
|
||||
throw new RequestError(response.status, body.error ?? `Request failed: ${response.status}`);
|
||||
throw new RequestError(
|
||||
response.status,
|
||||
body.error ?? `Request failed: ${response.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
return body;
|
||||
@@ -52,7 +57,11 @@ export function usePublicDrawing(slug: string): PublicDrawingState {
|
||||
}
|
||||
|
||||
setDrawing(null);
|
||||
setError(loadError instanceof Error ? loadError.message : "Failed to load drawing");
|
||||
setError(
|
||||
loadError instanceof Error
|
||||
? loadError.message
|
||||
: "Failed to load drawing",
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) {
|
||||
|
||||
@@ -21,7 +21,9 @@ const EXCALIDRAW_THEME_TOKEN_MAP = [
|
||||
["--overlay-bg-color", "--app-overlay-bg"],
|
||||
] as const;
|
||||
|
||||
export function useThemeTokenSync(appShellRef: RefObject<HTMLDivElement | null>) {
|
||||
export function useThemeTokenSync(
|
||||
appShellRef: RefObject<HTMLDivElement | null>,
|
||||
) {
|
||||
const themeSyncFrameRef = useRef<number | null>(null);
|
||||
|
||||
const syncThemeTokens = useCallback(() => {
|
||||
|
||||
@@ -182,14 +182,8 @@ input {
|
||||
|
||||
.publication-input {
|
||||
font-family:
|
||||
ui-monospace,
|
||||
SFMono-Regular,
|
||||
SFMono-Regular,
|
||||
Menlo,
|
||||
Monaco,
|
||||
Consolas,
|
||||
"Liberation Mono",
|
||||
monospace;
|
||||
ui-monospace, SFMono-Regular, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
"Liberation Mono", monospace;
|
||||
}
|
||||
|
||||
.publication-actions {
|
||||
|
||||
Reference in New Issue
Block a user