This commit is contained in:
2025-04-30 16:32:29 +08:00
parent ea7de13d28
commit 8f3ee2f831
6 changed files with 0 additions and 487 deletions

View File

@@ -1,300 +0,0 @@
"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;

View File

@@ -1,20 +0,0 @@
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;

View File

@@ -1,11 +0,0 @@
import React from 'react';
function Page() {
return (
<div>
berita
</div>
);
}
export default Page;

View File

@@ -1,33 +0,0 @@
'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;

View File

@@ -1,105 +0,0 @@
'use client'
import { Box, Container, Flex, Grid, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useSearchParams } from 'next/navigation';
import React from 'react';
const tx = `
Untuk menambahkan fitur berbagi nomor WhatsApp di kode yang Anda miliki, saya akan menjelaskan beberapa pendekatan yang bisa digunakan. Biasanya ini dilakukan dengan membuat link yang ketika diklik akan membuka aplikasi WhatsApp dengan nomor tujuan yang sudah diatur.
Berikut adalah cara mengimplementasikannya pada kode React Anda:
`
function Page() {
return (
<Stack>
<Grid>
<Grid.Col span={{
base: 12,
sm: 6,
md: 4,
xl: 10
}}>
<Box h={"200"} bg={"blue"}>1</Box>
</Grid.Col>
<Grid.Col span={{
base: 12,
sm: 6,
md: 8,
xl: 2
}}>
<Box h={"200"} bg={"red"}>1</Box>
</Grid.Col>
</Grid>
<SimpleGrid cols={{
base: 1,
sm: 2,
md: 4,
xl: 20
}}>
{Array.from({ length: 10 }).map((_, i) => (
<Box key={i} h={"60"} bg={"blue"}>1</Box>
))}
</SimpleGrid>
<Flex >
<Box w={400} h={"200"} bg={"blue"}>1</Box>
<Box w={400} bg={"red"}>
<Text fz={"42"} lineClamp={1} >{tx}</Text>
<Text bg={"blue"} style={{
fontSize: "2rem"
}} lineClamp={1} >{tx}</Text>
<Title order={1}>apa kabar</Title>
</Box>
</Flex>
<Page2/>
</Stack>
);
}
export default Page;
const Halaman = [Halaman1, Halaman2, Halaman3]
function Page2() {
const page = useSearchParams().get("p");
if (!page) return <Container >
<Stack>
<Text>halo 1</Text>
{Array.from({ length: 4 }).map((v, k) => <Skeleton h={100} key={k} />)}
</Stack>
</Container>
return (
<Container w={"100%"}>
<Stack>
<Text>halo 2</Text>
{Halaman[Number(page)-1]()}
</Stack>
</Container>
);
}
function Halaman1() {
return <Stack bg={"blue"}>
ini halaman 1
</Stack>
}
function Halaman2() {
return <Stack bg={"red"}>
ini halaman 2
</Stack>
}
function Halaman3() {
return <Stack bg={"grape"}>
ini halaman 3
</Stack>
}

View File

@@ -1,18 +0,0 @@
'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