├── .commitlintrc ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── documentation.md │ └── feature_request.md ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .lintstagedrc.js ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── __registry__ └── index.tsx ├── app ├── (app) │ ├── docs │ │ ├── [[...slug]] │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── not-found.tsx │ ├── layout.tsx │ └── page.tsx ├── layout.tsx └── not-found.tsx ├── components.json ├── components ├── active-theme.tsx ├── callout.tsx ├── changing-scramble-text.tsx ├── code-block-command.tsx ├── code-block-wrapper.tsx ├── command-menu.tsx ├── component-preview.tsx ├── component-source.tsx ├── contribute.tsx ├── copy-button.tsx ├── doc-grid-pattern.tsx ├── docs-nav.tsx ├── grid-pattern.tsx ├── icons.tsx ├── main-nav.tsx ├── mdx-components.tsx ├── mobile-nav.tsx ├── scramble-text.tsx ├── site-footer.tsx ├── site-header.tsx ├── theme-customizer.tsx ├── toc.tsx └── ui │ ├── accordion.tsx │ ├── alert.tsx │ ├── aspect-ratio.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── drawer.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── skeleton.tsx │ ├── sonner.tsx │ ├── tabs.tsx │ └── tooltip.tsx ├── config ├── colors.ts ├── docs.ts └── site.ts ├── content └── docs │ └── hooks │ ├── use-boolean.mdx │ ├── use-copy-to-clipboard.mdx │ ├── use-counter.mdx │ ├── use-debounce-callback.mdx │ ├── use-document-title.mdx │ ├── use-interval.mdx │ ├── use-isomorphic-layout-effect.mdx │ ├── use-mouse-position.mdx │ ├── use-timeout.mdx │ ├── use-toggle.mdx │ └── use-unmount.mdx ├── contentlayer.config.ts ├── eslint.config.mjs ├── hooks ├── use-config.ts └── use-meta-color.ts ├── lib ├── fonts.ts ├── github.ts ├── rehype-component.ts ├── rehype-npm-command.ts ├── toc.ts └── utils.ts ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── hero.png ├── og.png ├── r │ ├── hooks │ │ ├── use-boolean.json │ │ ├── use-copy-to-clipboard-demo.json │ │ ├── use-copy-to-clipboard.json │ │ ├── use-counter.json │ │ ├── use-debounce-callback.json │ │ ├── use-document-title-demo.json │ │ ├── use-document-title.json │ │ ├── use-interval-demo.json │ │ ├── use-interval.json │ │ ├── use-isomorphic-layout-effect.json │ │ ├── use-mouse-position-demo.json │ │ ├── use-mouse-position.json │ │ ├── use-timeout-demo.json │ │ ├── use-timeout.json │ │ ├── use-toggle.json │ │ └── use-unmount.json │ └── index.json └── site.webmanifest ├── registry ├── examples │ ├── use-copy-to-clipboard-demo.tsx │ ├── use-document-title-demo.tsx │ ├── use-interval-demo.tsx │ ├── use-mouse-position-demo.tsx │ └── use-timeout-demo.tsx ├── hooks │ ├── use-boolean.tsx │ ├── use-copy-to-clipboard.tsx │ ├── use-counter.tsx │ ├── use-debounce-callback.tsx │ ├── use-document-title.tsx │ ├── use-interval.tsx │ ├── use-isomorphic-layout-effect.tsx │ ├── use-mouse-position.tsx │ ├── use-timeout.tsx │ ├── use-toggle.tsx │ └── use-unmount.tsx ├── index.tsx ├── registry-examples.ts ├── registry-hooks.ts └── schema.ts ├── scripts └── build-registry.mts ├── styles ├── globals.css ├── mdx.css └── themes.css ├── tsconfig.json ├── tsconfig.scripts.json └── types ├── nav.ts └── unist.ts /.commitlintrc: -------------------------------------------------------------------------------- 1 | { "extends": ["@commitlint/config-conventional"] } 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Suggest an edit for the documentation 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the documentation change** 10 | A clear and concise description of what you'd like to see changed or added in the documentation. 11 | 12 | **Current documentation** 13 | If applicable, provide a link or reference to the current documentation that needs to be updated. 14 | 15 | **Proposed change** 16 | Describe the change you'd like to see in the documentation. Be as specific as possible. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the documentation request here. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | 42 | # contentlayer 43 | .contentlayer 44 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | pnpm dlx commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | pnpm run build 2 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | 3 | const buildEslintCommand = (filenames) => 4 | `next lint --fix --file ${filenames 5 | .map((f) => path.relative(process.cwd(), f)) 6 | .join(" --file ")}` 7 | 8 | const prettierCommand = "prettier --write" 9 | 10 | module.exports = { 11 | "*.{js,jsx,ts,tsx}": [prettierCommand, buildEslintCommand], 12 | "*.{json,css,md}": [prettierCommand], 13 | } 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "singleQuote": false, 5 | "tabWidth": 2, 6 | "trailingComma": "es5", 7 | "plugins": [ 8 | "prettier-plugin-tailwindcss", 9 | "@ianvs/prettier-plugin-sort-imports" 10 | ], 11 | "importOrder": [ 12 | "^(react/(.*)$)|^(react$)", 13 | "^(next/(.*)$)|^(next$)", 14 | "", 15 | "", 16 | "^types$", 17 | "^@/types/(.*)$", 18 | "^@/config/(.*)$", 19 | "^@/lib/(.*)$", 20 | "^@/hooks/(.*)$", 21 | "^@/components/ui/(.*)$", 22 | "^@/components/(.*)$", 23 | "^@/styles/(.*)$", 24 | "^@/app/(.*)$", 25 | "^@/registry/(.*)$", 26 | "", 27 | "^[./]" 28 | ], 29 | "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"] 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ghribi Ouassim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

