Added password input

This commit is contained in:
sianida26 2024-02-14 13:45:00 +07:00
parent 4fbcd5581d
commit dc4379b482
14 changed files with 47 additions and 450 deletions

View File

@ -1,88 +0,0 @@
"use client";
import { UserFormData } from "@/features/dashboard/users/formSchemas/userFormDataSchema";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import {
Avatar,
Button,
Center,
Flex,
Modal,
ScrollArea,
Text,
Stack,
TextInput,
Title,
} from "@mantine/core";
import deleteUser from "@/features/dashboard/users/actions/deleteUser";
import { showNotification } from "@/utils/notifications";
interface Props {
data: UserFormData;
}
export default function DeleteModal(props: Props) {
const router = useRouter();
const [isSubmitting, setSubmitting] = useState(false);
/**
* Closes the modal. It won't close if a submission is in progress.
*/
const closeModal = () => {
if (isSubmitting) return;
router.replace("?");
};
const confirmAction = () => {
setSubmitting(true)
deleteUser(props.data.id)
.then((response) => {
if (response.success){
showNotification(response.message);
router.replace("?")
return;
} else {
showNotification(response.message, "error")
}
})
.catch(() => {
//TODO: Handle Error
})
.finally(() => {
setSubmitting(false)
})
}
return (
<Modal opened onClose={closeModal} title={`Delete confirmation`}>
<Text size="sm">
Are you sure you want to delete user{" "}
<Text span fw={700}>
{props.data.name}
</Text>
? This action is irreversible.
</Text>
{/* Buttons */}
<Flex justify="flex-end" align="center" gap="lg" mt="lg">
<Button
variant="outline"
onClick={closeModal}
disabled={isSubmitting}
>
Cancel
</Button>
<Button
variant="subtle"
// leftSection={<TbDeviceFloppy size={20} />}
type="submit"
color="red"
loading={isSubmitting}
onClick={confirmAction}
>
Delete User
</Button>
</Flex>
</Modal>
);
}

View File

@ -1,3 +0,0 @@
import DeleteModal from "./DeleteModal";
export default DeleteModal;

View File

@ -1,11 +0,0 @@
import React from 'react'
import { FormModal } from '..'
import { UserFormData } from '@/features/dashboard/users/formSchemas/userFormDataSchema'
interface Props {
data: UserFormData
}
export default function DetailModal(props: Props) {
return <FormModal readonly modalTitle='Detail User' data={props.data} />
}

View File

@ -1,3 +0,0 @@
import DetailModal from "./DetailModal";
export default DetailModal;

View File

@ -1,11 +0,0 @@
import React from 'react'
import { FormModal } from '..'
import { UserFormData } from '@/features/dashboard/users/formSchemas/userFormDataSchema'
interface Props {
data: UserFormData
}
export default function EditModal(props: Props) {
return <FormModal modalTitle='Edit User' data={props.data} />
}

View File

@ -1,3 +0,0 @@
import EditModal from "./EditModal";
export default EditModal;

View File

