-
Notifications
You must be signed in to change notification settings - Fork 0
added ResourceDetail + ConfirmModal (DEV-78) #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,112 @@ | ||||||
| "use client"; | ||||||
|
|
||||||
| import Link from "next/link"; | ||||||
| import { useParams, useRouter } from "next/navigation"; | ||||||
| import ResourceDetail from "@/components/ResourceDetail"; | ||||||
| import { useAgency, useDeleteAgency } from "@/hooks/useApi"; | ||||||
| import type { Agency, ResourceDetailField } from "@/types"; | ||||||
|
|
||||||
| const agencyFields: ResourceDetailField<Agency>[] = [ | ||||||
| { key: "id", label: "ID" }, | ||||||
| { key: "name", label: "Name" }, | ||||||
| { key: "email", label: "Email" }, | ||||||
| { key: "phone", label: "Phone" }, | ||||||
| { key: "address", label: "Address" }, | ||||||
| { key: "city", label: "City" }, | ||||||
| { key: "province", label: "Province" }, | ||||||
| { key: "description", label: "Description", emptyValue: "No description" }, | ||||||
| { key: "status", label: "Status", emptyValue: "Unknown" }, | ||||||
| { key: "require_pre_payment", label: "Require Pre-payment" }, | ||||||
| { | ||||||
| key: "billing_profiles", | ||||||
| label: "Billing Profiles", | ||||||
| emptyValue: "None", | ||||||
| }, | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
| { | ||||||
| key: "created_at", | ||||||
| label: "Created", | ||||||
| render: (value) => new Date(String(value)).toLocaleString(), | ||||||
| }, | ||||||
| { | ||||||
| key: "updated_at", | ||||||
| label: "Updated", | ||||||
| render: (value) => new Date(String(value)).toLocaleString(), | ||||||
| }, | ||||||
| ]; | ||||||
|
|
||||||
| export default function AgencyDetailPage() { | ||||||
| const params = useParams<{ id: string }>(); | ||||||
| const router = useRouter(); | ||||||
| const rawId = params?.id; | ||||||
| const agencyId = Array.isArray(rawId) ? rawId[0] : rawId; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think Array.isArray branch is dead code so we can simplify it to:
Suggested change
|
||||||
|
|
||||||
| const { data: agency, isLoading, error } = useAgency(agencyId); | ||||||
| const deleteAgency = useDeleteAgency(); | ||||||
|
|
||||||
| const handleDelete = async () => { | ||||||
| if (!agencyId) { | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| await deleteAgency.mutateAsync(agencyId); | ||||||
| router.push("/agencies"); | ||||||
nuthanan06 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| }; | ||||||
|
|
||||||
| return ( | ||||||
| <div className="min-h-screen flex flex-col"> | ||||||
| <header className="w-full bg-gradient-to-r from-blue-600 to-blue-800 text-white shadow-lg"> | ||||||
| <div className="max-w-6xl mx-auto px-6 py-6 flex justify-between items-center"> | ||||||
| <h1 className="text-2xl font-bold">Agency Details</h1> | ||||||
| <div className="flex items-center gap-2"> | ||||||
| <Link | ||||||
| href="/agencies" | ||||||
| className="px-4 py-2 bg-white/20 rounded hover:bg-white/30 transition" | ||||||
| > | ||||||
| Back to Agencies | ||||||
| </Link> | ||||||
| <Link | ||||||
| href="/" | ||||||
| className="px-4 py-2 bg-white/20 rounded hover:bg-white/30 transition" | ||||||
| > | ||||||
| Home | ||||||
| </Link> | ||||||
| </div> | ||||||
| </div> | ||||||
| </header> | ||||||
|
|
||||||
| <main className="flex-1 max-w-6xl mx-auto px-6 py-8 w-full"> | ||||||
| {isLoading && <p className="text-gray-600">Loading agency details...</p>} | ||||||
|
|
||||||
| {error && ( | ||||||
| <div className="bg-red-50 border border-red-200 rounded p-4 text-red-800"> | ||||||
| <p>Failed to load agency details.</p> | ||||||
| </div> | ||||||
| )} | ||||||
|
|
||||||
| {!isLoading && !error && !agency && ( | ||||||
| <div className="bg-yellow-50 border border-yellow-200 rounded p-4 text-yellow-800"> | ||||||
| <p>Agency not found.</p> | ||||||
| </div> | ||||||
| )} | ||||||
|
|
||||||
| {agency && ( | ||||||
| <ResourceDetail | ||||||
| title={agency.name} | ||||||
| fields={agencyFields} | ||||||
| data={agency} | ||||||
| onDelete={handleDelete} | ||||||
| isDeleting={deleteAgency.isPending} | ||||||
| deleteTitle="Delete agency" | ||||||
| deleteMessage={`Are you sure you want to delete this agency (${agency.name})?`} | ||||||
| /> | ||||||
| )} | ||||||
|
|
||||||
| {deleteAgency.isError && ( | ||||||
| <div className="mt-4 bg-red-50 border border-red-200 rounded p-4 text-red-800"> | ||||||
| <p>Unable to delete agency. Please try again.</p> | ||||||
| </div> | ||||||
| )} | ||||||
| </main> | ||||||
| </div> | ||||||
| ); | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| "use client"; | ||
|
|
||
| interface ConfirmModalProps { | ||
| isOpen: boolean; | ||
| title: string; | ||
| message: string; | ||
| onConfirm: () => void; | ||
| onCancel: () => void; | ||
| isLoading?: boolean; | ||
| } | ||
|
|
||
| export default function ConfirmModal({ | ||
| isOpen, | ||
| title, | ||
| message, | ||
| onConfirm, | ||
| onCancel, | ||
| isLoading = false, | ||
| }: ConfirmModalProps) { | ||
| if (!isOpen) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4" | ||
| role="dialog" | ||
| aria-modal="true" | ||
| aria-labelledby="confirm-modal-title" | ||
| > | ||
| <div className="w-full max-w-md rounded-lg bg-white p-6 shadow-xl"> | ||
| <h2 id="confirm-modal-title" className="text-xl font-semibold text-gray-900"> | ||
| {title} | ||
| </h2> | ||
| <p className="mt-2 text-sm text-gray-600">{message}</p> | ||
|
|
||
nuthanan06 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <div className="mt-6 flex justify-end gap-3"> | ||
| <button | ||
| type="button" | ||
| onClick={onCancel} | ||
| disabled={isLoading} | ||
| className="rounded border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60" | ||
| > | ||
| Cancel | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={onConfirm} | ||
| disabled={isLoading} | ||
| className="rounded bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-60" | ||
| > | ||
| {isLoading ? "Deleting..." : "Confirm"} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| "use client"; | ||
|
|
||
| import { useState } from "react"; | ||
| import ConfirmModal from "@/components/ConfirmModal"; | ||
| import type { ResourceDetailProps } from "@/types"; | ||
|
|
||
| function defaultDisplayValue(value: unknown, emptyValue = "-") { | ||
| if (value === null || value === undefined || value === "") { | ||
| return emptyValue; | ||
| } | ||
|
|
||
| if (typeof value === "boolean") { | ||
| return value ? "Yes" : "No"; | ||
| } | ||
|
|
||
| if (Array.isArray(value) || typeof value === "object") { | ||
| return JSON.stringify(value); | ||
| } | ||
|
|
||
| return String(value); | ||
| } | ||
|
|
||
| export default function ResourceDetail<T extends object>({ | ||
| title, | ||
| fields, | ||
| data, | ||
| onDelete, | ||
| deleteTitle = "Confirm deletion", | ||
| deleteMessage = "Are you sure you want to delete this item?", | ||
| isDeleting = false, | ||
| }: ResourceDetailProps<T>) { | ||
| const [showConfirmModal, setShowConfirmModal] = useState(false); | ||
|
|
||
| const handleDeleteConfirm = async () => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If |
||
| if (!onDelete) { | ||
| return; | ||
| } | ||
|
|
||
| await onDelete(); | ||
| setShowConfirmModal(false); | ||
nuthanan06 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| <section className="rounded-lg border border-gray-200 bg-white shadow"> | ||
| <div className="flex items-center justify-between border-b border-gray-200 px-6 py-4"> | ||
| <h2 className="text-xl font-semibold text-gray-900"> | ||
| {title ?? "Resource Details"} | ||
| </h2> | ||
| {onDelete && ( | ||
| <button | ||
| type="button" | ||
| onClick={() => setShowConfirmModal(true)} | ||
| disabled={isDeleting} | ||
| className="rounded bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-60" | ||
| > | ||
| Delete | ||
| </button> | ||
| )} | ||
| </div> | ||
|
|
||
| <dl className="grid grid-cols-1 gap-4 p-6 sm:grid-cols-2"> | ||
| {fields.map((field) => { | ||
| const rawValue = data[field.key]; | ||
| const renderedValue = field.render | ||
| ? field.render(rawValue, data) | ||
| : defaultDisplayValue(rawValue, field.emptyValue); | ||
|
|
||
| return ( | ||
| <div | ||
| key={String(field.key)} | ||
| className="rounded-md border border-gray-100 bg-gray-50 px-4 py-3" | ||
| > | ||
| <dt className="text-xs font-medium uppercase tracking-wide text-gray-500"> | ||
| {field.label} | ||
| </dt> | ||
| <dd className="mt-1 break-words text-sm text-gray-900">{renderedValue}</dd> | ||
| </div> | ||
| ); | ||
| })} | ||
| </dl> | ||
| </section> | ||
|
|
||
| <ConfirmModal | ||
| isOpen={showConfirmModal} | ||
| title={deleteTitle} | ||
| message={deleteMessage} | ||
| onConfirm={handleDeleteConfirm} | ||
| onCancel={() => setShowConfirmModal(false)} | ||
| isLoading={isDeleting} | ||
| /> | ||
| </> | ||
| ); | ||
| } | ||
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.