├── .env.local.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── bun.lockb ├── components.json ├── drizzle.config.ts ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── prettier.config.js ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── (admin) │ │ ├── account │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── provider.tsx │ ├── (auth) │ │ └── access │ │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ └── db │ │ │ └── users │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── darkmode.tsx │ ├── navbar.tsx │ ├── sidebar.tsx │ └── ui │ │ ├── button.tsx │ │ ├── dropdown-menu.tsx │ │ └── sheet.tsx └── lib │ ├── auth.ts │ ├── database.ts │ ├── fonts.ts │ ├── react-query.ts │ └── utils.ts ├── tailwind.config.ts └── tsconfig.json /.env.local.example: -------------------------------------------------------------------------------- 1 | # hint: you can use https://github.com/nrjdalal/pglaunch to generate a postgres url 2 | POSTGRES_URL= 3 | 4 | # hint: you can use 'bunx auth secret' to generate a secret 5 | AUTH_SECRET= 6 | 7 | # ref: https://authjs.dev/getting-started/authentication/oauth 8 | AUTH_GITHUB_ID= 9 | AUTH_GITHUB_SECRET= 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

Onset Next.js Starter 2024

3 |
4 | 5 |

6 | An open source Next.js starter with step-by-step instructions if required. 7 |

8 | 9 |

10 | 11 | Follow Neeraj on Twitter 12 | 13 |

14 | 15 |

16 | Features · 17 | Step by Step · 18 | Roadmap · 19 | Author · 20 | Credits 21 |