@ -1,149 +0,0 @@
"use client";
import React, { useState } from "react";
import { useForm } from "@mantine/form";
import { useRouter } from "next/navigation";
import {
Avatar,
Button,
Center,
Flex,
Modal,
ScrollArea,
Stack,
TextInput,
Title,
} from "@mantine/core";
import { TbDeviceFloppy } from "react-icons/tb";
import editUser from "@/features/dashboard/users/actions/editUser";
import userFormDataSchema, {
UserFormData,
} from "@/features/dashboard/users/formSchemas/userFormDataSchema";
import { showNotification } from "@/utils/notifications";
import stringToColorHex from "@/utils/stringToColorHex";
import { zodResolver } from "@mantine/form";
interface Props {
readonly?: boolean;
modalTitle: string;
data: UserFormData;
}
export default function FormModal(props: Props) {
const router = useRouter();
const [isSubmitting, setSubmitting] = useState(false);
const form = useForm<UserFormData>({
initialValues: props.data,
validate: zodResolver(userFormDataSchema),
});
/**
* Closes the modal. It won't close if a submission is in progress.
*/
const closeModal = () => {
if (isSubmitting) return;
router
.replace("?")
};
/**
* Handles the form submission.
*
* @param data The data from the form.
*/
const handleSubmit = (data: UserFormData) => {
setSubmitting(true);
editUser(data)
.then((response) => {
if (response.success) {
showNotification(response.message);
router.replace("?")
return;
} else {
if (response.errors) {
form.setErrors(response.errors);
return;
}
showNotification(response.message, "error");
return;
}
})
.catch(() => {
//TODO: Handle Error
})
.finally(() => {
setSubmitting(false);
});
};
return (
<Modal
opened
onClose={closeModal}
title={<Title order={3}>{props.modalTitle}</Title>}
scrollAreaComponent={ScrollArea.Autosize}
>
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack mt="sm" gap="lg" px="lg">
{/* Avatar */}
<Center>
<Avatar
color={stringToColorHex(props.data.id)}
src={props.data.photoProfileUrl}
size={120}
>
{props.data.name?.[0].toUpperCase()}
</Avatar>
</Center>
{/* ID */}
<TextInput
label="ID"
readOnly
variant="filled"
{...form.getInputProps("id")}
/>
{/* Name */}
<TextInput
data-autofocus
label="Name"
readOnly={props.readonly}
disabled={isSubmitting}
{...form.getInputProps("name")}
/>
{/* Email */}
<TextInput
label="Email"
readOnly={props.readonly}
disabled={isSubmitting}
{...form.getInputProps("email")}
/>
{/* Buttons */}
<Flex justify="flex-end" align="center" gap="lg" mt="lg">
<Button
variant="outline"
onClick={closeModal}
disabled={isSubmitting}
>
Close
</Button>
{!props.readonly && (
<Button
variant="filled"
leftSection={<TbDeviceFloppy size={20} />}
type="submit"
loading={isSubmitting}
>
Save
</Button>
)}
</Flex>
</Stack>
</form>
</Modal>
);
}

View File

@ -1,3 +0,0 @@
import FormModal from "./FormModal";
export default FormModal;

View File

@ -1,4 +0,0 @@
export { default as DeleteModal } from "./DeleteModal"
export { default as DetailModal } from "./DetailModal"
export { default as EditModal } from "./EditModal"
export { default as FormModal } from "./FormModal"

View File

@ -1,74 +0,0 @@
"use client";
import React from "react";
import {
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import columns, { UserRow } from "./columns";
import { Table, Text } from "@mantine/core";
interface Props {
users: UserRow[];
}
export default function UsersTable({ users }: Props) {
const table = useReactTable({
data: users,
columns,
getCoreRowModel: getCoreRowModel(),
defaultColumn: {
cell: (props) => <Text>{props.getValue() as React.ReactNode}</Text>,
},
});
return (
<Table verticalSpacing="xs" horizontalSpacing="xs">
{/* Thead */}
<Table.Thead>
{table.getHeaderGroups().map((headerGroup) => (
<Table.Tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<Table.Th
key={header.id}
style={{
maxWidth: `${header.column.columnDef.maxSize}px`,
width: `${header.getSize()}`,
}}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</Table.Th>
))}
</Table.Tr>
))}
</Table.Thead>
{/* TBody */}
<Table.Tbody>
{table.getRowModel().rows.map((row) => (
<Table.Tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<Table.Td
key={cell.id}
style={{
maxWidth: `${cell.column.columnDef.maxSize}px`,
}}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</Table.Td>
))}
</Table.Tr>
))}
</Table.Tbody>
</Table>
);
}

View File