hookcn

4 | 5 |

6 | A collection of reusable react hooks that you can easily copy and paste into your apps or add directly through the shadcn CLI. 7 |
8 |
9 | Demo 10 | · 11 | Report Bug 12 | · 13 | Request Feature 14 |

15 | 16 | ## Table of Contents 17 | 18 | - [Table of Contents](#table-of-contents) 19 | - [About The Project](#about-the-project) 20 | - [Installation](#installation) 21 | - [Usage](#usage) 22 | - [Roadmap](#roadmap) 23 | - [Contributing](#contributing) 24 | - [License](#license) 25 | - [Contact](#contact) 26 | - [Acknowledgments](#acknowledgments) 27 | 28 | 29 | 30 | ## About The Project 31 | 32 | [![hookcn Screen Shot][product-screenshot]](https://hookcn.ouassim.tech) 33 | 34 | This project offers a collection of reusable React hooks to simplify common tasks like state management, timeouts, and element visibility. Inspired by [shadcn/ui](https://ui.shadcn.com), it lets you copy hooks directly or use a the [shadcn/ui CLI](https://ui.shadcn.com/docs/cli) for easy integration. Each hook is designed with best practices for clean, efficient, and maintainable code. 35 | 36 |

(back to top)

37 | 38 | 39 | 40 | ## Installation 41 | 42 | 1. Clone the repo 43 | 44 | ```sh 45 | git clone https://github.com/strlrd-29/hookcn 46 | ``` 47 | 48 | 2. Navigate to project dir 49 | 50 | ```sh 51 | cd hookcn 52 | ``` 53 | 54 | 3. Install NPM packages 55 | 56 | ```sh 57 | pnpm install 58 | ``` 59 | 60 |

(back to top)

61 | 62 | 63 | 64 | ## Usage 65 | 66 | Start the dev server 67 | 68 | ```sh 69 | pnpm dev 70 | ``` 71 | 72 | Open your browser and navigate to [http://localhost:3000](http://localhost:3000) to see the application in action. 73 | 74 |

(back to top)

