feat(i18n): localize recipients UI

This commit is contained in:
2026-06-14 18:33:57 +02:00
parent ea37fc8d70
commit c0ae7a034a
11 changed files with 665 additions and 34 deletions
@@ -9,21 +9,25 @@ export default async function RecipientEditPage({
params: Promise<{ recipientId: string }>
}) {
const { recipientId } = await params
const recipient = await RecipientService.findById(recipientId)
const { dictionary } = await getI18n()
const copy = dictionary.inventory.recipients
const recipient = await RecipientService.findById(recipientId)
if (!recipient) {
return <div>Recipient not found</div>
return <div>{copy.edit.notFound}</div>
}
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-4">
<h1 className="text-2xl font-bold">Edit Recipient</h1>
<h1 className="text-2xl font-bold">{copy.edit.title}</h1>
</div>
<RecipientForm
initialData={recipient}
mode="edit"
formCopy={copy.form}
departmentCopy={copy.departments}
fallbackCopy={copy.fallback}
submitButtonCopy={dictionary.common.submitButton}
/>
</div>
@@ -1,18 +1,23 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { getI18n } from "@/i18n/server"
import { AssignmentService } from "@/services/assignment.service"
import { RecipientService } from "@/services/recipient.service"
import { formatRecipientDepartment } from "../_components/recipient.copy"
export default async function RecipientInfoPage({
params,
}: {
params: Promise<{ recipientId: string }>
}) {
const { recipientId } = await params
const { dictionary } = await getI18n()
const copy = dictionary.inventory.recipients
const recipient = await RecipientService.findById(recipientId)
const assignments = await AssignmentService.findAllByRecipient(recipientId)
if (!recipient) {
return <div>Recipient not found</div>
return <div>{copy.detail.notFound}</div>
}
return (
@@ -26,20 +31,30 @@ export default async function RecipientInfoPage({
<CardContent>
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Username</span>
<span className="text-gray-600">
{copy.detail.labels.username}
</span>
<span>{recipient.username}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Email</span>
<span className="text-gray-600">{copy.detail.labels.email}</span>
<span>{recipient.email}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Phone</span>
<span className="text-gray-600">{copy.detail.labels.phone}</span>
<span>{recipient.phone}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Department</span>
<span>{recipient.department}</span>
<span className="text-gray-600">
{copy.detail.labels.department}
</span>
<span>
{formatRecipientDepartment(
recipient.department,
copy.departments,
copy.fallback,
)}
</span>
</div>
</div>
</CardContent>
@@ -0,0 +1,24 @@
import type { Dictionary } from "@/i18n/dictionaries"
export type RecipientListCopy = Dictionary["inventory"]["recipients"]["list"]
export type RecipientDetailCopy =
Dictionary["inventory"]["recipients"]["detail"]
export type RecipientFormCopy = Dictionary["inventory"]["recipients"]["form"]
export type RecipientDepartmentCopy =
Dictionary["inventory"]["recipients"]["departments"]
export type RecipientFallbackCopy =
Dictionary["inventory"]["recipients"]["fallback"]
export function formatRecipientDepartment(
department: string | null | undefined,
departmentCopy: RecipientDepartmentCopy,
fallbackCopy: RecipientFallbackCopy,
) {
if (!department) {
return fallbackCopy.unknownDepartment
}
return department in departmentCopy
? departmentCopy[department as keyof RecipientDepartmentCopy]
: fallbackCopy.unknownDepartment
}
@@ -20,15 +20,28 @@ import {
} from "@/schemas/recipient.schema"
import type { Recipient } from "@/types"
import {
formatRecipientDepartment,
type RecipientDepartmentCopy,
type RecipientFallbackCopy,
type RecipientFormCopy,
} from "./recipient.copy"
interface RecipientFormProps {
initialData?: Recipient
mode?: "create" | "edit"
formCopy: RecipientFormCopy
departmentCopy: RecipientDepartmentCopy
fallbackCopy: RecipientFallbackCopy
submitButtonCopy: SubmitButtonCopy
}
export default function RecipientForm({
initialData,
mode = "create",
formCopy,
departmentCopy,
fallbackCopy,
submitButtonCopy,
}: RecipientFormProps) {
const router = useRouter()
@@ -81,12 +94,12 @@ export default function RecipientForm({
<input type="hidden" {...register("id")} />
<div>
<label htmlFor="username" className="mb-2 block text-lg">
Username
{formCopy.usernameLabel}
</label>
<input
type="text"
id="username"
placeholder="Username"
placeholder={formCopy.usernamePlaceholder}
{...register("username")}
className={`w-full rounded-lg border px-4 py-2`}
/>
@@ -96,12 +109,12 @@ export default function RecipientForm({
</div>
<div>
<label htmlFor="firstName" className="mb-2 block text-lg">
First Name
{formCopy.firstNameLabel}
</label>
<input
type="text"
id="firstName"
placeholder="First Name"
placeholder={formCopy.firstNamePlaceholder}
{...register("firstName")}
className={`w-full rounded-lg border px-4 py-2`}
/>
@@ -111,12 +124,12 @@ export default function RecipientForm({
</div>
<div>
<label htmlFor="lastName" className="mb-2 block text-lg">
Last Name
{formCopy.lastNameLabel}
</label>
<input
type="text"
id="lastName"
placeholder="Last Name"
placeholder={formCopy.lastNamePlaceholder}
{...register("lastName")}
className={`w-full rounded-lg border px-4 py-2`}
/>
@@ -126,17 +139,21 @@ export default function RecipientForm({
</div>
<div>
<label htmlFor="department" className="mb-2 block text-lg">
Department
{formCopy.departmentLabel}
</label>
<select
id="department"
{...register("department")}
className="w-full rounded-lg border px-4 py-2"
>
<option value="">Select a department</option>
<option value="">{formCopy.departmentPlaceholder}</option>
{Object.keys(RECIPIENT_DEPARTMENTS).map((department) => (
<option key={department} value={department}>
{department}
{formatRecipientDepartment(
department,
departmentCopy,
fallbackCopy,
)}
</option>
))}
</select>
@@ -146,12 +163,12 @@ export default function RecipientForm({
</div>
<div>
<label htmlFor="email" className="mb-2 block text-lg">
Email
{formCopy.emailLabel}
</label>
<input
type="text"
id="email"
placeholder="Email"
placeholder={formCopy.emailPlaceholder}
{...register("email")}
className={`w-full rounded-lg border px-4 py-2`}
/>
@@ -159,12 +176,12 @@ export default function RecipientForm({
</div>
<div>
<label htmlFor="phone" className="mb-2 block text-lg">
Phone
{formCopy.phoneLabel}
</label>
<input
type="text"
id="phone"
placeholder="Phone"
placeholder={formCopy.phonePlaceholder}
{...register("phone")}
className={`w-full rounded-lg border px-4 py-2`}
/>
@@ -175,7 +192,7 @@ export default function RecipientForm({
isSubmitting={isSubmitting}
isSubmitSuccessful={isSubmitSuccessful}
>
{mode === "create" ? "Create Recipient" : "Update Recipient"}
{mode === "create" ? formCopy.createSubmit : formCopy.updateSubmit}
</SubmitButton>
</form>
)
+5 -1
View File
@@ -4,14 +4,18 @@ import RecipientForm from "../_components/recipient.form"
export default async function NewRecipientPage() {
const { dictionary } = await getI18n()
const copy = dictionary.inventory.recipients
return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-4">
<h1 className="text-2xl font-bold">Add Recipient</h1>
<h1 className="text-2xl font-bold">{copy.new.title}</h1>
</div>
<RecipientForm
mode="create"
formCopy={copy.form}
departmentCopy={copy.departments}
fallbackCopy={copy.fallback}
submitButtonCopy={dictionary.common.submitButton}
/>
</div>
+27 -10
View File
@@ -5,8 +5,11 @@ import PageHeader from "@/components/common/pageheader"
import PaginationButtons from "@/components/common/pagination"
import { Button } from "@/components/ui/button"
import type { Recipient } from "@/generated/prisma/client"
import { getI18n } from "@/i18n/server"
import { RecipientService } from "@/services/recipient.service"
import { formatRecipientDepartment } from "./_components/recipient.copy"
export default async function RecipientsPage(props: {
searchParams?: Promise<{
page?: string
@@ -22,38 +25,41 @@ export default async function RecipientsPage(props: {
pageSize: 10,
search,
})
const { dictionary } = await getI18n()
const copy = dictionary.inventory.recipients
return (
<div className="flex flex-col gap-4">
<PageHeader
title="Recipients"
title={copy.list.title}
link="/recipients/new"
addLabel={copy.list.addLabel}
data={recipients}
search={search}
/>
{recipients.length === 0 && <div>No recipients found</div>}
{recipients.length === 0 && <div>{copy.list.empty}</div>}
{recipients.length > 0 && (
<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">
Username
{copy.list.columns.username}
</th>
<th scope="col" className="p-4">
Name
{copy.list.columns.name}
</th>
<th scope="col" className="p-4">
Email
{copy.list.columns.email}
</th>
<th scope="col" className="p-4">
Phone
{copy.list.columns.phone}
</th>
<th scope="col" className="p-4">
Department
{copy.list.columns.department}
</th>
<th scope="col" className="p-4">
Actions
{copy.list.columns.actions}
</th>
</tr>
</thead>
@@ -66,10 +72,20 @@ export default async function RecipientsPage(props: {
</td>
<td className="p-4">{recipient.email}</td>
<td className="p-4">{recipient.phone}</td>
<td className="p-4">{recipient.department}</td>
<td className="p-4">
{formatRecipientDepartment(
recipient.department,
copy.departments,
copy.fallback,
)}
</td>
<td className="flex items-center gap-2 p-4">
<Link href={`/recipients/${recipient.id}`} passHref>
<Button variant="outline" size="icon">
<Button
variant="outline"
size="icon"
aria-label={copy.list.actions.view}
>
<Eye />
</Button>
</Link>
@@ -78,6 +94,7 @@ export default async function RecipientsPage(props: {
className="btn btn-primary"
variant="outline"
size="icon"
aria-label={copy.list.actions.edit}
>
<Pencil />
</Button>