diff --git a/.gitignore b/.gitignore index eb8b05766..11d47a338 100644 --- a/.gitignore +++ b/.gitignore @@ -222,3 +222,8 @@ generated # Reactuse **/registry.json + + +# Docgen. Generated content +packages/docs/content/docs/hooks/ +.source \ No newline at end of file diff --git a/.prettierrc.mjs b/.prettierrc.mjs index ac35937c6..a593d82d3 100644 --- a/.prettierrc.mjs +++ b/.prettierrc.mjs @@ -1,3 +1,3 @@ -import { prettier } from "@siberiacancode/prettier"; +import { prettier } from '@siberiacancode/prettier'; -export default { ...prettier, plugins: ["prettier-plugin-tailwindcss"] }; +export default { ...prettier, plugins: ['prettier-plugin-tailwindcss'] }; diff --git a/package.json b/package.json index 2be1ef18d..3246bc8b5 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "scripts": { "cli:build": "pnpm --filter @siberiacancode/cli build && pnpm --filter @siberiacancode/cli generate-registry", "cli:build:registry": "pnpm --filter useverse build:registry", - "docs:build": "pnpm --filter @siberiacancode/docs build", + "docs:build": "pnpm --filter @siberiacancode/docs build & pnpm format", "core:build:js": "pnpm --filter @siberiacancode/reactuse build:js", "lint": "pnpm --recursive lint", "format": "prettier --write .", diff --git a/packages/core/src/hooks/useClickOutside/useClickOutside.demo.tsx b/packages/core/src/hooks/useClickOutside/useClickOutside.demo.tsx index 128678850..e8af68fea 100644 --- a/packages/core/src/hooks/useClickOutside/useClickOutside.demo.tsx +++ b/packages/core/src/hooks/useClickOutside/useClickOutside.demo.tsx @@ -1,4 +1,4 @@ -import { cn } from '@siberiacancode/docs/utils'; +import { cn } from '../../../../docs/lib/utils'; import { useClickOutside, useCounter } from '@siberiacancode/reactuse'; const Demo = () => { diff --git a/packages/core/src/hooks/useGamepad/useGamepad.demo.tsx b/packages/core/src/hooks/useGamepad/useGamepad.demo.tsx index 59b05f090..475ac4666 100644 --- a/packages/core/src/hooks/useGamepad/useGamepad.demo.tsx +++ b/packages/core/src/hooks/useGamepad/useGamepad.demo.tsx @@ -1,3 +1,4 @@ +'use client'; import { useGamepad } from '@siberiacancode/reactuse'; const Demo = () => { diff --git a/packages/core/src/hooks/useIdle/useIdle.demo.tsx b/packages/core/src/hooks/useIdle/useIdle.demo.tsx index 659cee0ab..da8e5cf29 100644 --- a/packages/core/src/hooks/useIdle/useIdle.demo.tsx +++ b/packages/core/src/hooks/useIdle/useIdle.demo.tsx @@ -1,4 +1,4 @@ -import { cn } from '@siberiacancode/docs/utils'; +import { cn } from '../../../../docs/lib/utils'; import { useIdle } from '@siberiacancode/reactuse'; const Demo = () => { diff --git a/packages/core/src/hooks/useMergedRef/useMergedRef.demo.tsx b/packages/core/src/hooks/useMergedRef/useMergedRef.demo.tsx index c10d4e553..7e7f0d191 100644 --- a/packages/core/src/hooks/useMergedRef/useMergedRef.demo.tsx +++ b/packages/core/src/hooks/useMergedRef/useMergedRef.demo.tsx @@ -1,4 +1,4 @@ -import { cn } from '@siberiacancode/docs/utils'; +import { cn } from '../../../../docs/lib/utils'; import { useClickOutside, useCounter, diff --git a/packages/core/src/hooks/useMouse/useMouse.demo.tsx b/packages/core/src/hooks/useMouse/useMouse.demo.tsx index 6a9d67a81..87c853ac9 100644 --- a/packages/core/src/hooks/useMouse/useMouse.demo.tsx +++ b/packages/core/src/hooks/useMouse/useMouse.demo.tsx @@ -1,4 +1,4 @@ -import { cn } from '@siberiacancode/docs/utils'; +import { cn } from '../../../../docs/lib/utils'; import { useHover, useMouse } from '@siberiacancode/reactuse'; const Demo = () => { diff --git a/packages/core/src/hooks/useMutation/useMutation.test.ts b/packages/core/src/hooks/useMutation/useMutation.test.ts index 2fbd42d68..a4b6714b6 100644 --- a/packages/core/src/hooks/useMutation/useMutation.test.ts +++ b/packages/core/src/hooks/useMutation/useMutation.test.ts @@ -1,41 +1,35 @@ -import { act, renderHook, waitFor } from "@testing-library/react"; +import { act, renderHook, waitFor } from '@testing-library/react'; -import { renderHookServer } from "@/tests"; +import { renderHookServer } from '@/tests'; -import { useMutation } from "./useMutation"; +import { useMutation } from './useMutation'; -it("Should use mutation", () => { - const { result } = renderHook(() => - useMutation(() => Promise.resolve("data")) - ); +it('Should use mutation', () => { + const { result } = renderHook(() => useMutation(() => Promise.resolve('data'))); expect(result.current.data).toBeNull(); expect(result.current.error).toBeNull(); expect(result.current.isError).toBeFalsy(); expect(result.current.isLoading).toBeFalsy(); expect(result.current.isSuccess).toBeFalsy(); - expect(result.current.mutate).toBeTypeOf("function"); - expect(result.current.mutateAsync).toBeTypeOf("function"); + expect(result.current.mutate).toBeTypeOf('function'); + expect(result.current.mutateAsync).toBeTypeOf('function'); }); -it("Should use mutation on server side", () => { - const { result } = renderHookServer(() => - useMutation(() => Promise.resolve("data")) - ); +it('Should use mutation on server side', () => { + const { result } = renderHookServer(() => useMutation(() => Promise.resolve('data'))); expect(result.current.data).toBeNull(); expect(result.current.error).toBeNull(); expect(result.current.isError).toBeFalsy(); expect(result.current.isLoading).toBeFalsy(); expect(result.current.isSuccess).toBeFalsy(); - expect(result.current.mutate).toBeTypeOf("function"); - expect(result.current.mutateAsync).toBeTypeOf("function"); + expect(result.current.mutate).toBeTypeOf('function'); + expect(result.current.mutateAsync).toBeTypeOf('function'); }); -it("Should mutate data successfully", async () => { - const { result } = renderHook(() => - useMutation(() => Promise.resolve("data")) - ); +it('Should mutate data successfully', async () => { + const { result } = renderHook(() => useMutation(() => Promise.resolve('data'))); act(result.current.mutate); @@ -45,14 +39,12 @@ it("Should mutate data successfully", async () => { await waitFor(() => { expect(result.current.isLoading).toBeFalsy(); expect(result.current.isSuccess).toBeTruthy(); - expect(result.current.data).toBe("data"); + expect(result.current.data).toBe('data'); }); }); -it("Should handle errors", async () => { - const { result } = renderHook(() => - useMutation(() => Promise.reject(new Error("error"))) - ); +it('Should handle errors', async () => { + const { result } = renderHook(() => useMutation(() => Promise.reject(new Error('error')))); await act(async () => { try { @@ -66,29 +58,27 @@ it("Should handle errors", async () => { await waitFor(() => { expect(result.current.isLoading).toBeFalsy(); expect(result.current.isError).toBeTruthy(); - expect(result.current.error).toEqual(new Error("error")); + expect(result.current.error).toEqual(new Error('error')); expect(result.current.data).toBeNull(); }); }); -it("Should mutate async", async () => { - const { result } = renderHook(() => - useMutation((input) => Promise.resolve(`data-${input}`)) - ); +it('Should mutate async', async () => { + const { result } = renderHook(() => useMutation((input) => Promise.resolve(`data-${input}`))); await act(async () => { - const response = await result.current.mutateAsync("test"); - expect(response).toBe("data-test"); + const response = await result.current.mutateAsync('test'); + expect(response).toBe('data-test'); }); - expect(result.current.data).toBe("data-test"); + expect(result.current.data).toBe('data-test'); expect(result.current.isSuccess).toBeTruthy(); }); -it("Should triggered onSuccess callback", async () => { +it('Should triggered onSuccess callback', async () => { const { result } = renderHook(() => - useMutation(() => Promise.resolve("data"), { - onSuccess: (data) => expect(data).toBe("data"), + useMutation(() => Promise.resolve('data'), { + onSuccess: (data) => expect(data).toBe('data') }) ); @@ -97,10 +87,10 @@ it("Should triggered onSuccess callback", async () => { await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); }); -it("Should triggered onError callback", async () => { +it('Should triggered onError callback', async () => { const { result } = renderHook(() => - useMutation(() => Promise.reject(new Error("error")), { - onError: (error) => expect(error).toEqual(new Error("error")), + useMutation(() => Promise.reject(new Error('error')), { + onError: (error) => expect(error).toEqual(new Error('error')) }) ); @@ -109,19 +99,19 @@ it("Should triggered onError callback", async () => { await waitFor(() => expect(result.current.isError).toBeTruthy()); }); -it("Should retry on error once", async () => { +it('Should retry on error once', async () => { let retries = 0; const { result } = renderHook(() => useMutation( () => new Promise((resolve, reject) => { - if (retries === 1) resolve("data"); + if (retries === 1) resolve('data'); retries++; - reject(new Error("error")); + reject(new Error('error')); }), { - retry: true, + retry: true } ) ); @@ -130,22 +120,22 @@ it("Should retry on error once", async () => { expect(result.current.data).toBeNull(); - await waitFor(() => expect(result.current.data).toBe("data")); + await waitFor(() => expect(result.current.data).toBe('data')); }); -it("Should retry on error multiple times", async () => { +it('Should retry on error multiple times', async () => { let retries = 0; const { result } = renderHook(() => useMutation( () => new Promise((resolve, reject) => { - if (retries === 2) resolve("data"); + if (retries === 2) resolve('data'); retries++; - reject(new Error("error")); + reject(new Error('error')); }), { - retry: 2, + retry: 2 } ) ); @@ -154,16 +144,16 @@ it("Should retry on error multiple times", async () => { expect(result.current.data).toBeNull(); - await waitFor(() => expect(result.current.data).toBe("data")); + await waitFor(() => expect(result.current.data).toBe('data')); }); -it("Should override global options with mutate options", async () => { +it('Should override global options with mutate options', async () => { const globalOnSuccess = vi.fn(); const localOnSuccess = vi.fn(); const { result } = renderHook(() => - useMutation(() => Promise.resolve("data"), { - onSuccess: globalOnSuccess, + useMutation(() => Promise.resolve('data'), { + onSuccess: globalOnSuccess }) ); @@ -171,19 +161,19 @@ it("Should override global options with mutate options", async () => { await waitFor(() => expect(result.current.isSuccess).toBeTruthy()); - expect(localOnSuccess).toHaveBeenCalledWith("data"); + expect(localOnSuccess).toHaveBeenCalledWith('data'); expect(globalOnSuccess).not.toHaveBeenCalled(); }); -it("Should reset error state on successful mutation", async () => { +it('Should reset error state on successful mutation', async () => { let shouldFail = true; const { result } = renderHook(() => useMutation(() => { if (shouldFail) { - return Promise.reject(new Error("error")); + return Promise.reject(new Error('error')); } - return Promise.resolve("data"); + return Promise.resolve('data'); }) ); @@ -205,15 +195,15 @@ it("Should reset error state on successful mutation", async () => { }); }); -it("Should retry by number delay", async () => { +it('Should retry by number delay', async () => { let retries = 0; const { result } = renderHook(() => useMutation(() => { retries++; if (retries < 2) { - return Promise.reject(new Error("error")); + return Promise.reject(new Error('error')); } - return Promise.resolve("data"); + return Promise.resolve('data'); }) ); @@ -221,19 +211,19 @@ it("Should retry by number delay", async () => { expect(result.current.isLoading).toBeTruthy(); - await waitFor(() => expect(result.current.data).toBe("data")); + await waitFor(() => expect(result.current.data).toBe('data')); }); -it("Should retry by number global delay", async () => { +it('Should retry by number global delay', async () => { let retries = 0; const { result } = renderHook(() => useMutation( () => { retries++; if (retries < 2) { - return Promise.reject(new Error("error")); + return Promise.reject(new Error('error')); } - return Promise.resolve("data"); + return Promise.resolve('data'); }, { retryDelay: 100, retry: 1 } ) @@ -243,10 +233,10 @@ it("Should retry by number global delay", async () => { expect(result.current.isLoading).toBeTruthy(); - await waitFor(() => expect(result.current.data).toBe("data")); + await waitFor(() => expect(result.current.data).toBe('data')); }); -it("Should retry by function delay", async () => { +it('Should retry by function delay', async () => { const retryDelay = vi.fn(() => 100); let retries = 0; @@ -254,9 +244,9 @@ it("Should retry by function delay", async () => { useMutation(() => { retries++; if (retries < 2) { - return Promise.reject(new Error("error")); + return Promise.reject(new Error('error')); } - return Promise.resolve("data"); + return Promise.resolve('data'); }) ); @@ -265,7 +255,7 @@ it("Should retry by function delay", async () => { expect(result.current.isLoading).toBeTruthy(); await waitFor(() => { - expect(result.current.data).toBe("data"); + expect(result.current.data).toBe('data'); expect(retryDelay).toHaveBeenCalledOnce(); }); }); diff --git a/packages/core/src/hooks/useMutation/useMutation.ts b/packages/core/src/hooks/useMutation/useMutation.ts index 755234541..b7a12a88f 100644 --- a/packages/core/src/hooks/useMutation/useMutation.ts +++ b/packages/core/src/hooks/useMutation/useMutation.ts @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState } from 'react'; /* The type of the options */ interface UseMutationOptions { @@ -27,10 +27,7 @@ interface UseMutationReturn { /* The mutate function */ mutate: (body?: Body, options?: UseMutationOptions) => void; /* The mutate async function */ - mutateAsync: ( - body?: Body, - options?: UseMutationOptions - ) => Promise; + mutateAsync: (body?: Body, options?: UseMutationOptions) => Promise; } export interface RequestOptions extends UseMutationOptions { @@ -86,16 +83,16 @@ export const useMutation = ( }) .catch((error: Error) => { const retry = - typeof requestOptions?.retry === "function" + typeof requestOptions?.retry === 'function' ? requestOptions?.retry(attempt, error) : requestOptions?.retry; const retryDelay = - typeof requestOptions?.retryDelay === "function" + typeof requestOptions?.retryDelay === 'function' ? requestOptions?.retryDelay(attempt, error) : requestOptions?.retryDelay; - if (typeof retry === "boolean" && retry) { + if (typeof retry === 'boolean' && retry) { if (retryDelay) { setTimeout( () => request(body, { ...requestOptions, attempt: attempt + 1 }), @@ -131,21 +128,18 @@ export const useMutation = ( retry: mutateOptions?.retry ?? options?.retry, retryDelay: mutateOptions?.retryDelay ?? options?.retryDelay, onSuccess: mutateOptions?.onSuccess ?? options?.onSuccess, - onError: mutateOptions?.onError ?? options?.onError, + onError: mutateOptions?.onError ?? options?.onError }; request(body, requestOptions); }; - const mutateAsync = async ( - body: Body, - mutateOptions?: UseMutationOptions - ) => { + const mutateAsync = async (body: Body, mutateOptions?: UseMutationOptions) => { const requestOptions = { retry: mutateOptions?.retry ?? options?.retry, retryDelay: mutateOptions?.retryDelay ?? options?.retryDelay, onSuccess: mutateOptions?.onSuccess ?? options?.onSuccess, - onError: mutateOptions?.onError ?? options?.onError, + onError: mutateOptions?.onError ?? options?.onError }; return request(body, requestOptions) as Promise; @@ -158,6 +152,6 @@ export const useMutation = ( mutateAsync, isLoading, isError, - isSuccess, + isSuccess } as UseMutationReturn; }; diff --git a/packages/core/src/hooks/usePointerLock/usePointerLock.demo.tsx b/packages/core/src/hooks/usePointerLock/usePointerLock.demo.tsx index 681fa1f4e..01baef0dd 100644 --- a/packages/core/src/hooks/usePointerLock/usePointerLock.demo.tsx +++ b/packages/core/src/hooks/usePointerLock/usePointerLock.demo.tsx @@ -1,6 +1,6 @@ import type { CSSProperties } from 'react'; -import { cn } from '@siberiacancode/docs/utils'; +import { cn } from '../../../../docs/lib/utils'; import { useEventListener, usePointerLock } from '@siberiacancode/reactuse'; import { useRef } from 'react'; diff --git a/packages/core/src/hooks/useSticky/useSticky.demo.tsx b/packages/core/src/hooks/useSticky/useSticky.demo.tsx index 5fc2dead7..591144b36 100644 --- a/packages/core/src/hooks/useSticky/useSticky.demo.tsx +++ b/packages/core/src/hooks/useSticky/useSticky.demo.tsx @@ -1,4 +1,4 @@ -import { cn } from '@siberiacancode/docs/utils'; +import { cn } from '../../../../docs/lib/utils'; import { useSticky } from '@siberiacancode/reactuse'; import { useRef } from 'react'; diff --git a/packages/core/src/hooks/useToggle/useToggle.demo.tsx b/packages/core/src/hooks/useToggle/useToggle.demo.tsx index 2e93079b0..19f7c7e2a 100644 --- a/packages/core/src/hooks/useToggle/useToggle.demo.tsx +++ b/packages/core/src/hooks/useToggle/useToggle.demo.tsx @@ -1,4 +1,4 @@ -import { cn } from '@siberiacancode/docs/utils'; +import { cn } from '../../../../docs/lib/utils'; import { useToggle } from '@siberiacancode/reactuse'; const Demo = () => { diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index ec8ed0823..414078ab2 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -8,7 +8,7 @@ "module": "esnext", "moduleResolution": "bundler", "paths": { - "@siberiacancode/docs/*": ["../docs/src/*"], + "@siberiacancode/docs/*": ["../docs/*"], "@/*": ["./src/*"], "@/tests": ["./tests"] }, diff --git a/packages/core/vitest.config.mts b/packages/core/vitest.config.mts index 7c0093f02..e092db046 100644 --- a/packages/core/vitest.config.mts +++ b/packages/core/vitest.config.mts @@ -1,16 +1,16 @@ -import { vitest } from "@siberiacancode/vitest"; -import path from "node:path"; -import { defineConfig } from "vitest/config"; +import { vitest } from '@siberiacancode/vitest'; +import path from 'node:path'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { ...vitest, - setupFiles: "./tests/setupTests.ts", + setupFiles: './tests/setupTests.ts' }, resolve: { alias: { - "@/tests": path.resolve(__dirname, "./tests"), - "@": path.resolve(__dirname, "./src"), - }, - }, + '@/tests': path.resolve(__dirname, './tests'), + '@': path.resolve(__dirname, './src') + } + } }); diff --git a/packages/docs/README.md b/packages/docs/README.md deleted file mode 100644 index 9546239b2..000000000 --- a/packages/docs/README.md +++ /dev/null @@ -1 +0,0 @@ -packages/core/README.md diff --git a/packages/docs/app/(app)/(root)/page.tsx b/packages/docs/app/(app)/(root)/page.tsx new file mode 100644 index 000000000..07e5a7da0 --- /dev/null +++ b/packages/docs/app/(app)/(root)/page.tsx @@ -0,0 +1,151 @@ +import { + PageActions, + PageHeader, + PageHeaderDescription, + PageHeaderHeading +} from '@docs/components/page-header'; +import { siteConfig } from '@docs/lib/config'; +import { getContributors } from '@docs/lib/contributors'; +import { Avatar, AvatarFallback, AvatarImage } from '@docs/ui/avatar'; +import { Button } from '@docs/ui/button'; +import { Separator } from '@docs/ui/separator'; +import { + IconCube3dSphere, + IconIcons, + IconPalette, + IconTree, + IconUsers, + IconWorld +} from '@tabler/icons-react'; +import { type Metadata } from 'next'; +import Image from 'next/image'; +import Link from 'next/link'; +import simpleGit from 'simple-git'; + +const title = 'reactuse'; + +const description = + 'Improve your react applications with our library πŸ“¦ designed for comfort and speed'; + +const cardsData = [ + { + title: 'Lightweight & Scalable', + details: + 'Hooks are lightweight and easy to use, making it simple to integrate into any project.', + icon: IconCube3dSphere + }, + { + title: 'Clean & consistent', + details: 'Hooks follow a unified approach for consistency and maintainability.', + icon: IconWorld + }, + { + title: 'Customizable', + details: 'Install and customize hooks effortlessly using our CLI', + icon: IconPalette + }, + { + title: 'Large collection', + details: + 'Extensive collection of hooks for all your needs, from state management to browser APIs.', + icon: IconIcons + }, + { + title: 'Tree shakable', + details: + 'The hooks are tree shakable, so you only import the hooks you need in your application.', + icon: IconTree + }, + { + title: 'Active community', + details: 'Join our active community on Github and help make reactuse even better.', + icon: IconUsers + } +]; + +export const metadata: Metadata = { + title, + description +}; + +export default async function IndexPage() { + const contributors = await getContributors(); + + return ( +
+ +
+
+ +
+ + + reactuse + + the largest and most useful hook library + + {description} + + + + + +
+ {cardsData.map((card) => { + const Icon = card.icon; + return ( +
+
+ +
+

{card.title}

+

{card.details}

+
+ ); + })} +
+
+
Team & Contributors
+
+ +
+ SIBERIA CAN CODE +
SIBERIA CAN CODE
+
+ +
+
+ {contributors.map((contributor) => ( +
+ + + {contributor.name[0]} + +

{contributor.name}

+
+ ))} +
+
+ + + +
+ Released under the MIT License. + Copyright Β© 2026 siberiacancode +
+
+ ); +} diff --git a/packages/docs/app/(app)/layout.tsx b/packages/docs/app/(app)/layout.tsx new file mode 100644 index 000000000..fcc350bbd --- /dev/null +++ b/packages/docs/app/(app)/layout.tsx @@ -0,0 +1,10 @@ +import { SiteHeader } from '@docs/components/site-header'; + +export default function AppLayout({ children }: { children: React.ReactNode }) { + return ( +
+ +
{children}
+
+ ); +} diff --git a/packages/docs/app/.vitepress/config.mts b/packages/docs/app/.vitepress/config.mts deleted file mode 100644 index e3d07ffd6..000000000 --- a/packages/docs/app/.vitepress/config.mts +++ /dev/null @@ -1,233 +0,0 @@ -import type { DefaultTheme, MarkdownOptions } from 'vitepress'; - -import { transformerTwoslash } from '@shikijs/vitepress-twoslash'; -import tailwindcss from '@tailwindcss/vite'; -import { fileURLToPath } from 'node:url'; -import { defineConfig } from 'vitepress'; - -import { getContentItems } from '../../src/utils'; - -export default async () => { - const contentItems = await getContentItems(); - const sidebarContentItems = contentItems.reduce( - (categoryItems, contentItem) => { - const category = categoryItems.find((group) => group.text === contentItem.category); - - if (!category) { - categoryItems.push({ - text: contentItem.category, - items: [contentItem] - }); - } else { - category.items!.push(contentItem); - } - - return categoryItems; - }, - [] - ); - - return defineConfig({ - base: '/', - title: 'reactuse', - titleTemplate: false, - description: - 'Improve your react applications with our library πŸ“¦ designed for comfort and speed', - markdown: { - codeTransformers: [transformerTwoslash()], - languages: ['js', 'jsx', 'ts', 'tsx'] - } as unknown as MarkdownOptions, - vite: { - plugins: [tailwindcss()], - resolve: { - alias: { - '@siberiacancode/reactuse': fileURLToPath(new URL('../../../core/src', import.meta.url)), - '@siberiacancode/docs': fileURLToPath(new URL('../../src', import.meta.url)), - '@': fileURLToPath(new URL('../../../core/src', import.meta.url)) - } - } - }, - transformPageData: (pageData) => { - pageData.frontmatter.head ??= []; - pageData.frontmatter.head.push([ - 'meta', - { - name: 'og:image', - content: - 'https://repository-images.githubusercontent.com/799880708/0afee0cb-ca48-40a2-9c38-dc5b64ebdf65' - } - ]); - - if (pageData.relativePath.includes('hooks') && pageData.params?.name) { - const name = pageData.params.name as string; - const description = (pageData.params.description as string) ?? ''; - - pageData.title = `${name} React hook Reactuse`; - pageData.description = description; - - pageData.frontmatter.head.push( - ['meta', { property: 'og:title', content: pageData.title }], - ['meta', { property: 'og:description', content: pageData.description }] - ); - } - }, - head: [ - ['meta', { name: 'algolia-site-verification', content: '60FB6E25551CE504' }], - ['link', { rel: 'icon', href: '/favicon.ico' }], - ['link', { rel: 'manifest', href: '/manifest.json' }], - [ - 'script', - { type: 'text/javascript' }, - `(function(m,e,t,r,i,k,a){ - m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; - m[i].l=1*new Date(); - for (var j = 0; j < document.scripts.length; j++) { - if (document.scripts[j].src === r) { return; } - } - k=e.createElement(t),a=e.getElementsByTagName(t)[0], - k.async=1,k.src=r,a.parentNode.insertBefore(k,a) - })(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym"); - - ym(102942267, "init", { - clickmap:true, - trackLinks:true, - accurateTrackBounce:true - });` - ], - [ - 'noscript', - {}, - `
` - ], - - [ - 'script', - {}, - `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': - new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], - j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= - 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); - })(window,document,'script','dataLayer','GTM-5SMCHX9Z');` - ], - - [ - 'script', - { - async: '', - src: 'https://www.googletagmanager.com/gtag/js?id=G-RRECQP6XBW' - } - ], - - [ - 'script', - {}, - `window.dataLayer = window.dataLayer || []; - function gtag(){dataLayer.push(arguments);} - gtag('js', new Date()); - gtag('config', 'G-RRECQP6XBW', { - anonymize_ip: true, - client_storage: 'none', - allow_google_signals: false, - allow_ad_personalization_signals: false - });` - ] - ], - locales: { - root: { - label: 'English', - lang: 'en', - themeConfig: { - logo: { - src: '/logo.svg', - alt: 'reactuse' - }, - footer: { - message: 'Released under the MIT License.', - copyright: `Copyright Β© ${new Date().getFullYear()} siberiacancode` - }, - editLink: { - pattern: ({ filePath, params }) => { - if (filePath.includes('hooks') && params?.name) { - return `https://github.com/siberiacancode/reactuse/blob/main/packages/core/src/hooks/${params.name}/${params.name}.ts`; - } else { - return `https://github.com/siberiacancode/reactuse/blob/main/packages/docs/app/${filePath}`; - } - }, - text: 'Suggest changes to this page' - }, - nav: [ - { text: 'Home', link: '/' }, - { - text: 'Functions', - items: [ - { text: 'Get Started', link: '/introduction' }, - { text: 'Installation', link: '/installation' }, - { text: 'Hooks', link: '/functions/hooks/useAsync.html' } - ] - } - ], - sidebar: [ - { - text: 'Getting started', - items: [ - { text: 'Introduction', link: '/introduction' }, - { text: 'Installation', link: '/installation' }, - { text: 'reactuse.json', link: '/reactuse-json' }, - { text: 'CLI', link: '/cli' }, - { text: 'target', link: '/target' }, - { text: 'memoization', link: '/memoization' } - ] - }, - { - text: 'Installation', - items: [ - { text: 'Vite', link: '/installation/vite' }, - { text: 'Next.js', link: '/installation/nextjs' }, - { text: 'Astro', link: '/installation/astro' }, - { text: 'React Router', link: '/installation/react-router' }, - { - text: 'TanStack Router', - link: '/installation/tanstack-router' - }, - { text: 'TanStack Start', link: '/installation/tanstack' }, - { text: 'Preact', link: '/installation/preact' }, - { text: 'Manual', link: '/installation/manual' } - ] - }, - ...sidebarContentItems - ] - } - } - // ru: { - // label: 'Русский', - // lang: 'ru', - // themeConfig: { - // nav: [ - // { text: 'Главная', link: '/ru' }, - // { - // text: 'Π€ΡƒΠ½ΠΊΡ†ΠΈΠΈ', - // items: [{ text: 'Π₯ΡƒΠΊΠΈ', link: '/ru/functions/hooks' }] - // } - // ] - // } - // } - }, - themeConfig: { - search: { - provider: 'algolia', - options: { - appId: '62LROXAB1F', - apiKey: '87ab8dd07b4aba02814c082d98e4b8a7', - indexName: 'reactuse' - } - }, - socialLinks: [ - { icon: 'github', link: 'https://github.com/siberiacancode/reactuse' }, - { - icon: 'npm', - link: 'https://www.npmjs.com/package/@siberiacancode/reactuse' - } - ] - } - }); -}; diff --git a/packages/docs/app/.vitepress/sections/HomeContainer/HomeContainer.vue b/packages/docs/app/.vitepress/sections/HomeContainer/HomeContainer.vue deleted file mode 100644 index edc152e0d..000000000 --- a/packages/docs/app/.vitepress/sections/HomeContainer/HomeContainer.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/packages/docs/app/.vitepress/sections/HomeContributors/HomeContributors.data.ts b/packages/docs/app/.vitepress/sections/HomeContributors/HomeContributors.data.ts deleted file mode 100644 index b1c585955..000000000 --- a/packages/docs/app/.vitepress/sections/HomeContributors/HomeContributors.data.ts +++ /dev/null @@ -1,35 +0,0 @@ -import md5 from 'md5'; -import { simpleGit } from 'simple-git'; - -const git = simpleGit(); - -export default { - async load() { - try { - const log = await git.log(); - const contributorsMap = new Map(); - - log.all.forEach((commit) => { - const { author_email, author_name } = commit; - if (author_email && !contributorsMap.has(author_name)) { - contributorsMap.set(author_name, { - name: author_name, - email: author_email, - avatar: `https://gravatar.com/avatar/${md5(author_email)}?d=retro` - }); - } - }); - - const contributors = Array.from(contributorsMap.values()).sort((a, b) => - a.name.localeCompare(b.name) - ); - - return { - contributors - }; - } catch (error) { - console.error('Failed to load contributors:', error); - return { contributors: [] }; - } - } -}; diff --git a/packages/docs/app/.vitepress/sections/HomeContributors/HomeContributors.vue b/packages/docs/app/.vitepress/sections/HomeContributors/HomeContributors.vue deleted file mode 100644 index 47dc21af3..000000000 --- a/packages/docs/app/.vitepress/sections/HomeContributors/HomeContributors.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - diff --git a/packages/docs/app/.vitepress/sections/HomeHeroBefore/HomeHeroBefore.data.ts b/packages/docs/app/.vitepress/sections/HomeHeroBefore/HomeHeroBefore.data.ts deleted file mode 100644 index 29b1ff555..000000000 --- a/packages/docs/app/.vitepress/sections/HomeHeroBefore/HomeHeroBefore.data.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { getContent } from '../../../../src/utils'; - -export default { - async load() { - const hooks = await getContent('hook'); - - return { - count: hooks.length - }; - } -}; diff --git a/packages/docs/app/.vitepress/sections/HomeHeroBefore/HomeHeroBefore.vue b/packages/docs/app/.vitepress/sections/HomeHeroBefore/HomeHeroBefore.vue deleted file mode 100644 index 5a6ba6866..000000000 --- a/packages/docs/app/.vitepress/sections/HomeHeroBefore/HomeHeroBefore.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/packages/docs/app/.vitepress/sections/HomeHooks/HomeHooks.data.ts b/packages/docs/app/.vitepress/sections/HomeHooks/HomeHooks.data.ts deleted file mode 100644 index 391d89c89..000000000 --- a/packages/docs/app/.vitepress/sections/HomeHooks/HomeHooks.data.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { getContent } from '../../../../src/utils'; - -export default { - async load() { - const hooks = await getContent('hook'); - - return { - hooks - }; - } -}; diff --git a/packages/docs/app/.vitepress/sections/HomeHooks/HomeHooks.vue b/packages/docs/app/.vitepress/sections/HomeHooks/HomeHooks.vue deleted file mode 100644 index 87b1e4b5d..000000000 --- a/packages/docs/app/.vitepress/sections/HomeHooks/HomeHooks.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - diff --git a/packages/docs/app/.vitepress/sections/index.ts b/packages/docs/app/.vitepress/sections/index.ts deleted file mode 100644 index 84af7be55..000000000 --- a/packages/docs/app/.vitepress/sections/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as HomeContributors } from './HomeContributors/HomeContributors.vue'; -export { default as HomeHeroBefore } from './HomeHeroBefore/HomeHeroBefore.vue'; -export { default as HomeHooks } from './HomeHooks/HomeHooks.vue'; diff --git a/packages/docs/app/.vitepress/theme/global.css b/packages/docs/app/.vitepress/theme/global.css deleted file mode 100644 index 569efa627..000000000 --- a/packages/docs/app/.vitepress/theme/global.css +++ /dev/null @@ -1,27 +0,0 @@ -@import 'tailwindcss'; -@custom-variant dark (&:where(.dark, .dark *)); - -:root { - --vp-home-hero-name-color: transparent; - --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #61dafb, #3477d0); - --vp-home-hero-image-background-image: linear-gradient(130deg, #006eff, #00c8ff); - --vp-home-hero-image-filter: blur(40px) opacity(0.35); - - --vp-c-bg: #ffffff; - - --vp-c-brand-1: #087ea4; - --vp-c-brand-2: #4ca8c6; - --vp-c-brand-3: #52bde9; -} - -.dark { - --vp-c-brand-1: #58c4dc; - --vp-c-bg: #181818; - --vp-c-bg-soft: #1f1f1f; - --vp-c-bg-alt: #1d1c1c; - --vp-c-bg-elv: #2a2a2a; -} - -input[type='number'] { - min-width: 20rem; -} diff --git a/packages/docs/app/.vitepress/theme/index.ts b/packages/docs/app/.vitepress/theme/index.ts deleted file mode 100644 index c5e5acf06..000000000 --- a/packages/docs/app/.vitepress/theme/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { EnhanceAppContext } from 'vitepress'; - -import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client'; -import Theme from 'vitepress/theme'; -import { h } from 'vue'; - -import { HomeHeroBefore } from '../sections'; - -import '@shikijs/vitepress-twoslash/style.css'; -import './global.css'; - -export default { - extends: Theme, - Layout() { - return h(Theme.Layout, null, { - 'home-hero-before': () => h(HomeHeroBefore) - }); - }, - enhanceApp({ app }: EnhanceAppContext) { - app.use(TwoslashFloatingVue); - } -}; diff --git a/packages/docs/app/api/search/route.ts b/packages/docs/app/api/search/route.ts new file mode 100644 index 000000000..448c06686 --- /dev/null +++ b/packages/docs/app/api/search/route.ts @@ -0,0 +1,5 @@ +import { createFromSource } from 'fumadocs-core/search/server'; + +import { source } from '@docs/lib/source'; + +export const { GET } = createFromSource(source); diff --git a/packages/docs/app/docs/[[...slug]]/page.tsx b/packages/docs/app/docs/[[...slug]]/page.tsx new file mode 100644 index 000000000..7be521ff3 --- /dev/null +++ b/packages/docs/app/docs/[[...slug]]/page.tsx @@ -0,0 +1,154 @@ +import Link from 'next/link'; +import { notFound } from 'next/navigation'; +import { mdxComponents } from '@docs/mdx-components'; +import { IconArrowLeft, IconArrowRight } from '@tabler/icons-react'; +import { findNeighbour } from 'fumadocs-core/page-tree'; + +import { source } from '@docs/lib/source'; +import { absoluteUrl } from '@docs/lib/utils'; +import { Button } from '@docs/ui/button'; +import { DocsCopyPage } from '@docs/components/docs-copy-page'; +import { DocsTableOfContents } from '@docs/components/docs-toc'; + +export const revalidate = false; +export const dynamic = 'force-static'; +export const dynamicParams = false; + +export function generateStaticParams() { + return source.generateParams(); +} + +export async function generateMetadata(props: { params: Promise<{ slug: string[] }> }) { + const params = await props.params; + const page = source.getPage(params.slug); + + if (!page) { + notFound(); + } + + const doc = page.data; + + if (!doc.title || !doc.description) { + notFound(); + } + + return { + title: doc.title, + description: doc.description, + twitter: { + card: 'summary_large_image', + title: doc.title, + description: doc.description, + images: [ + { + url: `/og?title=${encodeURIComponent( + doc.title + )}&description=${encodeURIComponent(doc.description)}` + } + ] + } + }; +} + +export default async function Page(props: { params: Promise<{ slug: string[] }> }) { + const params = await props.params; + const page = source.getPage(params.slug); + if (!page) { + notFound(); + } + + const doc = page.data; + const MDX = doc.body; + const isChangelog = params.slug?.[0] === 'changelog'; + const neighbours = isChangelog + ? { previous: null, next: null } + : findNeighbour(source.pageTree, page.url); + const raw = await page.data.getText('raw'); + + return ( +
+
+
+
+
+
+
+

+ {doc.title} +

+
+
+ +
+
+ {neighbours.previous && ( + + )} + {neighbours.next && ( + + )} +
+
+
+ {doc.description && ( +

+ {doc.description} +

+ )} +
+
+
+ +
+
+ {neighbours.previous && ( + + )} + {neighbours.next && ( + + )} +
+
+
+
+
+ {doc.toc?.length && ( +
+ +
+ )} +
+
+ ); +} diff --git a/packages/docs/app/docs/layout.tsx b/packages/docs/app/docs/layout.tsx new file mode 100644 index 000000000..eaafe4c34 --- /dev/null +++ b/packages/docs/app/docs/layout.tsx @@ -0,0 +1,25 @@ +import { source } from '@docs/lib/source'; +import { DocsSidebar } from '@docs/components/docs-sidebar'; +import { SidebarProvider } from '@docs/ui/sidebar'; +import { SiteHeader } from '@docs/components/site-header'; + +export default function DocsLayout({ children }: { children: React.ReactNode }) { + return ( + <> + +
+ + +
{children}
+
+
+ + ); +} diff --git a/packages/docs/app/functions/hooks/[name].md b/packages/docs/app/functions/hooks/[name].md deleted file mode 100644 index 2cdfa2799..000000000 --- a/packages/docs/app/functions/hooks/[name].md +++ /dev/null @@ -1,113 +0,0 @@ - - -# {{ $params.name }} - -{{ $params.description }} - - - - - - - - - -## Installation - - - - - - - - - -## Usage - - - -## Demo - - - - - - - -## Source - - - -## Contributors - - diff --git a/packages/docs/app/functions/hooks/[name].paths.mts b/packages/docs/app/functions/hooks/[name].paths.mts deleted file mode 100644 index b00b89e08..000000000 --- a/packages/docs/app/functions/hooks/[name].paths.mts +++ /dev/null @@ -1,179 +0,0 @@ -import md5 from 'md5'; -import { codeToHtml } from 'shiki'; -import simpleGit from 'simple-git'; -import ts from 'typescript'; - -import { - checkTest, - getContent, - getContentFile, - matchJsdoc, - parseHookJsdoc -} from '../../../src/utils'; - -interface HookPageParams { - params: { - example: string; - description: string; - category: string; - lastModified: number; - usage: string; - apiParameters: any[]; - browserapi?: { - name: string; - description: string; - }; - id: string; - isTest: boolean; - name: string; - jsImplementation?: string; - typeDeclarations: string[]; - }; -} - -const git = simpleGit(); - -const extractTypeInfo = (sourceFile: ts.SourceFile) => { - const typeDeclarations: string[] = []; - const typeImports: string[] = []; - - const visit = (node: ts.Node) => { - if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) { - typeDeclarations.push(node.getText(sourceFile)); - } - - if (ts.isImportDeclaration(node)) { - const isTypeOnly = node.importClause?.isTypeOnly; - const hasTypeImports = - node.importClause?.namedBindings && - ts.isNamedImports(node.importClause.namedBindings) && - node.importClause.namedBindings.elements.some((element) => element.isTypeOnly); - - if (isTypeOnly || hasTypeImports) { - typeImports.push(node.getText(sourceFile)); - } - } - - ts.forEachChild(node, visit); - }; - visit(sourceFile); - - return [...typeImports, ...typeDeclarations].join('\n\n'); -}; - -const createHtmlCode = async (code: string) => - await codeToHtml(code, { - lang: 'ts', - themes: { - light: 'github-light', - dark: 'github-dark' - }, - defaultColor: false - }); - -export default { - async paths() { - const hooks = await getContent('hook'); - const helpers = await getContent('helper'); - - const content = [...hooks, ...helpers]; - - const params = await Promise.all( - content.map(async (element) => { - const content = await getContentFile(element.type, element.name); - const jsdocMatch = matchJsdoc(content); - - if (!jsdocMatch) { - console.error(`No jsdoc comment found for ${element.name}`); - return null; - } - - const jsdoc = parseHookJsdoc(jsdocMatch); - - if (!jsdoc.description || !jsdoc.examples.length) { - console.error(`No content found for ${element.name}`); - return null; - } - - const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true); - - const typeDeclarations = extractTypeInfo(sourceFile); - - const example = jsdoc.examples.reduce((acc, example, index) => { - if (index !== jsdoc.examples.length - 1) { - acc += `${example.description}\n// or\n`; - } else { - acc += example.description; - } - return acc; - }, ''); - - const isTest = await checkTest(element); - - const log = await git.log({ - file: `../core/src/${element.type}s/${element.name}/${element.name}.ts` - }); - const lastCommit = log.latest!; - - const contributorsMap = new Map( - log.all.map((commit) => [ - commit.author_email, - { name: commit.author_name, email: commit.author_email } - ]) - ); - - const contributors = Array.from(contributorsMap.values()).map((author) => ({ - name: author.name, - avatar: `https://gravatar.com/avatar/${md5(author.email)}?d=retro` - })); - - return { - params: { - code: await createHtmlCode(content), - id: element.name, - isTest, - type: element.type, - name: element.name, - ...(typeDeclarations && { - typeDeclarations: await createHtmlCode(typeDeclarations) - }), - usage: jsdoc.usage!.name ?? 'low', - ...(jsdoc.warning && { - warning: jsdoc.warning.description - }), - description: jsdoc.description.description, - category: jsdoc.category!.name, - lastModified: new Date(lastCommit?.date ?? new Date()).getTime(), - example: await createHtmlCode(example), - apiParameters: jsdoc.apiParameters ?? [], - contributors - } - }; - }) - ); - - const pages = params.filter(Boolean) as unknown as HookPageParams[]; - const testCoverage = pages.reduce((acc, page) => acc + Number(page.params.isTest), 0); - - console.log('\nElements injection report\n'); - console.log(`\x1B[32mInjected: ${pages.length}\x1B[0m`); - console.log( - `\x1B[35mTest coverage: ${Math.round( - (testCoverage / pages.length) * 100 - )}% (${testCoverage})\x1B[0m` - ); - const untested = pages.filter((page) => !page.params.isTest); - if (untested.length) - console.log( - `\x1B[35mUntested: ${untested - .map((page) => page.params.name) - .sort() - .join(', ')}\x1B[0m` - ); - - console.log(`\x1B[33mSkipped: ${content.length - pages.length}\x1B[0m`); - console.log(`\nTotal: ${content.length} elements`); - - return pages; - } -}; diff --git a/packages/docs/app/index.md b/packages/docs/app/index.md deleted file mode 100644 index 42df3bbbd..000000000 --- a/packages/docs/app/index.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -layout: home - -hero: - name: reactuse - text: the largest and most useful hook library - tagline: Improve your react applications with our library πŸ“¦ designed for comfort and speed - image: - src: /logo.svg - alt: reactuse - actions: - - theme: brand - text: Get Started - link: /introduction - - theme: alt - text: View on GitHub - link: https://github.com/siberiacancode/reactuse -features: - - title: Lightweight & Scalable - details: Hooks are lightweight and easy to use, making it simple to integrate into any project. - icon: | - - - title: Clean & consistent - details: Hooks follow a unified approach for consistency and maintainability. - icon: | - - - title: Customizable - details: Install and customize hooks effortlessly using our CLI - icon: | - - - title: Large collection - details: Extensive collection of hooks for all your needs, from state management to browser APIs. - icon: | - - - title: Tree shakable - details: The hooks are tree shakable, so you only import the hooks you need in your application. - icon: | - - - title: Active community - details: Join our active community on Github and help make reactuse even better. - icon: | - ---- - - - - - diff --git a/packages/docs/app/installation.md b/packages/docs/app/installation.md deleted file mode 100644 index cc2fc9fcf..000000000 --- a/packages/docs/app/installation.md +++ /dev/null @@ -1,57 +0,0 @@ - - -# Installation - -How to install dependencies and structure your app. - -## Install package - -::: code-group - -```bash [npm] -npm install @siberiacancode/reactuse -``` - -```bash [yarn] -yarn add @siberiacancode/reactuse -``` - -```bash [pnpm] -pnpm add @siberiacancode/reactuse -``` - -```bash [bun] -bun add @siberiacancode/reactuse -``` - -::: - -## Inject code to your framework - -How to install dependencies and structure your app with [cli](./cli.md) and [useverse](https://www.npmjs.com/package/useverse). - - diff --git a/packages/docs/app/installation/astro.md b/packages/docs/app/installation/astro.md deleted file mode 100644 index 2b3cd3f6e..000000000 --- a/packages/docs/app/installation/astro.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: Astro -description: Install and configure reactuse for Astro. ---- - -# Astro - -Install and configure reactuse for Astro. - -### Create project - -Start by creating a new Astro project with React integration: - -```bash -npx create-astro@latest astro-app --template with-tailwindcss --install --add react --git -``` - -### Edit tsconfig.json file - -Add the following to your `tsconfig.json` to resolve paths: - -```json -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - } -} -``` - -### Run the CLI - -Run the `useverse` init command to setup your project: - -```bash -npx useverse@latest init -``` - -This will create a configuration file [`reactuse.json`](../reactuse-json.md) in your project. - -### Add hooks - -You can now start adding hooks to your project. - -```bash -npx useverse@latest add useBoolean -``` - -The command above will add the `useBoolean` hook to your project. Use the hook in a React component with `client:load` or `client:visible` so it runs on the client. - -Example Astro page: - -```astro title="src/pages/index.astro" showLineNumbers ---- -import { ToggleButton } from '@/components/ToggleButton'; ---- - - - - - - Astro + reactuse - - -
- -
- - -``` - -React component using the hook: - -```tsx title="src/components/ToggleButton.tsx" showLineNumbers -import { useBoolean } from '@/shared/hooks'; - -export const ToggleButton = () => { - const [on, toggle] = useBoolean(); - - return ( - - ); -}; -``` diff --git a/packages/docs/app/layout.tsx b/packages/docs/app/layout.tsx new file mode 100644 index 000000000..f0ed6f65e --- /dev/null +++ b/packages/docs/app/layout.tsx @@ -0,0 +1,22 @@ +import type { ReactNode } from 'react'; +import { ThemeProvider } from '@docs/components/theme-provider'; +import { TooltipProvider } from '@docs/ui/tooltip'; + +import { Geist } from 'next/font/google'; +import { cn } from '@docs/lib/utils'; + +import '../styles/global.css'; + +const geist = Geist({ subsets: ['latin'], variable: '--font-sans' }); + +export default function Layout({ children }: { children: ReactNode }) { + return ( + + + + {children} + + + + ); +} diff --git a/packages/docs/app/public/android-chrome-192x192.png b/packages/docs/app/public/android-chrome-192x192.png deleted file mode 100644 index b9a240fc0..000000000 Binary files a/packages/docs/app/public/android-chrome-192x192.png and /dev/null differ diff --git a/packages/docs/app/public/android-chrome-512x512.png b/packages/docs/app/public/android-chrome-512x512.png deleted file mode 100644 index 66a6b95d8..000000000 Binary files a/packages/docs/app/public/android-chrome-512x512.png and /dev/null differ diff --git a/packages/docs/app/public/apple-touch-icon.png b/packages/docs/app/public/apple-touch-icon.png deleted file mode 100644 index 0740fd9ed..000000000 Binary files a/packages/docs/app/public/apple-touch-icon.png and /dev/null differ diff --git a/packages/docs/app/public/favicon-16x16.png b/packages/docs/app/public/favicon-16x16.png deleted file mode 100644 index ced28176c..000000000 Binary files a/packages/docs/app/public/favicon-16x16.png and /dev/null differ diff --git a/packages/docs/app/public/favicon-32x32.png b/packages/docs/app/public/favicon-32x32.png deleted file mode 100644 index 62e01aff2..000000000 Binary files a/packages/docs/app/public/favicon-32x32.png and /dev/null differ diff --git a/packages/docs/app/public/favicon.ico b/packages/docs/app/public/favicon.ico deleted file mode 100644 index ab41be7a6..000000000 Binary files a/packages/docs/app/public/favicon.ico and /dev/null differ diff --git a/packages/docs/app/public/logo.svg b/packages/docs/app/public/logo.svg deleted file mode 100644 index 349991d91..000000000 --- a/packages/docs/app/public/logo.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/packages/docs/app/public/manifest.json b/packages/docs/app/public/manifest.json deleted file mode 100644 index 22d6bce01..000000000 --- a/packages/docs/app/public/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "reactuse", - "short_name": "reactuse", - "description": "The largest and most useful hook library", - "icons": [ - { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, - { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/packages/docs/app/public/pop-down.mp3 b/packages/docs/app/public/pop-down.mp3 deleted file mode 100644 index 6650a3612..000000000 Binary files a/packages/docs/app/public/pop-down.mp3 and /dev/null differ diff --git a/packages/docs/components.json b/packages/docs/components.json new file mode 100644 index 000000000..484ca84f1 --- /dev/null +++ b/packages/docs/components.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "utils": "@docs/lib/utils", + "ui": "@docs/ui", + "lib": "@docs/lib", + "hooks": "@docs/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/packages/docs/components/callout.tsx b/packages/docs/components/callout.tsx new file mode 100644 index 000000000..76ce8c62a --- /dev/null +++ b/packages/docs/components/callout.tsx @@ -0,0 +1,29 @@ +import { cn } from '@docs/lib/utils'; +import { Alert, AlertDescription, AlertTitle } from '@docs/ui/alert'; + +export function Callout({ + title, + children, + icon, + className, + variant = 'default', + ...props +}: React.ComponentProps & { + icon?: React.ReactNode; + variant?: 'default' | 'info' | 'warning'; +}) { + return ( + + {icon} + {title && {title}} + {children} + + ); +} diff --git a/packages/docs/components/code-block-command.tsx b/packages/docs/components/code-block-command.tsx new file mode 100644 index 000000000..6834c2475 --- /dev/null +++ b/packages/docs/components/code-block-command.tsx @@ -0,0 +1,97 @@ +'use client'; + +import * as React from 'react'; +import { IconCheck, IconCopy, IconTerminal } from '@tabler/icons-react'; +import { copyToClipboardWithMeta } from '@docs/components/copy-button'; +import { Button } from '@docs/ui/button'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@docs/ui/tabs'; + +export function CodeBlockCommand({ + __npm__, + __yarn__, + __pnpm__, + __bun__ +}: React.ComponentProps<'pre'> & { + __npm__?: string; + __yarn__?: string; + __pnpm__?: string; + __bun__?: string; +}) { + const [hasCopied, setHasCopied] = React.useState(false); + + React.useEffect(() => { + if (hasCopied) { + const timer = setTimeout(() => setHasCopied(false), 2000); + return () => clearTimeout(timer); + } + }, [hasCopied]); + + const packageManager = 'pnpm'; + const tabs = React.useMemo(() => { + return { + pnpm: __pnpm__, + npm: __npm__, + yarn: __yarn__, + bun: __bun__ + }; + }, [__npm__, __pnpm__, __yarn__, __bun__]); + + const copyCommand = React.useCallback(() => { + const command = tabs[packageManager]; + + if (!command) { + return; + } + + copyToClipboardWithMeta(command); + setHasCopied(true); + }, [packageManager, tabs]); + + return ( +
+ +
+
+ +
+ + {Object.entries(tabs).map(([key]) => { + return ( + + {key} + + ); + })} + +
+
+ {Object.entries(tabs).map(([key, value]) => { + return ( + +
+                  
+                    {value}
+                  
+                
+
+ ); + })} +
+
+ +
+ ); +} diff --git a/packages/docs/components/code.tsx b/packages/docs/components/code.tsx new file mode 100644 index 000000000..609b36238 --- /dev/null +++ b/packages/docs/components/code.tsx @@ -0,0 +1,16 @@ +import { highlightCode } from '@docs/lib/highlight-code'; +import { CodeBlock } from 'fumadocs-ui/components/codeblock'; + +interface Props { + code: string; +} + +export const Code = async (props: Props) => { + const html = await highlightCode(props.code); + + return ( + +
+ + ); +}; diff --git a/packages/docs/components/command-menu.tsx b/packages/docs/components/command-menu.tsx new file mode 100644 index 000000000..413078205 --- /dev/null +++ b/packages/docs/components/command-menu.tsx @@ -0,0 +1,146 @@ +'use client'; +import { useMemo, useState } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger +} from '../ui/dialog'; +import { Button } from '../ui/button'; +import { cn } from '@docs/lib/utils'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from '../ui/command'; +import { useDocsSearch } from 'fumadocs-core/search/client'; +import { useDebouncedCallback } from '@docs/hooks/use-debounced-callback'; +import { usePathname, useRouter } from 'next/navigation'; +import { Spinner } from './spinner'; +import { getCurrentBase, getPagesFromFolder } from '@docs/lib/page-tree'; +import { source } from '@docs/lib/source'; + +interface Props { + tree: typeof source.pageTree; + navItems: { href: string; label: string }[]; +} + +export const CommandMenu = (props: Props) => { + const { tree, navItems } = props; + + const router = useRouter(); + const pathname = usePathname(); + const currentBase = getCurrentBase(pathname); + const [open, setOpen] = useState(false); + + const { setSearch, query } = useDocsSearch({ + type: 'fetch' + }); + + const handleChangeSearch = useDebouncedCallback((value: string) => { + setSearch(value); + }, 500); + + const pageGroupsSection = useMemo(() => { + return tree.children.map((group) => { + if (group.type !== 'folder') { + return null; + } + + const pages = getPagesFromFolder(group, currentBase); + + if (pages.length === 0) { + return null; + } + + return ( + + {pages.map((item) => { + return ( + handleRedirect(item.url)} + > + {item.name} + + ); + })} + + ); + }); + }, [tree.children, currentBase, router]); + + const handleRedirect = (href: string) => { + router.push(href); + setOpen(false); + }; + + return ( + + + + + + + Search documentation... + Search for a command to run... + + +
+ + {query.isLoading && ( +
+ +
+ )} +
+ + + {query.isLoading ? 'Searching...' : 'No results found.'} + + + {navItems.map((item) => ( + handleRedirect(item.href)} + > + {item.label} + + ))} + + {pageGroupsSection} + +
+
+
+ ); +}; diff --git a/packages/docs/components/copy-button.tsx b/packages/docs/components/copy-button.tsx new file mode 100644 index 000000000..2fd55032e --- /dev/null +++ b/packages/docs/components/copy-button.tsx @@ -0,0 +1,52 @@ +'use client'; + +import * as React from 'react'; +import { IconCheck, IconCopy } from '@tabler/icons-react'; + +import { cn } from '@docs/lib/utils'; +import { Button } from '@docs/ui/button'; + +export function copyToClipboardWithMeta(value: string) { + navigator.clipboard.writeText(value); +} + +export function CopyButton({ + value, + className, + variant = 'ghost', + ...props +}: React.ComponentProps & { + value: string; + src?: string; + tooltip?: string; +}) { + const [hasCopied, setHasCopied] = React.useState(false); + + React.useEffect(() => { + if (hasCopied) { + const timer = setTimeout(() => setHasCopied(false), 2000); + return () => clearTimeout(timer); + } + }, [hasCopied]); + + return ( + + ); +} diff --git a/packages/docs/components/docs-copy-page.tsx b/packages/docs/components/docs-copy-page.tsx new file mode 100644 index 000000000..c395143eb --- /dev/null +++ b/packages/docs/components/docs-copy-page.tsx @@ -0,0 +1,158 @@ +'use client'; + +import { IconCheck, IconChevronDown, IconCopy } from '@tabler/icons-react'; + +import { useCopyToClipboard } from '@docs/hooks/use-copy-to-clipboard'; +import { Button } from '@docs/ui/button'; +import { Separator } from '@docs/ui/separator'; + +function getPromptUrl(baseURL: string, url: string) { + return `${baseURL}?q=${encodeURIComponent( + `I’m looking at this shadcn/ui documentation: ${url}. +Help me understand how to use it. Be ready to explain concepts, give examples, or help debug based on it. + ` + )}`; +} + +const menuItems = { + markdown: (url: string) => ( + + + + + View as Markdown + + ), + v0: (url: string) => ( + + + + + Open in v0 + + ), + chatgpt: (url: string) => ( + + + + + Open in ChatGPT + + ), + claude: (url: string) => ( + + + + + Open in Claude + + ), + scira: (url: string) => ( + + + + + + + + + + + Open in Scira + + ) +}; + +export function DocsCopyPage({ page, url }: { page: string; url: string }) { + const { copyToClipboard, isCopied } = useCopyToClipboard(); + + const trigger = ( + + ); + + return ( +
+ + +
+ ); +} diff --git a/packages/docs/components/docs-sidebar.tsx b/packages/docs/components/docs-sidebar.tsx new file mode 100644 index 000000000..15a68ba5a --- /dev/null +++ b/packages/docs/components/docs-sidebar.tsx @@ -0,0 +1,126 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import { getCurrentBase, getPagesFromFolder } from '@docs/lib/page-tree'; +import type { source } from '@docs/lib/source'; +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem +} from '@docs/ui/sidebar'; + +const TOP_LEVEL_SECTIONS = [ + { name: 'Introduction', href: '/docs' }, + { + name: 'Installation', + href: '/docs/installation' + }, + { + name: 'reactuse.json', + href: '/docs/reactuse-json' + }, + { + name: 'CLI', + href: '/docs/cli' + }, + { + name: 'target', + href: '/docs/target' + }, + { + name: 'memoization', + href: '/docs/memoization' + } +]; + +const EXCLUDED_SECTIONS = ['Introduction']; + +export function DocsSidebar({ + tree, + ...props +}: React.ComponentProps & { tree: typeof source.pageTree }) { + const pathname = usePathname(); + const currentBase = getCurrentBase(pathname); + + return ( + +
+
+
+ + + + Sections + + + + {TOP_LEVEL_SECTIONS.map(({ name, href }) => { + return ( + + + + + {name} + + + + ); + })} + + + + {tree.children.map((item) => { + if (EXCLUDED_SECTIONS.includes((item.name as string) ?? '')) { + return null; + } + + return ( + + + {item.name} + + + {item.type === 'folder' && ( + + {getPagesFromFolder(item, currentBase).map((page) => { + return ( + + + + + {page.name} + + + + ); + })} + + )} + + + ); + })} +
+ + + ); +} diff --git a/packages/docs/components/docs-toc.tsx b/packages/docs/components/docs-toc.tsx new file mode 100644 index 000000000..9611e0212 --- /dev/null +++ b/packages/docs/components/docs-toc.tsx @@ -0,0 +1,116 @@ +'use client'; + +import * as React from 'react'; +import { IconMenu3 } from '@tabler/icons-react'; + +import { cn } from '@docs/lib/utils'; +import { Button } from '../ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from '../ui/dropdown-menu'; + +function useActiveItem(itemIds: string[]) { + const [activeId, setActiveId] = React.useState(null); + + React.useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + setActiveId(entry.target.id); + } + } + }, + { rootMargin: '0% 0% -80% 0%' } + ); + + for (const id of itemIds ?? []) { + const element = document.getElementById(id); + if (element) { + observer.observe(element); + } + } + + return () => { + for (const id of itemIds ?? []) { + const element = document.getElementById(id); + if (element) { + observer.unobserve(element); + } + } + }; + }, [itemIds]); + + return activeId; +} + +export function DocsTableOfContents({ + toc, + variant = 'list', + className +}: { + toc: { + title?: React.ReactNode; + url: string; + depth: number; + }[]; + variant?: 'dropdown' | 'list'; + className?: string; +}) { + const [open, setOpen] = React.useState(false); + const itemIds = React.useMemo(() => toc.map((item) => item.url.replace('#', '')), [toc]); + const activeHeading = useActiveItem(itemIds); + + if (!toc?.length) { + return null; + } + + if (variant === 'dropdown') { + return ( + + + + + + {toc.map((item) => ( + { + setOpen(false); + }} + data-depth={item.depth} + className='data-[depth=3]:pl-6 data-[depth=4]:pl-8' + > + {item.title} + + ))} + + + ); + } + + return ( +
+

+ On This Page +

+ {toc.map((item) => ( + + {item.title} + + ))} +
+ ); +} diff --git a/packages/docs/components/examples.tsx b/packages/docs/components/examples.tsx new file mode 100644 index 000000000..e0abbea25 --- /dev/null +++ b/packages/docs/components/examples.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const Examples = () => { + return
Examples
; +}; diff --git a/packages/docs/components/github-link.tsx b/packages/docs/components/github-link.tsx new file mode 100644 index 000000000..efd3ec22f --- /dev/null +++ b/packages/docs/components/github-link.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import Link from 'next/link'; + +import { siteConfig } from '@docs/lib/config'; +import { Icons } from '@docs/components/icons'; +import { Button } from '@docs/ui/button'; +import { Skeleton } from '@docs/ui/skeleton'; + +export function GitHubLink() { + return ( + + ); +} + +async function StarsCount() { + const data = await fetch('https://api.github.com/repos/siberiacancode/reactuse', { + next: { revalidate: 86400 } + }); + const json = await data.json(); + + const formattedCount = + json.stargazers_count >= 1000 + ? `${Math.round(json.stargazers_count / 1000)}k` + : json.stargazers_count.toLocaleString(); + + return {formattedCount}; +} diff --git a/packages/docs/components/hook-demo.tsx b/packages/docs/components/hook-demo.tsx new file mode 100644 index 000000000..e485a2d58 --- /dev/null +++ b/packages/docs/components/hook-demo.tsx @@ -0,0 +1,21 @@ +'use client'; + +import { HookProps } from '@docs/lib/parse-hook'; +import { ExamplesIndex } from '../.source/demo'; +import { cx } from 'class-variance-authority'; + +export const DocDemo = (props: HookProps) => { + const example = ExamplesIndex[props.name]; + + if (!example) { + return null; + } + + const Demo = example.component; + + return ( +
+ +
+ ); +}; diff --git a/packages/docs/components/hook-doc-page.tsx b/packages/docs/components/hook-doc-page.tsx new file mode 100644 index 000000000..321117827 --- /dev/null +++ b/packages/docs/components/hook-doc-page.tsx @@ -0,0 +1,108 @@ +import { Callout } from './callout'; +import { Badge } from '../ui/badge'; +import { HookProps } from '@docs/lib/parse-hook'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table'; +import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; +import { timeAgo } from '@docs/lib/utils'; +import { Code } from './code'; + +export const DocHeader = (props: HookProps) => ( + <> +
+
+ + {props.category} + + + {props.usage} + + + test coverage + +
+

Last changed: {timeAgo(props.lastModified)}

+
+ + {props.warning && ( + +

Important

+ {props.warning} +
+ )} + + {props.browserapi && ( + +

TIP

+ This hook uses {props.browserapi} browser api to provide enhanced functionality. Make sure + to check for compatibility with different browsers when using this api +
+ )} + +); + +export const DocUsageExamples = (props: HookProps) => + props.examples.map((example) => ); + +export const DocContributors = (props: HookProps) => ( +
+ {props.contributors.map(({ name, avatar }) => ( +
+ + + {name[0]} + +

{name}

+
+ ))} +
+); + +export const DocTableApi = (props: HookProps) => + props.apiParameters.map((group) => ( +
+ <> + {group.parameters.length > 0 && ( + <> +

Parameters

+ + + + Name + Type + Default + Note + + + + {group.parameters.map((parameter) => ( + + + {parameter.name} + {parameter.optional ? '?' : ''} + + {parameter.type} + {parameter.default ?? '-'} + {parameter.description} + + ))} + +
+ + )} + {group.returns && ( + <> +

Return

+ + {group.returns.type} + + + )} + +
+ )); diff --git a/packages/docs/components/hook-preview.tsx b/packages/docs/components/hook-preview.tsx new file mode 100644 index 000000000..608aabdff --- /dev/null +++ b/packages/docs/components/hook-preview.tsx @@ -0,0 +1,139 @@ +import * as React from 'react'; +import Image from 'next/image'; + +// import { ComponentPreviewTabs } from '@docs/components/component-preview-tabs'; +// import { ComponentSource } from '@docs/components/component-source'; +import { ExamplesIndex } from '@docs/__index__'; + +export function getDemoComponent(name: string) { + return ExamplesIndex?.[name]?.component; +} + +function DemoSuspense({ children, name }: { children: React.ReactNode; name: string }) { + return ( + + Loading demo for{' '} + + {name} + + … +

+ } + > + {children} +
+ ); +} + +export function HookPreview({ + name, + type, + className, + previewClassName, + align = 'center', + hideCode = false, + chromeLessOnMobile = false, + styleName = 'new-york-v4', + direction = 'ltr', + caption, + ...props +}: React.ComponentProps<'div'> & { + name: string; + styleName?: string; + align?: 'center' | 'start' | 'end'; + description?: string; + hideCode?: boolean; + type?: 'block' | 'component' | 'example'; + chromeLessOnMobile?: boolean; + previewClassName?: string; + direction?: 'ltr' | 'rtl'; + caption?: string; +}) { + if (type === 'block') { + const content = ( +
+ {name} + {name} +
+