├── .gitignore
├── .prettierrc
├── .vscode
└── settings.json
├── README.md
├── components.json
├── docs
├── connect-1.png
├── connection-string.png
└── drivers.png
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── prisma
└── schema.prisma
├── public
├── favicon.ico
└── vercel.svg
├── scripts
├── get-deps.js
└── update-deps.sh
├── src
├── app
│ ├── api
│ │ ├── auth
│ │ │ └── [...nextauth]
│ │ │ │ └── route.ts
│ │ └── preferences
│ │ │ └── route.ts
│ ├── error.tsx
│ ├── globals.css
│ ├── layout.tsx
│ ├── not-found.tsx
│ ├── page.tsx
│ ├── posts
│ │ ├── [id]
│ │ │ ├── loading.tsx
│ │ │ ├── page.tsx
│ │ │ └── template.tsx
│ │ ├── actions.ts
│ │ ├── create
│ │ │ ├── loading.tsx
│ │ │ ├── page.tsx
│ │ │ └── template.tsx
│ │ ├── layout.tsx
│ │ ├── loading.tsx
│ │ ├── page.tsx
│ │ └── template.tsx
│ ├── profile
│ │ ├── actions.ts
│ │ ├── page.tsx
│ │ └── update-profile.tsx
│ └── template.tsx
├── components
│ ├── animated-template.tsx
│ ├── layout
│ │ ├── auth-and-theme.tsx
│ │ ├── auth-button.tsx
│ │ ├── auth-provider.tsx
│ │ ├── index.ts
│ │ ├── links.ts
│ │ ├── menu.tsx
│ │ ├── navbar.tsx
│ │ ├── signin-dialog.tsx
│ │ ├── theme-provider.tsx
│ │ └── theme-toggle.tsx
│ ├── post
│ │ ├── create-post-form.tsx
│ │ ├── post-card-skeleton.tsx
│ │ ├── post-card.tsx
│ │ ├── post-delete-button.tsx
│ │ ├── post-details.tsx
│ │ └── post-form-skeleton.tsx
│ └── ui
│ │ ├── accordion.tsx
│ │ ├── alert-dialog.tsx
│ │ ├── alert.tsx
│ │ ├── aspect-ratio.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── button.tsx
│ │ ├── calendar.tsx
│ │ ├── card.tsx
│ │ ├── carousel.tsx
│ │ ├── chart.tsx
│ │ ├── checkbox.tsx
│ │ ├── collapsible.tsx
│ │ ├── command.tsx
│ │ ├── context-menu.tsx
│ │ ├── dialog.tsx
│ │ ├── drawer.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── hover-card.tsx
│ │ ├── index.ts
│ │ ├── input-otp.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── menubar.tsx
│ │ ├── navigation-menu.tsx
│ │ ├── pagination.tsx
│ │ ├── popover.tsx
│ │ ├── progress.tsx
│ │ ├── radio-group.tsx
│ │ ├── resizable.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── sidebar.tsx
│ │ ├── skeleton.tsx
│ │ ├── slider.tsx
│ │ ├── sonner.tsx
│ │ ├── switch.tsx
│ │ ├── table.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ ├── toggle-group.tsx
│ │ ├── toggle.tsx
│ │ └── tooltip.tsx
├── hooks
│ ├── index.ts
│ ├── use-mobile.tsx
│ └── use-toast.ts
├── lib
│ ├── auth.ts
│ ├── preferences.ts
│ ├── prisma.ts
│ └── utils.ts
├── schemas
│ └── index.ts
└── types
│ └── index.ts
├── tailwind.config.js
├── tsconfig.json
└── vercel.json
/.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 | # testing
8 | /coverage
9 | next-env.d.ts
10 | # next.js
11 | /.next/
12 | /out/
13 |
14 | # production
15 | /build
16 |
17 | # misc
18 | .DS_Store
19 | *.pem
20 | .env
21 | yarn.lock
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "jsxSingleQuote": true,
6 | "arrowParens": "avoid"
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules\\typescript\\lib"
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### A Next.js template
2 |
3 | A Next.js app that uses Shadcn, Prisma ORM, MongoDB PostgreSQL and Next Auth
4 |
5 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdanybeltran%2Fnextjs-typescript-and-mongodb)
6 |
7 | ### Updating deps
8 |
9 | To update the dependencies to their latest versions, run:
10 |
11 | ```
12 | ./scripts/update-deps.sh
13 | ```
14 |
15 | ---
16 |
17 | ### Development
18 |
19 | You need to pass an env. variable with the MongoDB connection string, as well as any variables required by `next-auth`:
20 |
21 | ```
22 | NEXTAUTH_SECRET=
23 | GOOGLE_APP_CLIENT_ID=
24 | GOOGLE_APP_CLIENT_SECRET=
25 | NEXTAUTH_URL=http://localhost:3000
26 | # Connect to Supabase via connection pooling with Supavisor.
27 | DATABASE_URL=
28 | # Direct connection to the database. Used for migrations.
29 | DIRECT_URL=
30 | ```
31 |
32 | (You don't need `NEXTAUTH_URL` if you are deploying to Vercel)
33 |
34 | How to get these variables?
35 |
36 | ---
37 |
38 | `DATABASE_URL` and `DIRECT_URL`: Visit the [Supabase documentation](https://supabase.com/partners/integrations/prisma)
39 |
40 | ---
41 |
42 |
64 |
65 | - [`GOOGLE_APP_CLIENT_ID` and `GOOGLE_APP_CLIENT_SECRET`](https://developers.google.com/identity/oauth2/web/guides/get-google-api-clientid)
66 |
67 | ---
68 |
69 | Use your preferred tool to generate the `NEXTAUTH_SECRET` hash:
70 |
71 | Using [This tool](https://generate-secret.vercel.app/32) is the quickest way to generate a hash. You can change the last segment of the url to get a hash of your preferred length, such as `https://generate-secret.vercel.app/44`
72 |
73 | **OpenSSL :**
74 |
75 | ```bash
76 | openssl rand -base64 32
77 | ```
78 |
79 | **Urandom :**
80 |
81 | ```bash
82 | head -c 32 /dev/urandom | base64
83 | ```
84 |
85 | **Python :**
86 |
87 | ```py
88 | import base64
89 | import os
90 |
91 | random_bytes = os.urandom(32)
92 | base64_string = base64.b64encode(random_bytes).decode('utf-8')
93 | print(base64_string)
94 | ```
95 |
96 | **JavaScript :**
97 |
98 | ```js
99 | const crypto = require('crypto')
100 |
101 | const randomBytes = crypto.randomBytes(32)
102 | const base64String = randomBytes.toString('base64')
103 | console.log(base64String)
104 | ```
105 |
106 | You can add those variables to a `.ENV` file (don't forget to add it to your `.gitignore` file!)
107 |
108 | Related documentation:
109 |
110 | - [`nextjs`](https://nextjs.org/docs)
111 |
112 | - [`next-auth`](https://next-auth.js.org/getting-started/introduction)
113 |
114 | - [`http-react`](https://httpr.vercel.app/docs)
115 |
116 | [Live preview](https://nextjs-typescript-and-mongodb-psi.vercel.app)
117 |
--------------------------------------------------------------------------------
/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.js",
8 | "css": "src/app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "atomic-utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/docs/connect-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danybeltran/nextjs-typescript-and-mongodb/3d21942264a6135c4a29e3cb9ac90e812456e0c4/docs/connect-1.png
--------------------------------------------------------------------------------
/docs/connection-string.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danybeltran/nextjs-typescript-and-mongodb/3d21942264a6135c4a29e3cb9ac90e812456e0c4/docs/connection-string.png
--------------------------------------------------------------------------------
/docs/drivers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danybeltran/nextjs-typescript-and-mongodb/3d21942264a6135c4a29e3cb9ac90e812456e0c4/docs/drivers.png
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type { import("next").NextConfig }
3 | */
4 | module.exports = {
5 | reactStrictMode: false
6 | }
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-11",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "npx prisma generate dev --name init && npx prisma db push && next dev",
7 | "build": "npx prisma generate init && npx prisma db push && next build",
8 | "migrate:dev": "npx prisma generate dev --name init",
9 | "start": "next start"
10 | },
11 | "repository": {
12 | "type": "github",
13 | "url": "https://github.com/danybeltran/nextjs-typescript-and-mongodb"
14 | },
15 | "dependencies": {
16 | "@hookform/resolvers": "^3.9.1",
17 | "@prisma/client": "^6.1.0",
18 | "@radix-ui/react-accordion": "^1.2.2",
19 | "@radix-ui/react-alert-dialog": "^1.1.4",
20 | "@radix-ui/react-aspect-ratio": "^1.1.1",
21 | "@radix-ui/react-avatar": "^1.1.2",
22 | "@radix-ui/react-checkbox": "^1.1.3",
23 | "@radix-ui/react-collapsible": "^1.1.2",
24 | "@radix-ui/react-context-menu": "^2.2.4",
25 | "@radix-ui/react-dialog": "^1.1.4",
26 | "@radix-ui/react-dropdown-menu": "^2.1.4",
27 | "@radix-ui/react-hover-card": "^1.1.4",
28 | "@radix-ui/react-label": "^2.1.1",
29 | "@radix-ui/react-menubar": "^1.1.4",
30 | "@radix-ui/react-navigation-menu": "^1.2.3",
31 | "@radix-ui/react-popover": "^1.1.4",
32 | "@radix-ui/react-progress": "^1.1.1",
33 | "@radix-ui/react-radio-group": "^1.2.2",
34 | "@radix-ui/react-scroll-area": "^1.2.2",
35 | "@radix-ui/react-select": "^2.1.4",
36 | "@radix-ui/react-separator": "^1.1.1",
37 | "@radix-ui/react-slider": "^1.2.2",
38 | "@radix-ui/react-slot": "^1.1.1",
39 | "@radix-ui/react-switch": "^1.1.2",
40 | "@radix-ui/react-tabs": "^1.1.2",
41 | "@radix-ui/react-toast": "^1.2.4",
42 | "@radix-ui/react-toggle": "^1.1.1",
43 | "@radix-ui/react-toggle-group": "^1.1.1",
44 | "@radix-ui/react-tooltip": "^1.1.6",
45 | "atomic-utils": "^1.5.8",
46 | "autoprefixer": "^10.4.20",
47 | "bs-icon": "^0.0.8",
48 | "class-variance-authority": "^0.7.1",
49 | "clsx": "^2.1.1",
50 | "cmdk": "^1.0.4",
51 | "date-fns": "^4.1.0",
52 | "easymde": "^2.18.0",
53 | "embla-carousel-react": "^8.5.1",
54 | "framer-motion": "^11.15.0",
55 | "geist": "^1.3.1",
56 | "input-otp": "^1.4.1",
57 | "js-cookie": "^3.0.5",
58 | "lucide-react": "^0.469.0",
59 | "next": "^15.1.3",
60 | "next-auth": "^4.24.11",
61 | "next-themes": "^0.4.4",
62 | "postcss": "^8.4.49",
63 | "react": "^19.0.0",
64 | "react-day-picker": "^9.4.4",
65 | "react-dom": "^19.0.0",
66 | "react-hook-form": "^7.54.2",
67 | "react-icons": "^5.4.0",
68 | "react-markdown": "^9.0.1",
69 | "react-resizable-panels": "^2.1.7",
70 | "recharts": "^2.15.0",
71 | "sonner": "^1.7.1",
72 | "tailwind-merge": "^2.6.0",
73 | "tailwindcss": "^3.4.17",
74 | "tailwindcss-animate": "^1.0.7",
75 | "vaul": "^1.1.2",
76 | "zod": "^3.24.1"
77 | },
78 | "devDependencies": {
79 | "@tailwindcss/typography": "^0.5.15",
80 | "@types/js-cookie": "^3.0.6",
81 | "@types/node": "^22.10.2",
82 | "@types/react": "^19.0.2",
83 | "@types/react-dom": "^19.0.2",
84 | "prisma": "^6.1.0",
85 | "typescript": "^5.7.2"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "postgresql"
3 | url = env("DATABASE_URL")
4 | directUrl = env("DIRECT_URL")
5 | }
6 |
7 | generator client {
8 | provider = "prisma-client-js"
9 | }
10 |
11 | model Post {
12 | id Int @id @default(autoincrement())
13 | title String
14 | date DateTime @default(now())
15 | content String
16 | }
17 |
18 | model Preferences {
19 | id Int @id @default(autoincrement())
20 | user_email String
21 | user_fullname String
22 | user_profile_picture String
23 | username String
24 | user_description String
25 | // Add more customizations
26 | }
27 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danybeltran/nextjs-typescript-and-mongodb/3d21942264a6135c4a29e3cb9ac90e812456e0c4/public/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/get-deps.js:
--------------------------------------------------------------------------------
1 | const { dependencies, devDependencies } = require('../package.json')
2 | const fs = require('fs')
3 | const path = require('path')
4 |
5 | const versions = {}
6 | const versionsDev = {}
7 |
8 | console.log('------- DEPENDENCIES -------')
9 | const installDeps =
10 | 'npm install ' +
11 | Object.keys(dependencies)
12 | .map(dep => {
13 | console.log(dep + ` : (${dependencies[dep]})`)
14 | versions[dep] = dependencies[dep]
15 | return dep
16 | })
17 | .join('@latest ') +
18 | '@latest -f'
19 |
20 | console.log('\n------- DEV DEPENDENCIES -------')
21 |
22 | const installDevDeps =
23 | 'npm install -D ' +
24 | Object.keys(devDependencies)
25 | .map(dep => {
26 | console.log(dep + ` : (${devDependencies[dep]})`)
27 | versionsDev[dep] = devDependencies[dep]
28 | return dep
29 | })
30 | .join('@latest ') +
31 | '@latest -f'
32 |
33 | fs.writeFile(
34 | path.join(__dirname, './installdeps.sh'),
35 | `${installDeps} && ${installDevDeps}`,
36 | () => {}
37 | )
38 |
--------------------------------------------------------------------------------
/scripts/update-deps.sh:
--------------------------------------------------------------------------------
1 | echo "Getting dependencies"
2 | echo
3 | node ./scripts/get-deps.js
4 | chmod +x ./scripts/installdeps.sh
5 | echo
6 | ./scripts/installdeps.sh
7 | echo
8 | rm ./scripts/installdeps.sh
9 |
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from 'next-auth'
2 | import { authOptions } from '@/lib/auth'
3 |
4 | const handler = NextAuth(authOptions)
5 |
6 | export { handler as GET, handler as POST }
7 |
--------------------------------------------------------------------------------
/src/app/api/preferences/route.ts:
--------------------------------------------------------------------------------
1 | import { getUserPreferences } from '@/lib/preferences'
2 |
3 | export async function GET() {
4 | const preferences = await getUserPreferences()
5 |
6 | return Response.json(preferences)
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/error.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Button } from '@/components/ui'
4 | import Icon from 'bs-icon'
5 |
6 | export default function ErrorPage() {
7 | return (
8 |
9 |
Internal server error
10 |
The page you requested failed to load.
11 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/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: 0 0% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 0 0% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 0 0% 3.9%;
13 | --primary: 0 0% 9%;
14 | --primary-foreground: 0 0% 98%;
15 | --secondary: 0 0% 96.1%;
16 | --secondary-foreground: 0 0% 9%;
17 | --muted: 0 0% 96.1%;
18 | --muted-foreground: 0 0% 45.1%;
19 | --accent: 0 0% 96.1%;
20 | --accent-foreground: 0 0% 9%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 0 0% 89.8%;
24 | --input: 0 0% 89.8%;
25 | --ring: 0 0% 3.9%;
26 | --radius: 0.5rem;
27 | --sidebar-background: 0 0% 98%;
28 | --sidebar-foreground: 240 5.3% 26.1%;
29 | --sidebar-primary: 240 5.9% 10%;
30 | --sidebar-primary-foreground: 0 0% 98%;
31 | --sidebar-accent: 240 4.8% 95.9%;
32 | --sidebar-accent-foreground: 240 5.9% 10%;
33 | --sidebar-border: 220 13% 91%;
34 | --sidebar-ring: 217.2 91.2% 59.8%;
35 | --chart-1: 12 76% 61%;
36 | --chart-2: 173 58% 39%;
37 | --chart-3: 197 37% 24%;
38 | --chart-4: 43 74% 66%;
39 | --chart-5: 27 87% 67%;
40 | }
41 |
42 | .dark {
43 | --background: 0 0% 3.9%;
44 | --foreground: 0 0% 98%;
45 | --card: 0 0% 3.9%;
46 | --card-foreground: 0 0% 98%;
47 | --popover: 0 0% 3.9%;
48 | --popover-foreground: 0 0% 98%;
49 | --primary: 0 0% 98%;
50 | --primary-foreground: 0 0% 9%;
51 | --secondary: 0 0% 14.9%;
52 | --secondary-foreground: 0 0% 98%;
53 | --muted: 0 0% 14.9%;
54 | --muted-foreground: 0 0% 63.9%;
55 | --accent: 0 0% 14.9%;
56 | --accent-foreground: 0 0% 98%;
57 | --destructive: 0 62.8% 30.6%;
58 | --destructive-foreground: 0 0% 98%;
59 | --border: 0 0% 14.9%;
60 | --input: 0 0% 14.9%;
61 | --ring: 0 0% 83.1%;
62 | --sidebar-background: 240 5.9% 10%;
63 | --sidebar-foreground: 240 4.8% 95.9%;
64 | --sidebar-primary: 224.3 76.3% 48%;
65 | --sidebar-primary-foreground: 0 0% 100%;
66 | --sidebar-accent: 240 3.7% 15.9%;
67 | --sidebar-accent-foreground: 240 4.8% 95.9%;
68 | --sidebar-border: 240 3.7% 15.9%;
69 | --sidebar-ring: 217.2 91.2% 59.8%;
70 | --chart-1: 220 70% 50%;
71 | --chart-2: 160 60% 45%;
72 | --chart-3: 30 80% 55%;
73 | --chart-4: 280 65% 60%;
74 | --chart-5: 340 75% 55%;
75 | }
76 | }
77 |
78 | @layer base {
79 | * {
80 | @apply border-border;
81 | }
82 | body {
83 | @apply bg-background text-foreground;
84 | }
85 | }
86 |
87 | body {
88 | --sb-track-color: #111619;
89 | --sb-thumb-color: rgb(94, 94, 94);
90 | --sb-size: 6px;
91 | scrollbar-color: var(--sb-thumb-color) var(--sb-track-color);
92 | scrollbar-gutter: stable;
93 | }
94 |
95 | body::-webkit-scrollbar {
96 | width: var(--sb-size);
97 | }
98 |
99 | body::-webkit-scrollbar-track {
100 | background: var(--sb-track-color);
101 | border-radius: 0px;
102 | }
103 |
104 | body::-webkit-scrollbar-thumb {
105 | background: var(--sb-thumb-color);
106 | border-radius: 2px;
107 | }
108 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { GeistSans } from 'geist/font/sans'
2 | import type { Metadata } from 'next'
3 | import './globals.css'
4 | import 'bs-icon/icons.css'
5 |
6 | import { AuthProvider, Navbar, ThemeProvider } from '@/components/layout'
7 |
8 | import { AtomicState } from 'atomic-utils'
9 | import { FetchConfig } from 'atomic-utils'
10 | import { getServerSession } from 'next-auth'
11 | import { getUserPreferences } from '@/lib/preferences'
12 | import { cookies } from 'next/headers'
13 |
14 | export const metadata: Metadata = {
15 | title: 'Home',
16 | description: 'Home page '
17 | }
18 |
19 | export default async function MainLayout({ children }) {
20 | const session = await getServerSession()
21 |
22 | const preferences = await getUserPreferences()
23 |
24 | const serverTheme = (await cookies()).get('theme')?.value ?? 'system'
25 |
26 | return (
27 |
28 |
29 | Next.js starter
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
42 |
49 |
50 |
51 | {children}
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui'
2 | import Icon from 'bs-icon'
3 | import Link from 'next/link'
4 |
5 | export default function NotFound() {
6 | return (
7 |
8 |
9 | The page you requested was not found
10 |
11 |
12 |
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { GoPlus } from 'react-icons/go'
3 | import { FaReact } from 'react-icons/fa'
4 | import { BiLogoMongodb } from 'react-icons/bi'
5 | import { TbBrandNextjs } from 'react-icons/tb'
6 | import { SiPrisma, SiTailwindcss } from 'react-icons/si'
7 | import { IoLogoGithub, IoLogoVercel } from 'react-icons/io5'
8 |
9 | import { Button } from '@/components/ui'
10 |
11 | export default function Page() {
12 | return (
13 | <>
14 |
15 |
16 |
17 | Hello world
18 |
19 |
This is a starter project
20 |
21 |
26 |
30 |
31 |
35 |
38 |
39 |
40 |
41 |
42 |
46 |
47 |
48 | Features
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
Next.js 14
57 |
App dir
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
React 18
66 |
67 | Server and Client Components
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
Database
81 |
82 | Prisma + MongoDB
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
ShadCN
92 |
93 | UI components built with TailwindCSS and Radix UI
94 |
95 |
96 |
97 |
98 |
99 |
100 |
104 |
105 |
106 | The code is available on{' '}
107 |
113 | GitHub
114 |
115 | .{' '}
116 |
117 |
118 |
119 | >
120 | )
121 | }
122 |
--------------------------------------------------------------------------------
/src/app/posts/[id]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowLeft } from 'lucide-react'
2 |
3 | import { Skeleton } from '@/components/ui'
4 |
5 | export default function PostIdLoading() {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/posts/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { cache } from 'react'
2 | import Link from 'next/link'
3 | import { ArrowLeft } from 'lucide-react'
4 |
5 | import PostDeleteButton from '@/components/post/post-delete-button'
6 | import PostDetails from '@/components/post/post-details'
7 |
8 | import { prisma } from '@/lib/prisma'
9 |
10 | export async function generateMetadata(props: PostPageProps) {
11 | const params = await props.params
12 | const post = await getPost(params)
13 |
14 | return {
15 | title: 'Post: ' + post?.title,
16 | description: 'Details for post ' + post?.id
17 | }
18 | }
19 |
20 | export default async function PostDetailsPage(props: PostPageProps) {
21 | const params = await props.params
22 |
23 | const post = await getPost(params)
24 |
25 | return (
26 |
27 |
28 |
29 | Back
30 |
31 | {post ? (
32 |
40 | ) : (
41 |
42 | Post not found
43 |
44 | )}
45 |
46 | )
47 | }
48 |
49 | const getPost = cache(async (params: { id: string }) => {
50 | try {
51 | return prisma.post.findFirst({
52 | where: {
53 | id: parseInt(params.id)
54 | }
55 | })
56 | } catch (err) {
57 | return null
58 | }
59 | })
60 |
61 | interface PostPageProps {
62 | params: Promise<{ id: string }>
63 | }
64 |
--------------------------------------------------------------------------------
/src/app/posts/[id]/template.tsx:
--------------------------------------------------------------------------------
1 | export { default } from '@/components/animated-template'
2 |
--------------------------------------------------------------------------------
/src/app/posts/actions.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 | import { revalidatePath } from 'next/cache'
3 | import { actionData } from 'atomic-utils'
4 |
5 | import { prisma } from '@/lib/prisma'
6 | import { postSchema } from '@/schemas'
7 |
8 | export async function createPost(post: any) {
9 | try {
10 | const validation = postSchema.safeParse(post)
11 |
12 | if (validation.success) {
13 | const newPost = await prisma.post.create({
14 | data: post
15 | })
16 |
17 | revalidatePath('/posts')
18 |
19 | return actionData(newPost)
20 | }
21 |
22 | return actionData(validation.error.format(), {
23 | status: 400
24 | })
25 | } catch {
26 | return {
27 | status: 500
28 | }
29 | }
30 | }
31 |
32 | export async function deletePost(id: number) {
33 | try {
34 | const deletedPost = await prisma.post.delete({
35 | where: {
36 | id: id
37 | }
38 | })
39 |
40 | revalidatePath('/posts')
41 |
42 | /**
43 | * actionResult formats a response so http-react can read data, status code and error
44 | * The code below will be formated as { data: deletedPost, status: 200 }.
45 | * You can ommit the status part like this `return actionResult(deletedPost)`
46 | */
47 | return actionData(deletedPost, {
48 | status: 200
49 | })
50 | } catch {
51 | return {
52 | status: 500
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/app/posts/create/loading.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowLeft } from 'lucide-react'
2 |
3 | import PostFormSkeleton from '@/components/post/post-form-skeleton'
4 |
5 | export default function CreateLoading() {
6 | return (
7 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/posts/create/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { ArrowLeft } from 'lucide-react'
3 |
4 | import PostForm from '@/components/post/create-post-form'
5 |
6 | export default function Create() {
7 | return (
8 |
9 |
10 |
11 | Back
12 |
13 |
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/posts/create/template.tsx:
--------------------------------------------------------------------------------
1 | export { default } from '@/components/animated-template'
2 |
--------------------------------------------------------------------------------
/src/app/posts/layout.tsx:
--------------------------------------------------------------------------------
1 | export { default } from '@/components/animated-template'
2 |
--------------------------------------------------------------------------------
/src/app/posts/loading.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowLeft } from 'lucide-react'
2 |
3 | import PostCardSkeleton from '@/components/post/post-card-skeleton'
4 | import { Button } from '@/components/ui'
5 |
6 | export default function PostsLoading() {
7 | return (
8 |
9 |
13 |
14 | All Posts
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/posts/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { ArrowLeft } from 'lucide-react'
3 | import { headers } from 'next/headers'
4 |
5 | import { Button } from '@/components/ui'
6 | import PostCard from '@/components/post/post-card'
7 |
8 | import { prisma } from '@/lib/prisma'
9 | import { RenderList } from 'atomic-utils'
10 |
11 | export const revalidate = 0
12 |
13 | export default async function Posts() {
14 | headers()
15 |
16 | const posts = (await prisma.post.findMany({})).reverse()
17 |
18 | return (
19 |
20 |
21 |
22 | Back
23 |
24 |
25 | All Posts
26 |
27 |
30 |
31 |
32 |
33 | {posts.length === 0 ? (
34 |
35 |
No posts to show
36 |
37 | ) : (
38 |
44 | )}
45 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/src/app/posts/template.tsx:
--------------------------------------------------------------------------------
1 | export { default } from '@/components/animated-template'
2 |
--------------------------------------------------------------------------------
/src/app/profile/actions.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 |
3 | import { getUserPreferences } from '@/lib/preferences'
4 | import { prisma } from '@/lib/prisma'
5 | import { UpdatePreferencesPayload } from '@/schemas'
6 | import { actionData } from 'atomic-utils'
7 |
8 | export async function updateUserPreferences(payload: UpdatePreferencesPayload) {
9 | const preferences = await getUserPreferences()
10 |
11 | const updatedPreferences = await prisma.preferences.update({
12 | where: {
13 | id: preferences.id
14 | },
15 | data: {
16 | user_fullname: payload.user_fullname.trim() || preferences.user_fullname,
17 | user_description:
18 | payload.user_description.trim() || preferences.user_description
19 | }
20 | })
21 |
22 | return actionData(updatedPreferences)
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/profile/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { usePreferences, useUser } from '@/hooks'
4 | import Link from 'next/link'
5 |
6 | import { Button } from '@/components/ui'
7 | import SigninDialog from '@/components/layout/signin-dialog'
8 | import { revalidate } from 'atomic-utils'
9 | import UpdateProfile from './update-profile'
10 |
11 | export default function ProfilePage() {
12 | const { data: user } = useUser()
13 | const { data: preferences } = usePreferences()
14 |
15 | if (!user) {
16 | return (
17 |
18 |
19 | You haven't logged in
20 |
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | return (
30 |
31 |
32 |
33 |
38 |
39 | Name: {preferences.user_fullname}
40 |
41 |
42 | Email: {preferences.user_email}
43 |
44 |
45 |
Bio:
46 |
{preferences.user_description}
47 |
48 |
49 |
50 |
51 | Raw data
52 | {JSON.stringify({ user, preferences }, null, 2)}
53 |
54 |
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/profile/update-profile.tsx:
--------------------------------------------------------------------------------
1 | import { revalidate, useObject, useServerAction } from 'atomic-utils'
2 | import { updateUserPreferences } from './actions'
3 | import { usePreferences, useUser } from '@/hooks'
4 | import { Button, Input, Textarea } from '@/components/ui'
5 | import { VscLoading } from 'react-icons/vsc'
6 | import { UpdatePreferencesPayload } from '@/schemas'
7 |
8 | export default function UpdateProfile() {
9 | const { data: preferences } = usePreferences()
10 |
11 | const [newPreferences, newPreferencesActions] =
12 | useObject({
13 | user_fullname: preferences.user_fullname,
14 | user_description: preferences.user_description
15 | })
16 |
17 | const { reFetch: savePreferences, isPending: savingPreferences } =
18 | useServerAction(updateUserPreferences, {
19 | params: newPreferences,
20 | onResolve() {
21 | revalidate('Preferences')
22 | }
23 | })
24 |
25 | const hasChanges = ['user_fullname', 'user_description'].some(
26 | preference => preferences[preference] !== newPreferences[preference]
27 | )
28 |
29 | return (
30 |
85 | )
86 | }
87 |
--------------------------------------------------------------------------------
/src/app/template.tsx:
--------------------------------------------------------------------------------
1 | export { default } from '@/components/animated-template'
2 |
--------------------------------------------------------------------------------
/src/components/animated-template.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { motion } from 'framer-motion'
4 |
5 | export default function Template({ children }: { children: React.ReactNode }) {
6 | return (
7 |
12 | {children}
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/layout/auth-and-theme.tsx:
--------------------------------------------------------------------------------
1 | import AuthButton from './auth-button'
2 | import { ThemeToggle } from './theme-toggle'
3 |
4 | export default function AuthAndTheme() {
5 | return (
6 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/layout/auth-button.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { IoIosLogIn } from 'react-icons/io'
4 |
5 | import {
6 | DropdownMenu,
7 | DropdownMenuContent,
8 | DropdownMenuItem,
9 | DropdownMenuLabel,
10 | DropdownMenuSeparator,
11 | DropdownMenuTrigger,
12 | Avatar,
13 | AvatarFallback,
14 | AvatarImage,
15 | AlertDialog,
16 | AlertDialogContent,
17 | AlertDialogTitle,
18 | AlertDialogCancel,
19 | AlertDialogDescription,
20 | AlertDialogAction,
21 | AlertDialogFooter
22 | } from '@/components/ui'
23 |
24 | import { usePreferences, useUser } from '@/hooks'
25 | import { useState } from 'react'
26 | import { signOut } from 'next-auth/react'
27 | import Icon from 'bs-icon'
28 | import SigninDialog from './signin-dialog'
29 |
30 | export default function AuthButton() {
31 | const { data: user } = useUser()
32 | const { data: preferences } = usePreferences()
33 |
34 | const [showSignoutDialog, setShowSignoutDialog] = useState(false)
35 |
36 | if (!user) {
37 | return (
38 |
39 |
42 |
43 | )
44 | }
45 |
46 | return (
47 | <>
48 |
49 |
50 | Sign out?
51 |
52 | You will be signed out of your account
53 |
54 |
55 | Cancel
56 |
58 | signOut({
59 | callbackUrl: location.href
60 | })
61 | }
62 | >
63 | Sign out
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | {preferences.user_fullname}
80 |
81 |
82 | setShowSignoutDialog(true)}
85 | >
86 | Logout
87 |
88 |
89 |
90 | >
91 | )
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/layout/auth-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | export { SessionProvider as default } from 'next-auth/react'
4 |
--------------------------------------------------------------------------------
/src/components/layout/index.ts:
--------------------------------------------------------------------------------
1 | export { default as AuthButton } from './auth-button'
2 | export { default as AuthProvider } from './auth-provider'
3 | export { default as Navbar } from './navbar'
4 | export { ThemeProvider } from './theme-provider'
5 | export { ThemeToggle } from './theme-toggle'
6 |
--------------------------------------------------------------------------------
/src/components/layout/links.ts:
--------------------------------------------------------------------------------
1 | export const LINKS: {
2 | children: string
3 | href: string
4 | target?: string
5 | rel?: string
6 | }[] = [
7 | {
8 | children: 'Posts',
9 | href: '/posts'
10 | },
11 | {
12 | children: 'Profile',
13 | href: '/profile'
14 | },
15 | {
16 | children: 'Github',
17 | href: 'https://github.com/danybeltran/nextjs-typescript-and-mongodb',
18 | target: '_blank',
19 | rel: 'noreferrer'
20 | }
21 | ]
22 |
--------------------------------------------------------------------------------
/src/components/layout/menu.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Link from 'next/link'
3 | import {
4 | Button,
5 | Dialog,
6 | DialogClose,
7 | DialogContent,
8 | DialogDescription,
9 | DialogTitle,
10 | DialogTrigger
11 | } from '@/components/ui'
12 | import { cn, RenderList } from 'atomic-utils'
13 | import Icon from 'bs-icon'
14 | import { Fragment, useState } from 'react'
15 | import AuthAndTheme from './auth-and-theme'
16 | import { LINKS } from './links'
17 | import { usePathname } from 'next/navigation'
18 |
19 | const useGetLinksStyle = () => {
20 | const pathname = usePathname()
21 | return function getLinkStyles(href: string) {
22 | return cn('text-sm text-foreground/70 hover:text-foreground', {
23 | 'text-blue-700 dark:text-blue-400 font-medium hover:text-blue-700':
24 | pathname.startsWith(href)
25 | })
26 | }
27 | }
28 |
29 | export default function Menu() {
30 | const getLinkStyle = useGetLinksStyle()
31 | const [isOpen, setIsOpen] = useState(false)
32 |
33 | const hideMenu = () => setIsOpen(false)
34 |
35 | return (
36 | <>
37 |
38 |
39 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | NEXT.JS
87 |
88 | (
91 |
96 | )}
97 | />
98 |
99 |
100 |
101 | >
102 | )
103 | }
104 |
--------------------------------------------------------------------------------
/src/components/layout/navbar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import Menu from './menu'
4 |
5 | export default function Navbar() {
6 | return (
7 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/layout/signin-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import {
4 | Button,
5 | Dialog,
6 | DialogClose,
7 | DialogContent,
8 | DialogDescription,
9 | DialogFooter,
10 | DialogTitle,
11 | DialogTrigger
12 | } from '@/components/ui'
13 | import { revalidate } from 'atomic-utils'
14 | import Icon from 'bs-icon'
15 | import { signIn } from 'next-auth/react'
16 |
17 | export default function SigninDialog({
18 | children
19 | }: {
20 | children?: React.ReactNode
21 | }) {
22 | const attemptGoogleSignin = () => {
23 | signIn('google', {
24 | callbackUrl: location.href
25 | })
26 | .then(() => {
27 | // Try to get the new session from API
28 | revalidate('GET /auth/session')
29 | })
30 | .catch(err => {})
31 | }
32 |
33 | return (
34 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/layout/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { ThemeProvider as NextThemesProvider } from 'next-themes'
3 | import { type ThemeProviderProps } from 'next-themes'
4 |
5 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
6 | return {children}
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/layout/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { IconType } from 'react-icons'
3 | import { LuMoon, LuSun, LuMonitor } from 'react-icons/lu'
4 |
5 | import { useTheme } from 'next-themes'
6 |
7 | import { Button } from '@/components/ui/button'
8 | import Cookies from 'js-cookie'
9 | import { create } from 'atomic-utils'
10 |
11 | const useServerTheme = create({
12 | key: 'server-theme'
13 | })
14 |
15 | export function ThemeToggle() {
16 | const [serverTheme] = useServerTheme()
17 |
18 | const { setTheme, theme = serverTheme } = useTheme()
19 |
20 | const nextTheme: any = {
21 | light: 'dark',
22 | dark: 'system',
23 | system: 'light'
24 | }
25 |
26 | const ThemeIcon = {
27 | undefined: LuSun,
28 | light: LuSun,
29 | dark: LuMoon,
30 | system: LuMonitor
31 | }[theme] as IconType
32 |
33 | return (
34 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/post/create-post-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { zodResolver } from '@hookform/resolvers/zod'
3 | import { useMutation, useServerAction } from 'atomic-utils'
4 | import { AlertCircle, Loader2 } from 'lucide-react'
5 | import { useRouter } from 'next/navigation'
6 | import { useForm } from 'react-hook-form'
7 | import { z } from 'zod'
8 | import { Types } from '@/types'
9 |
10 | import {
11 | Form,
12 | FormControl,
13 | FormField,
14 | FormItem,
15 | FormLabel,
16 | FormMessage,
17 | Alert,
18 | AlertTitle,
19 | Button,
20 | Input,
21 | Textarea
22 | } from '@/components/ui'
23 |
24 | import { postSchema } from '@/schemas'
25 | import { createPost } from '@/app/posts/actions'
26 |
27 | type FormSchema = z.infer
28 |
29 | export default function PostForm() {
30 | const router = useRouter()
31 |
32 | const form = useForm({
33 | resolver: zodResolver(postSchema),
34 | defaultValues: {
35 | title: '',
36 | content: ''
37 | }
38 | })
39 |
40 | // To learn how to use the `useMutation` hook with server actions
41 | // visit https://httpr.vercel.app/docs/server_actions#server-mutations
42 |
43 | const { refresh, loading, error } = useServerAction(createPost, {
44 | params: form.getValues(),
45 | onResolve(data: Types.Post) {
46 | router.replace('/posts/' + data.id)
47 | }
48 | })
49 |
50 | const onSubmit = form.handleSubmit(refresh)
51 |
52 | return (
53 |
107 |
108 | )
109 | }
110 |
--------------------------------------------------------------------------------
/src/components/post/post-card-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | CardContent,
4 | CardFooter,
5 | CardHeader,
6 | CardTitle,
7 | Skeleton
8 | } from '@/components/ui'
9 |
10 | export default function PostCardSkeleton() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/post/post-card.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Link from 'next/link'
3 | import ReactMarkdown from 'react-markdown'
4 |
5 | import {
6 | Card,
7 | CardContent,
8 | CardDescription,
9 | CardFooter,
10 | CardHeader,
11 | CardTitle,
12 | Button
13 | } from '@/components/ui'
14 | import { formatDate } from '@/lib/utils'
15 | import { Types } from '@/types'
16 |
17 | export default function PostCard({ post }: { post: Types.Post }) {
18 | return (
19 |
20 |
21 | {post.title}
22 | {formatDate(post.date)}
23 |
24 |
25 |
26 | {post.content}
27 |
28 |
29 |
30 |
33 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/post/post-delete-button.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useServerAction } from 'atomic-utils'
3 | import { Loader2 } from 'lucide-react'
4 | import { useRouter } from 'next/navigation'
5 |
6 | import {
7 | AlertDialog,
8 | AlertDialogAction,
9 | AlertDialogCancel,
10 | AlertDialogContent,
11 | AlertDialogDescription,
12 | AlertDialogFooter,
13 | AlertDialogHeader,
14 | AlertDialogTitle,
15 | AlertDialogTrigger,
16 | Button
17 | } from '@/components/ui'
18 | import { deletePost } from '@/app/posts/actions'
19 |
20 | export default function PostDeleteButton({ postId }: { postId: string }) {
21 | const router = useRouter()
22 |
23 | const { reFetch, loading } = useServerAction(deletePost, {
24 | params: parseInt(postId),
25 | onResolve: () => router.replace('/posts')
26 | })
27 |
28 | return (
29 |
30 |
31 |
40 |
41 |
42 |
43 | Delete this post?
44 |
45 | This action cannot be undone.
46 |
47 |
48 |
49 | Cancel
50 | Delete
51 |
52 |
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/post/post-details.tsx:
--------------------------------------------------------------------------------
1 | import { formatDate } from '@/lib/utils'
2 | import { Types } from '@/types'
3 |
4 | interface PostDetails {
5 | post: Types.Post
6 | }
7 |
8 | export default function PostDetails({ post }: PostDetails) {
9 | return (
10 |
11 |
12 |
13 | {post.title}
14 |
15 |
16 |
22 |
23 |
24 |
25 | {post.content}
26 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/post/post-form-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from '@/components/ui/skeleton'
2 |
3 | export default function PostFormSkeleton() {
4 | return (
5 |
6 |
7 | Title
8 |
9 |
10 |
11 | Description
12 |
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
5 | import { ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "atomic-utils"
8 |
9 | const Accordion = AccordionPrimitive.Root
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ))
21 | AccordionItem.displayName = "AccordionItem"
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ))
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ))
55 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
56 |
57 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
58 |
--------------------------------------------------------------------------------
/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
5 |
6 | import { cn } from 'atomic-utils'
7 | import { buttonVariants } from '@/components/ui/button'
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = 'AlertDialogHeader'
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = 'AlertDialogFooter'
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ))
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel
141 | }
142 |
--------------------------------------------------------------------------------
/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 "atomic-utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
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/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4 |
5 | const AspectRatio = AspectRatioPrimitive.Root
6 |
7 | export { AspectRatio }
8 |
--------------------------------------------------------------------------------
/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 "atomic-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/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "atomic-utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/src/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { ChevronRight, MoreHorizontal } from "lucide-react"
4 |
5 | import { cn } from "atomic-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]:w-3.5 [&>svg]:h-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 "atomic-utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
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/calendar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { ChevronLeft, ChevronRight } from 'lucide-react'
5 | import { DayPicker } from 'react-day-picker'
6 |
7 | import { cn } from 'atomic-utils'
8 | import { buttonVariants } from '@/components/ui/button'
9 |
10 | export type CalendarProps = React.ComponentProps
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
43 | : '[&:has([aria-selected])]:rounded-md'
44 | ),
45 | day: cn(
46 | buttonVariants({ variant: 'ghost' }),
47 | 'h-8 w-8 p-0 font-normal aria-selected:opacity-100'
48 | ),
49 | day_range_start: 'day-range-start',
50 | day_range_end: 'day-range-end',
51 | day_selected:
52 | 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
53 | day_today: 'bg-accent text-accent-foreground',
54 | day_outside:
55 | 'day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground',
56 | day_disabled: 'text-muted-foreground opacity-50',
57 | day_range_middle:
58 | 'aria-selected:bg-accent aria-selected:text-accent-foreground',
59 | day_hidden: 'invisible',
60 | ...classNames
61 | }}
62 | components={{
63 | // @ts-expect-error
64 | IconLeft: ({ className, ...props }) => (
65 |
66 | ),
67 | IconRight: ({ className, ...props }) => (
68 |
69 | )
70 | }}
71 | {...props}
72 | />
73 | )
74 | }
75 | Calendar.displayName = 'Calendar'
76 |
77 | export { Calendar }
78 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "atomic-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 | HTMLDivElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLDivElement,
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/carousel.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import useEmblaCarousel, {
5 | type UseEmblaCarouselType,
6 | } from "embla-carousel-react"
7 | import { ArrowLeft, ArrowRight } from "lucide-react"
8 |
9 | import { cn } from "atomic-utils"
10 | import { Button } from "@/components/ui/button"
11 |
12 | type CarouselApi = UseEmblaCarouselType[1]
13 | type UseCarouselParameters = Parameters
14 | type CarouselOptions = UseCarouselParameters[0]
15 | type CarouselPlugin = UseCarouselParameters[1]
16 |
17 | type CarouselProps = {
18 | opts?: CarouselOptions
19 | plugins?: CarouselPlugin
20 | orientation?: "horizontal" | "vertical"
21 | setApi?: (api: CarouselApi) => void
22 | }
23 |
24 | type CarouselContextProps = {
25 | carouselRef: ReturnType[0]
26 | api: ReturnType[1]
27 | scrollPrev: () => void
28 | scrollNext: () => void
29 | canScrollPrev: boolean
30 | canScrollNext: boolean
31 | } & CarouselProps
32 |
33 | const CarouselContext = React.createContext(null)
34 |
35 | function useCarousel() {
36 | const context = React.useContext(CarouselContext)
37 |
38 | if (!context) {
39 | throw new Error("useCarousel must be used within a ")
40 | }
41 |
42 | return context
43 | }
44 |
45 | const Carousel = React.forwardRef<
46 | HTMLDivElement,
47 | React.HTMLAttributes & CarouselProps
48 | >(
49 | (
50 | {
51 | orientation = "horizontal",
52 | opts,
53 | setApi,
54 | plugins,
55 | className,
56 | children,
57 | ...props
58 | },
59 | ref
60 | ) => {
61 | const [carouselRef, api] = useEmblaCarousel(
62 | {
63 | ...opts,
64 | axis: orientation === "horizontal" ? "x" : "y",
65 | },
66 | plugins
67 | )
68 | const [canScrollPrev, setCanScrollPrev] = React.useState(false)
69 | const [canScrollNext, setCanScrollNext] = React.useState(false)
70 |
71 | const onSelect = React.useCallback((api: CarouselApi) => {
72 | if (!api) {
73 | return
74 | }
75 |
76 | setCanScrollPrev(api.canScrollPrev())
77 | setCanScrollNext(api.canScrollNext())
78 | }, [])
79 |
80 | const scrollPrev = React.useCallback(() => {
81 | api?.scrollPrev()
82 | }, [api])
83 |
84 | const scrollNext = React.useCallback(() => {
85 | api?.scrollNext()
86 | }, [api])
87 |
88 | const handleKeyDown = React.useCallback(
89 | (event: React.KeyboardEvent) => {
90 | if (event.key === "ArrowLeft") {
91 | event.preventDefault()
92 | scrollPrev()
93 | } else if (event.key === "ArrowRight") {
94 | event.preventDefault()
95 | scrollNext()
96 | }
97 | },
98 | [scrollPrev, scrollNext]
99 | )
100 |
101 | React.useEffect(() => {
102 | if (!api || !setApi) {
103 | return
104 | }
105 |
106 | setApi(api)
107 | }, [api, setApi])
108 |
109 | React.useEffect(() => {
110 | if (!api) {
111 | return
112 | }
113 |
114 | onSelect(api)
115 | api.on("reInit", onSelect)
116 | api.on("select", onSelect)
117 |
118 | return () => {
119 | api?.off("select", onSelect)
120 | }
121 | }, [api, onSelect])
122 |
123 | return (
124 |
137 |
145 | {children}
146 |
147 |
148 | )
149 | }
150 | )
151 | Carousel.displayName = "Carousel"
152 |
153 | const CarouselContent = React.forwardRef<
154 | HTMLDivElement,
155 | React.HTMLAttributes
156 | >(({ className, ...props }, ref) => {
157 | const { carouselRef, orientation } = useCarousel()
158 |
159 | return (
160 |
171 | )
172 | })
173 | CarouselContent.displayName = "CarouselContent"
174 |
175 | const CarouselItem = React.forwardRef<
176 | HTMLDivElement,
177 | React.HTMLAttributes
178 | >(({ className, ...props }, ref) => {
179 | const { orientation } = useCarousel()
180 |
181 | return (
182 |
193 | )
194 | })
195 | CarouselItem.displayName = "CarouselItem"
196 |
197 | const CarouselPrevious = React.forwardRef<
198 | HTMLButtonElement,
199 | React.ComponentProps
200 | >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
201 | const { orientation, scrollPrev, canScrollPrev } = useCarousel()
202 |
203 | return (
204 |
222 | )
223 | })
224 | CarouselPrevious.displayName = "CarouselPrevious"
225 |
226 | const CarouselNext = React.forwardRef<
227 | HTMLButtonElement,
228 | React.ComponentProps
229 | >(({ className, variant = "outline", size = "icon", ...props }, ref) => {
230 | const { orientation, scrollNext, canScrollNext } = useCarousel()
231 |
232 | return (
233 |
251 | )
252 | })
253 | CarouselNext.displayName = "CarouselNext"
254 |
255 | export {
256 | type CarouselApi,
257 | Carousel,
258 | CarouselContent,
259 | CarouselItem,
260 | CarouselPrevious,
261 | CarouselNext,
262 | }
263 |
--------------------------------------------------------------------------------
/src/components/ui/chart.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RechartsPrimitive from "recharts"
5 |
6 | import { cn } from "atomic-utils"
7 |
8 | // Format: { THEME_NAME: CSS_SELECTOR }
9 | const THEMES = { light: "", dark: ".dark" } as const
10 |
11 | export type ChartConfig = {
12 | [k in string]: {
13 | label?: React.ReactNode
14 | icon?: React.ComponentType
15 | } & (
16 | | { color?: string; theme?: never }
17 | | { color?: never; theme: Record }
18 | )
19 | }
20 |
21 | type ChartContextProps = {
22 | config: ChartConfig
23 | }
24 |
25 | const ChartContext = React.createContext(null)
26 |
27 | function useChart() {
28 | const context = React.useContext(ChartContext)
29 |
30 | if (!context) {
31 | throw new Error("useChart must be used within a ")
32 | }
33 |
34 | return context
35 | }
36 |
37 | const ChartContainer = React.forwardRef<
38 | HTMLDivElement,
39 | React.ComponentProps<"div"> & {
40 | config: ChartConfig
41 | children: React.ComponentProps<
42 | typeof RechartsPrimitive.ResponsiveContainer
43 | >["children"]
44 | }
45 | >(({ id, className, children, config, ...props }, ref) => {
46 | const uniqueId = React.useId()
47 | const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
48 |
49 | return (
50 |
51 |
60 |
61 |
62 | {children}
63 |
64 |
65 |
66 | )
67 | })
68 | ChartContainer.displayName = "Chart"
69 |
70 | const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
71 | const colorConfig = Object.entries(config).filter(
72 | ([, config]) => config.theme || config.color
73 | )
74 |
75 | if (!colorConfig.length) {
76 | return null
77 | }
78 |
79 | return (
80 |