responsiveness and ui fixers

This commit is contained in:
2026-09-23 16:55:46 +05:30
parent 4c5e9b347a
commit f5e8e0e5b9
88 changed files with 9408 additions and 3576 deletions
@@ -21,6 +21,9 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { toast } from "@/components/ui/toast"
const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]
// Mobile has no page-size selector — tapping "Show more" just grows the page size and
// re-fetches from page 1, so previously loaded cards stay put and more append below.
const MOBILE_PAGE_SIZE_STEP = 10
interface CodeNamed {
code: string
@@ -58,6 +61,7 @@ export function CodeNameMasterPage<T extends CodeNamed>({
const [search, setSearch] = useState("")
const [debouncedSearch, setDebouncedSearch] = useState("")
const [actionPendingId, setActionPendingId] = useState<number | null>(null)
const [showMoreLoading, setShowMoreLoading] = useState(false)
const [open, setOpen] = useState(false)
const [code, setCode] = useState("")
@@ -80,6 +84,12 @@ export function CodeNameMasterPage<T extends CodeNamed>({
setPagination(res.pagination)
})
.catch((err) => setError(errorMessage(err)))
.finally(() => setShowMoreLoading(false))
}
function handleShowMore() {
setShowMoreLoading(true)
setPageSize((s) => s + MOBILE_PAGE_SIZE_STEP)
}
useEffect(load, [page, pageSize, debouncedSearch]) // eslint-disable-line react-hooks/exhaustive-deps
@@ -212,9 +222,9 @@ export function CodeNameMasterPage<T extends CodeNamed>({
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
</Field>
</FieldGroup>
<div className="flex justify-center gap-3 pt-2">
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
</div>
</DialogContent>
</Dialog>
@@ -243,9 +253,9 @@ export function CodeNameMasterPage<T extends CodeNamed>({
<FieldError errors={[editErrors.name ? { message: editErrors.name } : undefined]} />
</Field>
</FieldGroup>
<div className="flex justify-center gap-3 pt-2">
<Button variant="outline" className="min-w-32" onClick={() => setEditItem(null)} disabled={editSubmitting}>Cancel</Button>
<Button className="min-w-32" onClick={handleEditSave} disabled={editSubmitting || editLoading}>
<div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setEditItem(null)} disabled={editSubmitting}>Cancel</Button>
<Button className="w-full sm:w-auto sm:min-w-32" onClick={handleEditSave} disabled={editSubmitting || editLoading}>
{editSubmitting ? "Saving…" : "Save"}
</Button>
</div>
@@ -313,100 +323,191 @@ export function CodeNameMasterPage<T extends CodeNamed>({
)}
{!error && items !== null && items.length > 0 && (
<div className="overflow-hidden rounded-xl border">
<div className="overflow-x-auto">
<Table className="table-fixed text-base">
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="h-11 w-1/5 px-4 text-sm">Code</TableHead>
<TableHead className="h-11 w-1/5 px-4 text-sm">Name</TableHead>
<TableHead className="h-11 w-1/5 px-4 text-sm">Status</TableHead>
<TableHead className="h-11 w-1/5 px-4 text-sm">Created</TableHead>
<TableHead className="h-11 w-1/5 px-4 text-center text-sm">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={idOf(item)} className="group align-top hover:bg-muted/20">
<TableCell className="px-4 py-4 font-mono text-sm text-foreground">{item.code}</TableCell>
<TableCell className="px-4 py-4 text-sm text-foreground">{item.name}</TableCell>
<TableCell className="px-4 py-4">
<Badge
variant="outline"
className={cn(
"h-5 w-fit justify-center bg-white px-2 text-xs",
item.status === "Active" ? "border-success/40 text-success" : "border-input text-muted-foreground"
)}
>
{item.status}
</Badge>
</TableCell>
<TableCell className="px-4 py-4 text-sm text-muted-foreground">
{new Date(item.createdAt).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })}
</TableCell>
<TableCell className="px-4 py-4">
<div className="flex items-center justify-center gap-1">
<>
{/* Mobile: stacked cards. */}
<div className="flex flex-col gap-3 md:hidden">
{items.map((item) => (
<div key={idOf(item)} className="flex flex-col gap-3 rounded-xl border bg-card p-4 shadow-sm">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<p className="truncate font-semibold text-foreground">{item.name}</p>
<span className="font-mono text-xs text-muted-foreground">{item.code}</span>
</div>
<Badge
variant="outline"
className={cn(
"h-6 w-fit shrink-0 justify-center border-transparent px-2.5 text-sm",
item.status === "Active" ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"
)}
>
{item.status}
</Badge>
</div>
<div className="border-t pt-3 text-sm">
<p className="text-muted-foreground">Created</p>
<p className="font-medium text-foreground">
{new Date(item.createdAt).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })}
</p>
</div>
<div className="flex items-center justify-end gap-1 border-t pt-3">
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label={`Edit ${item.name}`}
title={`Edit ${item.name}`}
onClick={() => openEdit(item)}
>
<Pencil className="size-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
className={cn(
item.status === "Active"
? "text-destructive hover:bg-destructive/10"
: "text-success hover:bg-success/10"
)}
aria-label={item.status === "Active" ? `Deactivate ${item.name}` : `Activate ${item.name}`}
title={item.status === "Active" ? `Deactivate ${item.name}` : `Activate ${item.name}`}
disabled={actionPendingId === idOf(item)}
onClick={() => toggleStatus(item)}
>
{item.status === "Active" ? <XCircle className="size-4" /> : <CheckCircle2 className="size-4" />}
</Button>
<AlertDialog>
<AlertDialogTrigger
render={
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label={`Edit ${item.name}`}
title={`Edit ${item.name}`}
onClick={() => openEdit(item)}
>
<Pencil className="size-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
className={cn(
item.status === "Active"
? "text-destructive hover:bg-destructive/10"
: "text-success hover:bg-success/10"
)}
aria-label={item.status === "Active" ? `Deactivate ${item.name}` : `Activate ${item.name}`}
title={item.status === "Active" ? `Deactivate ${item.name}` : `Activate ${item.name}`}
className="text-destructive hover:bg-destructive/10 disabled:pointer-events-auto disabled:cursor-not-allowed"
aria-label={`Delete ${item.name}`}
title={`Delete ${item.name}`}
disabled={actionPendingId === idOf(item)}
onClick={() => toggleStatus(item)}
>
{item.status === "Active" ? <XCircle className="size-4" /> : <CheckCircle2 className="size-4" />}
</Button>
<AlertDialog>
<AlertDialogTrigger
render={
<Button
variant="ghost"
size="icon-sm"
className="text-destructive hover:bg-destructive/10 disabled:pointer-events-auto disabled:cursor-not-allowed"
aria-label={`Delete ${item.name}`}
title={`Delete ${item.name}`}
disabled={actionPendingId === idOf(item)}
/>
}
>
<Trash2 className="size-4" />
</AlertDialogTrigger>
<AlertDialogContent
variant="destructive"
title={`Delete ${item.name}?`}
description="This permanently removes this record. This cannot be undone. Blocked if it's referenced by other records — deactivate it instead in that case."
confirmLabel="Delete"
onConfirm={() => handleDelete(item)}
/>
</AlertDialog>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
/>
}
>
<Trash2 className="size-4" />
</AlertDialogTrigger>
<AlertDialogContent
variant="destructive"
title={`Delete ${item.name}?`}
description="This permanently removes this record. This cannot be undone. Blocked if it's referenced by other records — deactivate it instead in that case."
confirmLabel="Delete"
onConfirm={() => handleDelete(item)}
/>
</AlertDialog>
</div>
</div>
))}
{pagination && items.length < pagination.totalItems && (
<Button type="button" variant="outline" size="lg" className="w-full" onClick={handleShowMore} disabled={showMoreLoading}>
{showMoreLoading ? "Loading…" : "Show more"}
</Button>
)}
</div>
</div>
{/* Desktop: table. */}
<div className="hidden overflow-hidden rounded-xl border md:block">
<div className="overflow-x-auto">
<Table className="table-fixed text-base">
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="h-11 w-1/5 px-4 text-sm">Code</TableHead>
<TableHead className="h-11 w-1/5 px-4 text-sm">Name</TableHead>
<TableHead className="h-11 w-1/5 px-4 text-sm">Status</TableHead>
<TableHead className="h-11 w-1/5 px-4 text-sm">Created</TableHead>
<TableHead className="h-11 w-1/5 px-4 text-center text-sm">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={idOf(item)} className="group align-top hover:bg-muted/20">
<TableCell className="px-4 py-4 font-mono text-sm text-foreground">{item.code}</TableCell>
<TableCell className="px-4 py-4 text-sm text-foreground">{item.name}</TableCell>
<TableCell className="px-4 py-4">
<Badge
variant="outline"
className={cn(
"h-5 w-fit justify-center bg-white px-2 text-xs",
item.status === "Active" ? "border-success/40 text-success" : "border-input text-muted-foreground"
)}
>
{item.status}
</Badge>
</TableCell>
<TableCell className="px-4 py-4 text-sm text-muted-foreground">
{new Date(item.createdAt).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })}
</TableCell>
<TableCell className="px-4 py-4">
<div className="flex items-center justify-center gap-1">
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label={`Edit ${item.name}`}
title={`Edit ${item.name}`}
onClick={() => openEdit(item)}
>
<Pencil className="size-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
className={cn(
item.status === "Active"
? "text-destructive hover:bg-destructive/10"
: "text-success hover:bg-success/10"
)}
aria-label={item.status === "Active" ? `Deactivate ${item.name}` : `Activate ${item.name}`}
title={item.status === "Active" ? `Deactivate ${item.name}` : `Activate ${item.name}`}
disabled={actionPendingId === idOf(item)}
onClick={() => toggleStatus(item)}
>
{item.status === "Active" ? <XCircle className="size-4" /> : <CheckCircle2 className="size-4" />}
</Button>
<AlertDialog>
<AlertDialogTrigger
render={
<Button
variant="ghost"
size="icon-sm"
className="text-destructive hover:bg-destructive/10 disabled:pointer-events-auto disabled:cursor-not-allowed"
aria-label={`Delete ${item.name}`}
title={`Delete ${item.name}`}
disabled={actionPendingId === idOf(item)}
/>
}
>
<Trash2 className="size-4" />
</AlertDialogTrigger>
<AlertDialogContent
variant="destructive"
title={`Delete ${item.name}?`}
description="This permanently removes this record. This cannot be undone. Blocked if it's referenced by other records — deactivate it instead in that case."
confirmLabel="Delete"
onConfirm={() => handleDelete(item)}
/>
</AlertDialog>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
</>
)}
{pagination && pagination.totalItems > 0 && (
<div className="flex flex-col-reverse items-center gap-3 sm:flex-row sm:justify-between">
<div className="hidden items-center gap-3 md:flex md:justify-between">
<div className="flex items-center gap-3">
<p className="text-sm text-muted-foreground">
Showing <span className="font-medium text-foreground">{(pagination.page - 1) * pagination.pageSize + 1}</span>–