@ -1,76 +0,0 @@
import { createColumnHelper } from "@tanstack/react-table";
import Link from "next/link";
import { ActionIcon, Anchor, Avatar, Badge, Flex, Group, Text, Tooltip } from "@mantine/core";
import { TbEye, TbPencil, TbTrash } from "react-icons/tb";
import stringToColorHex from "@/utils/stringToColorHex";
export interface UserRow {
id: string,
name: string | null,
email: string | null,
photoUrl: string | null,
}
const columnHelper = createColumnHelper<UserRow>()
const columns = [
columnHelper.display({
id: "sequence",
header: "#",
cell: props => props.row.index + 1,
size: 1
}),
columnHelper.accessor('name', {
header: "Name",
cell: (props) => <Group>
<Avatar color={stringToColorHex(props.row.original.id)} src={props.row.original.photoUrl} size={26}>{props.getValue()?.[0].toUpperCase()}</Avatar>
<Text size="sm" fw={500}>{props.getValue()}</Text>
</Group>
}),
columnHelper.accessor('email', {
header: "Email",
cell: (props) => <Anchor href={`mailto:${props.getValue()}`} size="sm" component={Link}>{props.getValue()}</Anchor>
}),
columnHelper.display({
id: "status",
header: "Status",
cell: (props) => <Badge color="green">Active</Badge>
}),
columnHelper.display({
id: 'actions',
header: "Actions",
size: 10,
meta: {
className: "w-fit"
},
cell: (props) => <Flex gap="xs">
{/* Detail */}
<Tooltip label="Detail">
<ActionIcon variant="light" color="green" component={Link} href={`?detail=${props.row.original.id}`}>
<TbEye />
</ActionIcon>
</Tooltip>
{/* Edit */}
<Tooltip label="Edit">
<ActionIcon variant="light" color="yellow" component={Link} href={`?edit=${props.row.original.id}`}>
<TbPencil />
</ActionIcon>
</Tooltip>
{/* Delete */}
<Tooltip label="Delete">
<ActionIcon variant="light" color="red" component={Link} href={`?delete=${props.row.original.id}`}>
<TbTrash />
</ActionIcon>
</Tooltip>
</Flex>
})
];
export default columns;

View File