75 | 76 | 77 | 78 | 79 | 80 | ## Contributing 81 | 82 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 83 | 84 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 85 | Don't forget to give the project a star! Thanks again! 86 | 87 | 1. Fork the Project 88 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 89 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 90 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 91 | 5. Open a Pull Request 92 | 93 | 94 | 95 | ## License 96 | 97 | Distributed under the MIT License. See `LICENSE` for more information. 98 | 99 |

(back to top)

100 | 101 | 102 | 103 | ## Contact 104 | 105 | Ghribi Ouassim Abdelmalek - [@strlrd29](https://twitter.com/strlrd29) - 106 | 107 | Project Link: [hookcn.ouassim.tech](https://hookcn.ouassim.tech) 108 | 109 |

(back to top)

110 | 111 | 112 | 113 | ## Acknowledgments 114 | 115 | - [usehook-ts](https://usehooks-ts.com/) 116 | - [usehooks](https://usehooks.com/) 117 | 118 |

(back to top)

119 | 120 | [product-screenshot]: public/hero.png 121 | -------------------------------------------------------------------------------- /__registry__/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // 3 | // @ts-nocheck 4 | // This file is autogenerated by scripts/build-registry.ts 5 | // Do not edit this file directly. 6 | import * as React from "react" 7 | 8 | export const Index: Record = { 9 | "use-boolean": { 10 | name: "use-boolean", 11 | type: "registry:hook", 12 | registryDependencies: undefined, 13 | files: [ 14 | { 15 | path: "registry/hooks/use-boolean.tsx", 16 | type: "registry:hook", 17 | target: "", 18 | }, 19 | ], 20 | component: React.lazy(() => import("@/registry/hooks/use-boolean.tsx")), 21 | source: "", 22 | }, 23 | "use-toggle": { 24 | name: "use-toggle", 25 | type: "registry:hook", 26 | registryDependencies: undefined, 27 | files: [ 28 | { 29 | path: "registry/hooks/use-toggle.tsx", 30 | type: "registry:hook", 31 | target: "", 32 | }, 33 | ], 34 | component: React.lazy(() => import("@/registry/hooks/use-toggle.tsx")), 35 | source: "", 36 | }, 37 | "use-copy-to-clipboard": { 38 | name: "use-copy-to-clipboard", 39 | type: "registry:hook", 40 | registryDependencies: undefined, 41 | files: [ 42 | { 43 | path: "registry/hooks/use-copy-to-clipboard.tsx", 44 | type: "registry:hook", 45 | target: "", 46 | }, 47 | ], 48 | component: React.lazy( 49 | () => import("@/registry/hooks/use-copy-to-clipboard.tsx") 50 | ), 51 | source: "", 52 | }, 53 | "use-unmount": { 54 | name: "use-unmount", 55 | type: "registry:hook", 56 | registryDependencies: undefined, 57 | files: [ 58 | { 59 | path: "registry/hooks/use-unmount.tsx", 60 | type: "registry:hook", 61 | target: "", 62 | }, 63 | ], 64 | component: React.lazy(() => import("@/registry/hooks/use-unmount.tsx")), 65 | source: "", 66 | }, 67 | "use-debounce-callback": { 68 | name: "use-debounce-callback", 69 | type: "registry:hook", 70 | registryDependencies: ["https://hookcn.ouassim.tech/r/use-unmount"], 71 | files: [ 72 | { 73 | path: "registry/hooks/use-debounce-callback.tsx", 74 | type: "registry:hook", 75 | target: "", 76 | }, 77 | ], 78 | component: React.lazy( 79 | () => import("@/registry/hooks/use-debounce-callback.tsx") 80 | ), 81 | source: "", 82 | }, 83 | "use-isomorphic-layout-effect": { 84 | name: "use-isomorphic-layout-effect", 85 | type: "registry:hook", 86 | registryDependencies: undefined, 87 | files: [ 88 | { 89 | path: "registry/hooks/use-isomorphic-layout-effect.tsx", 90 | type: "registry:hook", 91 | target: "", 92 | }, 93 | ], 94 | component: React.lazy( 95 | () => import("@/registry/hooks/use-isomorphic-layout-effect.tsx") 96 | ), 97 | source: "", 98 | }, 99 | "use-interval": { 100 | name: "use-interval", 101 | type: "registry:hook", 102 | registryDependencies: undefined, 103 | files: [ 104 | { 105 | path: "registry/hooks/use-interval.tsx", 106 | type: "registry:hook", 107 | target: "", 108 | }, 109 | ], 110 | component: React.lazy(() => import("@/registry/hooks/use-interval.tsx")), 111 | source: "", 112 | }, 113 | "use-timeout": { 114 | name: "use-timeout", 115 | type: "registry:hook", 116 | registryDependencies: undefined, 117 | files: [ 118 | { 119 | path: "registry/hooks/use-timeout.tsx", 120 | type: "registry:hook", 121 | target: "", 122 | }, 123 | ], 124 | component: React.lazy(() => import("@/registry/hooks/use-timeout.tsx")), 125 | source: "", 126 | }, 127 | "use-document-title": { 128 | name: "use-document-title", 129 | type: "registry:hook", 130 | registryDependencies: undefined, 131 | files: [ 132 | { 133 | path: "registry/hooks/use-document-title.tsx", 134 | type: "registry:hook", 135 | target: "", 136 | }, 137 | ], 138 | component: React.lazy( 139 | () => import("@/registry/hooks/use-document-title.tsx") 140 | ), 141 | source: "", 142 | }, 143 | "use-counter": { 144 | name: "use-counter", 145 | type: "registry:hook", 146 | registryDependencies: undefined, 147 | files: [ 148 | { 149 | path: "registry/hooks/use-counter.tsx", 150 | type: "registry:hook", 151 | target: "", 152 | }, 153 | ], 154 | component: React.lazy(() => import("@/registry/hooks/use-counter.tsx")), 155 | source: "", 156 | }, 157 | "use-mouse-position": { 158 | name: "use-mouse-position", 159 | type: "registry:hook", 160 | registryDependencies: undefined, 161 | files: [ 162 | { 163 | path: "registry/hooks/use-mouse-position.tsx", 164 | type: "registry:hook", 165 | target: "", 166 | }, 167 | ], 168 | component: React.lazy( 169 | () => import("@/registry/hooks/use-mouse-position.tsx") 170 | ), 171 | source: "", 172 | }, 173 | "use-copy-to-clipboard-demo": { 174 | name: "use-copy-to-clipboard-demo", 175 | type: "registry:example", 176 | registryDependencies: undefined, 177 | files: [ 178 | { 179 | path: "registry/examples/use-copy-to-clipboard-demo.tsx", 180 | type: "registry:example", 181 | target: "", 182 | }, 183 | ], 184 | component: React.lazy( 185 | () => import("@/registry/examples/use-copy-to-clipboard-demo.tsx") 186 | ), 187 | source: "", 188 | }, 189 | "use-interval-demo": { 190 | name: "use-interval-demo", 191 | type: "registry:example", 192 | registryDependencies: undefined, 193 | files: [ 194 | { 195 | path: "registry/examples/use-interval-demo.tsx", 196 | type: "registry:example", 197 | target: "", 198 | }, 199 | ], 200 | component: React.lazy( 201 | () => import("@/registry/examples/use-interval-demo.tsx") 202 | ), 203 | source: "", 204 | }, 205 | "use-timeout-demo": { 206 | name: "use-timeout-demo", 207 | type: "registry:example", 208 | registryDependencies: undefined, 209 | files: [ 210 | { 211 | path: "registry/examples/use-timeout-demo.tsx", 212 | type: "registry:example", 213 | target: "", 214 | }, 215 | ], 216 | component: React.lazy( 217 | () => import("@/registry/examples/use-timeout-demo.tsx") 218 | ), 219 | source: "", 220 | }, 221 | "use-document-title-demo": { 222 | name: "use-document-title-demo", 223 | type: "registry:example", 224 | registryDependencies: undefined, 225 | files: [ 226 | { 227 | path: "registry/examples/use-document-title-demo.tsx", 228 | type: "registry:example", 229 | target: "", 230 | }, 231 | ], 232 | component: React.lazy( 233 | () => import("@/registry/examples/use-document-title-demo.tsx") 234 | ), 235 | source: "", 236 | }, 237 | "use-mouse-position-demo": { 238 | name: "use-mouse-position-demo", 239 | type: "registry:example", 240 | registryDependencies: undefined, 241 | files: [ 242 | { 243 | path: "registry/examples/use-mouse-position-demo.tsx", 244 | type: "registry:example", 245 | target: "", 246 | }, 247 | ], 248 | component: React.lazy( 249 | () => import("@/registry/examples/use-mouse-position-demo.tsx") 250 | ), 251 | source: "", 252 | }, 253 | } 254 | -------------------------------------------------------------------------------- /app/(app)/docs/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | import { notFound } from "next/navigation" 3 | import { allDocs } from "contentlayer/generated" 4 | import { ChevronRightIcon, ExternalLinkIcon } from "lucide-react" 5 | import { Balancer } from "react-wrap-balancer" 6 | 7 | import { siteConfig } from "@/config/site" 8 | import { getTableOfContents } from "@/lib/toc" 9 | import { absoluteUrl, cn } from "@/lib/utils" 10 | import { Mdx } from "@/components/mdx-components" 11 | import { DashboardTableOfContents } from "@/components/toc" 12 | 13 | import "@/styles/mdx.css" 14 | 15 | import Link from "next/link" 16 | 17 | import { badgeVariants } from "@/components/ui/badge" 18 | import { Contribute } from "@/components/contribute" 19 | import { DocGridPattern } from "@/components/doc-grid-pattern" 20 | import { ScrambleText } from "@/components/scramble-text" 21 | 22 | interface DocPageProps { 23 | params: { 24 | slug: string[] 25 | } 26 | } 27 | 28 | async function getDocFromParams({ params }: DocPageProps) { 29 | const slug = params.slug?.join("/") || "" 30 | const doc = allDocs.find((doc) => doc.slugAsParams === slug) 31 | 32 | if (!doc) { 33 | return null 34 | } 35 | 36 | return doc 37 | } 38 | 39 | export async function generateMetadata(props: { 40 | params: Promise 41 | }): Promise { 42 | const params = await props.params 43 | const doc = await getDocFromParams({ params }) 44 | 45 | if (!doc) { 46 | return {} 47 | } 48 | 49 | return { 50 | title: doc.title, 51 | description: doc.description, 52 | openGraph: { 53 | title: doc.title, 54 | description: doc.description, 55 | type: "article", 56 | url: absoluteUrl(doc.slug), 57 | images: [ 58 | { 59 | url: siteConfig.ogImage, 60 | width: 1200, 61 | height: 630, 62 | alt: siteConfig.name, 63 | }, 64 | ], 65 | }, 66 | twitter: { 67 | card: "summary_large_image", 68 | title: doc.title, 69 | description: doc.description, 70 | images: [siteConfig.ogImage], 71 | creator: "@strlrd29", 72 | }, 73 | } 74 | } 75 | 76 | export async function generateStaticParams(): Promise< 77 | DocPageProps["params"][] 78 | > { 79 | return allDocs.map((doc) => ({ 80 | slug: doc.slugAsParams.split("/"), 81 | })) 82 | } 83 | 84 | export default async function DocPage(props: { 85 | params: Promise 86 | }) { 87 | const params = await props.params 88 | 89 | const doc = await getDocFromParams({ params }) 90 | 91 | if (!doc) { 92 | notFound() 93 | } 94 | 95 | const toc = await getTableOfContents(doc.body.raw) 96 | 97 | return ( 98 | <> 99 | 100 |
101 |
102 |
103 |
Docs
104 | 105 |
{doc.title}
106 |
107 |
108 | 114 | {doc.description && ( 115 |

116 | {doc.description} 117 |

118 | )} 119 |
120 | {doc.links ? ( 121 |
122 | {Object.entries(doc.links).map(([k, v]) => ( 123 | 133 | {k} 134 | 135 | 136 | ))} 137 |
138 | ) : null} 139 |
140 | 141 |
142 |
143 |
144 |
145 |
146 | {doc.toc && } 147 | 148 |
149 |
150 |
151 |
152 | 153 | ) 154 | } 155 | -------------------------------------------------------------------------------- /app/(app)/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { docsConfig } from "@/config/docs" 4 | import { DocsNav } from "@/components/docs-nav" 5 | 6 | interface DocsLayoutProps { 7 | children: React.ReactNode 8 | } 9 | 10 | export default function DocsLayout({ children }: DocsLayoutProps) { 11 | return ( 12 |
13 | 18 | {children} 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /app/(app)/docs/not-found.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import { useRouter } from "next/navigation" 5 | import { AlertTriangleIcon, ChevronLeftIcon, HouseIcon } from "lucide-react" 6 | 7 | import { Button, buttonVariants } from "@/components/ui/button" 8 | 9 | export default function NotFound() { 10 | const router = useRouter() 11 | return ( 12 |
13 |
14 |

15 | 16 |

17 |

18 | Page not found 19 |

20 |

21 | The page you are looking for doesn't exist. 22 |

23 |
24 | 28 | 29 | 30 | Take me home 31 | 32 |
33 |
34 |
35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /app/(app)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SiteFooter } from "@/components/site-footer" 2 | import { SiteHeader } from "@/components/site-header" 3 | 4 | interface AppLayoutProps { 5 | children: React.ReactNode 6 | } 7 | 8 | export default function AppLayout({ children }: AppLayoutProps) { 9 | return ( 10 |
11 | 12 |
{children}
13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/(app)/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | import Balancer from "react-wrap-balancer" 3 | 4 | import { cn } from "@/lib/utils" 5 | import { buttonVariants } from "@/components/ui/button" 6 | import { ChangingScrambleText } from "@/components/changing-scramble-text" 7 | import { GridPattern } from "@/components/grid-pattern" 8 | 9 | export default function Home() { 10 | return ( 11 |
12 |
13 |
14 | 15 | 16 | A collection of reusable react hooks that you can easily copy and 17 | paste into your apps or add directly through the shadcn CLI. 18 | 19 |
20 | 21 | Explore the docs 22 | 23 |
24 | 43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata, Viewport } from "next" 2 | import { ThemeProvider } from "next-themes" 3 | 4 | import { META_THEME_COLORS, siteConfig } from "@/config/site" 5 | import { fontMono, fontSans } from "@/lib/fonts" 6 | import { cn } from "@/lib/utils" 7 | import { TooltipProvider } from "@/components/ui/tooltip" 8 | 9 | import "@/styles/globals.css" 10 | 11 | import { Toaster } from "@/components/ui/sonner" 12 | import { ActiveThemeProvider } from "@/components/active-theme" 13 | 14 | export const metadata: Metadata = { 15 | title: { 16 | default: siteConfig.name, 17 | template: `%s - ${siteConfig.name}`, 18 | }, 19 | metadataBase: new URL(siteConfig.url), 20 | description: siteConfig.description, 21 | keywords: [ 22 | "Next.js", 23 | "React", 24 | "Tailwind CSS", 25 | "Server Components", 26 | "Radix UI", 27 | ], 28 | authors: [ 29 | { 30 | name: "strlrd-29", 31 | url: "https://ouassim.tech", 32 | }, 33 | ], 34 | creator: "strlrd-29", 35 | openGraph: { 36 | type: "website", 37 | locale: "en_US", 38 | url: siteConfig.url, 39 | title: siteConfig.name, 40 | description: siteConfig.description, 41 | siteName: siteConfig.name, 42 | images: [ 43 | { 44 | url: siteConfig.ogImage, 45 | width: 1200, 46 | height: 630, 47 | alt: siteConfig.name, 48 | }, 49 | ], 50 | }, 51 | twitter: { 52 | card: "summary_large_image", 53 | title: siteConfig.name, 54 | description: siteConfig.description, 55 | images: [siteConfig.ogImage], 56 | creator: "@strlrd29", 57 | }, 58 | icons: { 59 | icon: "/favicon.ico", 60 | shortcut: "/favicon-16x16.png", 61 | apple: "/apple-touch-icon.png", 62 | }, 63 | manifest: `${siteConfig.url}/site.webmanifest`, 64 | } 65 | 66 | export const viewport: Viewport = { 67 | themeColor: META_THEME_COLORS.light, 68 | } 69 | 70 | interface RootLayoutProps { 71 | children: React.ReactNode 72 | } 73 | 74 | export default function RootLayout({ children }: RootLayoutProps) { 75 | return ( 76 | 77 | 78 | 83 | 84 | 92 | 99 | 100 | 101 |
102 | {children} 103 |
104 |
105 |
106 | 107 |
108 | 109 | 110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import { useRouter } from "next/navigation" 5 | import { AlertTriangleIcon, ChevronLeftIcon, HouseIcon } from "lucide-react" 6 | 7 | import { Button, buttonVariants } from "@/components/ui/button" 8 | 9 | export default function NotFound() { 10 | const router = useRouter() 11 | return ( 12 |
13 |
14 |

15 | 16 |

17 |

18 | Page not found 19 |

20 |

21 | The page you are looking for doesn't exist. 22 |

23 |
24 | 28 | 29 | 30 | Take me home 31 | 32 |
33 |
34 |
35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /components/active-theme.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | createContext, 5 | ReactNode, 6 | useContext, 7 | useEffect, 8 | useState, 9 | } from "react" 10 | 11 | const COOKIE_NAME = "active_theme" 12 | const DEFAULT_THEME = "default" 13 | 14 | function setThemeCookie(theme: string) { 15 | if (typeof window === "undefined") return 16 | 17 | document.cookie = `${COOKIE_NAME}=${theme}; path=/; max-age=31536000; SameSite=Lax; ${ 18 | window.location.protocol === "https:" ? "Secure;" : "" 19 | }` 20 | } 21 | 22 | type ThemeContextType = { 23 | activeTheme: string 24 | setActiveTheme: (theme: string) => void 25 | } 26 | 27 | const ThemeContext = createContext(undefined) 28 | 29 | export function ActiveThemeProvider({ children }: { children: ReactNode }) { 30 | const [activeTheme, setActiveTheme] = useState(DEFAULT_THEME) 31 | 32 | useEffect(() => { 33 | const cookie = document.cookie 34 | .split("; ") 35 | .find((row) => row.startsWith(`${COOKIE_NAME}=`)) 36 | if (cookie) { 37 | const cookieTheme = cookie.split("=")[1] 38 | if (cookieTheme && cookieTheme !== activeTheme) { 39 | setActiveTheme(cookieTheme) 40 | } 41 | } 42 | // eslint-disable-next-line 43 | }, []) 44 | 45 | useEffect(() => { 46 | setThemeCookie(activeTheme) 47 | 48 | Array.from(document.body.classList) 49 | .filter((className) => className.startsWith("theme-")) 50 | .forEach((className) => { 51 | document.body.classList.remove(className) 52 | }) 53 | 54 | document.body.classList.add(`theme-${activeTheme}`) 55 | if (activeTheme.endsWith("-scaled")) { 56 | document.body.classList.add("theme-scaled") 57 | } 58 | }, [activeTheme]) 59 | 60 | return ( 61 | 62 |