feat: add edit user page (#173)
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { Button, Divider, Group, Stack, Text } from "@homarr/ui";
|
||||
|
||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
||||
|
||||
interface DangerZoneAccordionProps {
|
||||
user: NonNullable<RouterOutputs["user"]["getById"]>;
|
||||
}
|
||||
|
||||
export const DangerZoneAccordion = ({ user }: DangerZoneAccordionProps) => {
|
||||
const t = useScopedI18n("management.page.user.edit.section.dangerZone");
|
||||
const router = useRouter();
|
||||
const { mutateAsync: mutateUserDeletionAsync } =
|
||||
clientApi.user.delete.useMutation({
|
||||
onSettled: async () => {
|
||||
await router.push("/manage/users");
|
||||
await revalidatePathAction("/manage/users");
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = React.useCallback(
|
||||
async () => await mutateUserDeletionAsync(user.id),
|
||||
[user, mutateUserDeletionAsync],
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Divider />
|
||||
<Group justify="space-between" px="md">
|
||||
<Stack gap={0}>
|
||||
<Text fw="bold" size="sm">
|
||||
{t("action.delete.label")}
|
||||
</Text>
|
||||
<Text size="sm">{t("action.delete.description")}</Text>
|
||||
</Stack>
|
||||
<Button onClick={handleDelete} variant="subtle" color="red">
|
||||
{t("action.delete.button")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useForm, zodResolver } from "@homarr/form";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { Button, Stack, TextInput } from "@homarr/ui";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
||||
|
||||
interface ProfileAccordionProps {
|
||||
user: NonNullable<RouterOutputs["user"]["getById"]>;
|
||||
}
|
||||
|
||||
export const ProfileAccordion = ({ user }: ProfileAccordionProps) => {
|
||||
const t = useScopedI18n("management.page.user.edit.section.profile");
|
||||
const { mutate, isPending } = clientApi.user.editProfile.useMutation({
|
||||
onSettled: async () => {
|
||||
await revalidatePathAction("/manage/users");
|
||||
},
|
||||
});
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name: user.name ?? "",
|
||||
email: user.email ?? "",
|
||||
},
|
||||
validate: zodResolver(validation.user.editProfile),
|
||||
validateInputOnBlur: true,
|
||||
validateInputOnChange: true,
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
mutate({
|
||||
userId: user.id,
|
||||
form: form.values,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label={t("form.username.label")}
|
||||
withAsterisk
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("form.email.label")}
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
<Button type="submit" disabled={!form.isValid()} loading={isPending}>
|
||||
Submit
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,25 @@
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
import { Title } from "@homarr/ui";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionControl,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Avatar,
|
||||
Group,
|
||||
IconAlertTriangleFilled,
|
||||
IconSettingsFilled,
|
||||
IconShieldLockFilled,
|
||||
IconUserFilled,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@homarr/ui";
|
||||
|
||||
import { api } from "~/trpc/server";
|
||||
import { DangerZoneAccordion } from "./_components/dangerZone.accordion";
|
||||
import { ProfileAccordion } from "./_components/profile.accordion";
|
||||
|
||||
interface Props {
|
||||
params: {
|
||||
@@ -24,6 +40,7 @@ export async function generateMetadata({ params }: Props) {
|
||||
}
|
||||
|
||||
export default async function EditUserPage({ params }: Props) {
|
||||
const t = await getScopedI18n("management.page.user.edit");
|
||||
const user = await api.user.getById({
|
||||
userId: params.userId,
|
||||
});
|
||||
@@ -32,5 +49,60 @@ export default async function EditUserPage({ params }: Props) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <Title>Edit User {user.name}!</Title>;
|
||||
return (
|
||||
<Stack>
|
||||
<Group mb="md">
|
||||
<Avatar>{user.name?.substring(0, 2)}</Avatar>
|
||||
<Title>{user.name}</Title>
|
||||
</Group>
|
||||
<Accordion variant="separated" defaultValue="general">
|
||||
<AccordionItem value="general">
|
||||
<AccordionControl icon={<IconUserFilled />}>
|
||||
<Text fw="bold" size="lg">
|
||||
{t("section.profile.title")}
|
||||
</Text>
|
||||
</AccordionControl>
|
||||
<AccordionPanel>
|
||||
<ProfileAccordion user={user} />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="preferences">
|
||||
<AccordionControl icon={<IconSettingsFilled />}>
|
||||
<Text fw="bold" size="lg">
|
||||
{t("section.preferences.title")}
|
||||
</Text>
|
||||
</AccordionControl>
|
||||
<AccordionPanel></AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="security">
|
||||
<AccordionControl icon={<IconShieldLockFilled />}>
|
||||
<Text fw="bold" size="lg">
|
||||
{t("section.security.title")}
|
||||
</Text>
|
||||
</AccordionControl>
|
||||
<AccordionPanel></AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem
|
||||
styles={{
|
||||
item: {
|
||||
"--__item-border-color": "rgba(248,81,73,0.4)",
|
||||
borderWidth: 4,
|
||||
},
|
||||
}}
|
||||
value="dangerZone"
|
||||
>
|
||||
<AccordionControl icon={<IconAlertTriangleFilled />}>
|
||||
<Text fw="bold" size="lg">
|
||||
{t("section.dangerZone.title")}
|
||||
</Text>
|
||||
</AccordionControl>
|
||||
<AccordionPanel
|
||||
styles={{ content: { paddingRight: 0, paddingLeft: 0 } }}
|
||||
>
|
||||
<DangerZoneAccordion user={user} />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user