@ -3,12 +3,15 @@
import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue"; import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue";
import prisma from "@/db"; import prisma from "@/db";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import userFormDataSchema, { UserFormData } from "../formSchemas/userFormSchema"; import userFormDataSchema, {
UserFormData,
} from "../formSchemas/userFormSchema";
import checkPermission from "@/modules/dashboard/services/checkPermission"; import checkPermission from "@/modules/dashboard/services/checkPermission";
import unauthorized from "@/modules/dashboard/utils/unauthorized"; import unauthorized from "@/modules/dashboard/utils/unauthorized";
import DashboardError from "@/modules/dashboard/errors/DashboardError"; import DashboardError from "@/modules/dashboard/errors/DashboardError";
import handleCatch from "@/modules/dashboard/utils/handleCatch"; import handleCatch from "@/modules/dashboard/utils/handleCatch";
import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction"; import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
import hashPassword from "@/modules/auth/utils/hashPassword";
/** /**
* Upserts a user based on the provided UserFormData. * Upserts a user based on the provided UserFormData.
@ -34,32 +37,37 @@ export default async function upsertUser(
const validatedFields = userFormDataSchema.safeParse(data); const validatedFields = userFormDataSchema.safeParse(data);
if (!validatedFields.success) { if (!validatedFields.success) {
throw new DashboardError({ throw new DashboardError({
errorCode: "INVALID_FORM_DATA", errorCode: "INVALID_FORM_DATA",
formErrors: mapObjectToFirstValue(validatedFields.error.flatten().fieldErrors) formErrors: mapObjectToFirstValue(
}) validatedFields.error.flatten().fieldErrors
),
});
} }
const userData = { const userData = {
id: validatedFields.data.id ? validatedFields.data.id : undefined, id: validatedFields.data.id ? validatedFields.data.id : undefined,
name: validatedFields.data.name, name: validatedFields.data.name,
photoProfile: validatedFields.data.photoProfileUrl ?? "", photoProfile: validatedFields.data.photoProfileUrl ?? "",
email: validatedFields.data.email email: validatedFields.data.email,
}; };
const passwordHash = await hashPassword(validatedFields.data.password!);
// Database operation // Database operation
if (isInsert) { if (isInsert) {
if (await prisma.user.findFirst({ if (
where: { await prisma.user.findFirst({
email: userData.email where: {
} email: userData.email,
})){ },
throw new DashboardError({ })
errorCode: "INVALID_FORM_DATA", ) {
formErrors: { throw new DashboardError({
email: "The user is already exists" errorCode: "INVALID_FORM_DATA",
} formErrors: {
}) email: "The user is already exists",
} },
await prisma.user.create({ data: userData }); });
}
await prisma.user.create({ data: { ...userData, passwordHash } });
} else { } else {
await prisma.user.update({ await prisma.user.update({
where: { id: validatedFields.data.id! }, where: { id: validatedFields.data.id! },
@ -78,6 +86,6 @@ export default async function upsertUser(
}.`, }.`,
}; };
} catch (error) { } catch (error) {
return handleCatch(error) return handleCatch(error);
} }
} }

View File

@ -1,17 +1,19 @@
import { z } from "zod"; import { z } from "zod";
export interface UserFormData { export interface UserFormData {
id: string; id: string | undefined;
name: string; name: string;
photoProfileUrl: string; photoProfileUrl: string;
email: string; email: string;
password: string | undefined
} }
const userFormDataSchema = z.object({ const userFormDataSchema = z.object({
id: z.string().nullable(), id: z.string().optional(),
name: z.string(), name: z.string(),
photoProfileUrl: z.union([z.string(), z.null()]), photoProfileUrl: z.union([z.string(), z.null()]),
email: z.string().email(), email: z.string().email(),
password: z.string().min(8).optional(),
}); });
export default userFormDataSchema; export default userFormDataSchema;

View File

@ -15,6 +15,7 @@ import {
Alert, Alert,
Center, Center,
Avatar, Avatar,
PasswordInput,
} from "@mantine/core"; } from "@mantine/core";
import { useForm, zodResolver } from "@mantine/form"; import { useForm, zodResolver } from "@mantine/form";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@ -55,6 +56,7 @@ export default function UserFormModal(props: ModalProps) {
email: "", email: "",
name: "", name: "",
photoProfileUrl: "", photoProfileUrl: "",
password: "",
}, },
validate: zodResolver(userFormDataSchema), validate: zodResolver(userFormDataSchema),
validateInputOnChange: false, validateInputOnChange: false,
@ -128,7 +130,7 @@ export default function UserFormModal(props: ModalProps) {
onClose={closeModal} onClose={closeModal}
title={props.title} title={props.title}
scrollAreaComponent={ScrollArea.Autosize} scrollAreaComponent={ScrollArea.Autosize}
size="xl" size="md"
> >
<form onSubmit={form.onSubmit(handleSubmit)}> <form onSubmit={form.onSubmit(handleSubmit)}>
<Stack mt="sm" gap="lg" px="lg"> <Stack mt="sm" gap="lg" px="lg">
@ -137,7 +139,7 @@ export default function UserFormModal(props: ModalProps) {
<Skeleton visible={isFetching}> <Skeleton visible={isFetching}>
<Center> <Center>
<Avatar <Avatar
color={stringToColorHex(form.values.id)} color={stringToColorHex(form.values.id ?? "")}
src={form.values.photoProfileUrl} src={form.values.photoProfileUrl}
size={120} size={120}
> >
@ -178,6 +180,16 @@ export default function UserFormModal(props: ModalProps) {
/> />
</Skeleton> </Skeleton>
{/* Password */}
{!form.values.id && !isFetching && (
<PasswordInput
label="Password"
readOnly={props.readonly}
disabled={isSubmitting}
{...form.getInputProps("password")}
/>
)}
{/* Buttons */} {/* Buttons */}
<Flex justify="flex-end" align="center" gap="lg" mt="lg"> <Flex justify="flex-end" align="center" gap="lg" mt="lg">
<Button <Button