v3: error cards across the app, friendly network errors, code quality pass
This commit is contained in:
@@ -4,7 +4,9 @@ import { useAuth } from "../hooks/useAuth.ts";
|
||||
import { DumpCreateModal } from "./DumpCreateModal.tsx";
|
||||
import { NotificationBell } from "./NotificationBell.tsx";
|
||||
|
||||
export function AppHeader({ centerSlot }: { centerSlot?: ReactNode }) {
|
||||
export function AppHeader(
|
||||
{ centerSlot, disableNew }: { centerSlot?: ReactNode; disableNew?: boolean },
|
||||
) {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const headerRef = useRef<HTMLElement>(null);
|
||||
@@ -55,6 +57,8 @@ export function AppHeader({ centerSlot }: { centerSlot?: ReactNode }) {
|
||||
type="button"
|
||||
className="btn-primary"
|
||||
onClick={() => setCreateModalOpen(true)}
|
||||
disabled={disableNew}
|
||||
title={disableNew ? "Server unreachable" : undefined}
|
||||
>
|
||||
+ New
|
||||
</button>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { deserializeComment } from "../model.ts";
|
||||
import { Avatar } from "./Avatar.tsx";
|
||||
import { Markdown } from "./Markdown.tsx";
|
||||
import { relativeTime } from "../utils/relativeTime.ts";
|
||||
import { ErrorCard } from "./ErrorCard.tsx";
|
||||
|
||||
interface CommentThreadProps {
|
||||
dumpId: string;
|
||||
@@ -204,7 +205,9 @@ function CommentNode({
|
||||
placeholder="Write a reply…"
|
||||
rows={3}
|
||||
/>
|
||||
{replyError && <p className="comment-form-error">{replyError}</p>}
|
||||
{replyError && (
|
||||
<ErrorCard title="Failed to post reply" message={replyError} />
|
||||
)}
|
||||
<div className="comment-form-actions">
|
||||
<button
|
||||
type="submit"
|
||||
@@ -323,7 +326,7 @@ export function CommentThread({
|
||||
rows={3}
|
||||
/>
|
||||
{topLevelError && (
|
||||
<p className="comment-form-error">{topLevelError}</p>
|
||||
<ErrorCard title="Failed to post comment" message={topLevelError} />
|
||||
)}
|
||||
<div className="comment-form-actions">
|
||||
<button
|
||||
|
||||
@@ -18,6 +18,8 @@ import RichContentCard from "./RichContentCard.tsx";
|
||||
import { MediaPlayer } from "./MediaPlayer.tsx";
|
||||
import type { RichContent } from "../model.ts";
|
||||
import { PlaylistCreateForm } from "./PlaylistCreateForm.tsx";
|
||||
import { ErrorCard } from "./ErrorCard.tsx";
|
||||
import { friendlyFetchError } from "../utils/apiError.ts";
|
||||
|
||||
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
||||
|
||||
@@ -219,8 +221,6 @@ export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
|
||||
});
|
||||
}
|
||||
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const apiResponse = await res.json();
|
||||
if (apiResponse.success) {
|
||||
const dump = deserializeDump(apiResponse.data as RawDump);
|
||||
@@ -244,14 +244,11 @@ export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
|
||||
} else {
|
||||
setSubmitState({
|
||||
status: "error",
|
||||
error: apiResponse.error.message,
|
||||
error: apiResponse.error?.message ?? "Failed to create dump.",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setSubmitState({
|
||||
status: "error",
|
||||
error: err instanceof Error ? err.message : "Failed to create dump.",
|
||||
});
|
||||
setSubmitState({ status: "error", error: friendlyFetchError(err) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -340,7 +337,10 @@ export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
|
||||
|
||||
<form onSubmit={handleSubmit} className="dump-form">
|
||||
{submitState.status === "error" && (
|
||||
<p className="form-error">{submitState.error}</p>
|
||||
<ErrorCard
|
||||
title="Failed to post"
|
||||
message={submitState.error}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode === "url"
|
||||
|
||||
20
src/components/ErrorCard.tsx
Normal file
20
src/components/ErrorCard.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function ErrorCard({ title, message, actions }: {
|
||||
title: string;
|
||||
message: string;
|
||||
actions?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="error-card-wrap">
|
||||
<div className="error-card" role="alert">
|
||||
<span className="error-card-icon">⚠</span>
|
||||
<div className="error-card-body">
|
||||
<h2 className="error-card-title">{title}</h2>
|
||||
<p className="error-card-message">{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
{actions && <div className="error-card-actions">{actions}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { PageShell } from "./PageShell.tsx";
|
||||
import { ErrorCard } from "./ErrorCard.tsx";
|
||||
|
||||
export function PageError({ message, actions }: {
|
||||
message: string;
|
||||
actions?: ReactNode;
|
||||
}) {
|
||||
export function PageError(
|
||||
{ title = "Something went wrong", message, actions }: {
|
||||
title?: string;
|
||||
message: string;
|
||||
actions?: ReactNode;
|
||||
},
|
||||
) {
|
||||
return (
|
||||
<PageShell>
|
||||
<div className="page-error">
|
||||
<h2>Error</h2>
|
||||
<p>{message}</p>
|
||||
{actions && <div className="page-error-actions">{actions}</div>}
|
||||
<div className="page-error-wrap">
|
||||
<ErrorCard title={title} message={message} actions={actions} />
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { API_URL } from "../config/api.ts";
|
||||
import type { Playlist, RawPlaylist } from "../model.ts";
|
||||
import { deserializePlaylist } from "../model.ts";
|
||||
import { useAuth } from "../hooks/useAuth.ts";
|
||||
import { ErrorCard } from "./ErrorCard.tsx";
|
||||
|
||||
interface PlaylistCreateFormProps {
|
||||
/** If provided, the new playlist will have this dump added to it. */
|
||||
@@ -88,7 +89,7 @@ export function PlaylistCreateForm(
|
||||
Private
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="form-error">{error}</p>}
|
||||
{error && <ErrorCard title="Failed to create playlist" message={error} />}
|
||||
<div className="form-actions">
|
||||
<div className="form-actions-right">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user