├── .env.example
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .husky
├── .gitignore
├── install.mjs
└── pre-commit
├── .prettierignore
├── .prettierrc.json
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── components.json
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
└── placeholder.png
├── screenshots
├── screenshot-1.png
├── screenshot-2.png
├── screenshot-3.png
└── screenshot-4.png
├── src
├── app
│ ├── api
│ │ └── auth
│ │ │ └── [...nextauth]
│ │ │ └── route.ts
│ ├── dashboard
│ │ ├── (example)
│ │ │ ├── account
│ │ │ │ └── page.tsx
│ │ │ ├── categories
│ │ │ │ └── page.tsx
│ │ │ ├── posts
│ │ │ │ ├── new
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── tags
│ │ │ │ └── page.tsx
│ │ │ └── users
│ │ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── admin-panel
│ │ ├── admin-panel-layout.tsx
│ │ ├── collapse-menu-button.tsx
│ │ ├── content-layout.tsx
│ │ ├── footer.tsx
│ │ ├── menu.tsx
│ │ ├── navbar.tsx
│ │ ├── sheet-menu.tsx
│ │ ├── sidebar-toggle.tsx
│ │ ├── sidebar.tsx
│ │ └── user-nav.tsx
│ ├── admin-provider.tsx
│ ├── forms
│ │ └── login.tsx
│ ├── icons.tsx
│ ├── mode-toggle.tsx
│ ├── placeholder-content.tsx
│ ├── theme-provider.tsx
│ └── ui
│ │ ├── alert.tsx
│ │ ├── avatar.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── collapsible.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── scroll-area.tsx
│ │ ├── sheet.tsx
│ │ └── tooltip.tsx
├── hooks
│ ├── use-sidebar-toggle.ts
│ └── use-store.ts
├── lib
│ ├── auth
│ │ ├── options.ts
│ │ ├── session.ts
│ │ └── validate.tsx
│ ├── env.ts
│ ├── menu-list.ts
│ └── utils.ts
├── middleware.ts
├── services
│ └── directus.ts
└── types
│ ├── default.d.ts
│ └── next-auth.d.ts
├── tailwind.config.js
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_DIRECTUS_API=http://127.0.0.1:8055
2 | NEXTAUTH_SECRET=
3 | NEXTAUTH_URL=http://localhost:3000
4 | NEXT_PUBLIC_SITE_NAME="Acme Inc"
5 | NEXT_PUBLIC_SITE_DESCRIPTION="This library has saved me countless hours of work and helped me deliver stunning designs to my clients faster than ever before."
6 |
7 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 |
3 | /types/next-auth.d.ts
4 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "next/core-web-vitals",
4 | "eslint:recommended",
5 | "plugin:prettier/recommended"
6 | ],
7 | "plugins": ["prettier", "import"],
8 | "overrides": [
9 | {
10 | "files": ["*.ts", "*.tsx"],
11 | "rules": {
12 | "no-undef": "off",
13 | "no-unused-vars": "off"
14 | }
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/.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.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 | .env
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
37 | .eslintcache
38 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/install.mjs:
--------------------------------------------------------------------------------
1 | // Skip Husky install in production and CI
2 | if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
3 | process.exit(0)
4 | }
5 | const husky = (await import("husky")).default
6 | console.log(husky())
7 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx next lint
5 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /.next
2 | /build
3 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": false,
3 | "jsxSingleQuote": false,
4 | "semi": false,
5 | "trailingComma": "es5",
6 | "endOfLine": "auto",
7 | "tabWidth": 2,
8 | "plugins": ["prettier-plugin-tailwindcss"]
9 | }
10 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.insertFinalNewline": true,
3 | "editor.formatOnSave": true,
4 | "editor.formatOnPaste": true,
5 | "editor.fontWeight": "normal",
6 | "editor.codeActionsOnSave": {
7 | "source.fixAll.eslint": "explicit"
8 | },
9 | "[javascript]": {
10 | "editor.defaultFormatter": "esbenp.prettier-vscode"
11 | },
12 | "[typescript]": {
13 | "editor.defaultFormatter": "vscode.typescript-language-features"
14 | },
15 | "[typescriptreact]": {
16 | "editor.defaultFormatter": "esbenp.prettier-vscode"
17 | },
18 | "json.format.enable": true,
19 | "[json]": {
20 | "editor.defaultFormatter": "esbenp.prettier-vscode"
21 | },
22 | "[css]": {
23 | "editor.defaultFormatter": "esbenp.prettier-vscode"
24 | },
25 | "files.associations": {
26 | "*.css": "tailwindcss"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Derian Pinto
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 | # Simple Dashboard Template with Next.js 14, Tailwind CSS, NextAuth.js, and Directus
2 |
3 | This project is a starting point for building a modern web dashboard using a robust technology stack. It provides you with the basic structure and tools necessary to kickstart the development of your own dashboard application.
4 |
5 | ## Screenshots
6 |
7 | #### Login Page
8 |
9 | 
10 |
11 | #### Light mode
12 |
13 | 
14 |
15 | #### Dark mode
16 |
17 | 
18 |
19 | #### Sheet menu
20 |
21 |
22 |
23 | ## Key Features
24 |
25 | - A solid foundation for developing a modern web dashboard.
26 | - An attractive and customizable style thanks to Tailwind CSS.
27 | - Secure user authentication through NextAuth.js.
28 | - Dynamic content management with Directus.
29 | - Streamlines the initial setup of your project, saving you time.
30 |
31 | ## Technologies Used
32 |
33 | - [Next.js](https://nextjs.org/): React framework for web applications.
34 | - [Tailwind CSS](https://tailwindcss.com/): CSS design framework.
35 | - [Shadcn](https://ui.shadcn.com/): Beautifully designed components built with Radix UI and Tailwind CSS.
36 | - [next-auth](https://next-auth.js.org/): Authentication and authorization in Next.js.
37 | - [next-themes](https://github.com/pacocoursey/next-themes): Theme switching in Next.js.
38 | - [TypeScript](https://www.typescriptlang.org/): Programming language.
39 | - [@directus/sdk](https://docs.directus.io/guides/sdk/): Client for Directus API.
40 | - [zustand](https://github.com/pmndrs/zustand): Lightweight state management for React.
41 | - [zod](https://github.com/colinhacks/zod): TypeScript schema validation.
42 | - [Husky](https://github.com/typicode/husky): Git hooks for running commands on pre-commit and pre-push actions.
43 | - [Prettier](https://prettier.io/): Automatic code formatter.
44 | - [ESLint](https://eslint.org/): Static code analysis tool.
45 | - [shadcn/ui sidebar](https://github.com/salimi-my/shadcn-ui-sidebar): Shadcn UI Sidebar.
46 |
47 | ## Requirements
48 |
49 | Make sure you have the following installed before running the project:
50 |
51 | - [Node.js](https://nodejs.org/): version 20.9.0
52 | - [npm](https://www.npmjs.com/): JavaScript package manager
53 |
54 | ## Configuration
55 |
56 | Clone the repository:
57 |
58 | ```bash
59 | git clone https://github.com/pintoderian/next-directus-auth-ts.git
60 | cd your-project
61 | ```
62 |
63 | Install dependencies:
64 |
65 | ```bash
66 | npm run install
67 | ```
68 |
69 | Prepare husky:
70 |
71 | ```bash
72 | npm run prepare
73 | ```
74 |
75 | Configure environment variables:
76 | Create a .env.local file at the root of the project and define the necessary environment variables. Here's an example:
77 |
78 | ```bash
79 | NEXT_PUBLIC_DIRECTUS_API=
80 | NEXTAUTH_SECRET=
81 | NEXTAUTH_URL=http://localhost:3000
82 | NEXT_PUBLIC_SITE_NAME="Acme Inc"
83 | NEXT_PUBLIC_SITE_DESCRIPTION="Lorem, ipsum dolor sit amet consectetur adipisicing elit."
84 |
85 | ```
86 |
87 | Command for **NEXTAUTH_SECRET**
88 |
89 | ```bash
90 | openssl rand -base64 32
91 | ```
92 |
93 | ## Running the Project
94 |
95 | To run the project in a development environment, use the following command:
96 |
97 | ```bash
98 | npm run dev
99 | ```
100 |
101 | The project will be available at http://localhost:3000.
102 |
103 | ## Production
104 |
105 | To build and run the project in a production environment, use the following commands:
106 |
107 | ```bash
108 | npm run build
109 |
110 | npm start
111 | ```
112 |
113 |
114 |
115 | ### If you like my work, consider buying me a coffee!
116 |
117 | [Ko-fi](https://ko-fi.com/dpinto)
118 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {}
3 |
4 | module.exports = nextConfig
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-app",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "prepare": "node .husky/install.mjs"
11 | },
12 | "dependencies": {
13 | "@directus/sdk": "^17.0.1",
14 | "@hookform/resolvers": "^3.9.0",
15 | "@radix-ui/react-avatar": "^1.1.0",
16 | "@radix-ui/react-collapsible": "^1.1.0",
17 | "@radix-ui/react-dialog": "^1.1.1",
18 | "@radix-ui/react-dropdown-menu": "^2.1.1",
19 | "@radix-ui/react-icons": "^1.3.0",
20 | "@radix-ui/react-label": "^2.1.0",
21 | "@radix-ui/react-scroll-area": "^1.1.0",
22 | "@radix-ui/react-slot": "^1.1.0",
23 | "@radix-ui/react-tooltip": "^1.1.2",
24 | "@types/node": "22.5.4",
25 | "@types/react": "18.3.5",
26 | "@types/react-dom": "18.3.0",
27 | "autoprefixer": "10.4.20",
28 | "class-variance-authority": "^0.7.0",
29 | "clsx": "^2.1.1",
30 | "eslint": "8.57.0",
31 | "eslint-config-next": "14.2.11",
32 | "lucide-react": "^0.441.0",
33 | "next": "14.2.11",
34 | "next-auth": "^4.24.7",
35 | "next-themes": "^0.3.0",
36 | "postcss": "8.4.45",
37 | "react": "18.3.1",
38 | "react-dom": "18.3.1",
39 | "react-hook-form": "^7.53.0",
40 | "tailwind-merge": "^2.5.2",
41 | "tailwindcss": "3.4.11",
42 | "tailwindcss-animate": "^1.0.7",
43 | "typescript": "5.6.2",
44 | "zod": "^3.23.8",
45 | "zustand": "^4.5.5"
46 | },
47 | "devDependencies": {
48 | "eslint-config-prettier": "^9.1.0",
49 | "eslint-plugin-import": "^2.30.0",
50 | "eslint-plugin-prettier": "^5.2.1",
51 | "husky": "^9.1.6",
52 | "prettier": "^3.3.3",
53 | "prettier-plugin-tailwindcss": "^0.6.6"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pintoderian/next-directus-auth-ts/7fd49dd9990bf28c31d0cc98f52eb6080a9d855f/public/placeholder.png
--------------------------------------------------------------------------------
/screenshots/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pintoderian/next-directus-auth-ts/7fd49dd9990bf28c31d0cc98f52eb6080a9d855f/screenshots/screenshot-1.png
--------------------------------------------------------------------------------
/screenshots/screenshot-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pintoderian/next-directus-auth-ts/7fd49dd9990bf28c31d0cc98f52eb6080a9d855f/screenshots/screenshot-2.png
--------------------------------------------------------------------------------
/screenshots/screenshot-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pintoderian/next-directus-auth-ts/7fd49dd9990bf28c31d0cc98f52eb6080a9d855f/screenshots/screenshot-3.png
--------------------------------------------------------------------------------
/screenshots/screenshot-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pintoderian/next-directus-auth-ts/7fd49dd9990bf28c31d0cc98f52eb6080a9d855f/screenshots/screenshot-4.png
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth"
2 | import { options } from "@/lib/auth/options"
3 |
4 | const handler = NextAuth(options)
5 |
6 | export { handler as GET, handler as POST }
7 |
--------------------------------------------------------------------------------
/src/app/dashboard/(example)/account/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import PlaceholderContent from "@/components/placeholder-content"
4 | import { ContentLayout } from "@/components/admin-panel/content-layout"
5 | import {
6 | Breadcrumb,
7 | BreadcrumbItem,
8 | BreadcrumbLink,
9 | BreadcrumbList,
10 | BreadcrumbPage,
11 | BreadcrumbSeparator,
12 | } from "@/components/ui/breadcrumb"
13 |
14 | export default function AccountPage() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Home
22 |
23 |
24 |
25 |
26 |
27 | Dashboard
28 |
29 |
30 |
31 |
32 | Account
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/dashboard/(example)/categories/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import PlaceholderContent from "@/components/placeholder-content"
4 | import { ContentLayout } from "@/components/admin-panel/content-layout"
5 | import {
6 | Breadcrumb,
7 | BreadcrumbItem,
8 | BreadcrumbLink,
9 | BreadcrumbList,
10 | BreadcrumbPage,
11 | BreadcrumbSeparator,
12 | } from "@/components/ui/breadcrumb"
13 |
14 | export default function CategoriesPage() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Home
22 |
23 |
24 |
25 |
26 |
27 | Dashboard
28 |
29 |
30 |
31 |
32 | Categories
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/dashboard/(example)/posts/new/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import PlaceholderContent from "@/components/placeholder-content"
4 | import { ContentLayout } from "@/components/admin-panel/content-layout"
5 | import {
6 | Breadcrumb,
7 | BreadcrumbItem,
8 | BreadcrumbLink,
9 | BreadcrumbList,
10 | BreadcrumbPage,
11 | BreadcrumbSeparator,
12 | } from "@/components/ui/breadcrumb"
13 |
14 | export default function NewPostPage() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Home
22 |
23 |
24 |
25 |
26 |
27 | Dashboard
28 |
29 |
30 |
31 |
32 |
33 | Posts
34 |
35 |
36 |
37 |
38 | New
39 |
40 |
41 |
42 |
43 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/dashboard/(example)/posts/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import PlaceholderContent from "@/components/placeholder-content"
4 | import { ContentLayout } from "@/components/admin-panel/content-layout"
5 | import {
6 | Breadcrumb,
7 | BreadcrumbItem,
8 | BreadcrumbLink,
9 | BreadcrumbList,
10 | BreadcrumbPage,
11 | BreadcrumbSeparator,
12 | } from "@/components/ui/breadcrumb"
13 |
14 | export default function PostsPage() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Home
22 |
23 |
24 |
25 |
26 |
27 | Dashboard
28 |
29 |
30 |
31 |
32 | Posts
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/dashboard/(example)/tags/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import PlaceholderContent from "@/components/placeholder-content"
4 | import { ContentLayout } from "@/components/admin-panel/content-layout"
5 | import {
6 | Breadcrumb,
7 | BreadcrumbItem,
8 | BreadcrumbLink,
9 | BreadcrumbList,
10 | BreadcrumbPage,
11 | BreadcrumbSeparator,
12 | } from "@/components/ui/breadcrumb"
13 |
14 | export default function TagsPage() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Home
22 |
23 |
24 |
25 |
26 |
27 | Dashboard
28 |
29 |
30 |
31 |
32 | Tags
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/dashboard/(example)/users/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import PlaceholderContent from "@/components/placeholder-content"
4 | import { ContentLayout } from "@/components/admin-panel/content-layout"
5 | import {
6 | Breadcrumb,
7 | BreadcrumbItem,
8 | BreadcrumbLink,
9 | BreadcrumbList,
10 | BreadcrumbPage,
11 | BreadcrumbSeparator,
12 | } from "@/components/ui/breadcrumb"
13 |
14 | export default function UsersPage() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Home
22 |
23 |
24 |
25 |
26 |
27 | Dashboard
28 |
29 |
30 |
31 |
32 | Users
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/dashboard/layout.tsx:
--------------------------------------------------------------------------------
1 | import AdminPanelLayout from "@/components/admin-panel/admin-panel-layout"
2 | import AdminProvider from "@/components/admin-provider"
3 | import { options } from "@/lib/auth/options"
4 | import { getServerSession } from "next-auth"
5 |
6 | export default async function DashboardLayout({
7 | children,
8 | }: {
9 | children: React.ReactNode
10 | }) {
11 | const session = await getServerSession(options)
12 |
13 | return (
14 |
15 | {children}
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import { ContentLayout } from "@/components/admin-panel/content-layout"
2 | import {
3 | Card,
4 | CardContent,
5 | CardDescription,
6 | CardHeader,
7 | CardTitle,
8 | } from "@/components/ui/card"
9 | import type { Metadata } from "next"
10 |
11 | export const metadata: Metadata = {
12 | title: "Dashboard",
13 | description: "Generated by create next app",
14 | }
15 |
16 | export default async function Dashboard() {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 | Total Revenue
25 |
26 |
38 |
39 |
40 | $45,231.89
41 |
42 | +20.1% from last month
43 |
44 |
45 |
46 |
47 |
48 |
49 | Subscriptions
50 |
51 |
65 |
66 |
67 | +2350
68 |
69 | +180.1% from last month
70 |
71 |
72 |
73 |
74 |
75 | Sales
76 |
89 |
90 |
91 | +12,234
92 |
93 | +19% from last month
94 |
95 |
96 |
97 |
98 |
99 | Active Now
100 |
112 |
113 |
114 | +573
115 |
116 | +201 since last hour
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 | Overview
125 |
126 | Hello World!
127 |
128 |
129 |
130 | Custom Text
131 |
132 |
133 |
134 | Lorem, ipsum dolor sit amet consectetur adipisicing elit.
135 | Molestias perspiciatis reprehenderit ipsum totam, facilis fugit
136 | explicabo quisquam saepe delectus sapiente aliquam recusandae,
137 | in similique sit? Nesciunt praesentium totam quae eius.
138 |
139 |
140 |
141 |
142 |
143 |
144 | )
145 | }
146 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 224 71.4% 4.1%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 224 71.4% 4.1%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 224 71.4% 4.1%;
13 | --primary: 262.1 83.3% 57.8%;
14 | --primary-foreground: 210 20% 98%;
15 | --secondary: 220 14.3% 95.9%;
16 | --secondary-foreground: 220.9 39.3% 11%;
17 | --muted: 220 14.3% 95.9%;
18 | --muted-foreground: 220 8.9% 46.1%;
19 | --accent: 220 14.3% 95.9%;
20 | --accent-foreground: 220.9 39.3% 11%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 210 20% 98%;
23 | --border: 220 13% 91%;
24 | --input: 220 13% 91%;
25 | --ring: 262.1 83.3% 57.8%;
26 | --radius: 0.5rem;
27 | --chart-1: 12 76% 61%;
28 | --chart-2: 173 58% 39%;
29 | --chart-3: 197 37% 24%;
30 | --chart-4: 43 74% 66%;
31 | --chart-5: 27 87% 67%;
32 | /* Variables adicionales */
33 | --sidebar: #693dc3;
34 | --background-white-30: rgb(255 255 255 / 30%);
35 | --card-foreground-30: rgba(255, 255, 255, 0.3);
36 | --button-foreground: rgb(255 255 255 / 30%);
37 | --button-50: rgb(255 255 255 / 50%);
38 | --outline: rgba(255, 255, 255, 0.05);
39 | --outline-50: rgba(255, 255, 255, 0.15);
40 | }
41 |
42 | .dark {
43 | --background: 240 10% 3.9%;
44 | --foreground: 0 0% 98%;
45 | --card: 240 10% 3.9%;
46 | --card-foreground: 0 0% 98%;
47 | --popover: 240 10% 3.9%;
48 | --popover-foreground: 0 0% 98%;
49 | --primary: 0 0% 98%;
50 | --primary-foreground: 240 5.9% 10%;
51 | --secondary: 240 3.7% 15.9%;
52 | --secondary-foreground: 0 0% 98%;
53 | --muted: 240 3.7% 15.9%;
54 | --muted-foreground: 240 5% 64.9%;
55 | --accent: 240 3.7% 15.9%;
56 | --accent-foreground: 0 0% 98%;
57 | --destructive: 0 62.8% 30.6%;
58 | --destructive-foreground: 0 0% 98%;
59 | --border: 240 3.7% 15.9%;
60 | --input: 240 3.7% 15.9%;
61 | --ring: 240 4.9% 83.9%;
62 | --chart-1: 220 70% 50%;
63 | --chart-2: 160 60% 45%;
64 | --chart-3: 30 80% 55%;
65 | --chart-4: 280 65% 60%;
66 | --chart-5: 340 75% 55%;
67 | /* Variables adicionales */
68 | --sidebar: #693dc3;
69 | --background-white-30: rgb(255 255 255 / 30%);
70 | --card-foreground-30: rgba(255, 255, 255, 0.3);
71 | --button-foreground: rgb(255 255 255 / 30%);
72 | --button-50: rgb(255 255 255 / 50%);
73 | --outline: rgba(255, 255, 255, 0.05);
74 | --outline-50: rgba(255, 255, 255, 0.15);
75 | }
76 | }
77 |
78 | @layer base {
79 | * {
80 | @apply border-border;
81 | }
82 | body {
83 | @apply bg-background text-foreground;
84 | }
85 | }
86 |
87 | @layer utilities {
88 | /* Chrome, Safari and Opera */
89 | .no-scrollbar::-webkit-scrollbar {
90 | display: none;
91 | }
92 |
93 | .no-scrollbar {
94 | -ms-overflow-style: none; /* IE and Edge */
95 | scrollbar-width: none; /* Firefox */
96 | }
97 | }
98 |
99 | .menu .active {
100 | @apply rounded bg-primary text-white;
101 | }
102 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css"
2 | import { Inter } from "next/font/google"
3 | const inter = Inter({ subsets: ["latin"] })
4 |
5 | export default function RootLayout({
6 | children,
7 | }: {
8 | children: React.ReactNode
9 | }) {
10 | return (
11 |
12 | {children}
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next"
2 | import env from "@/lib/env"
3 | import LoginForm from "@/components/forms/login"
4 | import Link from "next/link"
5 |
6 | export const metadata: Metadata = {
7 | title: "Create Next App",
8 | description: "Generated by create next app",
9 | }
10 |
11 | const { siteName, siteDescription } = env
12 |
13 | export default async function Home() {
14 | return (
15 | <>
16 |
17 |
18 |
19 |
20 |
32 | {siteName}
33 |
34 |
35 |
36 | “{siteDescription}”
37 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
Welcome
49 |
50 | Enter your credentials
51 |
52 |
53 |
54 |
55 |
56 |
57 | >
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/admin-panel/admin-panel-layout.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { cn } from "@/lib/utils"
4 | import { useStore } from "@/hooks/use-store"
5 | import { Footer } from "@/components/admin-panel/footer"
6 | import { Sidebar } from "@/components/admin-panel/sidebar"
7 | import { useSidebarToggle } from "@/hooks/use-sidebar-toggle"
8 |
9 | export default function AdminPanelLayout({
10 | children,
11 | }: {
12 | children: React.ReactNode
13 | }) {
14 | const sidebar = useStore(useSidebarToggle, (state) => state)
15 |
16 | if (!sidebar) return null
17 |
18 | return (
19 | <>
20 |
21 |
27 | {children}
28 |
29 |
37 | >
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/admin-panel/collapse-menu-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { useState } from "react"
5 | import { ChevronDown, Dot, LucideIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { Button } from "@/components/ui/button"
9 | import { DropdownMenuArrow } from "@radix-ui/react-dropdown-menu"
10 | import {
11 | Collapsible,
12 | CollapsibleContent,
13 | CollapsibleTrigger,
14 | } from "@/components/ui/collapsible"
15 | import {
16 | Tooltip,
17 | TooltipTrigger,
18 | TooltipContent,
19 | TooltipProvider,
20 | } from "@/components/ui/tooltip"
21 | import {
22 | DropdownMenu,
23 | DropdownMenuItem,
24 | DropdownMenuLabel,
25 | DropdownMenuTrigger,
26 | DropdownMenuContent,
27 | DropdownMenuSeparator,
28 | } from "@/components/ui/dropdown-menu"
29 |
30 | type Submenu = {
31 | href: string
32 | label: string
33 | active: boolean
34 | }
35 |
36 | interface CollapseMenuButtonProps {
37 | icon: LucideIcon
38 | label: string
39 | active: boolean
40 | submenus: Submenu[]
41 | isOpen: boolean | undefined
42 | }
43 |
44 | export function CollapseMenuButton({
45 | icon: Icon,
46 | label,
47 | active,
48 | submenus,
49 | isOpen,
50 | }: CollapseMenuButtonProps) {
51 | const isSubmenuActive = submenus.some((submenu) => submenu.active)
52 | const [isCollapsed, setIsCollapsed] = useState(isSubmenuActive)
53 |
54 | return isOpen ? (
55 |
60 |
64 |
99 |
100 |
101 | {submenus.map(({ href, label, active }, index) => (
102 |
124 | ))}
125 |
126 |
127 | ) : (
128 |
129 |
130 |
131 |
132 |
133 |
153 |
154 |
155 |
156 | {label}
157 |
158 |
159 |
160 |
161 |
162 | {label}
163 |
164 |
165 | {submenus.map(({ href, label }, index) => (
166 |
167 |
168 | {label}
169 |
170 |
171 | ))}
172 |
173 |
174 |
175 | )
176 | }
177 |
--------------------------------------------------------------------------------
/src/components/admin-panel/content-layout.tsx:
--------------------------------------------------------------------------------
1 | import { Navbar } from "@/components/admin-panel/navbar"
2 |
3 | interface ContentLayoutProps {
4 | title: string
5 | children: React.ReactNode
6 | }
7 |
8 | export function ContentLayout({ title, children }: ContentLayoutProps) {
9 | return (
10 |
11 |
12 |
{children}
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/admin-panel/footer.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | export function Footer() {
4 | return (
5 |
6 |
7 |
8 | Built on top of{" "}
9 |
15 | shadcn/ui
16 |
17 | . The source code is available on{" "}
18 |
24 | GitHub
25 |
26 | .
27 |
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/admin-panel/menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { Ellipsis, LogOut } from "lucide-react"
5 | import { usePathname } from "next/navigation"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { getMenuList } from "@/lib/menu-list"
9 | import { Button } from "@/components/ui/button"
10 | import { ScrollArea } from "@/components/ui/scroll-area"
11 | import { CollapseMenuButton } from "@/components/admin-panel/collapse-menu-button"
12 | import {
13 | Tooltip,
14 | TooltipTrigger,
15 | TooltipContent,
16 | TooltipProvider,
17 | } from "@/components/ui/tooltip"
18 | import { signOut } from "next-auth/react"
19 |
20 | interface MenuProps {
21 | isOpen: boolean | undefined
22 | }
23 |
24 | export function Menu({ isOpen }: MenuProps) {
25 | const pathname = usePathname()
26 | const menuList = getMenuList(pathname)
27 |
28 | return (
29 |
30 |
137 |
138 | )
139 | }
140 |
--------------------------------------------------------------------------------
/src/components/admin-panel/navbar.tsx:
--------------------------------------------------------------------------------
1 | import { ModeToggle } from "@/components/mode-toggle"
2 | import { UserNav } from "@/components/admin-panel/user-nav"
3 | import { SheetMenu } from "@/components/admin-panel/sheet-menu"
4 |
5 | interface NavbarProps {
6 | title: string
7 | }
8 |
9 | export function Navbar({ title }: NavbarProps) {
10 | return (
11 |
12 |
13 |
14 |
15 |
{title}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/admin-panel/sheet-menu.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 | import { MenuIcon, PanelsTopLeft } from "lucide-react"
3 |
4 | import { Button } from "@/components/ui/button"
5 | import { Menu } from "@/components/admin-panel/menu"
6 | import {
7 | Sheet,
8 | SheetHeader,
9 | SheetContent,
10 | SheetTrigger,
11 | SheetTitle,
12 | } from "@/components/ui/sheet"
13 |
14 | export function SheetMenu() {
15 | return (
16 |
17 |
18 |
21 |
22 |
23 |
24 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/admin-panel/sidebar-toggle.tsx:
--------------------------------------------------------------------------------
1 | import { ChevronLeft } from "lucide-react"
2 |
3 | import { cn } from "@/lib/utils"
4 | import { Button } from "@/components/ui/button"
5 |
6 | interface SidebarToggleProps {
7 | isOpen: boolean | undefined
8 | setIsOpen?: () => void
9 | }
10 |
11 | export function SidebarToggle({ isOpen, setIsOpen }: SidebarToggleProps) {
12 | return (
13 |
14 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/admin-panel/sidebar.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 | import { PanelsTopLeft } from "lucide-react"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { useStore } from "@/hooks/use-store"
6 | import { Button } from "@/components/ui/button"
7 | import { Menu } from "@/components/admin-panel/menu"
8 | import { useSidebarToggle } from "@/hooks/use-sidebar-toggle"
9 | import { SidebarToggle } from "@/components/admin-panel/sidebar-toggle"
10 |
11 | export function Sidebar() {
12 | const sidebar = useStore(useSidebarToggle, (state) => state)
13 |
14 | if (!sidebar) return null
15 |
16 | return (
17 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/admin-panel/user-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { LayoutGrid, LogOut, User } from "lucide-react"
5 |
6 | import { Button } from "@/components/ui/button"
7 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
8 | import {
9 | Tooltip,
10 | TooltipContent,
11 | TooltipTrigger,
12 | TooltipProvider,
13 | } from "@/components/ui/tooltip"
14 | import {
15 | DropdownMenu,
16 | DropdownMenuContent,
17 | DropdownMenuGroup,
18 | DropdownMenuItem,
19 | DropdownMenuLabel,
20 | DropdownMenuSeparator,
21 | DropdownMenuTrigger,
22 | } from "@/components/ui/dropdown-menu"
23 | import { signOut, useSession } from "next-auth/react"
24 |
25 | export function UserNav() {
26 | const { data: session } = useSession()
27 | const avatarLetters = session?.user?.name
28 | ?.split(" ")
29 | ?.map((palabra) => palabra.charAt(0).toUpperCase())
30 | ?.join("")
31 |
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
49 |
50 |
51 | Profile
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | {session?.user?.name}
60 |
61 |
62 | {session?.user?.email}
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | Dashboard
72 |
73 |
74 |
75 |
76 |
77 | Account
78 |
79 |
80 |
81 |
82 | signOut()}
85 | >
86 |
87 | Sign out
88 |
89 |
90 |
91 | )
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/admin-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import Validate from "@/lib/auth/validate"
3 | import { SessionProvider } from "next-auth/react"
4 | import { ThemeProvider } from "./theme-provider"
5 |
6 | export default function AdminProvider({
7 | session,
8 | children,
9 | }: {
10 | session: any
11 | children: React.ReactNode
12 | }) {
13 | return (
14 |
15 |
16 |
22 | {children}
23 |
24 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/forms/login.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { zodResolver } from "@hookform/resolvers/zod"
4 | import * as z from "zod"
5 |
6 | import { Button } from "@/components/ui/button"
7 | import {
8 | Form,
9 | FormControl,
10 | FormDescription,
11 | FormField,
12 | FormItem,
13 | FormLabel,
14 | FormMessage,
15 | } from "@/components/ui/form"
16 | import { Input } from "@/components/ui/input"
17 | import { useForm } from "react-hook-form"
18 | import { useState } from "react"
19 | import { signIn } from "next-auth/react"
20 | import { AlertCircle, Spinner } from "../icons"
21 | import { useRouter } from "next/navigation"
22 | import { Alert, AlertDescription } from "../ui/alert"
23 |
24 | const formSchema = z.object({
25 | email: z.string().email({
26 | message: "Email is required.",
27 | }),
28 | password: z.string().nonempty({ message: "Password is required" }),
29 | })
30 |
31 | export default function LoginForm() {
32 | const [isLoading, setIsLoading] = useState(false)
33 | const [error, setError] = useState("")
34 |
35 | const router = useRouter()
36 |
37 | const form = useForm>({
38 | resolver: zodResolver(formSchema),
39 | defaultValues: {
40 | email: "",
41 | password: "",
42 | },
43 | })
44 |
45 | const onSubmit = async (
46 | values: z.infer
47 | ): Promise => {
48 | setIsLoading(true)
49 | const res = await signIn("credentials", {
50 | email: values.email,
51 | password: values.password,
52 | callbackUrl: `/`,
53 | redirect: false,
54 | })
55 | if (res?.error) {
56 | setError(res?.error)
57 | setIsLoading(false)
58 | } else {
59 | router.push("/dashboard")
60 | }
61 | }
62 |
63 | return (
64 |
125 |
126 | )
127 | }
128 |
--------------------------------------------------------------------------------
/src/components/icons.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from "@/types/default"
2 | import { ReactElement } from "react"
3 |
4 | export const Spinner = (): ReactElement => {
5 | return (
6 |
20 | )
21 | }
22 |
23 | export const AlertCircle = (props: Icon): ReactElement => {
24 | return (
25 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { useTheme } from "next-themes"
5 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons"
6 |
7 | import { Button } from "@/components/ui/button"
8 | import {
9 | Tooltip,
10 | TooltipContent,
11 | TooltipTrigger,
12 | TooltipProvider,
13 | } from "@/components/ui/tooltip"
14 |
15 | export function ModeToggle() {
16 | const { setTheme, theme } = useTheme()
17 |
18 | return (
19 |
20 |
21 |
22 |
32 |
33 | Switch Theme
34 |
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/placeholder-content.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 | import Image from "next/image"
3 |
4 | import { Card, CardContent } from "@/components/ui/card"
5 |
6 | export default function PlaceholderContent() {
7 | return (
8 |
9 |
10 |
11 |
12 |
19 |
20 |
26 | Designed by Freepik
27 |
28 |
29 |
30 |
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { ThemeProvider as NextThemesProvider } from "next-themes"
4 | import { type ThemeProviderProps } from "next-themes/dist/types"
5 |
6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
7 | return {children}
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border p-3 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[0px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-3.5 [&>svg]:text-foreground",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "bg-destructive/75 text-white dark:border-destructive [&>svg]:text-white",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/src/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
3 | import { Slot } from "@radix-ui/react-slot"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<"nav"> & {
10 | separator?: React.ReactNode
11 | }
12 | >(({ ...props }, ref) => )
13 | Breadcrumb.displayName = "Breadcrumb"
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<"ol">
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | BreadcrumbList.displayName = "BreadcrumbList"
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<"li">
33 | >(({ className, ...props }, ref) => (
34 |
39 | ))
40 | BreadcrumbItem.displayName = "BreadcrumbItem"
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<"a"> & {
45 | asChild?: boolean
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : "a"
49 |
50 | return (
51 |
56 | )
57 | })
58 | BreadcrumbLink.displayName = "BreadcrumbLink"
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<"span">
63 | >(({ className, ...props }, ref) => (
64 |
72 | ))
73 | BreadcrumbPage.displayName = "BreadcrumbPage"
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<"li">) => (
80 | svg]:size-3.5", className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | )
89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"span">) => (
95 |
101 |
102 | More
103 |
104 | )
105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | }
116 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ))
54 | CardDescription.displayName = "CardDescription"
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ))
62 | CardContent.displayName = "CardContent"
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ))
74 | CardFooter.displayName = "CardFooter"
75 |
76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
77 |
--------------------------------------------------------------------------------
/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | const Collapsible = CollapsiblePrimitive.Root
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
12 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import {
6 | CheckIcon,
7 | ChevronRightIcon,
8 | DotFilledIcon,
9 | } from "@radix-ui/react-icons"
10 |
11 | import { cn } from "@/lib/utils"
12 |
13 | const DropdownMenu = DropdownMenuPrimitive.Root
14 |
15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
16 |
17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
18 |
19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
20 |
21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
22 |
23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
24 |
25 | const DropdownMenuSubTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef & {
28 | inset?: boolean
29 | }
30 | >(({ className, inset, children, ...props }, ref) => (
31 |
40 | {children}
41 |
42 |
43 | ))
44 | DropdownMenuSubTrigger.displayName =
45 | DropdownMenuPrimitive.SubTrigger.displayName
46 |
47 | const DropdownMenuSubContent = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, ...props }, ref) => (
51 |
59 | ))
60 | DropdownMenuSubContent.displayName =
61 | DropdownMenuPrimitive.SubContent.displayName
62 |
63 | const DropdownMenuContent = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, sideOffset = 4, ...props }, ref) => (
67 |
68 |
78 |
79 | ))
80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
81 |
82 | const DropdownMenuItem = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef & {
85 | inset?: boolean
86 | }
87 | >(({ className, inset, ...props }, ref) => (
88 |
97 | ))
98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
99 |
100 | const DropdownMenuCheckboxItem = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, children, checked, ...props }, ref) => (
104 |
113 |
114 |
115 |
116 |
117 |
118 | {children}
119 |
120 | ))
121 | DropdownMenuCheckboxItem.displayName =
122 | DropdownMenuPrimitive.CheckboxItem.displayName
123 |
124 | const DropdownMenuRadioItem = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, children, ...props }, ref) => (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | ))
144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
145 |
146 | const DropdownMenuLabel = React.forwardRef<
147 | React.ElementRef,
148 | React.ComponentPropsWithoutRef & {
149 | inset?: boolean
150 | }
151 | >(({ className, inset, ...props }, ref) => (
152 |
161 | ))
162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
163 |
164 | const DropdownMenuSeparator = React.forwardRef<
165 | React.ElementRef,
166 | React.ComponentPropsWithoutRef
167 | >(({ className, ...props }, ref) => (
168 |
173 | ))
174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
175 |
176 | const DropdownMenuShortcut = ({
177 | className,
178 | ...props
179 | }: React.HTMLAttributes) => {
180 | return (
181 |
185 | )
186 | }
187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
188 |
189 | export {
190 | DropdownMenu,
191 | DropdownMenuTrigger,
192 | DropdownMenuContent,
193 | DropdownMenuItem,
194 | DropdownMenuCheckboxItem,
195 | DropdownMenuRadioItem,
196 | DropdownMenuLabel,
197 | DropdownMenuSeparator,
198 | DropdownMenuShortcut,
199 | DropdownMenuGroup,
200 | DropdownMenuPortal,
201 | DropdownMenuSub,
202 | DropdownMenuSubContent,
203 | DropdownMenuSubTrigger,
204 | DropdownMenuRadioGroup,
205 | }
206 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath,
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath,
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 | import { cva, type VariantProps } from "class-variance-authority"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/src/hooks/use-sidebar-toggle.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand"
2 | import { persist, createJSONStorage } from "zustand/middleware"
3 |
4 | interface useSidebarToggleStore {
5 | isOpen: boolean
6 | setIsOpen: () => void
7 | }
8 |
9 | export const useSidebarToggle = create(
10 | persist(
11 | (set, get) => ({
12 | isOpen: true,
13 | setIsOpen: () => {
14 | set({ isOpen: !get().isOpen })
15 | },
16 | }),
17 | {
18 | name: "sidebarOpen",
19 | storage: createJSONStorage(() => localStorage),
20 | }
21 | )
22 | )
23 |
--------------------------------------------------------------------------------
/src/hooks/use-store.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react"
2 |
3 | export const useStore = (
4 | store: (callback: (state: T) => unknown) => unknown,
5 | callback: (state: T) => F
6 | ) => {
7 | const result = store(callback) as F
8 | const [data, setData] = useState()
9 |
10 | useEffect(() => {
11 | setData(result)
12 | }, [result])
13 |
14 | return data
15 | }
16 |
--------------------------------------------------------------------------------
/src/lib/auth/options.ts:
--------------------------------------------------------------------------------
1 | import { NextAuthOptions, Awaitable, User, Session } from "next-auth"
2 | import CredentialsProvider from "next-auth/providers/credentials"
3 | import { handleError } from "../utils"
4 | import { directus, login } from "@/services/directus"
5 | import { readMe, refresh } from "@directus/sdk"
6 | import { JWT } from "next-auth/jwt"
7 | import { AuthRefresh, UserSession, UserParams } from "@/types/next-auth"
8 |
9 | const userParams = (user: UserSession): UserParams => {
10 | return {
11 | id: user.id,
12 | email: user.email,
13 | first_name: user.first_name,
14 | last_name: user.last_name,
15 | name: `${user.first_name} ${user.last_name}`,
16 | }
17 | }
18 |
19 | export const options: NextAuthOptions = {
20 | providers: [
21 | CredentialsProvider({
22 | name: "credentials",
23 | credentials: {
24 | email: {
25 | label: "Email",
26 | type: "text",
27 | placeholder: "Enter your email",
28 | },
29 | password: {
30 | label: "Password",
31 | type: "password",
32 | placeholder: "Enter your password",
33 | },
34 | },
35 | authorize: async function (credentials) {
36 | try {
37 | const { email, password } = credentials as {
38 | email: string
39 | password: string
40 | }
41 | const auth = await login({ email, password })
42 | const apiAuth = directus(auth.access_token ?? "")
43 | const loggedInUser = await apiAuth.request(
44 | readMe({
45 | fields: ["id", "email", "first_name", "last_name"],
46 | })
47 | )
48 | const user: Awaitable = {
49 | id: loggedInUser.id,
50 | first_name: loggedInUser.first_name ?? "",
51 | last_name: loggedInUser.last_name ?? "",
52 | email: loggedInUser.email ?? "",
53 | access_token: auth.access_token ?? "",
54 | expires: Math.floor(Date.now() + (auth.expires ?? 0)),
55 | refresh_token: auth.refresh_token ?? "",
56 | }
57 | return user
58 | } catch (error: any) {
59 | handleError(error)
60 | return null
61 | }
62 | },
63 | }),
64 | ],
65 | session: {
66 | strategy: "jwt",
67 | },
68 | secret: process.env.NEXTAUTH_SECRET,
69 | callbacks: {
70 | async jwt({ token, account, user, trigger, session }): Promise {
71 | if (trigger === "update" && !session?.tokenIsRefreshed) {
72 | token.access_token = session.access_token
73 | token.refresh_token = session.refresh_token
74 | token.expires_at = session.expires_at
75 | token.tokenIsRefreshed = false
76 | }
77 |
78 | if (account) {
79 | return {
80 | access_token: user.access_token,
81 | expires_at: user.expires,
82 | refresh_token: user.refresh_token,
83 | user: userParams(user),
84 | error: null,
85 | }
86 | } else if (Date.now() < (token.expires_at ?? 0)) {
87 | return { ...token, error: null }
88 | } else {
89 | try {
90 | const api = directus()
91 | const result: AuthRefresh = await api.request(
92 | refresh("json", user?.refresh_token ?? token?.refresh_token ?? "")
93 | )
94 | const resultToken = {
95 | ...token,
96 | access_token: result.access_token ?? "",
97 | expires_at: Math.floor(Date.now() + (result.expires ?? 0)),
98 | refresh_token: result.refresh_token ?? "",
99 | error: null,
100 | tokenIsRefreshed: true,
101 | }
102 | return resultToken
103 | } catch (error) {
104 | return { ...token, error: "RefreshAccessTokenError" as const }
105 | }
106 | }
107 | },
108 | async session({ session, token }): Promise {
109 | if (token.error) {
110 | session.error = token.error
111 | session.expires = new Date(
112 | new Date().setDate(new Date().getDate() - 1)
113 | ).toISOString()
114 | } else {
115 | const { id, name, email } = token.user as UserParams
116 | session.user = { id, name, email }
117 | session.access_token = token.access_token
118 | session.tokenIsRefreshed = token?.tokenIsRefreshed ?? false
119 | session.expires_at = token.expires_at
120 | session.refresh_token = token.refresh_token
121 | }
122 | return session
123 | },
124 | },
125 | pages: {
126 | signIn: "/login",
127 | error: "/login",
128 | },
129 | }
130 |
--------------------------------------------------------------------------------
/src/lib/auth/session.ts:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth/next"
2 |
3 | import { options } from "./options"
4 |
5 | export async function getUser() {
6 | const session = await getServerSession(options)
7 | return session?.user
8 | }
9 |
--------------------------------------------------------------------------------
/src/lib/auth/validate.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { signOut, useSession } from "next-auth/react"
4 | import { useEffect } from "react"
5 |
6 | export default function Validate({ children }: { children: React.ReactNode }) {
7 | const { data: session, update } = useSession()
8 | useEffect(() => {
9 | if (session?.tokenIsRefreshed) {
10 | update({
11 | access_token: session.access_token,
12 | expires_at: session.expires_at,
13 | refresh_token: session.refresh_token,
14 | tokenIsRefreshed: false,
15 | })
16 | }
17 | if (session?.error && session.error === "RefreshAccessTokenError") {
18 | signOut()
19 | }
20 | }, [session, update])
21 |
22 | return children
23 | }
24 |
--------------------------------------------------------------------------------
/src/lib/env.ts:
--------------------------------------------------------------------------------
1 | const env = {
2 | siteName: process.env.NEXT_PUBLIC_SITE_NAME ?? "",
3 | siteDescription: process.env.NEXT_PUBLIC_SITE_DESCRIPTION ?? "",
4 | }
5 |
6 | export default env
7 |
--------------------------------------------------------------------------------
/src/lib/menu-list.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Tag,
3 | Users,
4 | Settings,
5 | Bookmark,
6 | SquarePen,
7 | LayoutGrid,
8 | LucideIcon,
9 | } from "lucide-react"
10 |
11 | type Submenu = {
12 | href: string
13 | label: string
14 | active: boolean
15 | }
16 |
17 | type Menu = {
18 | href: string
19 | label: string
20 | active: boolean
21 | icon: LucideIcon
22 | submenus: Submenu[]
23 | }
24 |
25 | type Group = {
26 | groupLabel: string
27 | menus: Menu[]
28 | }
29 |
30 | export function getMenuList(pathname: string): Group[] {
31 | return [
32 | {
33 | groupLabel: "",
34 | menus: [
35 | {
36 | href: "/dashboard",
37 | label: "Dashboard",
38 | active: pathname.includes("/dashboard"),
39 | icon: LayoutGrid,
40 | submenus: [],
41 | },
42 | ],
43 | },
44 | {
45 | groupLabel: "Contents",
46 | menus: [
47 | {
48 | href: "",
49 | label: "Posts",
50 | active: pathname.includes("/dashboard/posts"),
51 | icon: SquarePen,
52 | submenus: [
53 | {
54 | href: "/dashboard/posts",
55 | label: "All Posts",
56 | active: pathname === "/posts",
57 | },
58 | {
59 | href: "/dashboard/posts/new",
60 | label: "New Post",
61 | active: pathname === "/posts/new",
62 | },
63 | ],
64 | },
65 | {
66 | href: "/dashboard/categories",
67 | label: "Categories",
68 | active: pathname.includes("/dashboard/categories"),
69 | icon: Bookmark,
70 | submenus: [],
71 | },
72 | {
73 | href: "/dashboard/tags",
74 | label: "Tags",
75 | active: pathname.includes("/dashboard/tags"),
76 | icon: Tag,
77 | submenus: [],
78 | },
79 | ],
80 | },
81 | {
82 | groupLabel: "Settings",
83 | menus: [
84 | {
85 | href: "/dashboard/users",
86 | label: "Users",
87 | active: pathname.includes("/dashboard/users"),
88 | icon: Users,
89 | submenus: [],
90 | },
91 | {
92 | href: "/dashboard/account",
93 | label: "Account",
94 | active: pathname.includes("/dashboard/account"),
95 | icon: Settings,
96 | submenus: [],
97 | },
98 | ],
99 | },
100 | ]
101 | }
102 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
8 | export const handleError = (error: string) => {
9 | throw new Error(error)
10 | }
11 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { getToken } from "next-auth/jwt"
2 | import { withAuth } from "next-auth/middleware"
3 | import { NextResponse } from "next/server"
4 |
5 | export default withAuth(
6 | async function middleware(req) {
7 | const token = await getToken({ req })
8 | const isAuth = !!token
9 | const isAuthPage = req.nextUrl.pathname === "/"
10 | if (isAuthPage) {
11 | if (isAuth) {
12 | return NextResponse.redirect(new URL("/dashboard", req.url))
13 | }
14 | return null
15 | }
16 |
17 | if (!isAuth) {
18 | let from = req.nextUrl.pathname
19 | if (req.nextUrl.search) {
20 | from += req.nextUrl.search
21 | }
22 |
23 | return NextResponse.redirect(
24 | new URL(`/?from=${encodeURIComponent(from)}`, req.url)
25 | )
26 | }
27 | },
28 | {
29 | callbacks: {
30 | async authorized() {
31 | return true
32 | },
33 | },
34 | }
35 | )
36 |
--------------------------------------------------------------------------------
/src/services/directus.ts:
--------------------------------------------------------------------------------
1 | import {
2 | authentication,
3 | createDirectus,
4 | rest,
5 | staticToken,
6 | } from "@directus/sdk"
7 |
8 | export const directus = (token: string = "") => {
9 | if (token) {
10 | return createDirectus(process.env.NEXT_PUBLIC_DIRECTUS_API ?? "")
11 | .with(staticToken(token))
12 | .with(rest())
13 | }
14 | return createDirectus(process.env.NEXT_PUBLIC_DIRECTUS_API ?? "")
15 | .with(
16 | authentication("cookie", { credentials: "include", autoRefresh: true })
17 | )
18 | .with(rest())
19 | }
20 |
21 | export const login = async ({
22 | email,
23 | password,
24 | }: {
25 | email: string
26 | password: string
27 | }) => {
28 | const res = await fetch(
29 | `${process.env.NEXT_PUBLIC_DIRECTUS_API}/auth/login`,
30 | {
31 | method: "POST",
32 | body: JSON.stringify({ email, password }),
33 | headers: { "Content-Type": "application/json" },
34 | }
35 | )
36 | const user = await res.json()
37 | if (!res.ok && user) {
38 | throw new Error("Email address or password is invalid")
39 | }
40 | if (res.ok && user) {
41 | return user?.data
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/types/default.d.ts:
--------------------------------------------------------------------------------
1 | export type Icon = {
2 | className: string
3 | }
4 |
--------------------------------------------------------------------------------
/src/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import NextAuth, { DefaultSession } from "next-auth"
2 | import { JWT } from "next-auth/jwt"
3 |
4 | declare module "next-auth" {
5 | interface User {
6 | id: string
7 | email: string
8 | first_name: string
9 | last_name: string
10 | access_token: string
11 | expires: number
12 | refresh_token: string
13 | }
14 |
15 | interface Session {
16 | user: DefaultSession["user"] & {
17 | id?: string
18 | }
19 | access_token?: string
20 | expires_at?: number
21 | refresh_token?: string
22 | tokenIsRefreshed: boolean | null
23 | error?: string | null
24 | }
25 | }
26 |
27 | declare module "next-auth/jwt" {
28 | interface JWT {
29 | access_token?: string
30 | expires_at?: number
31 | refresh_token?: string
32 | error?: string | null
33 | tokenIsRefreshed?: boolean | null
34 | }
35 | }
36 |
37 | export type AuthRefresh = {
38 | access_token?: string | null
39 | expires?: number | null
40 | refresh_token?: string | null
41 | }
42 |
43 | export type UserSession = {
44 | id: string
45 | first_name: string
46 | last_name: string
47 | email: string
48 | access_token?: string
49 | expires?: number
50 | refresh_token?: string
51 | }
52 |
53 | export type UserParams = {
54 | id?: string
55 | name?: string
56 | first_name?: string
57 | last_name?: string
58 | email?: string
59 | }
60 |
61 | export type UserAuthenticated = {
62 | id?: string
63 | name?: string
64 | email?: string
65 | }
66 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | "./pages/**/*.{ts,tsx}",
6 | "./components/**/*.{ts,tsx}",
7 | "./app/**/*.{ts,tsx}",
8 | "./src/**/*.{ts,tsx}",
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | sidebar: "var(--sidebar)",
21 | border: "hsl(var(--border))",
22 | input: "hsl(var(--input))",
23 | ring: "hsl(var(--ring))",
24 | background: "hsl(var(--background))",
25 | foreground: "hsl(var(--foreground))",
26 | primary: {
27 | DEFAULT: "hsl(var(--primary))",
28 | foreground: "hsl(var(--primary-foreground))",
29 | },
30 | secondary: {
31 | DEFAULT: "hsl(var(--secondary))",
32 | foreground: "hsl(var(--secondary-foreground))",
33 | },
34 | destructive: {
35 | DEFAULT: "hsl(var(--destructive))",
36 | foreground: "hsl(var(--destructive-foreground))",
37 | },
38 | muted: {
39 | DEFAULT: "hsl(var(--muted))",
40 | foreground: "hsl(var(--muted-foreground))",
41 | },
42 | accent: {
43 | DEFAULT: "hsl(var(--accent))",
44 | foreground: "hsl(var(--accent-foreground))",
45 | },
46 | popover: {
47 | DEFAULT: "hsl(var(--popover))",
48 | foreground: "hsl(var(--popover-foreground))",
49 | },
50 | card: {
51 | DEFAULT: "hsl(var(--card))",
52 | foreground: "hsl(var(--card-foreground))",
53 | },
54 | },
55 | borderRadius: {
56 | lg: "var(--radius)",
57 | md: "calc(var(--radius) - 2px)",
58 | sm: "calc(var(--radius) - 4px)",
59 | },
60 | keyframes: {
61 | "accordion-down": {
62 | from: { height: 0 },
63 | to: { height: "var(--radix-accordion-content-height)" },
64 | },
65 | "accordion-up": {
66 | from: { height: "var(--radix-accordion-content-height)" },
67 | to: { height: 0 },
68 | },
69 | },
70 | animation: {
71 | "accordion-down": "accordion-down 0.2s ease-out",
72 | "accordion-up": "accordion-up 0.2s ease-out",
73 | },
74 | spacing: {
75 | 72.5: "18.125rem",
76 | },
77 | },
78 | },
79 | plugins: [require("tailwindcss-animate")],
80 | }
81 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": [
26 | "next-env.d.ts",
27 | "types/**/*.ts",
28 | "**/*.ts",
29 | "**/*.tsx",
30 | ".next/types/**/*.ts"
31 | ],
32 | "exclude": ["node_modules"]
33 | }
34 |
--------------------------------------------------------------------------------