feat(teams): add team tab UI components

This commit is contained in:
2026-06-26 00:07:07 +02:00
parent 575900427a
commit d3114326bb
4 changed files with 387 additions and 0 deletions
@@ -0,0 +1,97 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter } from "next/navigation"
import { useMemo } from "react"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { createTeamAction } from "@/actions/team.actions"
import {
SubmitButton,
type SubmitButtonCopy,
} from "@/components/forms/submitButton"
import type { Dictionary } from "@/i18n/dictionaries"
import {
buildCreateTeamSchema,
type CreateTeamFormType,
} from "@/schemas/team.schema"
type TeamFormCopy = Dictionary["inventory"]["teams"]["form"]
type TeamSchemaCopy = Dictionary["inventory"]["teams"]["schema"]
export default function TeamCreateForm({
formCopy,
schemaCopy,
submitButtonCopy,
}: {
formCopy: TeamFormCopy
schemaCopy: TeamSchemaCopy
submitButtonCopy: SubmitButtonCopy
}) {
const router = useRouter()
const schema = useMemo(
() => buildCreateTeamSchema(schemaCopy),
[schemaCopy],
)
const {
register,
handleSubmit,
reset,
setError,
formState: { errors, isSubmitting, isSubmitSuccessful },
} = useForm<CreateTeamFormType>({
resolver: zodResolver(schema),
})
const onSubmit = async (formData: CreateTeamFormType) => {
const response = await createTeamAction(formData)
if (response?.errors) {
Object.entries(response.errors).forEach(([fieldName, messages]) => {
messages.forEach((msg: string) => {
setError(fieldName as keyof CreateTeamFormType, {
type: "server",
message: msg,
})
toast.error(msg)
})
})
return
}
if (response?.success) {
toast.success(response.message)
reset()
router.refresh()
}
}
return (
<form
className="flex flex-col gap-4 rounded-lg border p-4"
onSubmit={handleSubmit(onSubmit)}
>
<div className="flex flex-col gap-2">
<label htmlFor="team-name" className="mb-2 block text-lg">
{formCopy.nameLabel}
</label>
<input
type="text"
id="team-name"
placeholder={formCopy.namePlaceholder}
{...register("name")}
className={`w-full rounded-lg border px-4 py-2 ${errors.name ? "border-error" : ""}`}
/>
{errors.name && <p className="text-error">{errors.name.message}</p>}
</div>
<SubmitButton
copy={submitButtonCopy}
isSubmitting={isSubmitting}
isSubmitSuccessful={isSubmitSuccessful}
>
{formCopy.createSubmit}
</SubmitButton>
</form>
)
}
@@ -0,0 +1,143 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { Pencil } from "lucide-react"
import { useRouter } from "next/navigation"
import { useMemo, useState } from "react"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { updateTeamAction } from "@/actions/team.actions"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
SubmitButton,
type SubmitButtonCopy,
} from "@/components/forms/submitButton"
import type { Dictionary } from "@/i18n/dictionaries"
import {
buildUpdateTeamSchema,
type UpdateTeamFormType,
} from "@/schemas/team.schema"
import type { TeamSummary } from "@/types"
type TeamFormCopy = Dictionary["inventory"]["teams"]["form"]
type TeamSchemaCopy = Dictionary["inventory"]["teams"]["schema"]
type TeamListCopy = Dictionary["inventory"]["teams"]["list"]
export default function TeamEditForm({
team,
formCopy,
schemaCopy,
listCopy,
submitButtonCopy,
}: {
team: TeamSummary
formCopy: TeamFormCopy
schemaCopy: TeamSchemaCopy
listCopy: TeamListCopy
submitButtonCopy: SubmitButtonCopy
}) {
const router = useRouter()
const [open, setOpen] = useState(false)
const schema = useMemo(
() => buildUpdateTeamSchema(schemaCopy),
[schemaCopy],
)
const {
register,
handleSubmit,
setError,
formState: { errors, isSubmitting, isSubmitSuccessful },
} = useForm<UpdateTeamFormType>({
resolver: zodResolver(schema),
defaultValues: {
id: team.id,
name: team.name,
},
})
const onSubmit = async (formData: UpdateTeamFormType) => {
const response = await updateTeamAction(formData)
if (response?.errors) {
Object.entries(response.errors).forEach(([fieldName, messages]) => {
messages.forEach((msg: string) => {
setError(fieldName as keyof UpdateTeamFormType, {
type: "server",
message: msg,
})
toast.error(msg)
})
})
return
}
if (response?.success) {
toast.success(response.message)
setOpen(false)
router.refresh()
}
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="outline"
size="icon"
aria-label={listCopy.actions.edit}
>
<Pencil />
</Button>
</DialogTrigger>
<DialogContent>
<form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
<DialogHeader>
<DialogTitle>{formCopy.updateSubmit}</DialogTitle>
<DialogDescription>{team.name}</DialogDescription>
</DialogHeader>
<input type="hidden" {...register("id")} />
<div className="flex flex-col gap-2">
<label htmlFor={`team-name-${team.id}`} className="mb-2 block text-lg">
{formCopy.nameLabel}
</label>
<input
type="text"
id={`team-name-${team.id}`}
placeholder={formCopy.namePlaceholder}
{...register("name")}
className={`w-full rounded-lg border px-4 py-2 ${errors.name ? "border-error" : ""}`}
/>
{errors.name && (
<p className="text-error">{errors.name.message}</p>
)}
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">
{formCopy.cancel}
</Button>
</DialogClose>
<SubmitButton
copy={submitButtonCopy}
isSubmitting={isSubmitting}
isSubmitSuccessful={isSubmitSuccessful}
>
{formCopy.updateSubmit}
</SubmitButton>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
@@ -0,0 +1,113 @@
"use client"
import { Trash } from "lucide-react"
import { useRouter } from "next/navigation"
import { useTransition } from "react"
import { toast } from "sonner"
import { deleteTeamAction } from "@/actions/team.actions"
import { Button } from "@/components/ui/button"
import type { Dictionary } from "@/i18n/dictionaries"
import type { TeamSummary } from "@/types"
import TeamEditForm from "./team.edit.form"
type TeamFormCopy = Dictionary["inventory"]["teams"]["form"]
type TeamSchemaCopy = Dictionary["inventory"]["teams"]["schema"]
type TeamListCopy = Dictionary["inventory"]["teams"]["list"]
type SubmitButtonCopy = Dictionary["common"]["submitButton"]
function DeleteTeamButton({
team,
copy,
}: {
team: TeamSummary
copy: TeamListCopy
}) {
const router = useRouter()
const [isPending, startTransition] = useTransition()
const handleDelete = (formData: FormData) => {
startTransition(async () => {
const response = await deleteTeamAction(formData)
if (!response.success && response.errors?.id) {
toast.error(response.errors.id[0])
return
}
if (response.success) {
toast.success(response.message)
router.refresh()
} else {
toast.error(response.message ?? copy.actions.delete)
}
})
}
return (
<form action={handleDelete}>
<input type="hidden" name="id" value={team.id} />
<Button
type="submit"
variant="outline"
size="icon"
disabled={isPending}
aria-label={copy.actions.delete}
>
<Trash />
</Button>
</form>
)
}
export default function TeamListTable({
teams,
formCopy,
schemaCopy,
listCopy,
submitButtonCopy,
}: {
teams: TeamSummary[]
formCopy: TeamFormCopy
schemaCopy: TeamSchemaCopy
listCopy: TeamListCopy
submitButtonCopy: SubmitButtonCopy
}) {
if (teams.length === 0) {
return <div>{listCopy.empty}</div>
}
return (
<div className="overflow-x-auto">
<table className="text-muted-foreground w-full text-left text-sm">
<thead className="border-b">
<tr>
<th scope="col" className="p-4">
{listCopy.columns.name}
</th>
<th scope="col" className="p-4">
{listCopy.columns.actions}
</th>
</tr>
</thead>
<tbody>
{teams.map((team) => (
<tr key={team.id} className="border-b">
<td className="p-4">{team.name}</td>
<td className="flex items-center gap-2 p-4">
<TeamEditForm
team={team}
formCopy={formCopy}
schemaCopy={schemaCopy}
listCopy={listCopy}
submitButtonCopy={submitButtonCopy}
/>
<DeleteTeamButton team={team} copy={listCopy} />
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
@@ -0,0 +1,34 @@
import PageHeader from "@/components/common/pageheader"
import { getI18n } from "@/i18n/server"
import { listTeamsUseCase } from "@/use-cases/team.use-cases"
import TeamCreateForm from "./team.create.form"
import TeamListTable from "./team.list.table"
export default async function TeamsTab() {
const teams = await listTeamsUseCase()
const { dictionary } = await getI18n()
const copy = dictionary.inventory.teams
return (
<div className="flex flex-col gap-4">
<PageHeader
title={copy.list.title}
addLabel={copy.list.addLabel}
data={teams}
/>
<TeamCreateForm
formCopy={copy.form}
schemaCopy={copy.schema}
submitButtonCopy={dictionary.common.submitButton}
/>
<TeamListTable
teams={teams}
formCopy={copy.form}
schemaCopy={copy.schema}
listCopy={copy.list}
submitButtonCopy={dictionary.common.submitButton}
/>
</div>
)
}