├── .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 |
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 |
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 |
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 |
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 |
Email Address
18 |
22 |
Password
23 |
27 |
28 |
41 |
42 |
43 |
44 | Or continue with
45 |
46 |
47 |
48 |
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 |
21 |
22 |
23 | Toggle theme
24 |
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 |
13 |
14 |
15 |
16 |
17 |
26 |
27 |
28 |
29 |
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 |
--------------------------------------------------------------------------------