22 | 23 | Onset is a Next.js starter that comes with step-by-step instructions to understand how everything works, easy for both beginners and experts alike and giving you the confidence to customize it to your needs. Built with Next.js 14, Drizzle (Postgres), NextAuth/Auth.js. 24 | 25 | 26 | 27 | 28 | ## Features 29 | 30 | ### Frameworks 31 | 32 | - [Next.js](https://nextjs.org/) – React framework for building performant apps with the best developer experience 33 | - [Auth.js](https://authjs.dev/) – Handle user authentication with ease with providers like Google, Twitter, GitHub, etc. 34 | - [Drizzle](https://orm.drizzle.team/) – Typescript-first ORM for Node.js 35 | 36 | ### Platforms 37 | 38 | - [Vercel](https://vercel.com/) – Easily preview & deploy changes with git 39 | - [Neon](https://neon.tech/) – The fully managed serverless Postgres with a generous free tier 40 | 41 | ## Automatic Setup 42 | 43 | ### Installation 44 | 45 | Clone & create this repo locally with the following command: 46 | 47 | > Note: You can use `npx` or `pnpx` as well 48 | 49 | ```bash 50 | bunx create-next-app onset-starter --example "https://github.com/nrjdalal/onset" 51 | ``` 52 | 53 | 1. Install dependencies using pnpm: 54 | 55 | ```sh 56 | bun install 57 | ``` 58 | 59 | 2. Copy `.env.example` to `.env.local` and update the variables. 60 | 61 | ```sh 62 | cp .env.example .env.local 63 | ``` 64 | 65 | 3. Run the database migrations: 66 | 67 | ```sh 68 | bun db:push 69 | ``` 70 | 71 | 3. Start the development server: 72 | 73 | ```sh 74 | bun dev 75 | ``` 76 | 77 | ## Step by Step 78 | 79 | > Hint: Using `bun` instead of `npm`/`pnpm` and `bunx` instead of `npx`/`pnpx`. You can use the latter if you want. 80 | 81 | ### Phase 1 (Initialization) 82 | 83 | #### 1. Initialize the project 84 | 85 | Refs: 86 | 87 | - [Installation](https://nextjs.org/docs/getting-started/installation) 88 | 89 | ```sh 90 | bunx create-next-app . --ts --eslint --tailwind --src-dir --app --import-alias "@/*" 91 | ``` 92 | 93 | #### 2. Install `prettier` and supporting plugins 94 | 95 | Refs: 96 | 97 | - [prettier-plugin-sort-imports](https://github.com/IanVS/prettier-plugin-sort-imports) 98 | - [prettier](https://prettier.io/) 99 | - [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) 100 | 101 | ```sh 102 | bun add -D @ianvs/prettier-plugin-sort-imports prettier prettier-plugin-tailwindcss 103 | ``` 104 | 105 | #### 3. Create `prettier.config.js` 106 | 107 | ```js 108 | /** @type {import('prettier').Config} */ 109 | module.exports = { 110 | semi: false, 111 | singleQuote: true, 112 | plugins: [ 113 | '@ianvs/prettier-plugin-sort-imports', 114 | 'prettier-plugin-tailwindcss', 115 | ], 116 | } 117 | ``` 118 | 119 | #### 4. Create `src/lib/fonts.ts` 120 | 121 | Refs: 122 | 123 | - [Font Optimization](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) 124 | 125 | ```ts 126 | import { 127 | JetBrains_Mono as FontMono, 128 | DM_Sans as FontSans, 129 | } from 'next/font/google' 130 | 131 | export const fontMono = FontMono({ 132 | subsets: ['latin'], 133 | variable: '--font-mono', 134 | }) 135 | 136 | export const fontSans = FontSans({ 137 | subsets: ['latin'], 138 | variable: '--font-sans', 139 | }) 140 | ``` 141 | 142 | #### 5. Install `clsx`, `tailwind-merge` and `nanoid` 143 | 144 | Refs: 145 | 146 | - [clsx](https://github.com/lukeed/clsx) 147 | - [tailwind-merge](https://github.com/dcastil/tailwind-merge) 148 | 149 | ```sh 150 | bun add clsx tailwind-merge nanoid 151 | ``` 152 | 153 | #### 6. Create `src/lib/utils.ts` 154 | 155 | ```ts 156 | import { clsx, type ClassValue } from 'clsx' 157 | import { customAlphabet } from 'nanoid' 158 | import { twMerge } from 'tailwind-merge' 159 | 160 | export const cn = (...inputs: ClassValue[]) => { 161 | return twMerge(clsx(inputs)) 162 | } 163 | 164 | export function generateId( 165 | { 166 | chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 167 | length = 12, 168 | }: { 169 | chars: string 170 | length: number 171 | } = { 172 | chars: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 173 | length: 12, 174 | }, 175 | ) { 176 | const nanoid = customAlphabet(chars, length) 177 | return nanoid() 178 | } 179 | ``` 180 | 181 | #### 7. Update `src/app/layout.tsx` 182 | 183 | ```ts 184 | import './globals.css' 185 | import { fontMono, fontSans } from '@/lib/fonts' 186 | import { cn } from '@/lib/utils' 187 | import type { Metadata } from 'next' 188 | 189 | export const metadata: Metadata = { 190 | title: 'Onset', 191 | description: 'The only Next.js starter you need', 192 | } 193 | 194 | export default function RootLayout({ 195 | children, 196 | }: { 197 | children: React.ReactNode 198 | }) { 199 | return ( 200 | 201 | 208 | {children} 209 | 210 | 211 | ) 212 | } 213 | ``` 214 | 215 | ### Phase 2 (Database) 216 | 217 | #### 1. Install `drizzle` and supporting packages 218 | 219 | Refs: 220 | 221 | - [Drizzle Postgres](https://orm.drizzle.team/docs/quick-postgresql/postgresjs) 222 | 223 | ```sh 224 | bun add drizzle-orm postgres 225 | bun add -D drizzle-kit 226 | ``` 227 | 228 | #### 2. Create `src/lib/database.ts` 229 | 230 | Refs: 231 | 232 | - [Drizzle NextAuth Schema](https://authjs.dev/getting-started/adapters/drizzle) 233 | 234 | ```ts 235 | import { 236 | integer, 237 | pgTable, 238 | primaryKey, 239 | text, 240 | timestamp, 241 | } from 'drizzle-orm/pg-core' 242 | import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js' 243 | import postgres from 'postgres' 244 | 245 | const queryClient = postgres(process.env.POSTGRES_URL as string) 246 | export const db: PostgresJsDatabase = drizzle(queryClient) 247 | 248 | export const users = pgTable('user', { 249 | id: text('id') 250 | .primaryKey() 251 | .$defaultFn(() => crypto.randomUUID()), 252 | publicId: text('publicId').unique().notNull(), 253 | name: text('name'), 254 | email: text('email').notNull(), 255 | emailVerified: timestamp('emailVerified', { mode: 'date' }), 256 | image: text('image'), 257 | }) 258 | 259 | export const accounts = pgTable( 260 | 'account', 261 | { 262 | userId: text('userId') 263 | .notNull() 264 | .references(() => users.id, { onDelete: 'cascade' }), 265 | type: text('type').notNull(), 266 | provider: text('provider').notNull(), 267 | providerAccountId: text('providerAccountId').notNull(), 268 | refresh_token: text('refresh_token'), 269 | access_token: text('access_token'), 270 | expires_at: integer('expires_at'), 271 | token_type: text('token_type'), 272 | scope: text('scope'), 273 | id_token: text('id_token'), 274 | session_state: text('session_state'), 275 | }, 276 | (account) => ({ 277 | compoundKey: primaryKey({ 278 | columns: [account.provider, account.providerAccountId], 279 | }), 280 | }), 281 | ) 282 | 283 | export const sessions = pgTable('session', { 284 | id: text('id').notNull(), 285 | sessionToken: text('sessionToken').primaryKey(), 286 | userId: text('userId') 287 | .notNull() 288 | .references(() => users.id, { onDelete: 'cascade' }), 289 | expires: timestamp('expires', { mode: 'date' }).notNull(), 290 | }) 291 | 292 | export const verificationTokens = pgTable( 293 | 'verificationToken', 294 | { 295 | identifier: text('identifier').notNull(), 296 | token: text('token').notNull(), 297 | expires: timestamp('expires', { mode: 'date' }).notNull(), 298 | }, 299 | (vt) => ({ 300 | compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), 301 | }), 302 | ) 303 | ``` 304 | 305 | #### 3. Create `drizzle.config.ts` 306 | 307 | ```ts 308 | import type { Config } from 'drizzle-kit' 309 | 310 | export default { 311 | schema: './src/lib/database.ts', 312 | dialect: 'postgresql', 313 | dbCredentials: { 314 | url: process.env.POSTGRES_URL as string, 315 | }, 316 | } satisfies Config 317 | ``` 318 | 319 | #### 4. Copy `.env.local.example` to `.env.local` 320 | 321 | > Hint: You can use [`pglaunch`](https://github.com/nrjdalal/pglaunch) to generate a postgres url 322 | 323 | ```env 324 | POSTGRES_URL="**********" 325 | ``` 326 | 327 | #### 5. Update `package.json` 328 | 329 | ```json 330 | { 331 | // ... 332 | "scripts": { 333 | // ... 334 | "db:push": "bun --env-file=.env.local drizzle-kit push", 335 | "db:studio": "bun --env-file=.env.local drizzle-kit studio" 336 | } 337 | // ... 338 | } 339 | ``` 340 | 341 | ##### 6. Run `db:push` to create tables 342 | 343 | ```sh 344 | bun db:push 345 | ``` 346 | 347 | ### Phase 3 (Authentication) 348 | 349 | #### 1. Install `next-auth` 350 | 351 | ```sh 352 | bun add next-auth@beta @auth/drizzle-adapter 353 | ``` 354 | 355 | #### 2. Update `.env.local` 356 | 357 | ```env 358 | # ... 359 | AUTH_SECRET="**********" 360 | 361 | AUTH_GITHUB_ID="**********" 362 | AUTH_GITHUB_SECRET="**********" 363 | ``` 364 | 365 | 3. Create `src/lib/auth.ts` 366 | 367 | ```ts 368 | import { db, users } from '@/lib/database' 369 | import { generateId } from '@/lib/utils' 370 | import { DrizzleAdapter } from '@auth/drizzle-adapter' 371 | import { eq } from 'drizzle-orm' 372 | import NextAuth from 'next-auth' 373 | import GitHub from 'next-auth/providers/github' 374 | 375 | export const { handlers, signIn, signOut, auth } = NextAuth({ 376 | adapter: { 377 | ...DrizzleAdapter(db), 378 | async createUser(user) { 379 | return await db 380 | .insert(users) 381 | .values({ 382 | ...user, 383 | publicId: generateId(), 384 | }) 385 | .returning() 386 | .then((res) => res[0]) 387 | }, 388 | }, 389 | providers: [GitHub], 390 | session: { 391 | strategy: 'jwt', 392 | }, 393 | callbacks: { 394 | async session({ session, token }) { 395 | if (token) { 396 | session.user.id = token.id as string 397 | session.user.publicId = token.publicId as string 398 | session.user.name = token.name as string 399 | session.user.email = token.email as string 400 | session.user.image = token.image as string 401 | } 402 | 403 | return session 404 | }, 405 | 406 | async jwt({ token, user }) { 407 | const [result] = await db 408 | .select() 409 | .from(users) 410 | .where(eq(users.email, token.email as string)) 411 | .limit(1) 412 | 413 | if (!result) { 414 | if (user) { 415 | token.id = user.id 416 | } 417 | 418 | return token 419 | } 420 | 421 | return { 422 | id: result.id, 423 | publicId: result.publicId, 424 | name: result.name, 425 | email: result.email, 426 | image: result.image, 427 | } 428 | }, 429 | }, 430 | }) 431 | 432 | declare module 'next-auth' { 433 | interface Session { 434 | user: { 435 | id: string 436 | publicId: string 437 | name: string 438 | email: string 439 | image: string 440 | } 441 | } 442 | } 443 | ``` 444 | 445 | #### 3. Create `src/app/api/auth/[...nextauth]/route.ts` 446 | 447 | ```ts 448 | import { handlers } from '@/lib/auth' 449 | 450 | export const { GET, POST } = handlers 451 | ``` 452 | 453 | #### 4. Create `src/middleware.ts` - not supported yet 454 | 455 | ```ts 456 | import { getToken } from 'next-auth/jwt' 457 | import { withAuth } from 'next-auth/middleware' 458 | import { NextResponse } from 'next/server' 459 | 460 | export default withAuth( 461 | async function middleware(req) { 462 | const token = await getToken({ req }) 463 | 464 | const isAuth = !!token 465 | const isAuthPage = req.nextUrl.pathname.startsWith('/access') 466 | 467 | if (isAuthPage) { 468 | if (isAuth) { 469 | return NextResponse.redirect(new URL('/dashboard', req.url)) 470 | } 471 | 472 | return null 473 | } 474 | 475 | if (!isAuth) { 476 | let from = req.nextUrl.pathname 477 | if (req.nextUrl.search) { 478 | from += req.nextUrl.search 479 | } 480 | 481 | return NextResponse.redirect( 482 | new URL(`/access?from=${encodeURIComponent(from)}`, req.url), 483 | ) 484 | } 485 | }, 486 | { 487 | callbacks: { 488 | async authorized() { 489 | return true 490 | }, 491 | }, 492 | }, 493 | ) 494 | 495 | export const config = { 496 | matcher: ['/access', '/dashboard/:path*'], 497 | } 498 | ``` 499 | 500 | #### 5. Create `src/app/(auth)/access/page.tsx` 501 | 502 | ```tsx 503 | import { auth, signIn } from '@/lib/auth' 504 | import { redirect } from 'next/navigation' 505 | 506 | const Page = async () => { 507 | const session = await auth() 508 | if (session) return redirect('/dashboard') 509 | 510 | return ( 511 |
512 |
{ 514 | 'use server' 515 | await signIn('github') 516 | }} 517 | > 518 | 521 |
522 |
523 | ) 524 | } 525 | 526 | export default Page 527 | ``` 528 | 529 | #### 6. Create `src/app/(admin)/dashboard/page.tsx` 530 | 531 | ```tsx 532 | import { auth, signOut } from '@/lib/auth' 533 | import { redirect } from 'next/navigation' 534 | 535 | const Page = async () => { 536 | const session = await auth() 537 | if (!session) return redirect('/access') 538 | 539 | return ( 540 |
541 |
542 | {Object.entries(session.user).map(([key, value]) => ( 543 |

544 | {key}: {value} 545 |

546 | ))} 547 |
548 | 549 |
{ 551 | 'use server' 552 | await signOut() 553 | }} 554 | > 555 | 558 |
559 |
560 | ) 561 | } 562 | 563 | export default Page 564 | ``` 565 | 566 | ## Roadmap 567 | 568 | - [ ] Light and dark mode 569 | - [ ] To added fine-grained instructions 570 | - [ ] More features and points to be added 571 | 572 | ## Author 573 | 574 | Created by [@nrjdalal](https://twitter.com/nrjdalal_com) in 2023, released under the [MIT license](https://github.com/nrjdalal/onset/blob/main/LICENSE.md). 575 | 576 | ## Credits 577 | 578 | This project is inspired by [@shadcn](https://twitter.com/shadcn)'s [Taxonomy](https://github.com/shadcn-ui/taxonomy). 579 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrjdalal/onset/cbc966ba3a4f7b712f931da4bfbb1726fc0e808b/bun.lockb -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'drizzle-kit' 2 | 3 | export default { 4 | schema: './src/lib/database.ts', 5 | dialect: 'postgresql', 6 | dbCredentials: { 7 | url: process.env.POSTGRES_URL as string, 8 | }, 9 | } satisfies Config 10 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | export default nextConfig 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "onset", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "db:push": "bun --env-file=.env.local drizzle-kit push", 11 | "db:studio": "bun --env-file=.env.local drizzle-kit studio" 12 | }, 13 | "dependencies": { 14 | "@auth/drizzle-adapter": "^1.2.0", 15 | "@radix-ui/react-dialog": "^1.0.5", 16 | "@radix-ui/react-dropdown-menu": "^2.0.6", 17 | "@radix-ui/react-icons": "^1.3.0", 18 | "@radix-ui/react-slot": "^1.0.2", 19 | "@tanstack/react-query": "^5.45.1", 20 | "class-variance-authority": "^0.7.0", 21 | "clsx": "^2.1.1", 22 | "drizzle-orm": "^0.31.1", 23 | "nanoid": "^5.0.7", 24 | "next": "14.2.3", 25 | "next-auth": "^5.0.0-beta.19", 26 | "next-themes": "^0.3.0", 27 | "postgres": "^3.4.4", 28 | "react": "^18.3.1", 29 | "react-dom": "^18.3.1", 30 | "tailwind-merge": "^2.3.0" 31 | }, 32 | "devDependencies": { 33 | "@ianvs/prettier-plugin-sort-imports": "^4.2.1", 34 | "@tanstack/react-query-devtools": "^5.45.1", 35 | "@types/node": "^20.14.1", 36 | "@types/react": "^18.3.3", 37 | "@types/react-dom": "^18.3.0", 38 | "drizzle-kit": "^0.22.2", 39 | "eslint": "^8.57.0", 40 | "eslint-config-next": "14.2.3", 41 | "postcss": "^8.4.38", 42 | "prettier": "^3.3.0", 43 | "prettier-plugin-tailwindcss": "^0.6.1", 44 | "tailwindcss": "^3.4.3", 45 | "tailwindcss-animate": "^1.0.7", 46 | "typescript": "^5.4.5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | } 7 | 8 | export default config 9 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | module.exports = { 3 | semi: false, 4 | singleQuote: true, 5 | plugins: [ 6 | '@ianvs/prettier-plugin-sort-imports', 7 | 'prettier-plugin-tailwindcss', 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/(admin)/account/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | import { auth, signOut } from '@/lib/auth' 3 | import { Pencil1Icon } from '@radix-ui/react-icons' 4 | import { redirect } from 'next/navigation' 5 | 6 | export default async function Page() { 7 | const session = await auth() 8 | if (!session) return redirect('/access') 9 | 10 | return ( 11 |
12 |
13 |

Account

14 |

Easily manage your account

15 |
16 | 17 |
18 |
19 |

User ID

20 |

{session.user.publicId}

21 |

22 | Please use this as reference ID when contacting support 23 |

24 |
25 | 26 |
27 |

Name

28 |
29 |

{session.user.name}

30 | 31 |
32 |
33 | 34 |
35 |

Email

36 |
37 |

{session.user.email}

38 | 39 |
40 |
41 | 42 |
{ 44 | 'use server' 45 | await signOut() 46 | }} 47 | > 48 | 54 |
55 |
56 |
57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/app/(admin)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 |

Dashboard

8 |

9 | Welcome back! Here's a quick overview of your account! 10 |

11 |
12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/app/(admin)/layout.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | 3 | import Provider from '@/app/(admin)/provider' 4 | import Navbar from '@/components/navbar' 5 | import Sidebar from '@/components/sidebar' 6 | import { auth } from '@/lib/auth' 7 | import { redirect } from 'next/navigation' 8 | 9 | export default async function Layout({ 10 | children, 11 | }: { 12 | children: React.ReactNode 13 | }) { 14 | const session = await auth() 15 | if (!session) return redirect('/access') 16 | 17 | return ( 18 | 19 |
20 | 21 | 22 |
23 | 24 | 25 |
{children}
26 |
27 |
28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/app/(admin)/provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 4 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools' 5 | import { ThemeProvider as NextThemesProvider } from 'next-themes' 6 | import React from 'react' 7 | 8 | export default function Provider({ children }: { children: React.ReactNode }) { 9 | const [queryClient] = React.useState(() => new QueryClient()) 10 | 11 | return ( 12 | // 13 | 14 | 20 | {children} 21 | 22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/app/(auth)/access/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth, signIn } from '@/lib/auth' 2 | import { ChevronLeftIcon } from '@radix-ui/react-icons' 3 | import Link from 'next/link' 4 | import { redirect } from 'next/navigation' 5 | 6 | const Page = async () => { 7 | const session = await auth() 8 | if (session) return redirect('/dashboard') 9 | 10 | return ( 11 |
12 | 13 | {' '} 14 | Back to Home 15 | 16 |
17 | 18 | 22 | 23 | 27 | 28 |
{ 30 | 'use server' 31 | await signIn('email') 32 | }} 33 | > 34 | 40 |
41 | 42 |
43 |
44 | Or continue with 45 |
46 |
47 | 48 |
49 |
{ 51 | 'use server' 52 | await signIn('github') 53 | }} 54 | > 55 | 72 |
73 | 74 |
{ 76 | 'use server' 77 | await signIn('google') 78 | }} 79 | > 80 | 111 |
112 |
113 |
114 |
115 | ) 116 | } 117 | 118 | export default Page 119 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from '@/lib/auth' 2 | 3 | export const { GET, POST } = handlers 4 | -------------------------------------------------------------------------------- /src/app/api/db/users/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth' 2 | import { NextResponse } from 'next/server' 3 | 4 | export async function GET() { 5 | const session = await auth() 6 | 7 | if (!session) { 8 | return NextResponse.redirect('/access') 9 | } 10 | 11 | return NextResponse.json({ 12 | ...session.user, 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrjdalal/onset/cbc966ba3a4f7b712f931da4bfbb1726fc0e808b/src/app/favicon.ico -------------------------------------------------------------------------------- /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: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import { fontMono, fontSans } from '@/lib/fonts' 3 | import { cn } from '@/lib/utils' 4 | import type { Metadata } from 'next' 5 | 6 | export const metadata: Metadata = { 7 | title: 'Onset', 8 | description: 'The only Next.js starter you need', 9 | } 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode 15 | }) { 16 | return ( 17 | 18 | 25 | {children} 26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | export default async function Page() { 4 | return ( 5 |
6 |
7 |
8 | 13 | Follow along on Twitter 14 | 15 |

16 | An open source Next.js bare starter. 17 |
18 | With step-by-step instructions if required. 19 |

20 |

21 | Onset is a Next.js starter that comes with step-by-step instructions 22 | to understand how everything works, easy for both beginners and 23 | experts alike and giving you the confidence to customize it to your 24 | needs. Built with Next.js 14, Drizzle (Postgres), NextAuth/Auth.js. 25 |

26 |
27 | 31 | Login 32 | 33 | 39 | GitHub 40 | 41 |
42 |
43 |
44 | 45 |
46 |
47 |

48 | Proudly Open Source 49 |

50 |

51 | Onset is open source and powered by open source software.
The 52 | code is available on{' '} 53 | 59 | GitHub 60 | 61 | .{' '} 62 |

63 |
64 |
65 |
66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /src/components/darkmode.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Button } from '@/components/ui/button' 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuTrigger, 9 | } from '@/components/ui/dropdown-menu' 10 | import { MoonIcon, SunIcon } from '@radix-ui/react-icons' 11 | import { useTheme } from 'next-themes' 12 | import * as React from 'react' 13 | 14 | export default function ModeToggle() { 15 | const { setTheme } = useTheme() 16 | 17 | return ( 18 | 19 | 20 | 25 | 26 | 27 | setTheme('light')}> 28 | Light 29 | 30 | setTheme('dark')}> 31 | Dark 32 | 33 | setTheme('system')}> 34 | System 35 | 36 | 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/components/navbar.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | 3 | 'use client' 4 | 5 | import { MobileSidebar } from '@/components/sidebar' 6 | import { SyncUser } from '@/lib/react-query' 7 | import Link from 'next/link' 8 | 9 | export default function Navbar() { 10 | const { data: userData } = SyncUser() 11 | return ( 12 | 30 | ) 31 | } 32 | 33 | export const Logo = ({ ...props }: any) => { 34 | return ( 35 | 40 | 47 | 48 | 49 |

ACME

50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Darkmode from '@/components/darkmode' 4 | import { Logo } from '@/components/navbar' 5 | import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet' 6 | import { SyncUser } from '@/lib/react-query' 7 | import { cn } from '@/lib/utils' 8 | import { 9 | HamburgerMenuIcon, 10 | PlusIcon, 11 | StitchesLogoIcon, 12 | } from '@radix-ui/react-icons' 13 | import Link from 'next/link' 14 | import { usePathname } from 'next/navigation' 15 | import { useState } from 'react' 16 | 17 | const data = [ 18 | { 19 | title: 'Dashboard', 20 | href: '/dashboard', 21 | }, 22 | { 23 | title: 'Account', 24 | href: '/account', 25 | }, 26 | ] as { 27 | title: string 28 | href: string 29 | devOnly?: boolean 30 | }[] 31 | 32 | export const MobileSidebar = () => { 33 | const pathname = usePathname() 34 | 35 | const [open, setOpen] = useState(false) 36 | 37 | return ( 38 | setOpen(!open)}> 39 | 40 | 41 | 42 | 43 |
44 | { 46 | setOpen(!open) 47 | }} 48 | /> 49 |
50 | 51 | { 54 | setOpen(!open) 55 | }} 56 | /> 57 |
58 |
59 | ) 60 | } 61 | 62 | export default function Sidebar() { 63 | const pathname = usePathname() 64 | 65 | return ( 66 |
67 | 68 |
69 | ) 70 | } 71 | 72 | interface SidebarItemsProps { 73 | pathname: string 74 | onClick?: () => void 75 | } 76 | 77 | const SidebarItems = ({ pathname, ...props }: SidebarItemsProps) => { 78 | const { data: userData, isLoading: userIsLoading } = SyncUser() 79 | 80 | return ( 81 |
82 |
83 | {data.map((item) => ( 84 | 92 | ))} 93 |
94 | 95 |
96 | 97 | 98 |
99 | ) 100 | } 101 | 102 | const SidebarItem = ({ 103 | title, 104 | href, 105 | active, 106 | devOnly, 107 | ...props 108 | }: { 109 | title: string 110 | href: string 111 | active: boolean 112 | devOnly?: boolean 113 | props?: any 114 | }) => { 115 | return ( 116 | 125 | {title} 126 | 127 | ) 128 | } 129 | -------------------------------------------------------------------------------- /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/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/sheet.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { cn } from '@/lib/utils' 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 | import * as React from 'react' 8 | 9 | const Sheet = SheetPrimitive.Root 10 | 11 | const SheetTrigger = SheetPrimitive.Trigger 12 | 13 | const SheetClose = SheetPrimitive.Close 14 | 15 | const SheetPortal = SheetPrimitive.Portal 16 | 17 | const SheetOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName 31 | 32 | const sheetVariants = cva( 33 | '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', 34 | { 35 | variants: { 36 | side: { 37 | top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', 38 | bottom: 39 | 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', 40 | 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', 41 | right: 42 | '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', 43 | }, 44 | }, 45 | defaultVariants: { 46 | side: 'right', 47 | }, 48 | }, 49 | ) 50 | 51 | interface SheetContentProps 52 | extends React.ComponentPropsWithoutRef, 53 | VariantProps {} 54 | 55 | const SheetContent = React.forwardRef< 56 | React.ElementRef, 57 | SheetContentProps 58 | >(({ side = 'right', className, children, ...props }, ref) => ( 59 | 60 | 61 | 66 | {children} 67 | 68 | 69 | Close 70 | 71 | 72 | 73 | )) 74 | SheetContent.displayName = SheetPrimitive.Content.displayName 75 | 76 | const SheetHeader = ({ 77 | className, 78 | ...props 79 | }: React.HTMLAttributes) => ( 80 |
87 | ) 88 | SheetHeader.displayName = 'SheetHeader' 89 | 90 | const SheetFooter = ({ 91 | className, 92 | ...props 93 | }: React.HTMLAttributes) => ( 94 |
101 | ) 102 | SheetFooter.displayName = 'SheetFooter' 103 | 104 | const SheetTitle = React.forwardRef< 105 | React.ElementRef, 106 | React.ComponentPropsWithoutRef 107 | >(({ className, ...props }, ref) => ( 108 | 113 | )) 114 | SheetTitle.displayName = SheetPrimitive.Title.displayName 115 | 116 | const SheetDescription = React.forwardRef< 117 | React.ElementRef, 118 | React.ComponentPropsWithoutRef 119 | >(({ className, ...props }, ref) => ( 120 | 125 | )) 126 | SheetDescription.displayName = SheetPrimitive.Description.displayName 127 | 128 | export { 129 | Sheet, 130 | SheetPortal, 131 | SheetOverlay, 132 | SheetTrigger, 133 | SheetClose, 134 | SheetContent, 135 | SheetHeader, 136 | SheetFooter, 137 | SheetTitle, 138 | SheetDescription, 139 | } 140 | -------------------------------------------------------------------------------- /src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { db, users } from '@/lib/database' 2 | import { generateId } from '@/lib/utils' 3 | import { DrizzleAdapter } from '@auth/drizzle-adapter' 4 | import { eq } from 'drizzle-orm' 5 | import NextAuth from 'next-auth' 6 | import GitHub from 'next-auth/providers/github' 7 | 8 | export const { handlers, signIn, signOut, auth } = NextAuth({ 9 | adapter: { 10 | ...DrizzleAdapter(db), 11 | async createUser(user) { 12 | return await db 13 | .insert(users) 14 | .values({ 15 | ...user, 16 | publicId: generateId(), 17 | }) 18 | .returning() 19 | .then((res) => res[0]) 20 | }, 21 | }, 22 | providers: [GitHub], 23 | session: { 24 | strategy: 'jwt', 25 | }, 26 | callbacks: { 27 | async session({ session, token }) { 28 | if (token) { 29 | session.user.id = token.id as string 30 | session.user.publicId = token.publicId as string 31 | session.user.name = token.name as string 32 | session.user.email = token.email as string 33 | session.user.image = token.image as string 34 | } 35 | 36 | return session 37 | }, 38 | 39 | async jwt({ token, user }) { 40 | const [result] = await db 41 | .select() 42 | .from(users) 43 | .where(eq(users.email, token.email as string)) 44 | .limit(1) 45 | 46 | if (!result) { 47 | if (user) { 48 | token.id = user.id 49 | } 50 | 51 | return token 52 | } 53 | 54 | return { 55 | id: result.id, 56 | publicId: result.publicId, 57 | name: result.name, 58 | email: result.email, 59 | image: result.image, 60 | } 61 | }, 62 | }, 63 | }) 64 | 65 | declare module 'next-auth' { 66 | interface Session { 67 | user: { 68 | id: string 69 | publicId: string 70 | name: string 71 | email: string 72 | image: string 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/lib/database.ts: -------------------------------------------------------------------------------- 1 | import { 2 | boolean, 3 | integer, 4 | pgTable, 5 | primaryKey, 6 | text, 7 | timestamp, 8 | } from 'drizzle-orm/pg-core' 9 | import { drizzle } from 'drizzle-orm/postgres-js' 10 | import type { AdapterAccountType } from 'next-auth/adapters' 11 | import postgres from 'postgres' 12 | 13 | const connectionString = process.env.POSTGRES_URL as string 14 | const pool = postgres(connectionString, { max: 1 }) 15 | 16 | export const db = drizzle(pool) 17 | 18 | export const users = pgTable('user', { 19 | id: text('id') 20 | .primaryKey() 21 | .$defaultFn(() => crypto.randomUUID()), 22 | publicId: text('publicId').unique().notNull(), 23 | name: text('name'), 24 | email: text('email').notNull(), 25 | emailVerified: timestamp('emailVerified', { mode: 'date' }), 26 | image: text('image'), 27 | }) 28 | 29 | export const accounts = pgTable( 30 | 'account', 31 | { 32 | userId: text('userId') 33 | .notNull() 34 | .references(() => users.id, { onDelete: 'cascade' }), 35 | type: text('type').$type().notNull(), 36 | provider: text('provider').notNull(), 37 | providerAccountId: text('providerAccountId').notNull(), 38 | refresh_token: text('refresh_token'), 39 | access_token: text('access_token'), 40 | expires_at: integer('expires_at'), 41 | token_type: text('token_type'), 42 | scope: text('scope'), 43 | id_token: text('id_token'), 44 | session_state: text('session_state'), 45 | }, 46 | (account) => ({ 47 | compoundKey: primaryKey({ 48 | columns: [account.provider, account.providerAccountId], 49 | }), 50 | }), 51 | ) 52 | 53 | export const sessions = pgTable('session', { 54 | sessionToken: text('sessionToken').primaryKey(), 55 | userId: text('userId') 56 | .notNull() 57 | .references(() => users.id, { onDelete: 'cascade' }), 58 | expires: timestamp('expires', { mode: 'date' }).notNull(), 59 | }) 60 | 61 | export const verificationTokens = pgTable( 62 | 'verificationToken', 63 | { 64 | identifier: text('identifier').notNull(), 65 | token: text('token').notNull(), 66 | expires: timestamp('expires', { mode: 'date' }).notNull(), 67 | }, 68 | (verificationToken) => ({ 69 | compositePk: primaryKey({ 70 | columns: [verificationToken.identifier, verificationToken.token], 71 | }), 72 | }), 73 | ) 74 | -------------------------------------------------------------------------------- /src/lib/fonts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JetBrains_Mono as FontMono, 3 | DM_Sans as FontSans, 4 | } from 'next/font/google' 5 | 6 | export const fontMono = FontMono({ 7 | subsets: ['latin'], 8 | variable: '--font-mono', 9 | }) 10 | 11 | export const fontSans = FontSans({ 12 | subsets: ['latin'], 13 | variable: '--font-sans', 14 | }) 15 | -------------------------------------------------------------------------------- /src/lib/react-query.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query' 2 | 3 | export const SyncUser = () => { 4 | return useQuery({ 5 | queryKey: ['user'], 6 | queryFn: async () => { 7 | const response = await fetch('/api/db/users') 8 | return response.json() 9 | }, 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx' 2 | import { customAlphabet } from 'nanoid' 3 | import { twMerge } from 'tailwind-merge' 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)) 7 | } 8 | 9 | export function generateId( 10 | { 11 | chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 12 | length = 12, 13 | }: { 14 | chars: string 15 | length: number 16 | } = { 17 | chars: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 18 | length: 12, 19 | }, 20 | ) { 21 | const nanoid = customAlphabet(chars, length) 22 | return nanoid() 23 | } 24 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config = { 4 | darkMode: ['class'], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | prefix: '', 12 | theme: { 13 | extend: { 14 | fontFamily: { 15 | sans: ['var(--font-sans)', 'sans-serif'], 16 | mono: ['var(--font-mono)', 'monospace'], 17 | }, 18 | colors: { 19 | border: 'hsl(var(--border))', 20 | input: 'hsl(var(--input))', 21 | ring: 'hsl(var(--ring))', 22 | background: 'hsl(var(--background))', 23 | foreground: 'hsl(var(--foreground))', 24 | primary: { 25 | DEFAULT: 'hsl(var(--primary))', 26 | foreground: 'hsl(var(--primary-foreground))', 27 | }, 28 | secondary: { 29 | DEFAULT: 'hsl(var(--secondary))', 30 | foreground: 'hsl(var(--secondary-foreground))', 31 | }, 32 | destructive: { 33 | DEFAULT: 'hsl(var(--destructive))', 34 | foreground: 'hsl(var(--destructive-foreground))', 35 | }, 36 | muted: { 37 | DEFAULT: 'hsl(var(--muted))', 38 | foreground: 'hsl(var(--muted-foreground))', 39 | }, 40 | accent: { 41 | DEFAULT: 'hsl(var(--accent))', 42 | foreground: 'hsl(var(--accent-foreground))', 43 | }, 44 | popover: { 45 | DEFAULT: 'hsl(var(--popover))', 46 | foreground: 'hsl(var(--popover-foreground))', 47 | }, 48 | card: { 49 | DEFAULT: 'hsl(var(--card))', 50 | foreground: 'hsl(var(--card-foreground))', 51 | }, 52 | }, 53 | borderRadius: { 54 | lg: 'var(--radius)', 55 | md: 'calc(var(--radius) - 2px)', 56 | sm: 'calc(var(--radius) - 4px)', 57 | }, 58 | keyframes: { 59 | 'accordion-down': { 60 | from: { height: '0' }, 61 | to: { height: 'var(--radix-accordion-content-height)' }, 62 | }, 63 | 'accordion-up': { 64 | from: { height: 'var(--radix-accordion-content-height)' }, 65 | to: { height: '0' }, 66 | }, 67 | }, 68 | animation: { 69 | 'accordion-down': 'accordion-down 0.2s ease-out', 70 | 'accordion-up': 'accordion-up 0.2s ease-out', 71 | }, 72 | }, 73 | }, 74 | plugins: [require('tailwindcss-animate')], 75 | } satisfies Config 76 | 77 | export default config 78 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------