Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"vitest": "5.0.1"
},
"dependencies": {
"@project-graphite/ui": "https://github.com/project-graphite/graphite-ui/releases/download/v0.2.1/project-graphite-ui-0.2.1.tgz",
"@project-graphite/ui": "https://github.com/project-graphite/graphite-ui/releases/download/v0.3.0/project-graphite-ui-0.3.0.tgz",
"uqr": "0.1.3"
}
}
15 changes: 14 additions & 1 deletion app/frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ function reportOutage() {

const gatewayStatuses = [500, 502, 503, 504];

const statusMessages: Record<number, string> = {
400: 'Something in that request was not accepted. Check it and try again.',
401: 'Your session has ended. Sign in again.',
403: 'You don’t have permission to do that.',
404: 'That no longer exists, or you can’t see it.',
409: 'Someone changed this at the same time. Reload and try again.',
413: 'That is too large.',
429: 'Too many attempts. Wait a minute, then try again.',
};

export const readBlob = (response: Response) => response.blob();

async function readJson(response: Response) {
Expand Down Expand Up @@ -77,7 +87,10 @@ export async function apiRequest<T>(
const message = Array.isArray(body?.message)
? body.message.join(', ')
: body?.message;
throw new ApiError(message ?? `Something went wrong (error ${response.status}). Try again.`, response.status);
throw new ApiError(
message ?? statusMessages[response.status] ?? `Something went wrong (error ${response.status}). Try again.`,
response.status,
);
}
return (await read(response)) as T;
}
27 changes: 27 additions & 0 deletions app/frontend/src/components/CrashBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Component, type ReactNode } from 'react';
import { ErrorState } from '@project-graphite/ui';

export class CrashBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
state = { failed: false };

static getDerivedStateFromError() {
return { failed: true };
}

render() {
if (!this.state.failed) return this.props.children;
return (
<ErrorState
action={
<a className="secondary-button px-3 py-2 text-sm no-underline" href="/">
Go home
</a>
}
onRetry={() => window.location.reload()}
title="This page hit a problem"
>
Reloading usually fixes it. If it keeps happening, the page may be broken for now.
</ErrorState>
);
}
}
3 changes: 1 addition & 2 deletions app/frontend/src/components/EventEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function EventEditor({
const custom = event?.repeatRule && !repeatPresets.some(([value]) => value === event.repeatRule);

return (
<Dialog eyebrow="event" onClose={onClose} title={event ? `Edit ${event.title}` : 'New event'}>
<Dialog onClose={onClose} title={event ? `Edit ${event.title}` : 'New event'}>
<form
className="mt-5 grid gap-4"
noValidate
Expand Down Expand Up @@ -124,7 +124,6 @@ export function EventEditor({
<TextField defaultValue={event?.location ?? ''} label="Where" maxLength={200} name="location" />
<TextField defaultValue={event?.meetingUrl ?? ''} inputMode="url" label="Meeting link" {...form.field('meetingUrl')} />
<TextAreaField defaultValue={event?.description ?? ''} label="Description" maxLength={5000} name="description" rows={3} />
{saving.status}
<div className="flex justify-end gap-3">
<button className="secondary-button" onClick={onClose} type="button">
Cancel
Expand Down
43 changes: 43 additions & 0 deletions app/frontend/src/components/FormDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { ReactNode } from 'react';
import { Dialog } from '@project-graphite/ui';

export function FormDialog({
busy,
busyLabel,
children,
onClose,
onSubmit,
submitLabel,
title,
}: {
busy: boolean;
busyLabel: string;
children: ReactNode;
onClose: () => void;
onSubmit: (form: HTMLFormElement) => void;
submitLabel: string;
title: string;
}) {
return (
<Dialog onClose={onClose} title={title}>
<form
className="mt-5 grid gap-4"
noValidate
onSubmit={(event) => {
event.preventDefault();
onSubmit(event.currentTarget);
}}
>
{children}
<div className="mt-1 flex justify-end gap-3">
<button className="secondary-button" onClick={onClose} type="button">
Cancel
</button>
<button className="primary-button" disabled={busy} type="submit">
{busy ? busyLabel : submitLabel}
</button>
</div>
</form>
</Dialog>
);
}
22 changes: 22 additions & 0 deletions app/frontend/src/components/LoadError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ErrorState, Icon } from '@project-graphite/ui';

export function LoadError({ compact = false, error, onRetry }: { compact?: boolean; error: string; onRetry: () => void }) {
if (compact) {
return (
<div className="flex items-start gap-2 text-sm" role="alert">
<span className="mt-0.5 shrink-0 text-danger">
<Icon name="alert" size={16} />
</span>
<span className="min-w-0 flex-1 text-muted">{error}</span>
<button className="text-button shrink-0 text-sm" onClick={onRetry} type="button">
Retry
</button>
</div>
);
}
return (
<ErrorState onRetry={onRetry} title="This didn’t load">
{error}
</ErrorState>
);
}
8 changes: 3 additions & 5 deletions app/frontend/src/components/ResendVerification.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { useState } from 'react';
import { errorMessage } from '@project-graphite/ui';
import { errorMessage, useSnackbar } from '@project-graphite/ui';
import { apiRequest } from '../api';

export function ResendVerification({ email }: { email: string }) {
const [state, setState] = useState<'idle' | 'sending' | 'sent'>('idle');
const [error, setError] = useState('');
const show = useSnackbar();

async function resend() {
setState('sending');
setError('');
try {
await apiRequest('/auth/resend-verification', {
method: 'POST',
body: JSON.stringify({ email }),
});
setState('sent');
} catch (reason) {
setError(errorMessage(reason, 'Could not send the link'));
show({ message: errorMessage(reason, 'Could not send the link'), tone: 'error' });
setState('idle');
}
}
Expand All @@ -38,7 +37,6 @@ export function ResendVerification({ email }: { email: string }) {
</button>
</p>
)}
{error && <p className="error-message mt-2 mb-0">{error}</p>}
</div>
);
}
Loading
Loading