Skip to content
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

feat: Next + Server + Client Support #103

Merged
merged 14 commits into from
Feb 7, 2025
Binary file modified apps/api/bun.lockb
Binary file not shown.
20 changes: 20 additions & 0 deletions apps/api/next.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[
{
"id": 1,
"title": "useActiveRoute",
"description": "Checks if current pathname is equal to the routes array object that we passed in.",
"content": {
"server": "type Route<T = {}> = (\n | { path: string; href?: never }\n | { href: string; path?: never }\n) &\n T;\n\ntype Props<T = {}> = {\n routes: Array<Route<T>>;\n pathname: string;\n};\n\nexport function isActiveRoute<T = {}>({ routes, pathname }: Props<T>): boolean {\n const isActive = routes.some((route) => {\n const routePath = route.path || route.href;\n return routePath === pathname;\n });\n\n return isActive;\n}\n",
"client": "\"use client\";\n\nimport { usePathname } from \"next/navigation\";\n\ntype Route<T = {}> = (\n | { path: string; href?: never }\n | { href: string; path?: never }\n) &\n T;\n\ntype Props<T = {}> = {\n routes: Array<Route<T>>;\n};\n\nexport function useActiveRoute<T = {}>({ routes }: Props<T>): boolean {\n const pathname = usePathname();\n\n const isActive = routes.some((route) => {\n const routePath = route.path || route.href;\n return routePath === pathname;\n });\n\n return isActive;\n}\n"
}
},
{
"id": 2,
"title": "useGetQueries",
"description": "Returns the query params from the URL with both Server and Client support.",
"content": {
"server": "import { URLSearchParams } from \"url\";\n\ntype QueryParams<T extends Record<string, unknown>> = {\n [K in keyof T]: T[K];\n};\n\nexport function getQueries<T extends Record<string, unknown>>(\n searchParams: URLSearchParams | Record<string, string | string[]>,\n): QueryParams<T> {\n const params: Partial<QueryParams<T>> = {};\n\n const paramsObject =\n searchParams instanceof URLSearchParams\n ? Object.fromEntries(searchParams.entries())\n : searchParams;\n\n for (const [key, value] of Object.entries(paramsObject)) {\n const stringValue = Array.isArray(value) ? value.join(\",\") : value;\n\n if (stringValue === \"true\") {\n params[key as keyof T] = true as T[keyof T];\n } else if (stringValue.includes(\",\")) {\n params[key as keyof T] = stringValue.split(\",\") as T[keyof T];\n } else {\n params[key as keyof T] = stringValue as T[keyof T];\n }\n }\n\n return params as QueryParams<T>;\n}\n",
"client": "\"use client\";\n\nimport { useSearchParams } from \"next/navigation\";\n\ntype QueryParams<T extends Record<string, unknown>> = {\n [K in keyof T]: T[K];\n};\n\nexport function useGetQueries<\n T extends Record<string, unknown>,\n>(): QueryParams<T> {\n const searchParams = useSearchParams();\n const params: Partial<QueryParams<T>> = {};\n\n searchParams.forEach((value, key) => {\n if (value === \"true\") {\n params[key as keyof T] = true as T[keyof T];\n } else if (value.includes(\",\")) {\n params[key as keyof T] = value.split(\",\") as T[keyof T];\n } else {\n params[key as keyof T] = value as T[keyof T];\n }\n });\n\n return params as QueryParams<T>;\n}\n"
}
}
]
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"elysia": "latest"
},
"devDependencies": {
"@types/bun": "^1.2.2",
"bun-types": "latest",
"dotenv": "^16.4.7",
"zod": "^3.24.1"
Expand Down
File renamed without changes.
3 changes: 2 additions & 1 deletion apps/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { react, root, next } from "./routes";
import { swagger } from "@elysiajs/swagger";
import { Server } from "./classes/server";
import { react, root } from "./routes";
import { ENV } from "./schema/server";
import { cors } from "@elysiajs/cors";
import { Elysia } from "elysia";
Expand All @@ -26,6 +26,7 @@ const app = new Elysia()
.use(cors())
.use(root)
.use(react)
.use(next)
.listen(server.port);

console.table(server);
1 change: 1 addition & 0 deletions apps/api/src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { react } from "./react";
export { next } from "./next";
export { root } from "./root";
18 changes: 18 additions & 0 deletions apps/api/src/routes/next.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { getHooks } from "../services/hooks.service";
import { Elysia, t } from "elysia";

export const next = new Elysia()
.get(
"/next",
({ query }) =>
getHooks({ search: query.search, limit: query.limit, type: "next" }),
{
query: t.Object({
search: t.Optional(t.String()),
limit: t.Optional(t.Integer()),
}),
},
)
.get("/next/:title", ({ params }) =>
getHooks({ search: params.title, type: "next" }),
);
1 change: 1 addition & 0 deletions apps/api/src/routes/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const returnRoot = {
version: "1.0.0",
endpoints: {
react: ["/react", "/react/:title"],
next: ["/next", "/next/:title"],
},
};

Expand Down
33 changes: 25 additions & 8 deletions apps/api/src/services/hooks.service.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,44 @@
import { createFilter, createLimit } from "../utilities/creators";
import type { Hook } from "../types/hook";
import type { React, Next, CondHooks } from "../types/hook";

const path = "src/data/hooks.json";
const file = Bun.file(path);
const reactPath = "react.json";
const nextPath = "next.json";
const reactFile = Bun.file(reactPath);
const nextFile = Bun.file(nextPath);

type QueryParams = {
search?: string;
limit?: number;
type?: "react" | "next";
};

type Response = Hook[];

async function getHooks({ search, limit }: QueryParams): Promise<Response> {
const hooks: Hook[] = await file.json();
type Response = CondHooks[];

async function getHooks({
search,
limit,
type = "react",
}: QueryParams): Promise<Response> {
let hooks: CondHooks[];
if (type === "react") {
hooks = await reactFile.json();
} else if (type === "next") {
hooks = await nextFile.json();
} else {
const reactHooks: React[] = await reactFile.json();
const nextHooks: Next[] = await nextFile.json();
hooks = [...reactHooks, ...nextHooks];
}

const applySearch = search ? createFilter("title")(search) : () => true;
const filteredHooks = hooks.filter(applySearch);

const limitedHooks = limit
? createLimit(limit)(filteredHooks)
: filteredHooks;

if (!limitedHooks.length) {
throw new Error("Couldn't find the requsted hook.");
throw new Error("Couldn't find the requested hook.");
}

return limitedHooks;
Expand Down
16 changes: 15 additions & 1 deletion apps/api/src/types/hook.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
export type Hook = Readonly<{
export type React = Readonly<{
id: number;
title: string;
description: string;
content: string;
}>;

export type Next = Readonly<{
id: number;
title: string;
description: string;
content: Opts;
}>;

type Opts = {
server: string;
client: string;
};

export type CondHooks = Next | React;
9 changes: 5 additions & 4 deletions apps/api/src/utilities/creators.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import type { Hook } from "~/types/hook";
import type { CondHooks } from "~/types/hook";

const createFilter =
<T extends keyof Hook>(key: T) =>
<T extends keyof CondHooks>(key: T) =>
(search: string) =>
(hook: Hook) => {
(hook: CondHooks) => {
const value = hook[key];
if (typeof value === "string") {
return value.toLowerCase().includes(search.toLowerCase());
}
return false;
};

const createLimit = (limit: number) => (hooks: Hook[]) => hooks.slice(0, limit);
const createLimit = (limit: number) => (hooks: CondHooks[]) =>
hooks.slice(0, limit);

export { createFilter, createLimit };
3 changes: 3 additions & 0 deletions packages/cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Rehooks Related
rehooks.json
/ss
4 changes: 4 additions & 0 deletions packages/core/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
module.exports = {
extends: ["@rehooks/eslint-config/core.js"],
rules: {
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/prefer-nullish-coalescing": "off",
},
};
5 changes: 3 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@
},
"devDependencies": {
"@rehooks/eslint-config": "workspace:*",
"@rehooks/tsconfig": "workspace:*",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
"@rehooks/tsconfig": "workspace:*",
"tsup": "^8.3.0",
"typescript": "^5.6.3",
"ua-parser-js": "^1.0.39"
},
"dependencies": {
"react": "^18.3.1"
"next": "^15.1.6",
"react": "^19.0.0"
},
"license": "MIT",
"repository": {
Expand Down
69 changes: 37 additions & 32 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
export * from "./useBattery";
export * from "./useClipboard";
export * from "./useCountDown";
export * from "./useCounter";
export * from "./useCountUp";
export * from "./useDebounceCallback";
export * from "./useDebounceValue";
export * from "./useDevice";
export * from "./useEventCallback";
export * from "./useEventListener";
export * from "./useFetch";
export * from "./useFocus";
export * from "./useHover";
export * from "./useInterval";
export * from "./useIsClient";
export * from "./useIsmorphicLayoutEffect";
export * from "./useIsMounted";
export * from "./useKeyPress";
export * from "./useLang";
export * from "./useLocalStorage";
export * from "./useMap";
export * from "./usePrevious";
export * from "./useScroll";
export * from "./useSessionStorage";
export * from "./useSleep";
export * from "./useStatus";
export * from "./useStep";
export * from "./useThrottle";
export * from "./useTitle";
export * from "./useToggle";
export * from "./useUnmount";
export * from "./useWindowSize";
// React
export * from "./react/useBattery";
export * from "./react/useClipboard";
export * from "./react/useCountDown";
export * from "./react/useCounter";
export * from "./react/useCountUp";
export * from "./react/useDebounceCallback";
export * from "./react/useDebounceValue";
export * from "./react/useDevice";
export * from "./react/useEventCallback";
export * from "./react/useEventListener";
export * from "./react/useFetch";
export * from "./react/useFocus";
export * from "./react/useHover";
export * from "./react/useInterval";
export * from "./react/useIsClient";
export * from "./react/useIsmorphicLayoutEffect";
export * from "./react/useIsMounted";
export * from "./react/useKeyPress";
export * from "./react/useLang";
export * from "./react/useLocalStorage";
export * from "./react/useMap";
export * from "./react/usePrevious";
export * from "./react/useScroll";
export * from "./react/useSessionStorage";
export * from "./react/useSleep";
export * from "./react/useStatus";
export * from "./react/useStep";
export * from "./react/useThrottle";
export * from "./react/useTitle";
export * from "./react/useToggle";
export * from "./react/useUnmount";
export * from "./react/useWindowSize";

// Next
export * from "./next/useActiveRoute";
export * from "./next/useGetQueries";
24 changes: 24 additions & 0 deletions packages/core/src/next/useActiveRoute/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";

import { usePathname } from "next/navigation";

type Route<T = {}> = (
| { path: string; href?: never }
| { href: string; path?: never }
) &
T;

type Props<T = {}> = {
routes: Route<T>[];
};

export function useActiveRoute<T = {}>({ routes }: Props<T>): boolean {
const pathname = usePathname();

const isActive = routes.some((route) => {
const routePath = route.path || route.href;
return routePath === pathname;
});

return isActive;
}
5 changes: 5 additions & 0 deletions packages/core/src/next/useActiveRoute/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const description =
"Checks if current pathname is equal to the routes array object that we passed in.";

export * from "./client";
export * from "./server";
19 changes: 19 additions & 0 deletions packages/core/src/next/useActiveRoute/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
type Route<T = {}> = (
| { path: string; href?: never }
| { href: string; path?: never }
) &
T;

type Props<T = {}> = {
routes: Route<T>[];
pathname: string;
};

export function isActiveRoute<T = {}>({ routes, pathname }: Props<T>): boolean {
const isActive = routes.some((route) => {
const routePath = route.path || route.href;
return routePath === pathname;
});

return isActive;
}
26 changes: 26 additions & 0 deletions packages/core/src/next/useGetQueries/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import { useSearchParams } from "next/navigation";

type QueryParams<T extends Record<string, unknown>> = {
[K in keyof T]: T[K];
};

export function useGetQueries<
T extends Record<string, unknown>,
>(): QueryParams<T> {
const searchParams = useSearchParams();
const params: Partial<QueryParams<T>> = {};

searchParams.forEach((value, key) => {
if (value === "true") {
params[key as keyof T] = true as T[keyof T];
} else if (value.includes(",")) {
params[key as keyof T] = value.split(",") as T[keyof T];
} else {
params[key as keyof T] = value as T[keyof T];
}
});

return params as QueryParams<T>;
}
5 changes: 5 additions & 0 deletions packages/core/src/next/useGetQueries/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const description =
"Returns the query params from the URL with both Server and Client support.";

export * from "./client";
export * from "./server";
30 changes: 30 additions & 0 deletions packages/core/src/next/useGetQueries/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { URLSearchParams } from "url";

type QueryParams<T extends Record<string, unknown>> = {
[K in keyof T]: T[K];
};

export function getQueries<T extends Record<string, unknown>>(
searchParams: URLSearchParams | Record<string, string | string[]>,
): QueryParams<T> {
const params: Partial<QueryParams<T>> = {};

const paramsObject =
searchParams instanceof URLSearchParams
? Object.fromEntries(searchParams.entries())
: searchParams;

for (const [key, value] of Object.entries(paramsObject)) {
const stringValue = Array.isArray(value) ? value.join(",") : value;

if (stringValue === "true") {
params[key as keyof T] = true as T[keyof T];
} else if (stringValue.includes(",")) {
params[key as keyof T] = stringValue.split(",") as T[keyof T];
} else {
params[key as keyof T] = stringValue as T[keyof T];
}
}

return params as QueryParams<T>;
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading