From af6e4b9c79052626d7619c391418304e78c427d3 Mon Sep 17 00:00:00 2001 From: Anthonius Munthi Date: Mon, 6 Apr 2026 23:30:51 +0800 Subject: [PATCH 1/6] feat(dash): enhance organization detail and add delete feature - Added `OrgHeader` with cover and logo upload support. - Implemented inline editing for organization basic info and parent. - Added `DeleteOrgButton` with an `AlertDialog` confirmation. - Refactored `UnitList` and `UnitForm` for a more compact and consistent UI. - Updated Indonesian and English translations for the new features. - Cleaned up redundant `"use client"` directive in shadcn button. --- apps/dash/app/(auth)/login/page.tsx | 2 +- apps/dash/src/pages/auth/ui/LoginPage.tsx | 14 +- apps/dash/src/pages/event/ui/EventPage.tsx | 8 +- .../dash/src/pages/finance/ui/FinancePage.tsx | 8 +- apps/dash/src/pages/home/actions/home.ts | 3 - .../home/ui/components/AgendaSection.tsx | 16 +- .../home/ui/components/LayananSection.tsx | 13 +- .../pages/home/ui/components/TugasSection.tsx | 22 +- apps/dash/src/pages/org/ui/JoinPage.tsx | 21 +- .../dash/src/pages/org/ui/JoinSuccessPage.tsx | 10 +- apps/dash/src/pages/org/ui/OrgDetailPage.tsx | 82 ++--- apps/dash/src/pages/org/ui/OrgNewPage.tsx | 9 +- apps/dash/src/pages/org/ui/OrgPage.tsx | 6 +- .../org/ui/components/DeleteOrgButton.tsx | 95 ++++++ .../src/pages/org/ui/components/JoinForm.tsx | 118 +++---- .../pages/org/ui/components/OrgFilters.tsx | 29 +- .../src/pages/org/ui/components/OrgForm.tsx | 50 +-- .../src/pages/org/ui/components/OrgHeader.tsx | 62 ++-- .../org/ui/components/OrgParentSelect.tsx | 10 +- .../org/ui/components/OrganizationCard.tsx | 8 +- .../org/ui/components/OrganizationList.tsx | 4 +- .../src/pages/org/ui/components/UnitForm.tsx | 18 +- .../src/pages/org/ui/components/UnitList.tsx | 42 +-- apps/dash/src/pages/rsvp/ui/RsvpPage.tsx | 9 +- apps/dash/src/shared/config/actions.ts | 18 -- apps/dash/src/shared/i18n/messages/en.json | 291 +++++++++++++++++- apps/dash/src/shared/i18n/messages/id.json | 290 ++++++++++++++++- .../src/shared/ui/shadcn/alert-dialog.tsx | 187 +++++++++++ apps/dash/src/shared/ui/shadcn/button.tsx | 2 - 29 files changed, 1119 insertions(+), 328 deletions(-) create mode 100644 apps/dash/src/pages/org/ui/components/DeleteOrgButton.tsx create mode 100644 apps/dash/src/shared/ui/shadcn/alert-dialog.tsx diff --git a/apps/dash/app/(auth)/login/page.tsx b/apps/dash/app/(auth)/login/page.tsx index 48a5246..03e1ebd 100644 --- a/apps/dash/app/(auth)/login/page.tsx +++ b/apps/dash/app/(auth)/login/page.tsx @@ -1,4 +1,4 @@ export { - LoginMetadata as metadata, + generateMetadata, LoginPage as default, } from "@/pages/auth/ui"; diff --git a/apps/dash/src/pages/auth/ui/LoginPage.tsx b/apps/dash/src/pages/auth/ui/LoginPage.tsx index 8bece98..374c628 100644 --- a/apps/dash/src/pages/auth/ui/LoginPage.tsx +++ b/apps/dash/src/pages/auth/ui/LoginPage.tsx @@ -1,13 +1,19 @@ import c, { Environment } from "@domus/config"; -import type { Metadata } from "next"; import { useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; import { Card, CardContent } from "@/shared/ui/shadcn/card"; import { Brand } from "@/widgets/layout/ui/components/Brand"; import { LoginForm } from "./components/LoginForm"; -export const LoginMetadata: Metadata = { - title: "Login to PKRBT", -}; +/** + * Generates metadata for the login page. + */ +export async function generateMetadata() { + const t = await getTranslations("LoginPage"); + return { + title: t("metadataTitle"), + }; +} /** * Login page UI component. diff --git a/apps/dash/src/pages/event/ui/EventPage.tsx b/apps/dash/src/pages/event/ui/EventPage.tsx index 4c27c00..4e2bc4b 100644 --- a/apps/dash/src/pages/event/ui/EventPage.tsx +++ b/apps/dash/src/pages/event/ui/EventPage.tsx @@ -1,3 +1,4 @@ +import { useTranslations } from "next-intl"; import { UnderConstruction } from "@/shared/ui/components/UnderConstruction"; /** @@ -7,10 +8,9 @@ import { UnderConstruction } from "@/shared/ui/components/UnderConstruction"; * @returns The EventPage component. */ export function EventPage() { + const t = useTranslations("EventPage"); + return ( - + ); } diff --git a/apps/dash/src/pages/finance/ui/FinancePage.tsx b/apps/dash/src/pages/finance/ui/FinancePage.tsx index a8603f0..d767c70 100644 --- a/apps/dash/src/pages/finance/ui/FinancePage.tsx +++ b/apps/dash/src/pages/finance/ui/FinancePage.tsx @@ -1,3 +1,4 @@ +import { useTranslations } from "next-intl"; import { UnderConstruction } from "@/shared/ui/components/UnderConstruction"; /** @@ -7,10 +8,9 @@ import { UnderConstruction } from "@/shared/ui/components/UnderConstruction"; * @returns The FinancePage component. */ export function FinancePage() { + const t = useTranslations("FinancePage"); + return ( - + ); } diff --git a/apps/dash/src/pages/home/actions/home.ts b/apps/dash/src/pages/home/actions/home.ts index 4b037fd..96443ce 100644 --- a/apps/dash/src/pages/home/actions/home.ts +++ b/apps/dash/src/pages/home/actions/home.ts @@ -41,12 +41,9 @@ import type { LucideIcon } from "lucide-react"; /** A parish service shortcut card. */ export interface LayananItem { id: string; - title: string; - description: string; icon: LucideIcon; color: string; hoverTextColor: string; - actionLabel: string; href: string; } diff --git a/apps/dash/src/pages/home/ui/components/AgendaSection.tsx b/apps/dash/src/pages/home/ui/components/AgendaSection.tsx index 6bc1977..6c6d6cb 100644 --- a/apps/dash/src/pages/home/ui/components/AgendaSection.tsx +++ b/apps/dash/src/pages/home/ui/components/AgendaSection.tsx @@ -3,7 +3,7 @@ import { EventStatus, RsvpStatus } from "@domus/core"; import { Calendar, Check, HelpCircle, MapPin, X } from "lucide-react"; import Image from "next/image"; -import { useTranslations } from "next-intl"; +import { useLocale, useTranslations } from "next-intl"; import { useState } from "react"; import type { AgendaItem } from "../../actions/home"; @@ -16,6 +16,7 @@ interface AgendaSectionProps { */ export function AgendaSection({ items }: AgendaSectionProps) { const t = useTranslations("HomePage"); + const locale = useLocale(); const [rsvpMap, setRsvpMap] = useState>( () => { return Object.fromEntries(items.map((i) => [i.id, i.rsvpStatus])); @@ -93,11 +94,14 @@ export function AgendaSection({ items }: AgendaSectionProps) {
- {new Intl.DateTimeFormat("id-ID", { - day: "2-digit", - month: "short", - year: "numeric", - }).format(item.startDateTime)} + {new Intl.DateTimeFormat( + locale === "id" ? "id-ID" : "en-US", + { + day: "2-digit", + month: "short", + year: "numeric", + }, + ).format(item.startDateTime)}
diff --git a/apps/dash/src/pages/home/ui/components/LayananSection.tsx b/apps/dash/src/pages/home/ui/components/LayananSection.tsx index d97b065..d97d945 100644 --- a/apps/dash/src/pages/home/ui/components/LayananSection.tsx +++ b/apps/dash/src/pages/home/ui/components/LayananSection.tsx @@ -10,15 +10,16 @@ interface LayananSectionProps { * Displays a grid of service shortcut cards. Static/Server Component. */ export function LayananSection({ items }: LayananSectionProps) { - const t = useTranslations("HomePage"); + const tHome = useTranslations("HomePage"); + const tActions = useTranslations("DashboardActions"); return (

- {t("servicesTitle")} + {tHome("servicesTitle")}

- {t("servicesSubtitle")} + {tHome("servicesSubtitle")}

@@ -37,12 +38,12 @@ export function LayananSection({ items }: LayananSectionProps) {

- {item.title} + {tActions(`${item.id}.title`)}

- {item.description} + {tActions(`${item.id}.description`)}

@@ -50,7 +51,7 @@ export function LayananSection({ items }: LayananSectionProps) { type="button" className={`mt-6 w-full rounded-lg bg-white/10 px-4 py-2 text-[10px] font-bold uppercase tracking-wider text-white border border-white/20 transition-all hover:bg-white ${item.hoverTextColor}`} > - {item.actionLabel} + {tActions(`${item.id}.actionLabel`)} ); diff --git a/apps/dash/src/pages/home/ui/components/TugasSection.tsx b/apps/dash/src/pages/home/ui/components/TugasSection.tsx index 0bf0eaf..1997ae0 100644 --- a/apps/dash/src/pages/home/ui/components/TugasSection.tsx +++ b/apps/dash/src/pages/home/ui/components/TugasSection.tsx @@ -2,7 +2,7 @@ import { Calendar, CheckCircle2 } from "lucide-react"; import Image from "next/image"; -import { useTranslations } from "next-intl"; +import { useLocale, useTranslations } from "next-intl"; import { useState } from "react"; import type { ParishionerRequest } from "../../actions/home"; @@ -15,6 +15,7 @@ interface TugasSectionProps { */ export function TugasSection({ items }: TugasSectionProps) { const t = useTranslations("HomePage"); + const locale = useLocale(); const [confirmed, setConfirmed] = useState>(new Set()); const handleConfirm = (id: string) => { @@ -85,14 +86,17 @@ export function TugasSection({ items }: TugasSectionProps) {
- {new Intl.DateTimeFormat("id-ID", { - weekday: "long", - day: "2-digit", - month: "short", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - }).format(item.createdAt)} + {new Intl.DateTimeFormat( + locale === "id" ? "id-ID" : "en-US", + { + weekday: "long", + day: "2-digit", + month: "short", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }, + ).format(item.createdAt)}
diff --git a/apps/dash/src/pages/org/ui/JoinPage.tsx b/apps/dash/src/pages/org/ui/JoinPage.tsx index 333e8e0..e6f4e2a 100644 --- a/apps/dash/src/pages/org/ui/JoinPage.tsx +++ b/apps/dash/src/pages/org/ui/JoinPage.tsx @@ -1,6 +1,7 @@ import { AlertCircle, ArrowLeft } from "lucide-react"; import { headers } from "next/headers"; import Link from "next/link"; +import { getTranslations } from "next-intl/server"; import { Button } from "@/shared/ui/shadcn/button"; import { Card } from "@/shared/ui/shadcn/card"; import { getSession } from "../../../shared/auth/server"; @@ -27,14 +28,16 @@ interface JoinPageProps { */ export async function JoinPage({ params }: JoinPageProps) { const { joinId } = await params; + const t = await getTranslations("JoinPage"); // 1. Get session (Auth is required, proxy should have redirected if not logged in) const session = await getSession(await headers()); if (!session) { return ( ); } @@ -42,7 +45,13 @@ export async function JoinPage({ params }: JoinPageProps) { // 2. Fetch Organization and Unit data const [data, error] = await getJoinDataAction(joinId); if (error) { - return ; + return ( + + ); } const { organization, units } = data; @@ -56,7 +65,7 @@ export async function JoinPage({ params }: JoinPageProps) { className="group mb-6 flex w-fit items-center gap-2 text-xs font-semibold text-[#44474c] transition-colors hover:text-[#a23c29]" > - Kembali ke Beranda + {t("backHome")} {/* Form Container */} @@ -87,9 +96,11 @@ export async function JoinPage({ params }: JoinPageProps) { function JoinErrorScreen({ title, message, + backHome, }: { title: string; message: string; + backHome: string; }) { return (
@@ -106,7 +117,7 @@ function JoinErrorScreen({ variant="default" className="mt-8 bg-[#233345] hover:bg-[#324559] h-10 px-8 rounded-lg" > - Kembali ke Beranda + {backHome}
); diff --git a/apps/dash/src/pages/org/ui/JoinSuccessPage.tsx b/apps/dash/src/pages/org/ui/JoinSuccessPage.tsx index 4864e67..5a65421 100644 --- a/apps/dash/src/pages/org/ui/JoinSuccessPage.tsx +++ b/apps/dash/src/pages/org/ui/JoinSuccessPage.tsx @@ -1,5 +1,6 @@ import { CheckCircle, Home } from "lucide-react"; import Link from "next/link"; +import { getTranslations } from "next-intl/server"; import { Button } from "@/shared/ui/shadcn/button"; import { Card } from "@/shared/ui/shadcn/card"; @@ -33,6 +34,7 @@ export async function JoinSuccessPage({ await params; const { exists } = await searchParams; const isExisting = exists === "true"; + const t = await getTranslations("JoinSuccessPage"); return (
@@ -42,12 +44,10 @@ export async function JoinSuccessPage({

- {isExisting ? "Pendaftaran Sudah Ada" : "Pendaftaran Berhasil!"} + {isExisting ? t("titleExists") : t("titleSuccess")}

- {isExisting - ? "Anda sudah terdaftar atau memiliki pendaftaran yang sedang diproses untuk organisasi ini. Silakan hubungi pengurus organisasi untuk informasi lebih lanjut." - : "Data pendaftaran Anda telah kami terima. Pengurus organisasi akan segera meninjau dan memproses profil Anda. Silakan hubungi sekretariat paroki untuk informasi lebih lanjut."} + {isExisting ? t("descExists") : t("descSuccess")}

diff --git a/apps/dash/src/pages/org/ui/OrgDetailPage.tsx b/apps/dash/src/pages/org/ui/OrgDetailPage.tsx index 89d02af..07cae6c 100644 --- a/apps/dash/src/pages/org/ui/OrgDetailPage.tsx +++ b/apps/dash/src/pages/org/ui/OrgDetailPage.tsx @@ -1,14 +1,16 @@ import { UserRole } from "@domus/core"; import { headers } from "next/headers"; import Link from "next/link"; -import { notFound, redirect } from "next/navigation"; +import { notFound } from "next/navigation"; +import { getTranslations } from "next-intl/server"; import { getSession } from "@/shared/auth/server"; import { organization as organizationService, unit as unitService, } from "@/shared/core"; -import { Button } from "@/shared/ui/shadcn/button"; +import { Badge } from "@/shared/ui/shadcn/badge"; import { Card, CardContent } from "@/shared/ui/shadcn/card"; +import { DeleteOrgButton } from "./components/DeleteOrgButton"; import { OrgHeader } from "./components/OrgHeader"; import { UnitList } from "./components/UnitList"; @@ -23,6 +25,8 @@ interface OrgDetailPageProps { * @param props - Component properties. */ export async function OrgDetailPage({ params }: OrgDetailPageProps) { + const t = await getTranslations("OrgDetailPage"); + const tEnums = await getTranslations("Enums"); const { id } = await params; // Parallel fetch org and units @@ -40,25 +44,13 @@ export async function OrgDetailPage({ params }: OrgDetailPageProps) { session?.context?.roles.includes(UserRole.SuperAdmin) || session?.context?.roles.includes(UserRole.ParishAdmin); - // Server action to remove the organization - const handleRemove = async () => { - "use server"; - const { removeAction } = await import("../actions/delete"); - const res = await removeAction(id); - if (res.success) { - redirect("/org"); - } else { - console.error(res.message); - } - }; - return (
{/* Navigation Breadcrumb-like */}
- Organisasi + {t("breadcrumb")} / {org.name} @@ -82,30 +74,33 @@ export async function OrgDetailPage({ params }: OrgDetailPageProps) {

- Detail Organisasi + {t("detailsTitle")}

- - Jenis - - - {org.type} + + {t("labelType")} + + {tEnums(`OrgType.${org.type}`)} +
{org.parentId && (
- Induk + {t("labelParent")} - Organisasi Terlampir + {t("labelAttachedOrg")}
)}
- Dibuat Pada + {t("labelCreatedAt")} {new Date(org.createdAt).toLocaleDateString("id-ID", { @@ -121,19 +116,9 @@ export async function OrgDetailPage({ params }: OrgDetailPageProps) { {canRemove && (

- Tindakan Berbahaya + {t("dangerZoneTitle")}

-
- -
+
)} @@ -144,28 +129,3 @@ export async function OrgDetailPage({ params }: OrgDetailPageProps) {
); } - -// Helper icons -function Trash2(props: React.SVGProps) { - return ( - - Trash Icon - - - - - - - ); -} diff --git a/apps/dash/src/pages/org/ui/OrgNewPage.tsx b/apps/dash/src/pages/org/ui/OrgNewPage.tsx index bafc6bc..9a25d36 100644 --- a/apps/dash/src/pages/org/ui/OrgNewPage.tsx +++ b/apps/dash/src/pages/org/ui/OrgNewPage.tsx @@ -1,3 +1,4 @@ +import { getTranslations } from "next-intl/server"; import { Card } from "@/shared/ui/shadcn/card"; import { createAction } from "../actions/create"; import { OrgForm } from "./components/OrgForm"; @@ -7,6 +8,7 @@ import { OrgForm } from "./components/OrgForm"; * Server component that renders the organization creation flow. */ export async function OrgNewPage() { + const t = await getTranslations("OrgNewPage"); return (
@@ -16,12 +18,9 @@ export async function OrgNewPage() { data-testid="org-new-title" className="font-headline text-3xl font-extrabold text-[#233345] tracking-tight mb-2 sm:text-4xl" > - Tambah Organisasi Baru + {t("title")} -

- Lengkapi detail informasi untuk mendaftarkan struktur organisasi - baru dalam sistem. -

+

{t("description")}

{/* Shadcn-style Card Container with Stitch styling */} diff --git a/apps/dash/src/pages/org/ui/OrgPage.tsx b/apps/dash/src/pages/org/ui/OrgPage.tsx index 7a852c3..162c325 100644 --- a/apps/dash/src/pages/org/ui/OrgPage.tsx +++ b/apps/dash/src/pages/org/ui/OrgPage.tsx @@ -1,4 +1,5 @@ import { headers } from "next/headers"; +import { getTranslations } from "next-intl/server"; import { getSession } from "@/shared/auth/server"; import { listAction } from "../actions/list"; import { OrgPageContent } from "./components/OrgPageContent"; @@ -9,15 +10,14 @@ import { OrgPageContent } from "./components/OrgPageContent"; */ export async function OrgPage() { const session = await getSession(await headers()); + const t = await getTranslations("OrgPage"); if (!session?.context) { console.error("[OrgPage] No session context found. Redirecting..."); // Ideally this should be handled by proxy, but as a safety measure: return (
-

- Sesi tidak valid. Silakan login kembali. -

+

{t("sessionError")}

); } diff --git a/apps/dash/src/pages/org/ui/components/DeleteOrgButton.tsx b/apps/dash/src/pages/org/ui/components/DeleteOrgButton.tsx new file mode 100644 index 0000000..22aaefb --- /dev/null +++ b/apps/dash/src/pages/org/ui/components/DeleteOrgButton.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { Trash2 } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; +import { useState } from "react"; +import { toast } from "sonner"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/shared/ui/shadcn/alert-dialog"; +import { Button } from "@/shared/ui/shadcn/button"; +import { removeAction } from "../../actions/delete"; + +interface DeleteOrgButtonProps { + id: string; + orgName: string; +} + +/** + * Delete organization button with confirmation dialog. + * + * @param props - Component properties. + */ +export function DeleteOrgButton({ id }: DeleteOrgButtonProps) { + const t = useTranslations("OrgDetailPage"); + const router = useRouter(); + const [isDeleting, setIsDeleting] = useState(false); + + const handleDelete = async () => { + setIsDeleting(true); + try { + const res = await removeAction(id); + if (res.success) { + toast.success(t("deleteSuccess")); + router.push("/org"); + router.refresh(); + } else { + toast.error(res.message || t("deleteError")); + } + } catch (error) { + console.error("Delete error:", error); + toast.error(t("deleteError")); + } finally { + setIsDeleting(false); + } + }; + + return ( + + + + {t("deleteBtn")} + + } + /> + + + {t("confirmDeleteTitle")} + + {t("confirmDeleteDesc")} + + + + + {t("confirmDeleteCancel")} + + { + e.preventDefault(); + handleDelete(); + }} + disabled={isDeleting} + className="bg-[#a23c29] hover:bg-[#a23c29]/90 text-white" + > + {isDeleting ? "..." : t("confirmDeleteAction")} + + + + + ); +} diff --git a/apps/dash/src/pages/org/ui/components/JoinForm.tsx b/apps/dash/src/pages/org/ui/components/JoinForm.tsx index 76ba09f..84f87f5 100644 --- a/apps/dash/src/pages/org/ui/components/JoinForm.tsx +++ b/apps/dash/src/pages/org/ui/components/JoinForm.tsx @@ -21,6 +21,7 @@ import { } from "lucide-react"; import Image from "next/image"; import { useRouter } from "next/navigation"; +import { useLocale, useTranslations } from "next-intl"; import { useRef, useState } from "react"; import { toast } from "sonner"; import type { ActionResult } from "@/shared/types"; @@ -45,24 +46,6 @@ import { SelectValue, } from "@/shared/ui/shadcn/select"; -/** - * Labels for Indonesian UI translation. - */ -const EducationLevelLabels: Record = { - primary: "SD / Sederajat", - junior: "SMP / Sederajat", - senior: "SMA / SMK / Sederajat", - diploma: "Diploma (D3)", - bachelor: "Sarjana (S1)", - master: "Magister (S2)", - doctorate: "Doktor (S3)", -}; - -const GenderLabels: Record = { - male: "Laki-laki", - female: "Perempuan", -}; - /** * Honorific suggestions for registration. */ @@ -108,6 +91,10 @@ export function OrganizationJoinForm({ action, }: JoinFormProps) { const router = useRouter(); + const t = useTranslations("JoinForm"); + const tEnums = useTranslations("Enums"); + const locale = useLocale(); + const [isSubmitting, setIsSubmitting] = useState(false); const [actionError, setActionError] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); @@ -136,19 +123,19 @@ export function OrganizationJoinForm({ const result = await action(joinId, value); if (result.success) { if (result.data?.isNew) { - toast.success("Berhasil mendaftar!"); + toast.success(t("toastSuccess")); router.push(`/join/${joinId}/success`); } else { - toast.info("Pendaftaran Anda sudah terdaftar sebelumnya."); + toast.info(t("toastInfoExists")); router.push(`/join/${joinId}/success?exists=true`); } } else { - setActionError(result.message || "Gagal mendaftar."); - toast.error(result.message || "Gagal mendaftar."); + setActionError(result.message || t("toastError")); + toast.error(result.message || t("toastError")); } } catch (_err) { - setActionError("Terjadi kesalahan yang tidak terduga."); - toast.error("Terjadi kesalahan yang tidak terduga."); + setActionError(t("toastError")); + toast.error(t("toastError")); } finally { setIsSubmitting(false); } @@ -224,7 +211,7 @@ export function OrganizationJoinForm({

- {organization.description || "Organisasi Katolik Barong Tongkok."} + {organization.description || t("defaultOrgDescription")}

@@ -260,10 +247,10 @@ export function OrganizationJoinForm({

- Identitas Diri + {t("identityHeader")}

- Informasi dasar identitas Anda sebagai anggota. + {t("identitySub")}

@@ -275,7 +262,7 @@ export function OrganizationJoinForm({ htmlFor={field.name} className="text-sm font-semibold text-[#233345] font-body" > - Gelar Depan + {t("labelHonorific")} field.handleChange(e.target.value)} - placeholder="RD, Sr, Dr..." + placeholder={t("placeholderHonorific")} list="honorifics" className="w-full px-4 py-2.5 bg-[#f3f4f6] border-none rounded-lg focus-visible:ring-2 focus-visible:ring-[#a23c29]/20 border-b-2 border-[#74777d] focus-visible:border-[#a23c29] transition-all text-sm placeholder:text-[#c4c6cc]" /> @@ -303,7 +290,8 @@ export function OrganizationJoinForm({ htmlFor={field.name} className="text-sm font-semibold text-[#233345] font-body" > - Nama Lengkap * + {t("labelFullName")}{" "} + * field.handleChange(e.target.value)} - placeholder="Masukkan nama lengkap Anda" + placeholder={t("placeholderFullName")} className="w-full px-4 py-2.5 bg-[#f3f4f6] border-none rounded-lg focus-visible:ring-2 focus-visible:ring-[#a23c29]/20 border-b-2 border-[#74777d] focus-visible:border-[#a23c29] transition-all text-sm placeholder:text-[#c4c6cc]" /> {field.state.meta.errors.length > 0 && ( @@ -328,7 +316,7 @@ export function OrganizationJoinForm({ htmlFor={field.name} className="text-sm font-semibold text-[#233345] font-body" > - Gelar Belakang + {t("labelSuffix")} field.handleChange(e.target.value)} - placeholder="S.Pd, M.T" + placeholder={t("placeholderSuffix")} className="w-full px-4 py-2.5 bg-[#f3f4f6] border-none rounded-lg focus-visible:ring-2 focus-visible:ring-[#a23c29]/20 border-b-2 border-[#74777d] focus-visible:border-[#a23c29] transition-all text-sm placeholder:text-[#c4c6cc]" /> {field.state.meta.errors.length > 0 && ( @@ -352,10 +340,10 @@ export function OrganizationJoinForm({

- Data Pribadi + {t("personalHeader")}

- Informasi tambahan untuk profil kependudukan Anda. + {t("personalSub")}

@@ -367,7 +355,7 @@ export function OrganizationJoinForm({ htmlFor={field.name} className="text-sm font-semibold text-[#233345] font-body" > - Tempat Lahir + {t("labelBirthPlace")} field.handleChange(e.target.value)} - placeholder="Contoh: Barong Tongkok" + placeholder={t("placeholderBirthPlace")} className="w-full px-4 py-2.5 bg-[#f3f4f6] border-none rounded-lg focus-visible:ring-2 focus-visible:ring-[#a23c29]/20 border-b-2 border-[#74777d] focus-visible:border-[#a23c29] transition-all text-sm placeholder:text-[#c4c6cc]" /> {field.state.meta.errors.length > 0 && ( @@ -392,7 +380,7 @@ export function OrganizationJoinForm({ htmlFor={field.name} className="text-sm font-semibold text-[#233345] font-body" > - Tanggal Lahir + {t("labelBirthDate")} {field.state.value ? ( - format(field.state.value, "PPP", { locale: id }) + format(field.state.value, "PPP", { + locale: locale === "id" ? id : undefined, + }) ) : ( - Pilih tanggal + {t("placeholderBirthDate")} )} @@ -433,7 +423,7 @@ export function OrganizationJoinForm({ {(field) => (
- {GenderLabels[value]} + {tEnums(`Gender.${value}`)}
))} @@ -467,7 +457,7 @@ export function OrganizationJoinForm({ htmlFor={field.name} className="text-sm font-semibold text-[#233345] font-body" > - Pendidikan Terakhir + {t("labelEducation")} field.handleChange(e.target.value)} - placeholder="Contoh: Anggota, Pengurus..." + placeholder={t("placeholderPosition")} className="w-full px-4 py-2.5 bg-[#f3f4f6] border-none rounded-lg focus-visible:ring-2 focus-visible:ring-[#a23c29]/20 border-b-2 border-[#74777d] focus-visible:border-[#a23c29] transition-all text-sm placeholder:text-[#c4c6cc]" />

- Default: anggota + {t("positionHelp")}

{field.state.meta.errors.length > 0 && ( @@ -720,15 +707,12 @@ export function OrganizationJoinForm({ ) : ( <> - Bergabung Sekarang + {t("btnSubmit")} )}

- Dengan menekan tombol di atas, saya bersedia bergabung dengan - organisasi{" "} - {organization.name}{" "} - dan mematuhi segala ketentuan yang berlaku. + {t("consentText", { orgName: organization.name })}

diff --git a/apps/dash/src/pages/org/ui/components/OrgFilters.tsx b/apps/dash/src/pages/org/ui/components/OrgFilters.tsx index fcfdae4..6b3652a 100644 --- a/apps/dash/src/pages/org/ui/components/OrgFilters.tsx +++ b/apps/dash/src/pages/org/ui/components/OrgFilters.tsx @@ -40,22 +40,13 @@ export function OrgFilters({ onClear, }: OrgFiltersProps) { const t = useTranslations("OrgPage"); + const tEnums = useTranslations("Enums"); const isFiltered = search !== "" || type !== "all"; // Explicit label mapping to ensure translations render correctly in SelectValue const getTypeLabel = (val: string) => { - switch (val) { - case OrgType.Region: - return t("typeRegion"); - case OrgType.BEC: - return t("typeBec"); - case OrgType.Station: - return t("typeStation"); - case OrgType.Categorical: - return t("typeCategorical"); - default: - return t("filterAll"); - } + if (val === "all") return t("filterAll"); + return tEnums(`OrgType.${val}`); }; return ( @@ -88,11 +79,17 @@ export function OrgFilters({ {t("filterAll")} - {t("typeRegion")} - {t("typeBec")} - {t("typeStation")} + + {tEnums(`OrgType.${OrgType.Region}`)} + + + {tEnums(`OrgType.${OrgType.BEC}`)} + + + {tEnums(`OrgType.${OrgType.Station}`)} + - {t("typeCategorical")} + {tEnums(`OrgType.${OrgType.Categorical}`)} diff --git a/apps/dash/src/pages/org/ui/components/OrgForm.tsx b/apps/dash/src/pages/org/ui/components/OrgForm.tsx index 90fb8a7..589d4cb 100644 --- a/apps/dash/src/pages/org/ui/components/OrgForm.tsx +++ b/apps/dash/src/pages/org/ui/components/OrgForm.tsx @@ -11,6 +11,7 @@ import { useForm } from "@tanstack/react-form"; import { Camera, Check, Image as ImageIcon, X } from "lucide-react"; import Image from "next/image"; import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import { useState } from "react"; import type { ActionResult } from "@/shared/types"; import ValidationErrors from "@/shared/ui/components/ValidationErrors"; @@ -53,6 +54,8 @@ type OrgFormProps = { */ export function OrgForm({ action, defaultValues, isUpdate }: OrgFormProps) { const router = useRouter(); + const t = useTranslations("OrgForm"); + const tEnum = useTranslations("Enums.OrgType"); const [isSubmitting, setIsSubmitting] = useState(false); const [actionError, setActionError] = useState(null); @@ -109,10 +112,10 @@ export function OrgForm({ action, defaultValues, isUpdate }: OrgFormProps) {

- Informasi Dasar + {t("basicInfoTitle")}

- Detail struktural dan penamaan organisasi. + {t("basicInfoDesc")}

@@ -125,7 +128,7 @@ export function OrgForm({ action, defaultValues, isUpdate }: OrgFormProps) { htmlFor={field.name} className="text-sm font-semibold text-[#233345] font-body" > - Nama Organisasi + {t("nameLabel")} field.handleChange(e.target.value)} className="w-full px-4 py-2.5 bg-[#f3f4f6] border-none rounded-lg focus-visible:ring-2 focus-visible:ring-[#a23c29]/20 border-b-2 border-[#74777d] focus-visible:border-[#a23c29] transition-all text-sm placeholder:text-[#c4c6cc]" - placeholder="Masukkan nama organisasi..." + placeholder={t("namePlaceholder")} /> {field.state.meta.errors.length > 0 && ( @@ -153,7 +156,7 @@ export function OrgForm({ action, defaultValues, isUpdate }: OrgFormProps) { htmlFor={field.name} className="text-sm font-semibold text-[#233345] font-body" > - Jenis Organisasi + {t("typeLabel")} @@ -198,7 +205,7 @@ export function OrgForm({ action, defaultValues, isUpdate }: OrgFormProps) { htmlFor={field.name} className="text-sm font-semibold text-[#233345] font-body" > - Organisasi Induk + {t("parentLabel")} - Batal + {t("btnCancel")}
@@ -250,10 +257,10 @@ export function OrgForm({ action, defaultValues, isUpdate }: OrgFormProps) {
diff --git a/apps/dash/src/pages/org/ui/components/OrganizationList.tsx b/apps/dash/src/pages/org/ui/components/OrganizationList.tsx index bf8cbfb..c201b4c 100644 --- a/apps/dash/src/pages/org/ui/components/OrganizationList.tsx +++ b/apps/dash/src/pages/org/ui/components/OrganizationList.tsx @@ -62,7 +62,7 @@ export function OrganizationList({ onClick={() => window.location.reload()} className="rounded-xl px-8 h-11 hover:bg-destructive/5 hover:text-destructive hover:border-destructive/30 transition-all font-medium" > - Coba Lagi + {t("retry")} @@ -82,7 +82,7 @@ export function OrganizationList({ {t("emptyState")}

- Coba sesuaikan kata kunci atau filter Anda. + {t("emptyStateDesc")}

{(searchQuery || selectedType !== "all") && ( diff --git a/apps/dash/src/pages/org/ui/components/UnitForm.tsx b/apps/dash/src/pages/org/ui/components/UnitForm.tsx index 3fbfb37..c342a70 100644 --- a/apps/dash/src/pages/org/ui/components/UnitForm.tsx +++ b/apps/dash/src/pages/org/ui/components/UnitForm.tsx @@ -8,6 +8,7 @@ import { } from "@domus/core"; import { useForm } from "@tanstack/react-form"; import { Check, X } from "lucide-react"; +import { useTranslations } from "next-intl"; import { useState } from "react"; import type { ActionResult } from "@/shared/types"; import ValidationErrors from "@/shared/ui/components/ValidationErrors"; @@ -60,6 +61,7 @@ export function UnitForm({ onSuccess, onCancel, }: UnitFormProps) { + const t = useTranslations("UnitForm"); const [isSubmitting, setIsSubmitting] = useState(false); const [actionError, setActionError] = useState(null); @@ -115,7 +117,7 @@ export function UnitForm({ htmlFor={field.name} className="text-sm font-semibold text-[#233345]" > - Nama Unit + {t("nameLabel")} field.handleChange(e.target.value)} - placeholder="Masukkan nama unit (misal: Seksi Kerohanian)" + placeholder={t("namePlaceholder")} className="w-full" /> {field.state.meta.errors.length > 0 && ( @@ -149,7 +151,7 @@ export function UnitForm({ htmlFor={field.name} className="text-sm font-semibold text-[#233345]" > - Deskripsi (Opsional) + {t("descLabel")}