Extract shared pagination, search box, and escapeLike helpers

Pull PER_PAGE, parsePage, and halamanHref into lib/pagination, the search
box into components/cari-master, and escapeLike into lib/utils so other
master features can reuse them. Pelanggan switches to these helpers and
its list now calls getCurrentUser() in its render chain.
main
Deardo Satria 20 hours ago
parent 930bfd2205
commit 6f1234aa49

@ -70,6 +70,8 @@ app/
components/
ui/ komponen shadcn (generated, jangan diedit manual kecuali perlu)
<nama>.tsx komponen yang dipakai lintas fitur
cari-master.tsx kotak pencarian master (prop basePath + placeholder)
pagination.tsx tombol halaman daftar (prop page, hasNext, hrefPage)
data/ Data Access Layer: fungsi baca/tulis DB, selalu 'server-only'
db/
index.ts, schema.ts koneksi + schema Postgres
@ -77,7 +79,8 @@ db/
lib/
auth.ts config Better Auth (server)
auth-client.ts client Better Auth
utils.ts helper umum (dibuat shadcn)
pagination.ts PER_PAGE, parse `?page=`, dan href halaman
utils.ts helper umum: cn (dibuat shadcn) dan escapeLike untuk LIKE
drizzle/ file migrasi Postgres (generated, ikut di-commit)
proxy.ts pengganti middleware di Next 16; hanya cek ringan (redirect)
```

@ -2,7 +2,9 @@ import { PencilIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Pagination } from "@/components/pagination";
import { getCurrentUser } from "@/data/auth";
import { listPelanggan } from "@/data/pelanggan";
import { halamanHref, parsePage } from "@/lib/pagination";
import { HapusPelangganButton } from "./hapus-pelanggan-button";
import { PelangganForm } from "./pelanggan-form";
@ -11,10 +13,10 @@ export async function DaftarPelanggan({
}: {
searchParams: Promise<{ search?: string; page?: string }>;
}) {
await getCurrentUser();
const { search, page: pageParam } = await searchParams;
const parsed = Number.parseInt(pageParam ?? "1", 10);
// Batas atas mencegah offset melebihi BIGINT MariaDB (query error).
const page = Number.isSafeInteger(parsed) && parsed >= 1 ? parsed : 1;
const page = parsePage(pageParam);
const { rows, hasNext } = await listPelanggan(search, page);
return (
@ -95,16 +97,8 @@ export async function DaftarPelanggan({
<Pagination
page={page}
hasNext={hasNext}
hrefPage={(p) => halamanHref(p, search)}
hrefPage={(p) => halamanHref("/master/pelanggan", p, search)}
/>
</>
);
}
function halamanHref(page: number, search?: string): string {
const params = new URLSearchParams();
if (search) params.set("search", search);
if (page > 1) params.set("page", String(page));
const query = params.toString();
return query ? `/master/pelanggan?${query}` : "/master/pelanggan";
}

@ -1,8 +1,8 @@
import { Suspense } from "react";
import { CariMaster } from "@/components/cari-master";
import { DaftarPelanggan } from "./_components/daftar-pelanggan";
import { TambahPelangganButton } from "./_components/tambah-pelanggan-button";
import { CariPelanggan } from "./_components/cari-pelanggan";
export default function PelangganPage({
searchParams,
@ -15,7 +15,10 @@ export default function PelangganPage({
<h1 className="text-lg font-semibold">Data Pelanggan</h1>
<div className="flex items-center gap-2">
<Suspense fallback={null}>
<CariPelanggan />
<CariMaster
basePath="/master/pelanggan"
placeholder="Cari pelanggan..."
/>
</Suspense>
<TambahPelangganButton />
</div>

@ -6,7 +6,12 @@ import { SearchIcon } from "lucide-react";
import { Input } from "@/components/ui/input";
export function CariPelanggan() {
type CariMasterProps = {
basePath: string;
placeholder?: string;
};
export function CariMaster({ basePath, placeholder }: CariMasterProps) {
const router = useRouter();
const searchParams = useSearchParams();
const [nilai, setNilai] = useState(searchParams.get("search") ?? "");
@ -17,19 +22,19 @@ export function CariPelanggan() {
const timer = setTimeout(() => {
const url = nilai
? `/master/pelanggan?search=${encodeURIComponent(nilai)}`
: "/master/pelanggan";
? `${basePath}?search=${encodeURIComponent(nilai)}`
: basePath;
router.replace(url);
}, 300);
return () => clearTimeout(timer);
}, [nilai, searchParams, router]);
}, [nilai, searchParams, router, basePath]);
return (
<div className="relative">
<SearchIcon className="absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
className="pl-8"
placeholder="Cari pelanggan..."
placeholder={placeholder}
value={nilai}
onChange={(e) => setNilai(e.target.value)}
/>

@ -3,6 +3,8 @@ import "server-only";
import { asc, desc, like, or, eq } from "drizzle-orm";
import { amigoDb } from "@/db/amigo";
import { pelanggan } from "@/db/amigo/schema";
import { PER_PAGE } from "@/lib/pagination";
import { escapeLike } from "@/lib/utils";
export type Pelanggan = {
kdCustomer: string;
@ -25,8 +27,6 @@ export type PelangganPage = {
hasNext: boolean;
};
const PER_PAGE = 25;
// kdCustomer dibuat sistem, nilaiBon diisi transaksi BON; keduanya bukan isian form.
export type PelangganInput = Omit<Pelanggan, "kdCustomer" | "nilaiBon">;
@ -38,11 +38,6 @@ function todayWib(): string {
);
}
// `\`, `%`, dan `_` adalah karakter khusus LIKE (escape default MariaDB `\`).
function escapeLike(value: string): string {
return value.replace(/[\\%_]/g, "\\$&");
}
function isKdCustomerExist(error: unknown): boolean {
const e = error as {
errno?: number;

@ -0,0 +1,19 @@
export const PER_PAGE = 25;
// Batas atas mencegah offset melebihi BIGINT MariaDB (query error).
export function parsePage(pageParam?: string): number {
const parsed = Number.parseInt(pageParam ?? "1", 10);
return Number.isSafeInteger(parsed) && parsed >= 1 ? parsed : 1;
}
export function halamanHref(
basePath: string,
page: number,
search?: string,
): string {
const params = new URLSearchParams();
if (search) params.set("search", search);
if (page > 1) params.set("page", String(page));
const query = params.toString();
return query ? `${basePath}?${query}` : basePath;
}

@ -1 +1,6 @@
export { cn } from "cn"
export { cn } from "cn";
// `\`, `%`, dan `_` adalah karakter khusus LIKE (escape default MariaDB `\`).
export function escapeLike(value: string): string {
return value.replace(/[\\%_]/g, "\\$&");
}

Loading…
Cancel
Save