Admin Dashboard Bagian Data Kesehatan
This commit is contained in:
300
src/app/percobaan/_lib/ClientRouter.txt
Normal file
300
src/app/percobaan/_lib/ClientRouter.txt
Normal file
@@ -0,0 +1,300 @@
|
||||
"use client";
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
||||
import { z } from "zod";
|
||||
|
||||
type RouterLeaf<T extends z.ZodType = z.ZodObject<{}>> = {
|
||||
get: () => string;
|
||||
query: (params: z.infer<T>) => string;
|
||||
parse: (searchParams: URLSearchParams) => z.infer<T>;
|
||||
};
|
||||
|
||||
// Helper type to convert dashes to camelCase
|
||||
type DashToCamelCase<S extends string> = S extends `${infer F}-${infer R}`
|
||||
? `${F}${Capitalize<DashToCamelCase<R>>}`
|
||||
: S;
|
||||
|
||||
// Modified RouterPath to handle dash conversion
|
||||
type RouterPath<
|
||||
T extends z.ZodType = z.ZodObject<{}>,
|
||||
Segments extends string[] = []
|
||||
> = Segments extends [infer Head extends string, ...infer Tail extends string[]]
|
||||
? { [K in DashToCamelCase<Head>]: RouterPath<T, Tail> }
|
||||
: RouterLeaf<T>;
|
||||
|
||||
type RemoveLeadingSlash<S extends string> = S extends `/${infer Rest}`
|
||||
? Rest
|
||||
: S;
|
||||
|
||||
type SplitPath<S extends string> = S extends `${infer Head}/${infer Tail}`
|
||||
? [Head, ...SplitPath<Tail>]
|
||||
: S extends ""
|
||||
? []
|
||||
: [S];
|
||||
|
||||
type WibuRouterOptions = {
|
||||
prefix?: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export class V2ClientRouter<Routes = {}> {
|
||||
private tree: any = {};
|
||||
private prefix: string = "";
|
||||
private name: string = "";
|
||||
private querySchemas: Map<string, z.ZodType> = new Map();
|
||||
|
||||
constructor(options?: WibuRouterOptions) {
|
||||
if (options?.prefix) {
|
||||
// Ensure prefix starts with / and doesn't end with /
|
||||
this.prefix = options.prefix.startsWith("/")
|
||||
? options.prefix
|
||||
: `/${options.prefix}`;
|
||||
|
||||
if (this.prefix.endsWith("/")) {
|
||||
this.prefix = this.prefix.slice(0, -1);
|
||||
}
|
||||
}
|
||||
|
||||
if (options?.name) {
|
||||
this.name = options.name;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert dash-case to camelCase
|
||||
private toCamelCase(str: string): string {
|
||||
return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
||||
}
|
||||
|
||||
add<
|
||||
Path extends string,
|
||||
NormalizedPath extends string = RemoveLeadingSlash<Path>,
|
||||
Segments extends string[] = SplitPath<NormalizedPath>,
|
||||
T extends z.ZodType = z.ZodObject<{}>
|
||||
>(
|
||||
path: Path,
|
||||
schema?: { query: T }
|
||||
): V2ClientRouter<
|
||||
Routes &
|
||||
(NormalizedPath extends ""
|
||||
? RouterLeaf<T>
|
||||
: {
|
||||
[K in Segments[0] as DashToCamelCase<K>]: RouterPath<
|
||||
T,
|
||||
Segments extends [any, ...infer Rest] ? Rest : []
|
||||
>;
|
||||
})
|
||||
> {
|
||||
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
||||
const fullPath = `${this.prefix}${normalizedPath}`;
|
||||
const segments = normalizedPath.split("/").filter(Boolean);
|
||||
|
||||
// Store the Zod schema for this path
|
||||
if (schema) {
|
||||
this.querySchemas.set(fullPath, schema.query);
|
||||
} else {
|
||||
// Default empty schema
|
||||
this.querySchemas.set(fullPath, z.object({}));
|
||||
}
|
||||
|
||||
const handleQuery = (params: any): string => {
|
||||
if (!params || Object.keys(params).length === 0) return fullPath;
|
||||
|
||||
// Validate params against schema
|
||||
const schema = this.querySchemas.get(fullPath);
|
||||
if (schema) {
|
||||
try {
|
||||
schema.parse(params);
|
||||
} catch (error) {
|
||||
console.error("Query params validation failed:", error);
|
||||
throw new Error("Invalid query parameters");
|
||||
}
|
||||
}
|
||||
|
||||
const queryString = Object.entries(params)
|
||||
.map(
|
||||
([key, value]) =>
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
|
||||
)
|
||||
.join("&");
|
||||
return `${fullPath}?${queryString}`;
|
||||
};
|
||||
|
||||
const handleGet = () => fullPath;
|
||||
|
||||
const handleParse = (searchParams: URLSearchParams): any => {
|
||||
const schema = this.querySchemas.get(fullPath);
|
||||
if (!schema) return {};
|
||||
|
||||
// Convert URLSearchParams to object
|
||||
const queryObject: Record<string, any> = {};
|
||||
searchParams.forEach((value, key) => {
|
||||
queryObject[key] = value;
|
||||
});
|
||||
|
||||
// Parse through Zod schema
|
||||
try {
|
||||
return schema.parse(queryObject);
|
||||
} catch (error) {
|
||||
console.error("Failed to parse search params:", error);
|
||||
// Return safe default values
|
||||
const safeParsed = schema.safeParse(queryObject);
|
||||
if (safeParsed.success) {
|
||||
return safeParsed.data;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
// Special case for root path "/"
|
||||
if (segments.length === 0) {
|
||||
this.tree.get = handleGet;
|
||||
this.tree.query = handleQuery;
|
||||
this.tree.parse = handleParse;
|
||||
} else {
|
||||
let current = this.tree;
|
||||
for (const segment of segments) {
|
||||
// Use camelCase version for the property name
|
||||
const camelSegment = this.toCamelCase(segment);
|
||||
if (!current[camelSegment]) {
|
||||
current[camelSegment] = {};
|
||||
}
|
||||
current = current[camelSegment];
|
||||
}
|
||||
|
||||
current.get = handleGet;
|
||||
current.query = handleQuery;
|
||||
current.parse = handleParse;
|
||||
}
|
||||
|
||||
return this as any;
|
||||
}
|
||||
|
||||
// Add a method to incorporate another router's routes into this one
|
||||
use<N extends string, ChildRoutes>(
|
||||
name: N,
|
||||
childRouter: V2ClientRouter<ChildRoutes>
|
||||
): V2ClientRouter<Routes & Record<DashToCamelCase<N>, ChildRoutes>> {
|
||||
const camelName = this.toCamelCase(name);
|
||||
|
||||
if (!this.tree[camelName]) {
|
||||
this.tree[camelName] = {};
|
||||
}
|
||||
|
||||
// Copy query schemas from child router
|
||||
childRouter.querySchemas.forEach((schema, path) => {
|
||||
const newPath = `${this.prefix}/${name}${path.substring(
|
||||
childRouter.prefix.length
|
||||
)}`;
|
||||
this.querySchemas.set(newPath, schema);
|
||||
});
|
||||
|
||||
// Create a deep copy of the child router's tree with updated paths
|
||||
const updatePaths = (obj: any, childPrefix: string): any => {
|
||||
const result: any = {};
|
||||
|
||||
for (const key in obj) {
|
||||
if (key === "get" && typeof obj[key] === "function") {
|
||||
// Capture the original path from the child router
|
||||
const originalPath = obj[key]();
|
||||
// Create a new function that returns the combined path
|
||||
result[key] = () => {
|
||||
const newPath = `${this.prefix}/${name}${originalPath.substring(
|
||||
childPrefix.length
|
||||
)}`;
|
||||
return newPath;
|
||||
};
|
||||
} else if (key === "query" && typeof obj[key] === "function") {
|
||||
// Capture the child router's prefix for path adjustment
|
||||
result[key] = (params: any) => {
|
||||
// Get the original result without query params
|
||||
const originalPathWithoutParams = obj["get"]();
|
||||
// Create the proper path with our parent prefix
|
||||
const newBasePath = `${
|
||||
this.prefix
|
||||
}/${name}${originalPathWithoutParams.substring(
|
||||
childPrefix.length
|
||||
)}`;
|
||||
|
||||
// Add query params if any
|
||||
if (!params || Object.keys(params).length === 0) return newBasePath;
|
||||
|
||||
// Validate params against schema
|
||||
const newPath = `${
|
||||
this.prefix
|
||||
}/${name}${originalPathWithoutParams.substring(
|
||||
childPrefix.length
|
||||
)}`;
|
||||
const schema = this.querySchemas.get(newPath);
|
||||
|
||||
if (schema) {
|
||||
try {
|
||||
schema.parse(params);
|
||||
} catch (error) {
|
||||
console.error("Query params validation failed:", error);
|
||||
throw new Error("Invalid query parameters");
|
||||
}
|
||||
}
|
||||
|
||||
const queryString = Object.entries(params)
|
||||
.map(
|
||||
([k, v]) =>
|
||||
`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`
|
||||
)
|
||||
.join("&");
|
||||
return `${newBasePath}?${queryString}`;
|
||||
};
|
||||
} else if (key === "parse" && typeof obj[key] === "function") {
|
||||
result[key] = (searchParams: URLSearchParams) => {
|
||||
const originalPath = obj["get"]();
|
||||
const newPath = `${this.prefix}/${name}${originalPath.substring(
|
||||
childPrefix.length
|
||||
)}`;
|
||||
const schema = this.querySchemas.get(newPath);
|
||||
|
||||
if (!schema) return {};
|
||||
|
||||
// Convert URLSearchParams to object
|
||||
const queryObject: Record<string, any> = {};
|
||||
searchParams.forEach((value, key) => {
|
||||
queryObject[key] = value;
|
||||
});
|
||||
|
||||
// Parse through Zod schema
|
||||
try {
|
||||
return schema.parse(queryObject);
|
||||
} catch (error) {
|
||||
console.error("Failed to parse search params:", error);
|
||||
// Return safe default values
|
||||
const safeParsed = schema.safeParse(queryObject);
|
||||
if (safeParsed.success) {
|
||||
return safeParsed.data;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
};
|
||||
} else if (typeof obj[key] === "object" && obj[key] !== null) {
|
||||
result[key] = updatePaths(obj[key], childPrefix);
|
||||
} else {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Copy the child router's tree into this router
|
||||
this.tree[camelName] = updatePaths(
|
||||
(childRouter as any).tree,
|
||||
childRouter.prefix
|
||||
);
|
||||
|
||||
return this as any;
|
||||
}
|
||||
|
||||
// Allow access to the tree with strong typing
|
||||
get routes(): Routes {
|
||||
return this.tree as Routes;
|
||||
}
|
||||
}
|
||||
|
||||
export default V2ClientRouter;
|
||||
20
src/app/percobaan/_router/router.txt
Normal file
20
src/app/percobaan/_router/router.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
import { z } from "zod";
|
||||
import V2ClientRouter from "../_lib/ClientRouter";
|
||||
|
||||
const dashboard = new V2ClientRouter({
|
||||
prefix: "/dashboard",
|
||||
name: "dashboard",
|
||||
})
|
||||
.add("/", {
|
||||
query: z.object({
|
||||
page: z.string(),
|
||||
}),
|
||||
})
|
||||
.add("/berita");
|
||||
|
||||
const router = new V2ClientRouter({
|
||||
prefix: "/percobaan",
|
||||
name: "percobaan",
|
||||
}).use("dashboard", dashboard);
|
||||
|
||||
export default router;
|
||||
11
src/app/percobaan/dashboard/berita/page.txt
Normal file
11
src/app/percobaan/dashboard/berita/page.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
berita
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
33
src/app/percobaan/dashboard/page.txt
Normal file
33
src/app/percobaan/dashboard/page.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import router from '../_router/router';
|
||||
import { Box } from '@mantine/core';
|
||||
|
||||
|
||||
function Page() {
|
||||
const { page } = router.routes.dashboard.parse(useSearchParams())
|
||||
switch (page) {
|
||||
case "1":
|
||||
return <Page1 />
|
||||
case "2":
|
||||
return <Page2 />
|
||||
case "3":
|
||||
return <Page3 />
|
||||
default:
|
||||
return <Page1 />
|
||||
}
|
||||
}
|
||||
|
||||
const Page1 = () => {
|
||||
return <Box h={200} bg="red">Page 1</Box>
|
||||
}
|
||||
|
||||
const Page2 = () => {
|
||||
return <Box h={200} bg="blue">Page 2</Box>
|
||||
}
|
||||
|
||||
const Page3 = () => {
|
||||
return <Box h={200} bg="green">Page 3</Box>
|
||||
}
|
||||
|
||||
export default Page;
|
||||
18
src/app/percobaan/page2.txt
Normal file
18
src/app/percobaan/page2.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
import { Button, Group, Stack } from "@mantine/core"
|
||||
import { Link } from "next-view-transitions"
|
||||
import router from "./_router/router"
|
||||
|
||||
const Page = () => {
|
||||
return <Group>
|
||||
<Stack>
|
||||
{[1, 2, 3].map((v) => (<Button component={Link} href={router.routes.dashboard.query({
|
||||
page: v.toString(),
|
||||
})} key={v}>ke dashboard {v}</Button>))}
|
||||
|
||||
<Button component={Link} href={router.routes.dashboard.berita.get()}>berita</Button>
|
||||
</Stack>
|
||||
</Group>
|
||||
}
|
||||
|
||||
export default Page
|
||||
Reference in New Issue
Block a user