105 lines
3.4 KiB
TypeScript
105 lines
3.4 KiB
TypeScript
/**
|
|
* Renders the inline editor of the signed-in user's description, under their
|
|
* profile's header.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import { t } from "@lingui/core/macro";
|
|
import { Trans } from "@lingui/react/macro";
|
|
import { Controller, FormProvider } from "react-hook-form";
|
|
import { API_URL } from "../../config/api.ts";
|
|
import { VALIDATION } from "../../../shared/validation.ts";
|
|
import type { Wire } from "../../../shared/api.ts";
|
|
import type { PublicUser, UpdateUserRequest } from "../../../shared/user.ts";
|
|
import { expectOk } from "../../utils/shared/api.ts";
|
|
import { useAuth } from "../../hooks/auth/useAuth.ts";
|
|
import { useApiForm } from "../../hooks/form/useApiForm.ts";
|
|
import { TextEditor } from "../editor/TextEditor.tsx";
|
|
import { FormError } from "../form/FormError.tsx";
|
|
import { SubmitButton } from "../form/SubmitButton.tsx";
|
|
|
|
/**
|
|
* Passed to {@link DescriptionEditor} by `ProfileDescription`, which shows it
|
|
* in place of the description while editing.
|
|
*/
|
|
export interface DescriptionEditorProps {
|
|
initialDescription: string;
|
|
onCancel: () => void;
|
|
onSaved: (description: string | undefined) => void;
|
|
}
|
|
|
|
/**
|
|
* Renders the description's Markdown editor, saved to `PATCH /api/me`.
|
|
* Other browsers see the change through the socket's `user_updated` event.
|
|
*/
|
|
export function DescriptionEditor(
|
|
{ initialDescription, onCancel, onSaved }: DescriptionEditorProps,
|
|
): React.JSX.Element {
|
|
const { authFetch } = useAuth();
|
|
const form = useApiForm<{ description: string }>({
|
|
defaultValues: { description: initialDescription },
|
|
});
|
|
const description = form.watch("description");
|
|
const submitting = form.formState.isSubmitting;
|
|
|
|
const onSubmit = form.submit(async ({ description }) => {
|
|
const trimmed = description.trim();
|
|
// Sent even when empty, since the server keeps the old one when it is left
|
|
// out.
|
|
await expectOk<Wire<PublicUser>>(
|
|
await authFetch(`${API_URL}/api/me`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(
|
|
{ description: trimmed } satisfies UpdateUserRequest,
|
|
),
|
|
}),
|
|
);
|
|
onSaved(trimmed || undefined);
|
|
});
|
|
|
|
return (
|
|
<FormProvider {...form}>
|
|
<form
|
|
className="profile-description-editor animate-in"
|
|
onSubmit={onSubmit}
|
|
>
|
|
<Controller
|
|
name="description"
|
|
control={form.control}
|
|
render={({ field }) => (
|
|
<TextEditor
|
|
className="comment-reply-textarea"
|
|
value={field.value}
|
|
onChange={field.onChange}
|
|
onSubmit={() => void onSubmit()}
|
|
onCancel={submitting ? undefined : onCancel}
|
|
placeholder={t`Who am I?`}
|
|
autoResize
|
|
maxLength={VALIDATION.USER_DESCRIPTION_MAX}
|
|
/>
|
|
)}
|
|
/>
|
|
<div className="profile-description-actions">
|
|
<SubmitButton
|
|
pendingLabel={<Trans>Saving…</Trans>}
|
|
disabled={description.length > VALIDATION.USER_DESCRIPTION_MAX}
|
|
>
|
|
<Trans>Save</Trans>
|
|
</SubmitButton>
|
|
<button
|
|
type="button"
|
|
className="btn-secondary"
|
|
onClick={onCancel}
|
|
disabled={submitting}
|
|
>
|
|
<Trans>Cancel</Trans>
|
|
</button>
|
|
<FormError title={t`Failed to save`} />
|
|
</div>
|
|
</form>
|
|
</FormProvider>
|
|
);
|
|
}
|