├── .env.example
├── .gitignore
├── LICENSE.md
├── README.md
├── bun.lock
├── components.json
├── docker-compose.yml
├── drizzle.config.ts
├── drizzle
└── migrations
│ ├── 0000_deep_obadiah_stane.sql
│ └── meta
│ ├── 0000_snapshot.json
│ └── _journal.json
├── next.config.ts
├── package.json
├── postcss.config.mjs
├── public
├── goku.svg
└── og.png
├── src
├── app
│ ├── (auth)
│ │ ├── layout.tsx
│ │ ├── sign-in
│ │ │ └── page.tsx
│ │ └── sign-up
│ │ │ └── page.tsx
│ ├── (dashboard)
│ │ ├── dashboard
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── (marketing)
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── api
│ │ └── auth
│ │ │ └── [...all]
│ │ │ └── route.ts
│ ├── error.tsx
│ ├── layout.tsx
│ └── not-found.tsx
├── components
│ ├── auth-card.tsx
│ ├── icons.tsx
│ ├── providers
│ │ └── index.tsx
│ ├── theme
│ │ ├── provider.tsx
│ │ └── toggler.tsx
│ ├── ui
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── popover.tsx
│ │ └── sonner.tsx
│ └── user-profile.tsx
├── config
│ └── site.config.ts
├── env.ts
├── lib
│ ├── auth-client.ts
│ ├── auth-utils.ts
│ ├── auth.ts
│ ├── db
│ │ ├── index.ts
│ │ ├── migrate.ts
│ │ └── schema.ts
│ └── utils.ts
├── middleware.ts
├── styles
│ └── globals.css
└── types
│ └── index.d.ts
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | # Better Auth
2 | BETTER_AUTH_SECRET=
3 | BETTER_AUTH_URL=
4 |
5 | # Database
6 | DATABASE_URL=
7 |
8 | # Github
9 | GITHUB_CLIENT_ID=
10 | GITHUB_CLIENT_SECRET=
11 |
12 | # Google
13 | GOOGLE_CLIENT_ID=
14 | GOOGLE_CLIENT_SECRET=
15 |
16 | # Discord
17 | DISCORD_CLIENT_ID=
18 | DISCORD_CLIENT_SECRET=
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # keep .env.example
37 | !.env.example
38 |
39 | # vercel
40 | .vercel
41 |
42 | # typescript
43 | *.tsbuildinfo
44 | next-env.d.ts
45 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright (c) 2025 rds_agi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Titan
2 |
3 | Next.js 15 fullstack template with better-auth for authentication and drizzle-orm as the ORM.
4 |
5 | 
6 |
7 | > [!WARNING]
8 | > This project uses Next.js 15-canary to support node runtime on middleware. This is not yet supported in stable version.
9 |
10 | ## Tech Stack
11 |
12 | - Full-stack framework: Next.js 15-canary
13 | - UI: Tailwind CSS v4
14 | - Component library: Shadcn UI
15 | - Authentication: better-auth
16 | - Database: postgres
17 | - ORM: drizzle-orm
18 |
19 | ## Features
20 |
21 | - Authentication
22 | - Social login
23 | - Google
24 | - Github
25 | - Discord
26 | - Database
27 | - Postgres (Neon)
28 | - ORM: drizzle-orm
29 | - Next.js API, server actions, and middleware
30 |
31 | ## Getting Started
32 |
33 | Clone the repository
34 |
35 | ```bash
36 | git clone https://github.com/rudrodip/titan.git
37 | ```
38 |
39 | Install dependencies
40 |
41 | ```bash
42 | bun install
43 | ```
44 |
45 | Create environment file
46 |
47 | ```bash
48 | cp .env.example .env
49 | ```
50 |
51 | Provide environment variables in `.env` file
52 |
53 | - `BETTER_AUTH_SECRET`: Secret key for Better Auth authentication generate one [here](https://www.better-auth.com/docs/installation#set-environment-variables)
54 | - `BETTER_AUTH_URL`: Better Auth URL (e.g., `http://localhost:3000`)
55 | - `DATABASE_URL`: PostgreSQL connection string provided from Neon (e.g., `postgresql://username:password@neon:5432/titan`)
56 |
57 | Generate database schema
58 |
59 | ```bash
60 | bun run db:generate
61 | ```
62 |
63 | Migrate database
64 |
65 | ```bash
66 | bun run db:migrate
67 | ```
68 |
69 | Run the development server
70 |
71 | ```bash
72 | bun dev
73 | ```
74 |
75 | Open the browser and navigate to `http://localhost:3000`
76 |
77 | ## Using a Local Database
78 |
79 | Have Docker installed on your system. Before running the db:generate command from Getting Started, run the following command in the project directory to start a local database:
80 |
81 | ```bash
82 | docker-compose up -d
83 | ```
84 |
85 | Use the following environment variables in `.env` file:
86 | - `DATABASE_URL`: `postgres://postgres:postgres@localhost:5432/titan`
87 |
88 | Add the `pg` and `@types/pg` dependencies to your project:
89 |
90 | ```bash
91 | bun add pg
92 | bun add -D @types/pg
93 | ```
94 |
95 | Then, change the `/src/lib/db/index.ts` file to use the `drizzle-orm/node-postgres` and `pg` package instead of `@neondatabase/serverless`:
96 |
97 | ```typescript
98 | import * as schema from "@/lib/db/schema";
99 | import { drizzle } from "drizzle-orm/node-postgres";
100 | import { Pool } from "pg";
101 |
102 | const sql = new Pool({
103 | connectionString: process.env.DATABASE_URL,
104 | });
105 | export const db = drizzle(sql, { schema });
106 | ```
107 |
108 | Continue steps from Getting Started e.g. generating the database schema, applying migrations, and running the dev server.
109 |
110 | Open the browser and navigate to `http://localhost:3000`
111 |
--------------------------------------------------------------------------------
/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": "",
8 | "css": "src/styles/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | postgres:
5 | image: postgres:16
6 | container_name: postgres_db
7 | restart: always
8 | environment:
9 | POSTGRES_USER: postgres
10 | POSTGRES_PASSWORD: postgres
11 | POSTGRES_DB: titan
12 | ports:
13 | - "5432:5432"
14 | volumes:
15 | - postgres_data:/var/lib/postgresql/data
16 |
17 | volumes:
18 | postgres_data:
19 | driver: local
20 |
--------------------------------------------------------------------------------
/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import { config } from 'dotenv';
2 | import { defineConfig } from "drizzle-kit";
3 |
4 | config({ path: '.env.local' });
5 |
6 | export default defineConfig({
7 | schema: "./src/lib/db/schema.ts",
8 | out: "./drizzle/migrations",
9 | dialect: "postgresql",
10 | dbCredentials: {
11 | url: process.env.DATABASE_URL!,
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/drizzle/migrations/0000_deep_obadiah_stane.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE "account" (
2 | "id" text PRIMARY KEY NOT NULL,
3 | "account_id" text NOT NULL,
4 | "provider_id" text NOT NULL,
5 | "user_id" text NOT NULL,
6 | "access_token" text,
7 | "refresh_token" text,
8 | "id_token" text,
9 | "access_token_expires_at" timestamp,
10 | "refresh_token_expires_at" timestamp,
11 | "scope" text,
12 | "password" text,
13 | "created_at" timestamp NOT NULL,
14 | "updated_at" timestamp NOT NULL
15 | );
16 | --> statement-breakpoint
17 | CREATE TABLE "session" (
18 | "id" text PRIMARY KEY NOT NULL,
19 | "expires_at" timestamp NOT NULL,
20 | "token" text NOT NULL,
21 | "created_at" timestamp NOT NULL,
22 | "updated_at" timestamp NOT NULL,
23 | "ip_address" text,
24 | "user_agent" text,
25 | "user_id" text NOT NULL,
26 | CONSTRAINT "session_token_unique" UNIQUE("token")
27 | );
28 | --> statement-breakpoint
29 | CREATE TABLE "user" (
30 | "id" text PRIMARY KEY NOT NULL,
31 | "name" text NOT NULL,
32 | "email" text NOT NULL,
33 | "email_verified" boolean NOT NULL,
34 | "image" text,
35 | "created_at" timestamp NOT NULL,
36 | "updated_at" timestamp NOT NULL,
37 | CONSTRAINT "user_email_unique" UNIQUE("email")
38 | );
39 | --> statement-breakpoint
40 | CREATE TABLE "verification" (
41 | "id" text PRIMARY KEY NOT NULL,
42 | "identifier" text NOT NULL,
43 | "value" text NOT NULL,
44 | "expires_at" timestamp NOT NULL,
45 | "created_at" timestamp,
46 | "updated_at" timestamp
47 | );
48 | --> statement-breakpoint
49 | ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
50 | ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
--------------------------------------------------------------------------------
/drizzle/migrations/meta/0000_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "ae865c9a-dea7-4ef0-8c0e-5c3bbb0555da",
3 | "prevId": "00000000-0000-0000-0000-000000000000",
4 | "version": "7",
5 | "dialect": "postgresql",
6 | "tables": {
7 | "public.account": {
8 | "name": "account",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "text",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "account_id": {
18 | "name": "account_id",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true
22 | },
23 | "provider_id": {
24 | "name": "provider_id",
25 | "type": "text",
26 | "primaryKey": false,
27 | "notNull": true
28 | },
29 | "user_id": {
30 | "name": "user_id",
31 | "type": "text",
32 | "primaryKey": false,
33 | "notNull": true
34 | },
35 | "access_token": {
36 | "name": "access_token",
37 | "type": "text",
38 | "primaryKey": false,
39 | "notNull": false
40 | },
41 | "refresh_token": {
42 | "name": "refresh_token",
43 | "type": "text",
44 | "primaryKey": false,
45 | "notNull": false
46 | },
47 | "id_token": {
48 | "name": "id_token",
49 | "type": "text",
50 | "primaryKey": false,
51 | "notNull": false
52 | },
53 | "access_token_expires_at": {
54 | "name": "access_token_expires_at",
55 | "type": "timestamp",
56 | "primaryKey": false,
57 | "notNull": false
58 | },
59 | "refresh_token_expires_at": {
60 | "name": "refresh_token_expires_at",
61 | "type": "timestamp",
62 | "primaryKey": false,
63 | "notNull": false
64 | },
65 | "scope": {
66 | "name": "scope",
67 | "type": "text",
68 | "primaryKey": false,
69 | "notNull": false
70 | },
71 | "password": {
72 | "name": "password",
73 | "type": "text",
74 | "primaryKey": false,
75 | "notNull": false
76 | },
77 | "created_at": {
78 | "name": "created_at",
79 | "type": "timestamp",
80 | "primaryKey": false,
81 | "notNull": true
82 | },
83 | "updated_at": {
84 | "name": "updated_at",
85 | "type": "timestamp",
86 | "primaryKey": false,
87 | "notNull": true
88 | }
89 | },
90 | "indexes": {},
91 | "foreignKeys": {
92 | "account_user_id_user_id_fk": {
93 | "name": "account_user_id_user_id_fk",
94 | "tableFrom": "account",
95 | "tableTo": "user",
96 | "columnsFrom": [
97 | "user_id"
98 | ],
99 | "columnsTo": [
100 | "id"
101 | ],
102 | "onDelete": "cascade",
103 | "onUpdate": "no action"
104 | }
105 | },
106 | "compositePrimaryKeys": {},
107 | "uniqueConstraints": {},
108 | "policies": {},
109 | "checkConstraints": {},
110 | "isRLSEnabled": false
111 | },
112 | "public.session": {
113 | "name": "session",
114 | "schema": "",
115 | "columns": {
116 | "id": {
117 | "name": "id",
118 | "type": "text",
119 | "primaryKey": true,
120 | "notNull": true
121 | },
122 | "expires_at": {
123 | "name": "expires_at",
124 | "type": "timestamp",
125 | "primaryKey": false,
126 | "notNull": true
127 | },
128 | "token": {
129 | "name": "token",
130 | "type": "text",
131 | "primaryKey": false,
132 | "notNull": true
133 | },
134 | "created_at": {
135 | "name": "created_at",
136 | "type": "timestamp",
137 | "primaryKey": false,
138 | "notNull": true
139 | },
140 | "updated_at": {
141 | "name": "updated_at",
142 | "type": "timestamp",
143 | "primaryKey": false,
144 | "notNull": true
145 | },
146 | "ip_address": {
147 | "name": "ip_address",
148 | "type": "text",
149 | "primaryKey": false,
150 | "notNull": false
151 | },
152 | "user_agent": {
153 | "name": "user_agent",
154 | "type": "text",
155 | "primaryKey": false,
156 | "notNull": false
157 | },
158 | "user_id": {
159 | "name": "user_id",
160 | "type": "text",
161 | "primaryKey": false,
162 | "notNull": true
163 | }
164 | },
165 | "indexes": {},
166 | "foreignKeys": {
167 | "session_user_id_user_id_fk": {
168 | "name": "session_user_id_user_id_fk",
169 | "tableFrom": "session",
170 | "tableTo": "user",
171 | "columnsFrom": [
172 | "user_id"
173 | ],
174 | "columnsTo": [
175 | "id"
176 | ],
177 | "onDelete": "cascade",
178 | "onUpdate": "no action"
179 | }
180 | },
181 | "compositePrimaryKeys": {},
182 | "uniqueConstraints": {
183 | "session_token_unique": {
184 | "name": "session_token_unique",
185 | "nullsNotDistinct": false,
186 | "columns": [
187 | "token"
188 | ]
189 | }
190 | },
191 | "policies": {},
192 | "checkConstraints": {},
193 | "isRLSEnabled": false
194 | },
195 | "public.user": {
196 | "name": "user",
197 | "schema": "",
198 | "columns": {
199 | "id": {
200 | "name": "id",
201 | "type": "text",
202 | "primaryKey": true,
203 | "notNull": true
204 | },
205 | "name": {
206 | "name": "name",
207 | "type": "text",
208 | "primaryKey": false,
209 | "notNull": true
210 | },
211 | "email": {
212 | "name": "email",
213 | "type": "text",
214 | "primaryKey": false,
215 | "notNull": true
216 | },
217 | "email_verified": {
218 | "name": "email_verified",
219 | "type": "boolean",
220 | "primaryKey": false,
221 | "notNull": true
222 | },
223 | "image": {
224 | "name": "image",
225 | "type": "text",
226 | "primaryKey": false,
227 | "notNull": false
228 | },
229 | "created_at": {
230 | "name": "created_at",
231 | "type": "timestamp",
232 | "primaryKey": false,
233 | "notNull": true
234 | },
235 | "updated_at": {
236 | "name": "updated_at",
237 | "type": "timestamp",
238 | "primaryKey": false,
239 | "notNull": true
240 | }
241 | },
242 | "indexes": {},
243 | "foreignKeys": {},
244 | "compositePrimaryKeys": {},
245 | "uniqueConstraints": {
246 | "user_email_unique": {
247 | "name": "user_email_unique",
248 | "nullsNotDistinct": false,
249 | "columns": [
250 | "email"
251 | ]
252 | }
253 | },
254 | "policies": {},
255 | "checkConstraints": {},
256 | "isRLSEnabled": false
257 | },
258 | "public.verification": {
259 | "name": "verification",
260 | "schema": "",
261 | "columns": {
262 | "id": {
263 | "name": "id",
264 | "type": "text",
265 | "primaryKey": true,
266 | "notNull": true
267 | },
268 | "identifier": {
269 | "name": "identifier",
270 | "type": "text",
271 | "primaryKey": false,
272 | "notNull": true
273 | },
274 | "value": {
275 | "name": "value",
276 | "type": "text",
277 | "primaryKey": false,
278 | "notNull": true
279 | },
280 | "expires_at": {
281 | "name": "expires_at",
282 | "type": "timestamp",
283 | "primaryKey": false,
284 | "notNull": true
285 | },
286 | "created_at": {
287 | "name": "created_at",
288 | "type": "timestamp",
289 | "primaryKey": false,
290 | "notNull": false
291 | },
292 | "updated_at": {
293 | "name": "updated_at",
294 | "type": "timestamp",
295 | "primaryKey": false,
296 | "notNull": false
297 | }
298 | },
299 | "indexes": {},
300 | "foreignKeys": {},
301 | "compositePrimaryKeys": {},
302 | "uniqueConstraints": {},
303 | "policies": {},
304 | "checkConstraints": {},
305 | "isRLSEnabled": false
306 | }
307 | },
308 | "enums": {},
309 | "schemas": {},
310 | "sequences": {},
311 | "roles": {},
312 | "policies": {},
313 | "views": {},
314 | "_meta": {
315 | "columns": {},
316 | "schemas": {},
317 | "tables": {}
318 | }
319 | }
--------------------------------------------------------------------------------
/drizzle/migrations/meta/_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "7",
3 | "dialect": "postgresql",
4 | "entries": [
5 | {
6 | "idx": 0,
7 | "version": "7",
8 | "when": 1747230620435,
9 | "tag": "0000_deep_obadiah_stane",
10 | "breakpoints": true
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | experimental: {
5 | nodeMiddleware: true,
6 | },
7 | };
8 |
9 | export default nextConfig;
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "titan",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "db:generate": "drizzle-kit generate",
10 | "db:migrate": "bun run src/lib/db/migrate.ts",
11 | "db:studio": "drizzle-kit studio",
12 | "lint": "next lint"
13 | },
14 | "dependencies": {
15 | "@hookform/resolvers": "^5.0.1",
16 | "@neondatabase/serverless": "^1.0.0",
17 | "@radix-ui/react-avatar": "^1.1.9",
18 | "@radix-ui/react-dropdown-menu": "^2.1.14",
19 | "@radix-ui/react-label": "^2.1.6",
20 | "@radix-ui/react-popover": "^1.1.13",
21 | "@radix-ui/react-slot": "^1.2.2",
22 | "@t3-oss/env-nextjs": "^0.13.4",
23 | "better-auth": "^1.2.7",
24 | "class-variance-authority": "^0.7.1",
25 | "clsx": "^2.1.1",
26 | "dotenv": "^16.5.0",
27 | "drizzle-orm": "^0.43.1",
28 | "lucide-react": "^0.503.0",
29 | "next": "15.4.0-canary.34",
30 | "next-themes": "^0.4.6",
31 | "react": "^19.1.0",
32 | "react-dom": "^19.1.0",
33 | "react-hook-form": "^7.56.3",
34 | "sonner": "^2.0.3",
35 | "tailwind-merge": "^3.3.0",
36 | "zod": "^3.24.4"
37 | },
38 | "devDependencies": {
39 | "@tailwindcss/postcss": "^4.1.6",
40 | "@types/node": "^20.17.47",
41 | "@types/react": "^19.1.4",
42 | "@types/react-dom": "^19.1.5",
43 | "drizzle-kit": "^0.31.1",
44 | "tailwindcss": "^4.1.6",
45 | "tw-animate-css": "^1.2.9",
46 | "typescript": "^5.8.3"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: ["@tailwindcss/postcss"],
3 | };
4 |
5 | export default config;
6 |
--------------------------------------------------------------------------------
/public/goku.svg:
--------------------------------------------------------------------------------
1 |
42 |
--------------------------------------------------------------------------------
/public/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rudrodip/titan/797e40c6155424ab6f886a08b651622aeddcc53c/public/og.png
--------------------------------------------------------------------------------
/src/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Goku } from "@/app/(marketing)/page";
2 |
3 | export default function AuthLayout({
4 | children,
5 | }: {
6 | children: React.ReactNode;
7 | }) {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | {children}
15 |
16 |
17 | );
18 | }
--------------------------------------------------------------------------------
/src/app/(auth)/sign-in/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import AuthCard from "@/components/auth-card"
4 |
5 | export default function SignInPage() {
6 | return (
7 |
8 | )
9 | }
--------------------------------------------------------------------------------
/src/app/(auth)/sign-up/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import AuthCard from "@/components/auth-card"
4 |
5 | export default function SignUpPage() {
6 | return (
7 |
8 | )
9 | }
--------------------------------------------------------------------------------
/src/app/(dashboard)/dashboard/loading.tsx:
--------------------------------------------------------------------------------
1 | export default function Loading() {
2 | return (
3 |
28 | )
29 | }
--------------------------------------------------------------------------------
/src/app/(dashboard)/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import { getUser } from "@/lib/auth-utils";
2 | import { Button } from "@/components/ui/button";
3 | import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
4 | import { Github } from "lucide-react";
5 | import { siteConfig } from "@/config/site.config";
6 | import Link from "next/link";
7 |
8 | export default async function DashboardPage() {
9 | const user = await getUser();
10 |
11 | return (
12 |
13 |
14 |
15 |
32 | Protected Dashboard
33 |
34 | This is a protected route accessible only to authenticated users.
35 |
36 |
37 |
38 |
39 |
Your Dashboard Awaits
40 |
41 | Start building your ideal dashboard by adding components and data visualizations.
42 |
43 |
66 |
67 |
68 |
69 | Signed in as: {user?.email}
70 | Built with Titan
71 |
72 |
73 |
74 | );
75 | }
--------------------------------------------------------------------------------
/src/app/(dashboard)/layout.tsx:
--------------------------------------------------------------------------------
1 | import ThemeToggler from "@/components/theme/toggler";
2 | import { Button } from "@/components/ui/button";
3 | import { UserProfile } from "@/components/user-profile";
4 | import { siteConfig } from "@/config/site.config";
5 | import { ArrowUpRight, BarChart2, CreditCard, Layout, Settings, Users } from "lucide-react";
6 | import Link from "next/link";
7 |
8 | export default function DashboardLayout({
9 | children,
10 | }: {
11 | children: React.ReactNode;
12 | }) {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | {children}
20 |
21 |
22 |
23 | )
24 | }
25 |
26 | const MockNavbar = () => {
27 | return (
28 |
29 |
30 |
35 |
36 |
58 |
59 | )
60 | }
61 |
62 | const MockSidebar = () => {
63 | return (
64 |
98 | )
99 | }
--------------------------------------------------------------------------------
/src/app/(marketing)/layout.tsx:
--------------------------------------------------------------------------------
1 | export default function MarketingLayout({
2 | children,
3 | }: {
4 | children: React.ReactNode;
5 | }) {
6 | return (
7 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/(marketing)/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import ThemeToggler from "@/components/theme/toggler";
5 | import { Button } from "@/components/ui/button";
6 | import { UserProfile } from "@/components/user-profile";
7 | import { siteConfig } from "@/config/site.config";
8 | import { cn } from "@/lib/utils";
9 | import { PanelsTopLeft, Shield, Database, Server, Component, Code, ArrowRight, Sparkle, Github, Copy, Check, ArrowUpRight } from "lucide-react";
10 | import Link from "next/link";
11 | import { useSession } from "@/lib/auth-client";
12 |
13 | export default function Home() {
14 | const [copied, setCopied] = useState(false);
15 | const { data: session, isPending } = useSession();
16 |
17 | const handleCopy = () => {
18 | navigator.clipboard.writeText(`git clone ${siteConfig.socials.github}`);
19 | setCopied(true);
20 | setTimeout(() => {
21 | setCopied(false);
22 | }, 2000);
23 | }
24 |
25 | return (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | {siteConfig.origin.replace("https://", "")}
40 |
41 | {!isPending && (session ? (
42 |
51 | ) : (
52 |
61 | ))}
62 |
63 |
64 |
65 |
66 |
Titan
67 |
{siteConfig.description}
68 |
69 |
70 |
71 |
72 | git clone {siteConfig.socials.github}
73 |
74 |
77 |
78 |
79 |
94 |
95 |
96 |
137 |
138 |
139 | )
140 | }
141 |
142 | const techConfig = [
143 | {
144 | icon: ,
145 | category: "Full-stack Framework",
146 | name: "Next.js 15",
147 | description: "Modern, full-stack React framework for building web applications.",
148 | link: "https://nextjs.org"
149 | },
150 | {
151 | icon: ,
152 | category: "Authentication",
153 | name: "Better-Auth",
154 | description: "Secure authentication solution with OAuth, email/password, magic links, and more",
155 | link: "https://better-auth.com"
156 | },
157 | {
158 | icon: ,
159 | category: "ORM",
160 | name: "Drizzle ORM",
161 | description: "TypeScript ORM with a focus on type safety and developer experience.",
162 | link: "https://orm.drizzle.team/"
163 | },
164 | {
165 | icon: ,
166 | category: "Database",
167 | name: "Postgres",
168 | description: "It's a Postgres database, what else do you need?",
169 | link: "https://neon.tech"
170 | },
171 | {
172 | icon: ,
173 | category: "UI Components",
174 | name: "ShadCN/UI",
175 | description: "Beautifully designed components built with Radix UI and Tailwind CSS.",
176 | link: "https://ui.shadcn.com"
177 | },
178 | {
179 | icon:
,
180 | category: "CSS Framework",
181 | name: "Tailwindcss v4",
182 | description: "Utility-first CSS framework for rapidly building custom user interfaces.",
183 | link: "https://tailwindcss.com"
184 | },
185 | ];
186 |
187 | export const Goku = () => {
188 | return (
189 |
230 | )
231 | }
--------------------------------------------------------------------------------
/src/app/api/auth/[...all]/route.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@/lib/auth";
2 | import { toNextJsHandler } from "better-auth/next-js";
3 |
4 | export const { GET, POST } = toNextJsHandler(auth.handler);
--------------------------------------------------------------------------------
/src/app/error.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useEffect } from "react"
4 | import Link from "next/link"
5 | import { Terminal, RefreshCw } from "lucide-react"
6 | import { Button } from "@/components/ui/button"
7 | import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
8 |
9 | export default function Error({
10 | error,
11 | reset,
12 | }: {
13 | error: Error & { digest?: string }
14 | reset: () => void
15 | }) {
16 | useEffect(() => {
17 | console.error(error)
18 | }, [error])
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 | system_failure.sh
27 |
28 |
29 |
30 |
31 |
32 | $
33 | status
34 |
35 |
36 |
500
37 |
Server error encountered
38 |
39 |
40 | $
41 | error_trace
42 |
43 |
44 |
{error?.digest || "Unknown error occurred"}
45 |
46 |
47 |
$
48 |
49 | repair_system
50 |
51 |
52 |
53 |
54 |
55 |
56 |
59 |
62 |
63 |
64 |
65 | )
66 | }
67 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Manrope, JetBrains_Mono, Inter } from "next/font/google";
3 | import "@/styles/globals.css";
4 | import { siteConfig } from "@/config/site.config";
5 | import { cn } from "@/lib/utils";
6 | import RootProviders from "@/components/providers";
7 |
8 | const fontSans = Manrope({
9 | variable: "--font-sans",
10 | subsets: ["latin"],
11 | });
12 |
13 | const fontMono = JetBrains_Mono({
14 | variable: "--font-mono",
15 | subsets: ["latin"],
16 | });
17 |
18 | const fontHeading = Inter({
19 | variable: "--font-inter",
20 | subsets: ["latin"],
21 | });
22 |
23 | export const metadata: Metadata = {
24 | metadataBase: new URL(siteConfig.origin),
25 | title: siteConfig.title,
26 | description: siteConfig.description,
27 | keywords: siteConfig.keywords,
28 | creator: siteConfig.name,
29 | icons: {
30 | icon: "/goku.svg",
31 | shortcut: "/goku.svg",
32 | },
33 | openGraph: {
34 | title: siteConfig.title,
35 | description: siteConfig.description,
36 | url: siteConfig.origin,
37 | siteName: siteConfig.name,
38 | images: [
39 | {
40 | url: siteConfig.og,
41 | width: 2880,
42 | height: 1800,
43 | alt: siteConfig.name,
44 | },
45 | ],
46 | type: "website",
47 | locale: "en_US",
48 | },
49 | twitter: {
50 | card: "summary_large_image",
51 | site: siteConfig.socials.x,
52 | title: siteConfig.title,
53 | description: siteConfig.description,
54 | images: {
55 | url: siteConfig.og,
56 | width: 2880,
57 | height: 1800,
58 | alt: siteConfig.name,
59 | },
60 | },
61 | };
62 |
63 |
64 | export default function RootLayout({
65 | children,
66 | }: Readonly<{
67 | children: React.ReactNode;
68 | }>) {
69 | return (
70 |
71 |
79 |
80 | {children}
81 |
82 |
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 | import { Terminal } from "lucide-react"
3 | import { Button } from "@/components/ui/button"
4 | import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
5 |
6 | export default function NotFound() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | system_error.sh
14 |
15 |
16 |
17 |
18 |
19 | $
20 | status
21 |
22 |
23 |
404
24 |
Page not found
25 |
26 |
27 |
$
28 |
29 | locate_page
30 |
31 |
32 |
33 |
34 |
35 |
36 |
39 |
40 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/auth-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
5 | import { useState } from "react";
6 | import { signIn } from "@/lib/auth-client";
7 | import { cn } from "@/lib/utils";
8 | import { Loader2 } from "lucide-react";
9 | import { Icons } from "@/components/icons";
10 | import Link from "next/link";
11 |
12 | export default function AuthCard({
13 | title,
14 | description,
15 | mode = "sign-in",
16 | }: {
17 | title: string;
18 | description: string;
19 | mode?: "sign-in" | "sign-up";
20 | }) {
21 | const [githubLoading, setGithubLoading] = useState(false);
22 | const [googleLoading, setGoogleLoading] = useState(false);
23 | const [discordLoading, setDiscordLoading] = useState(false);
24 |
25 | return (
26 |
27 |
28 | {title}
29 | {description}
30 |
31 |
32 |
33 |
37 | }
44 | />
45 | }
52 | />
53 | }
60 | />
61 |
62 |
63 |
64 |
65 |
66 | {mode === "sign-in" ? (
67 | <>
68 | Don't have an account?{" "}
69 |
70 | Sign up
71 |
72 | >
73 | ) : (
74 | <>
75 | Already have an account?{" "}
76 |
77 | Sign in
78 |
79 | >
80 | )}
81 |
82 |
83 |
84 | );
85 | }
86 |
87 | const SignInButton = ({
88 | title,
89 | provider,
90 | loading,
91 | setLoading,
92 | callbackURL,
93 | icon,
94 | }: {
95 | title: string;
96 | provider: "github" | "google" | "discord";
97 | loading: boolean;
98 | setLoading: (loading: boolean) => void;
99 | callbackURL: string;
100 | icon: React.ReactNode;
101 | }) => {
102 | return (
103 |
125 | )
126 | }
127 |
--------------------------------------------------------------------------------
/src/components/icons.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import Image from "next/image";
3 |
4 | export const Icons = {
5 | Logo: function Logo({
6 | width = 100,
7 | height = 60,
8 | className,
9 | }: {
10 | width?: number;
11 | height?: number;
12 | className?: string;
13 | }) {
14 | return (
15 |
23 | );
24 | },
25 | Twitter: function Twitter({
26 | width = 25,
27 | height = 25,
28 | className,
29 | }: {
30 | width?: number;
31 | height?: number;
32 | className?: string;
33 | }) {
34 | return (
35 |
48 | );
49 | },
50 | LinkedIn: function LinkedIn({
51 | width = 32,
52 | height = 32,
53 | className,
54 | }: {
55 | width?: number;
56 | height?: number;
57 | className?: string;
58 | }) {
59 | return (
60 |
86 | );
87 | },
88 | Discord: function Discord({
89 | width = 27,
90 | height = 27,
91 | className,
92 | ...props
93 | }: {
94 | width?: number;
95 | height?: number;
96 | className?: string;
97 | props?: React.ComponentProps<"svg">;
98 | }) {
99 | return (
100 |
113 | );
114 | },
115 | Google: function Google(props: React.ComponentProps<"svg">) {
116 | return (
117 |
131 | );
132 | },
133 | Gmail: function Gmail({
134 | width = 30,
135 | height = 30,
136 | className,
137 | }: {
138 | width?: number;
139 | height?: number;
140 | className?: string;
141 | }) {
142 | return (
143 |
150 | );
151 | },
152 | Trello: function Trello({
153 | width = 30,
154 | height = 30,
155 | className,
156 | }: {
157 | width?: number;
158 | height?: number;
159 | className?: string;
160 | }) {
161 | return (
162 |
169 | );
170 | },
171 | Linear: function Linear({
172 | width = 30,
173 | height = 30,
174 | className,
175 | }: {
176 | width?: number;
177 | height?: number;
178 | className?: string;
179 | }) {
180 | return (
181 |
188 | );
189 | },
190 | Slack: function Slack({
191 | width = 30,
192 | height = 30,
193 | className,
194 | }: {
195 | width?: number;
196 | height?: number;
197 | className?: string;
198 | }) {
199 | return (
200 |
207 | );
208 | },
209 | Notion: function Notion({
210 | width = 30,
211 | height = 30,
212 | className,
213 | }: {
214 | width?: number;
215 | height?: number;
216 | className?: string;
217 | }) {
218 | return (
219 |
226 | );
227 | },
228 | PlusIcon: (props: React.ComponentProps<"svg">) => (
229 |
274 | ),
275 | Globe: (props: React.ComponentProps<"svg">) => (
276 |
300 | ),
301 | Github: (props: React.ComponentProps<"svg">) => (
302 |
308 | ),
309 | notion: (props: React.ComponentProps<"svg">) => (
310 |
329 | ),
330 | openai: (props: React.ComponentProps<"svg">) => (
331 |
334 | ),
335 | googleDrive: (props: React.ComponentProps<"svg">) => (
336 |
362 | ),
363 | whatsapp: (props: React.ComponentProps<"svg">) => (
364 |
415 | ),
416 | googleDocs: (props: React.ComponentProps<"svg">) => (
417 |
604 | ),
605 | zapier: (props: React.ComponentProps<"svg">) => (
606 |
648 | ),
649 | messenger: (props: React.ComponentProps<"svg">) => (
650 |
692 | ),
693 | };
--------------------------------------------------------------------------------
/src/components/providers/index.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from "@/components/theme/provider";
2 | import { Toaster } from "@/components/ui/sonner"
3 |
4 | export default function RootProviders({ children }: { children: React.ReactNode }) {
5 | return (
6 |
12 |
13 | {children}
14 |
15 | );
16 | }
--------------------------------------------------------------------------------
/src/components/theme/provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ThemeProvider as NextThemesProvider } from "next-themes"
5 |
6 | export function ThemeProvider({
7 | children,
8 | ...props
9 | }: React.ComponentProps) {
10 | return {children}
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/theme/toggler.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { MoonIcon, SunIcon } from "lucide-react";
4 | import { useTheme } from "next-themes";
5 | import { Button } from "@/components/ui/button";
6 | import { cn } from "@/lib/utils";
7 |
8 | type ThemeTogglerProps = {
9 | className?: string;
10 | }
11 |
12 | export default function ThemeToggler({ className }: ThemeTogglerProps) {
13 | const { resolvedTheme, setTheme } = useTheme();
14 |
15 | const switchTheme = () => {
16 | setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
17 | };
18 |
19 | const toggleTheme = () => {
20 | //@ts-ignore
21 | if (!document.startViewTransition) switchTheme();
22 |
23 | //@ts-ignore
24 | document.startViewTransition(switchTheme);
25 | };
26 |
27 |
28 | return (
29 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Avatar({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | function AvatarImage({
25 | className,
26 | ...props
27 | }: React.ComponentProps) {
28 | return (
29 |
34 | )
35 | }
36 |
37 | function AvatarFallback({
38 | className,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
50 | )
51 | }
52 |
53 | export { Avatar, AvatarImage, AvatarFallback }
54 |
--------------------------------------------------------------------------------
/src/components/ui/badge.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 badgeVariants = cva(
8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14 | secondary:
15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16 | destructive:
17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18 | outline:
19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | },
25 | }
26 | )
27 |
28 | function Badge({
29 | className,
30 | variant,
31 | asChild = false,
32 | ...props
33 | }: React.ComponentProps<"span"> &
34 | VariantProps & { asChild?: boolean }) {
35 | const Comp = asChild ? Slot : "span"
36 |
37 | return (
38 |
43 | )
44 | }
45 |
46 | export { Badge, badgeVariants }
47 |
--------------------------------------------------------------------------------
/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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 | outline:
17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost:
21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 | icon: "size-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | )
37 |
38 | function Button({
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | ...props
44 | }: React.ComponentProps<"button"> &
45 | VariantProps & {
46 | asChild?: boolean
47 | }) {
48 | const Comp = asChild ? Slot : "button"
49 |
50 | return (
51 |
56 | )
57 | }
58 |
59 | export { Button, buttonVariants }
60 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Card({ className, ...props }: React.ComponentProps<"div">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19 | return (
20 |
28 | )
29 | }
30 |
31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32 | return (
33 |
38 | )
39 | }
40 |
41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42 | return (
43 |
48 | )
49 | }
50 |
51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52 | return (
53 |
61 | )
62 | }
63 |
64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65 | return (
66 |
71 | )
72 | }
73 |
74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75 | return (
76 |
81 | )
82 | }
83 |
84 | export {
85 | Card,
86 | CardHeader,
87 | CardFooter,
88 | CardTitle,
89 | CardAction,
90 | CardDescription,
91 | CardContent,
92 | }
93 |
--------------------------------------------------------------------------------
/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 { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | function DropdownMenu({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function DropdownMenuPortal({
16 | ...props
17 | }: React.ComponentProps) {
18 | return (
19 |
20 | )
21 | }
22 |
23 | function DropdownMenuTrigger({
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
31 | )
32 | }
33 |
34 | function DropdownMenuContent({
35 | className,
36 | sideOffset = 4,
37 | ...props
38 | }: React.ComponentProps) {
39 | return (
40 |
41 |
50 |
51 | )
52 | }
53 |
54 | function DropdownMenuGroup({
55 | ...props
56 | }: React.ComponentProps) {
57 | return (
58 |
59 | )
60 | }
61 |
62 | function DropdownMenuItem({
63 | className,
64 | inset,
65 | variant = "default",
66 | ...props
67 | }: React.ComponentProps & {
68 | inset?: boolean
69 | variant?: "default" | "destructive"
70 | }) {
71 | return (
72 |
82 | )
83 | }
84 |
85 | function DropdownMenuCheckboxItem({
86 | className,
87 | children,
88 | checked,
89 | ...props
90 | }: React.ComponentProps) {
91 | return (
92 |
101 |
102 |
103 |
104 |
105 |
106 | {children}
107 |
108 | )
109 | }
110 |
111 | function DropdownMenuRadioGroup({
112 | ...props
113 | }: React.ComponentProps) {
114 | return (
115 |
119 | )
120 | }
121 |
122 | function DropdownMenuRadioItem({
123 | className,
124 | children,
125 | ...props
126 | }: React.ComponentProps) {
127 | return (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | )
144 | }
145 |
146 | function DropdownMenuLabel({
147 | className,
148 | inset,
149 | ...props
150 | }: React.ComponentProps & {
151 | inset?: boolean
152 | }) {
153 | return (
154 |
163 | )
164 | }
165 |
166 | function DropdownMenuSeparator({
167 | className,
168 | ...props
169 | }: React.ComponentProps) {
170 | return (
171 |
176 | )
177 | }
178 |
179 | function DropdownMenuShortcut({
180 | className,
181 | ...props
182 | }: React.ComponentProps<"span">) {
183 | return (
184 |
192 | )
193 | }
194 |
195 | function DropdownMenuSub({
196 | ...props
197 | }: React.ComponentProps) {
198 | return
199 | }
200 |
201 | function DropdownMenuSubTrigger({
202 | className,
203 | inset,
204 | children,
205 | ...props
206 | }: React.ComponentProps & {
207 | inset?: boolean
208 | }) {
209 | return (
210 |
219 | {children}
220 |
221 |
222 | )
223 | }
224 |
225 | function DropdownMenuSubContent({
226 | className,
227 | ...props
228 | }: React.ComponentProps) {
229 | return (
230 |
238 | )
239 | }
240 |
241 | export {
242 | DropdownMenu,
243 | DropdownMenuPortal,
244 | DropdownMenuTrigger,
245 | DropdownMenuContent,
246 | DropdownMenuGroup,
247 | DropdownMenuLabel,
248 | DropdownMenuItem,
249 | DropdownMenuCheckboxItem,
250 | DropdownMenuRadioGroup,
251 | DropdownMenuRadioItem,
252 | DropdownMenuSeparator,
253 | DropdownMenuShortcut,
254 | DropdownMenuSub,
255 | DropdownMenuSubTrigger,
256 | DropdownMenuSubContent,
257 | }
258 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | FormProvider,
9 | useFormContext,
10 | useFormState,
11 | type ControllerProps,
12 | type FieldPath,
13 | type FieldValues,
14 | } from "react-hook-form"
15 |
16 | import { cn } from "@/lib/utils"
17 | import { Label } from "@/components/ui/label"
18 |
19 | const Form = FormProvider
20 |
21 | type FormFieldContextValue<
22 | TFieldValues extends FieldValues = FieldValues,
23 | TName extends FieldPath = FieldPath,
24 | > = {
25 | name: TName
26 | }
27 |
28 | const FormFieldContext = React.createContext(
29 | {} as FormFieldContextValue
30 | )
31 |
32 | const FormField = <
33 | TFieldValues extends FieldValues = FieldValues,
34 | TName extends FieldPath = FieldPath,
35 | >({
36 | ...props
37 | }: ControllerProps) => {
38 | return (
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | const useFormField = () => {
46 | const fieldContext = React.useContext(FormFieldContext)
47 | const itemContext = React.useContext(FormItemContext)
48 | const { getFieldState } = useFormContext()
49 | const formState = useFormState({ name: fieldContext.name })
50 | const fieldState = getFieldState(fieldContext.name, formState)
51 |
52 | if (!fieldContext) {
53 | throw new Error("useFormField should be used within ")
54 | }
55 |
56 | const { id } = itemContext
57 |
58 | return {
59 | id,
60 | name: fieldContext.name,
61 | formItemId: `${id}-form-item`,
62 | formDescriptionId: `${id}-form-item-description`,
63 | formMessageId: `${id}-form-item-message`,
64 | ...fieldState,
65 | }
66 | }
67 |
68 | type FormItemContextValue = {
69 | id: string
70 | }
71 |
72 | const FormItemContext = React.createContext(
73 | {} as FormItemContextValue
74 | )
75 |
76 | function FormItem({ className, ...props }: React.ComponentProps<"div">) {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
86 |
87 | )
88 | }
89 |
90 | function FormLabel({
91 | className,
92 | ...props
93 | }: React.ComponentProps) {
94 | const { error, formItemId } = useFormField()
95 |
96 | return (
97 |
104 | )
105 | }
106 |
107 | function FormControl({ ...props }: React.ComponentProps) {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | }
124 |
125 | function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
126 | const { formDescriptionId } = useFormField()
127 |
128 | return (
129 |
135 | )
136 | }
137 |
138 | function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
139 | const { error, formMessageId } = useFormField()
140 | const body = error ? String(error?.message ?? "") : props.children
141 |
142 | if (!body) {
143 | return null
144 | }
145 |
146 | return (
147 |
153 | {body}
154 |
155 | )
156 | }
157 |
158 | export {
159 | useFormField,
160 | Form,
161 | FormItem,
162 | FormLabel,
163 | FormControl,
164 | FormDescription,
165 | FormMessage,
166 | FormField,
167 | }
168 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
18 | )
19 | }
20 |
21 | export { Input }
22 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Label({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Popover({
9 | ...props
10 | }: React.ComponentProps) {
11 | return
12 | }
13 |
14 | function PopoverTrigger({
15 | ...props
16 | }: React.ComponentProps) {
17 | return
18 | }
19 |
20 | function PopoverContent({
21 | className,
22 | align = "center",
23 | sideOffset = 4,
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
28 |
38 |
39 | )
40 | }
41 |
42 | function PopoverAnchor({
43 | ...props
44 | }: React.ComponentProps) {
45 | return
46 | }
47 |
48 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
49 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner, ToasterProps } from "sonner"
5 |
6 | const Toaster = ({ ...props }: ToasterProps) => {
7 | const { theme = "system" } = useTheme()
8 |
9 | return (
10 |
23 | )
24 | }
25 |
26 | export { Toaster }
27 |
--------------------------------------------------------------------------------
/src/components/user-profile.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState } from "react";
4 | import { Button } from "@/components/ui/button"
5 | import {
6 | DropdownMenu,
7 | DropdownMenuContent,
8 | DropdownMenuItem,
9 | DropdownMenuSeparator,
10 | DropdownMenuTrigger,
11 | } from "@/components/ui/dropdown-menu"
12 | import {
13 | Avatar,
14 | AvatarFallback,
15 | AvatarImage,
16 | } from "@/components/ui/avatar"
17 | import { useSession, signOut } from "@/lib/auth-client"
18 | import { useRouter } from "next/navigation";
19 | import Link from "next/link"
20 | import { siteConfig } from "@/config/site.config"
21 | import { toast } from "sonner"
22 | import { ExternalLinkIcon, LogOutIcon } from "lucide-react"
23 | import { cn } from "@/lib/utils";
24 |
25 | export function UserProfile({ className }: { className?: string }) {
26 | const [signingOut, setSigningOut] = useState(false);
27 | const { data: session, isPending } = useSession();
28 | const router = useRouter();
29 |
30 | if (isPending) {
31 | return (
32 |
35 | );
36 | }
37 |
38 | if (!session) {
39 | return null;
40 | }
41 |
42 | return (
43 |
44 |
45 |
51 |
52 |
53 |
54 |
55 |
56 |
{session.user.name}
57 |
{session.user.email}
58 |
59 |
60 |
61 | {session.user.name?.charAt(0)}
62 |
63 |
64 |
65 |
66 |
67 | Dashboard
68 |
69 |
70 |
71 |
72 | Github
73 |
74 |
75 |
76 |
77 |
78 | X
79 |
80 |
81 |
82 |
83 | signOut({
86 | fetchOptions: {
87 | onRequest: () => {
88 | setSigningOut(true);
89 | toast.loading("Signing out...");
90 | },
91 | onSuccess: () => {
92 | setSigningOut(false);
93 | toast.success("Signed out successfully");
94 | toast.dismiss();
95 | router.push("/");
96 | },
97 | onError: () => {
98 | setSigningOut(false);
99 | toast.error("Failed to sign out");
100 | },
101 | }
102 | })}
103 | >
104 | Sign Out
105 |
106 |
107 |
108 |
109 | )
110 | }
111 |
--------------------------------------------------------------------------------
/src/config/site.config.ts:
--------------------------------------------------------------------------------
1 | import { SiteConfig } from "@/types";
2 |
3 | export const siteConfig: SiteConfig = {
4 | name: "Titan",
5 | title: "Titan - Powerful Next.js 15 Template with Better-Auth, Drizzle ORM, PostgreSQL, and Shadcn UI",
6 | description: "Modern Next.js 15 stack with Better-Auth, Drizzle ORM, PostgreSQL, Shadcn UI, and Tailwind v4 for fast, secure web app development.",
7 | origin: "https://titan.rdsx.dev",
8 | keywords: [
9 | "Next.js 15",
10 | "Authentication",
11 | "Drizzle ORM",
12 | "PostgreSQL",
13 | "Tailwind CSS",
14 | "Tailwind CSS V4",
15 | "Shadcn UI",
16 | "TypeScript",
17 | "Full-Stack Template"
18 | ],
19 | og: "https://titan.rdsx.dev/og.png",
20 | creator: {
21 | name: "rds_agi",
22 | url: "https://rdsx.dev",
23 | },
24 | socials: {
25 | github: "https://github.com/rudrodip/titan",
26 | x: "https://x.com/rds_agi",
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/env.ts:
--------------------------------------------------------------------------------
1 | import { createEnv } from "@t3-oss/env-nextjs";
2 | import { z } from "zod";
3 |
4 | export const env = createEnv({
5 | server: {
6 | // BetterAuth
7 | BETTER_AUTH_URL: z.string().min(1),
8 | BETTER_AUTH_SECRET: z.string().min(1),
9 | // Database
10 | DATABASE_URL: z.string().min(1),
11 | // Github
12 | GITHUB_CLIENT_ID: z.string().min(1),
13 | GITHUB_CLIENT_SECRET: z.string().min(1),
14 | // Google
15 | GOOGLE_CLIENT_ID: z.string().min(1),
16 | GOOGLE_CLIENT_SECRET: z.string().min(1),
17 | // Discord
18 | DISCORD_CLIENT_ID: z.string().min(1),
19 | DISCORD_CLIENT_SECRET: z.string().min(1),
20 | },
21 | client: {},
22 | experimental__runtimeEnv: {},
23 | });
24 |
--------------------------------------------------------------------------------
/src/lib/auth-client.ts:
--------------------------------------------------------------------------------
1 | import { createAuthClient } from "better-auth/react";
2 | export const authClient = createAuthClient();
3 |
4 | export const { signIn, signOut, signUp, useSession } = authClient;
5 |
--------------------------------------------------------------------------------
/src/lib/auth-utils.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@/lib/auth";
2 | import { headers } from "next/headers";
3 |
4 | export const getSession = async () => {
5 | const session = await auth.api.getSession({
6 | headers: await headers()
7 | })
8 |
9 | return session;
10 | }
11 |
12 | export const getUser = async () => {
13 | const session = await getSession();
14 | return session?.user;
15 | }
--------------------------------------------------------------------------------
/src/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import { betterAuth } from "better-auth";
2 | import { nextCookies } from "better-auth/next-js";
3 | import { drizzleAdapter } from "better-auth/adapters/drizzle";
4 | import { db } from "@/lib/db";
5 | import { env } from "@/env";
6 |
7 | export const auth = betterAuth({
8 | database: drizzleAdapter(db, {
9 | provider: "pg",
10 | }),
11 | socialProviders: {
12 | github: {
13 | clientId: env.GITHUB_CLIENT_ID,
14 | clientSecret: env.GITHUB_CLIENT_SECRET,
15 | },
16 | google: {
17 | clientId: env.GOOGLE_CLIENT_ID,
18 | clientSecret: env.GOOGLE_CLIENT_SECRET,
19 | },
20 | discord: {
21 | clientId: env.DISCORD_CLIENT_ID,
22 | clientSecret: env.DISCORD_CLIENT_SECRET,
23 | }
24 | },
25 | plugins: [nextCookies()]
26 | });
27 |
--------------------------------------------------------------------------------
/src/lib/db/index.ts:
--------------------------------------------------------------------------------
1 | import { drizzle } from "drizzle-orm/neon-http";
2 | import { neon } from "@neondatabase/serverless";
3 | import * as schema from "@/lib/db/schema";
4 | import { env } from "@/env";
5 |
6 | const sql = neon(env.DATABASE_URL);
7 | export const db = drizzle(sql, { schema });
8 |
--------------------------------------------------------------------------------
/src/lib/db/migrate.ts:
--------------------------------------------------------------------------------
1 | import { migrate } from "drizzle-orm/neon-http/migrator";
2 | import { db } from ".";
3 |
4 | const main = async () => {
5 | const startTime = Date.now();
6 | console.log(`[${new Date().toISOString()}] Starting database migration...`);
7 |
8 | try {
9 | await migrate(db, { migrationsFolder: "drizzle/migrations" });
10 |
11 | const duration = ((Date.now() - startTime) / 1000).toFixed(2);
12 | console.log(`[${new Date().toISOString()}] ✅ Migration completed successfully in ${duration}s`);
13 | } catch (error: unknown) {
14 | const duration = ((Date.now() - startTime) / 1000).toFixed(2);
15 | console.error(`[${new Date().toISOString()}] ❌ Migration failed after ${duration}s`);
16 |
17 | if (error instanceof Error) {
18 | console.error(`Error details: ${error.message}`);
19 | if (error.stack) {
20 | console.error(`Stack trace:\n${error.stack}`);
21 | }
22 | } else {
23 | console.error(`Error details: ${String(error)}`);
24 | }
25 |
26 | process.exit(1);
27 | }
28 |
29 | process.exit(0);
30 | };
31 |
32 | main();
--------------------------------------------------------------------------------
/src/lib/db/schema.ts:
--------------------------------------------------------------------------------
1 | import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
2 |
3 | export const user = pgTable("user", {
4 | id: text("id").primaryKey(),
5 | name: text("name").notNull(),
6 | email: text("email").notNull().unique(),
7 | emailVerified: boolean("email_verified").notNull(),
8 | image: text("image"),
9 | createdAt: timestamp("created_at").notNull(),
10 | updatedAt: timestamp("updated_at").notNull(),
11 | });
12 |
13 | export const session = pgTable("session", {
14 | id: text("id").primaryKey(),
15 | expiresAt: timestamp("expires_at").notNull(),
16 | token: text("token").notNull().unique(),
17 | createdAt: timestamp("created_at").notNull(),
18 | updatedAt: timestamp("updated_at").notNull(),
19 | ipAddress: text("ip_address"),
20 | userAgent: text("user_agent"),
21 | userId: text("user_id")
22 | .notNull()
23 | .references(() => user.id, { onDelete: "cascade" }),
24 | });
25 |
26 | export const account = pgTable("account", {
27 | id: text("id").primaryKey(),
28 | accountId: text("account_id").notNull(),
29 | providerId: text("provider_id").notNull(),
30 | userId: text("user_id")
31 | .notNull()
32 | .references(() => user.id, { onDelete: "cascade" }),
33 | accessToken: text("access_token"),
34 | refreshToken: text("refresh_token"),
35 | idToken: text("id_token"),
36 | accessTokenExpiresAt: timestamp("access_token_expires_at"),
37 | refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
38 | scope: text("scope"),
39 | password: text("password"),
40 | createdAt: timestamp("created_at").notNull(),
41 | updatedAt: timestamp("updated_at").notNull(),
42 | });
43 |
44 | export const verification = pgTable("verification", {
45 | id: text("id").primaryKey(),
46 | identifier: text("identifier").notNull(),
47 | value: text("value").notNull(),
48 | expiresAt: timestamp("expires_at").notNull(),
49 | createdAt: timestamp("created_at"),
50 | updatedAt: timestamp("updated_at"),
51 | });
52 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { headers } from "next/headers";
3 | import { auth } from "@/lib/auth";
4 |
5 | const protectedRoutes = ["/dashboard"];
6 | const authRoutes = ["/sign-in", "/sign-up"];
7 |
8 | export async function middleware(request: NextRequest) {
9 | const session = await auth.api.getSession({
10 | headers: await headers()
11 | })
12 |
13 | const { pathname } = request.nextUrl;
14 |
15 | // If user is not authenticated and trying to access protected route
16 | if (!session && protectedRoutes.some(route => pathname.startsWith(route))) {
17 | return NextResponse.redirect(new URL("/sign-in", request.url));
18 | }
19 |
20 | // If user is authenticated but trying to access auth routes
21 | if (session && authRoutes.some(route => pathname.startsWith(route))) {
22 | return NextResponse.redirect(new URL("/dashboard", request.url));
23 | }
24 |
25 | return NextResponse.next();
26 | }
27 |
28 | export const config = {
29 | runtime: "nodejs",
30 | matcher: [
31 | '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
32 | '/(api|trpc)(.*)',
33 | ],
34 | };
35 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "tw-animate-css";
3 |
4 | @custom-variant dark (&:is(.dark *));
5 |
6 | @theme inline {
7 | --color-background: var(--background);
8 | --color-foreground: var(--foreground);
9 | --font-sans: var(--font-geist-sans);
10 | --font-mono: var(--font-geist-mono);
11 | --color-sidebar-ring: var(--sidebar-ring);
12 | --color-sidebar-border: var(--sidebar-border);
13 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
14 | --color-sidebar-accent: var(--sidebar-accent);
15 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
16 | --color-sidebar-primary: var(--sidebar-primary);
17 | --color-sidebar-foreground: var(--sidebar-foreground);
18 | --color-sidebar: var(--sidebar);
19 | --color-chart-5: var(--chart-5);
20 | --color-chart-4: var(--chart-4);
21 | --color-chart-3: var(--chart-3);
22 | --color-chart-2: var(--chart-2);
23 | --color-chart-1: var(--chart-1);
24 | --color-ring: var(--ring);
25 | --color-input: var(--input);
26 | --color-border: var(--border);
27 | --color-destructive: var(--destructive);
28 | --color-accent-foreground: var(--accent-foreground);
29 | --color-accent: var(--accent);
30 | --color-muted-foreground: var(--muted-foreground);
31 | --color-muted: var(--muted);
32 | --color-secondary-foreground: var(--secondary-foreground);
33 | --color-secondary: var(--secondary);
34 | --color-primary-foreground: var(--primary-foreground);
35 | --color-primary: var(--primary);
36 | --color-popover-foreground: var(--popover-foreground);
37 | --color-popover: var(--popover);
38 | --color-card-foreground: var(--card-foreground);
39 | --color-card: var(--card);
40 | --radius-sm: calc(var(--radius) - 4px);
41 | --radius-md: calc(var(--radius) - 2px);
42 | --radius-lg: var(--radius);
43 | --radius-xl: calc(var(--radius) + 4px);
44 | --font-sans: var(--font-sans);
45 | --font-mono: var(--font-mono);
46 | --font-heading: var(--font-inter);
47 | }
48 |
49 | :root {
50 | --radius: 0rem;
51 | --background: oklch(1 0 0);
52 | --foreground: oklch(0.145 0 0);
53 | --card: oklch(1 0 0);
54 | --card-foreground: oklch(0.145 0 0);
55 | --popover: oklch(1 0 0);
56 | --popover-foreground: oklch(0.145 0 0);
57 | --primary: oklch(0.205 0 0);
58 | --primary-foreground: oklch(0.985 0 0);
59 | --secondary: oklch(0.97 0 0);
60 | --secondary-foreground: oklch(0.205 0 0);
61 | --muted: oklch(0.97 0 0);
62 | --muted-foreground: oklch(0.556 0 0);
63 | --accent: oklch(0.97 0 0);
64 | --accent-foreground: oklch(0.205 0 0);
65 | --destructive: oklch(0.577 0.245 27.325);
66 | --border: oklch(0.922 0 0);
67 | --input: oklch(0.922 0 0);
68 | --ring: oklch(0.708 0 0);
69 | --chart-1: oklch(0.646 0.222 41.116);
70 | --chart-2: oklch(0.6 0.118 184.704);
71 | --chart-3: oklch(0.398 0.07 227.392);
72 | --chart-4: oklch(0.828 0.189 84.429);
73 | --chart-5: oklch(0.769 0.188 70.08);
74 | --sidebar: oklch(0.985 0 0);
75 | --sidebar-foreground: oklch(0.145 0 0);
76 | --sidebar-primary: oklch(0.205 0 0);
77 | --sidebar-primary-foreground: oklch(0.985 0 0);
78 | --sidebar-accent: oklch(0.97 0 0);
79 | --sidebar-accent-foreground: oklch(0.205 0 0);
80 | --sidebar-border: oklch(0.922 0 0);
81 | --sidebar-ring: oklch(0.708 0 0);
82 | }
83 |
84 | .dark {
85 | --background: oklch(0.145 0 0);
86 | --foreground: oklch(0.985 0 0);
87 | --card: oklch(0.205 0 0);
88 | --card-foreground: oklch(0.985 0 0);
89 | --popover: oklch(0.205 0 0);
90 | --popover-foreground: oklch(0.985 0 0);
91 | --primary: oklch(0.922 0 0);
92 | --primary-foreground: oklch(0.205 0 0);
93 | --secondary: oklch(0.269 0 0);
94 | --secondary-foreground: oklch(0.985 0 0);
95 | --muted: oklch(0.269 0 0);
96 | --muted-foreground: oklch(0.708 0 0);
97 | --accent: oklch(0.269 0 0);
98 | --accent-foreground: oklch(0.985 0 0);
99 | --destructive: oklch(0.704 0.191 22.216);
100 | --border: oklch(1 0 0 / 10%);
101 | --input: oklch(1 0 0 / 15%);
102 | --ring: oklch(0.556 0 0);
103 | --chart-1: oklch(0.488 0.243 264.376);
104 | --chart-2: oklch(0.696 0.17 162.48);
105 | --chart-3: oklch(0.769 0.188 70.08);
106 | --chart-4: oklch(0.627 0.265 303.9);
107 | --chart-5: oklch(0.645 0.246 16.439);
108 | --sidebar: oklch(0.205 0 0);
109 | --sidebar-foreground: oklch(0.985 0 0);
110 | --sidebar-primary: oklch(0.488 0.243 264.376);
111 | --sidebar-primary-foreground: oklch(0.985 0 0);
112 | --sidebar-accent: oklch(0.269 0 0);
113 | --sidebar-accent-foreground: oklch(0.985 0 0);
114 | --sidebar-border: oklch(1 0 0 / 10%);
115 | --sidebar-ring: oklch(0.556 0 0);
116 | }
117 |
118 | @layer base {
119 | * {
120 | @apply border-border outline-ring/50;
121 | }
122 | body {
123 | @apply bg-background text-foreground;
124 | }
125 | }
126 |
127 |
128 | ::-moz-selection {
129 | color: var(--background);
130 | background: var(--foreground);
131 | }
132 |
133 | ::selection {
134 | color: var(--background);
135 | background: var(--foreground);
136 | }
137 |
138 | /* tailwind styles */
139 | .head-text-lg {
140 | @apply text-3xl md:text-5xl lg:text-6xl font-bold font-heading tracking-tight leading-[1.5];
141 | }
142 |
143 | .head-text-md {
144 | @apply text-2xl md:text-4xl lg:text-5xl font-bold font-heading tracking-tight leading-[1.5];
145 | }
146 |
147 | .head-text-sm {
148 | @apply text-lg md:text-xl lg:text-2xl font-bold font-heading tracking-tight leading-[1.5];
149 | }
150 |
151 | /* view transition */
152 | ::view-transition-group(root) {
153 | animation-duration: 0.5s;
154 | animation-timing-function: var(--expo-in);
155 | }
156 |
157 | ::view-transition-new(root) {
158 | animation-name: reveal;
159 | }
160 |
161 | ::view-transition-old(root),
162 | .dark::view-transition-old(root) {
163 | animation: none;
164 | z-index: -1;
165 | }
166 |
167 | @keyframes reveal {
168 | from {
169 | clip-path: polygon(0% 50%, 100% 50%, 100% 50%, 0% 50%);
170 | }
171 | to {
172 | clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | export type SiteConfig = {
2 | name: string;
3 | title: string;
4 | description: string;
5 | origin: string;
6 | og: string;
7 | keywords: string[];
8 | creator: {
9 | name: string;
10 | url: string;
11 | }
12 | socials: {
13 | github: string;
14 | x: string;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------