├── .dev.vars.example ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── auth-schema.ts ├── bun.lockb ├── components.json ├── drizzle.config.ts ├── eslint.config.mjs ├── next.config.ts ├── package.json ├── postcss.config.mjs ├── prettier.config.mjs ├── public ├── file.svg ├── globe.svg ├── next.svg ├── vercel.svg └── window.svg ├── src ├── app │ ├── api │ │ └── [...route] │ │ │ └── route.ts │ ├── create │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── login │ │ └── page.tsx │ ├── not-found.tsx │ ├── page.tsx │ ├── post │ │ └── [slug] │ │ │ └── page.tsx │ └── register │ │ └── page.tsx ├── components │ ├── blog-list-skeleton.tsx │ ├── blog-list.tsx │ ├── blog-post-skeleton.tsx │ ├── blog-post.tsx │ ├── create-post-form.tsx │ ├── edit-post-form.tsx │ ├── login-form.tsx │ ├── page-header.tsx │ ├── query-provider.tsx │ ├── register-form.tsx │ ├── shell.tsx │ ├── site-footer.tsx │ ├── site-header.tsx │ ├── theme-provider.tsx │ ├── theme-toggle.tsx │ └── ui │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── button.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── skeleton.tsx │ │ └── textarea.tsx ├── config │ └── constants.ts ├── env │ ├── client.ts │ └── server.ts ├── lib │ ├── auth-client.ts │ ├── auth-utils.ts │ ├── auth.ts │ ├── client-rpc.ts │ ├── server-rpc.ts │ ├── types.ts │ └── utils.ts └── server │ ├── db │ ├── auth-schema.sql.ts │ ├── index.ts │ ├── migrations │ │ ├── 0000_bizarre_lester.sql │ │ ├── 0001_perfect_maddog.sql │ │ ├── 0002_vengeful_power_man.sql │ │ └── meta │ │ │ ├── 0000_snapshot.json │ │ │ ├── 0001_snapshot.json │ │ │ ├── 0002_snapshot.json │ │ │ └── _journal.json │ └── post-schema.sql.ts │ ├── hono-factory.ts │ ├── index.ts │ ├── middlewares │ ├── cors-middleware.ts │ ├── csrf-middleware.ts │ └── session-middleware.ts │ ├── routes │ ├── auth-route.ts │ └── posts-route.ts │ └── validations │ ├── auth.schema.ts │ └── post.schema.ts ├── tsconfig.json └── wrangler.jsonc /.dev.vars.example: -------------------------------------------------------------------------------- 1 | BETTER_AUTH_SECRET = "your-secret-key-change-in-production" 2 | BETTER_AUTH_URL = "http://localhost:3000" 3 | FRONTEND_URL = "http://localhost:3000" 4 | GOOGLE_CLIENT_ID = "your-google-client-id" 5 | GOOGLE_CLIENT_SECRET = "your-google-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 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | .dev.vars 44 | .wrangler -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js Blog with Hono and Better Auth 2 | 3 | A modern blog application built with Next.js, Hono, and Better Auth, featuring server-side rendering, authentication, and a clean UI. 4 | 5 | ## Prerequisites 6 | 7 | - [Bun](https://bun.sh/) (v1.0.0 or later) 8 | - [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/) 9 | - [Cloudflare account](https://dash.cloudflare.com/sign-up) 10 | 11 | ## Getting Started 12 | 13 | ### 1. Clone the Repository 14 | 15 | ```bash 16 | git clone https://github.com/raikusy/nextjs-hono-better-auth-d1 17 | cd nextjs-hono-better-auth-d1 18 | ``` 19 | 20 | ### 2. Install Dependencies 21 | 22 | ```bash 23 | bun install 24 | ``` 25 | 26 | ### 3. Set Up Cloudflare D1 Database 27 | 28 | First, create a new D1 database using Wrangler: 29 | 30 | ```bash 31 | wrangler d1 create nextjs-hono-better-auth-d1 32 | ``` 33 | 34 | This command will output something like: 35 | 36 | ``` 37 | Created database 'nextjs-hono-better-auth-d1' (ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) 38 | ``` 39 | 40 | ### 4. Configure Environment Variables 41 | 42 | 1. Copy the example environment files: 43 | 44 | ```bash 45 | cp .dev.vars.example .dev.vars 46 | cp .env.example .env 47 | ``` 48 | 49 | 2. Update `wrangler.jsonc` with your database information: 50 | 51 | ```jsonc 52 | { 53 | "d1_databases": [ 54 | { 55 | "binding": "DB", 56 | "database_name": "nextjs-hono-better-auth-d1", 57 | "database_id": "YOUR_DATABASE_ID", // Replace with the ID from step 3 58 | }, 59 | ], 60 | } 61 | ``` 62 | 63 | 3. Update the environment variables in `.env`: 64 | 65 | ```env 66 | NEXT_PUBLIC_API_URL=http://localhost:8787 67 | BETTER_AUTH_URL=http://localhost:8787 68 | ``` 69 | 70 | 4. Update the environment variables in `.dev.vars`: 71 | 72 | ```env 73 | BETTER_AUTH_SECRET=your-secret-key 74 | GOOGLE_CLIENT_ID=your-google-client-id 75 | GOOGLE_CLIENT_SECRET=your-google-client-secret 76 | ``` 77 | 78 | ### 5. Run Database Migrations 79 | 80 | ```bash 81 | bun run db:migrate 82 | ``` 83 | 84 | ### 6. Start the Development Servers 85 | 86 | 1. Start the Next.js development server: 87 | 88 | ```bash 89 | bun run dev 90 | ``` 91 | 92 | 2. In a new terminal, start the Hono worker: 93 | 94 | ```bash 95 | bun run worker:dev 96 | ``` 97 | 98 | The application should now be running at: 99 | 100 | - Frontend: http://localhost:3000 101 | - API: http://localhost:8787 102 | 103 | ## Available Scripts 104 | 105 | - `bun run dev` - Start the Next.js development server 106 | - `bun run worker:dev` - Start the Hono worker development server 107 | - `bun run db:migrate` - Run database migrations 108 | - `bun run db:migrate-remote` - Run database migrations on the remote database 109 | - `bun run build` - Build the Next.js application 110 | - `bun run start` - Start the production Next.js server 111 | - `bun run lint` - Run ESLint 112 | - `bun run format` - Format code with Prettier 113 | 114 | ## Project Structure 115 | 116 | ``` 117 | src/ 118 | ├── app/ # Next.js app directory 119 | ├── components/ # React components 120 | ├── config/ # Configuration files 121 | ├── lib/ # Utility functions and shared code 122 | ├── server/ # Hono server code 123 | │ ├── routes/ # API routes 124 | │ ├── validations/ # Zod validation schemas 125 | │ └── hono-factory.ts 126 | └── types/ # TypeScript type definitions 127 | ``` 128 | 129 | ## Features 130 | 131 | - Server-side rendering with Next.js 132 | - API routes with Hono 133 | - Authentication with Better Auth 134 | - Database with Cloudflare D1 135 | - Modern UI with Tailwind CSS and Shadcn UI 136 | - Type-safe development with TypeScript 137 | - Form validation with Zod 138 | - State management with TanStack Query 139 | 140 | ## Contributing 141 | 142 | 1. Fork the repository 143 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 144 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 145 | 4. Push to the branch (`git push origin feature/amazing-feature`) 146 | 5. Open a Pull Request 147 | 148 | ## License 149 | 150 | This project is licensed under the MIT License - see the LICENSE file for details. 151 | -------------------------------------------------------------------------------- /auth-schema.ts: -------------------------------------------------------------------------------- 1 | import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; 2 | 3 | export const users = sqliteTable("users", { 4 | id: text("id").primaryKey(), 5 | name: text("name").notNull(), 6 | email: text("email").notNull().unique(), 7 | emailVerified: integer("email_verified", { mode: "boolean" }).notNull(), 8 | image: text("image"), 9 | createdAt: integer("created_at", { mode: "timestamp" }).notNull(), 10 | updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), 11 | }); 12 | 13 | export const sessions = sqliteTable("sessions", { 14 | id: text("id").primaryKey(), 15 | expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), 16 | token: text("token").notNull().unique(), 17 | createdAt: integer("created_at", { mode: "timestamp" }).notNull(), 18 | updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), 19 | ipAddress: text("ip_address"), 20 | userAgent: text("user_agent"), 21 | userId: text("user_id") 22 | .notNull() 23 | .references(() => users.id, { onDelete: "cascade" }), 24 | }); 25 | 26 | export const accounts = sqliteTable("accounts", { 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(() => users.id, { onDelete: "cascade" }), 33 | accessToken: text("access_token"), 34 | refreshToken: text("refresh_token"), 35 | idToken: text("id_token"), 36 | accessTokenExpiresAt: integer("access_token_expires_at", { 37 | mode: "timestamp", 38 | }), 39 | refreshTokenExpiresAt: integer("refresh_token_expires_at", { 40 | mode: "timestamp", 41 | }), 42 | scope: text("scope"), 43 | password: text("password"), 44 | createdAt: integer("created_at", { mode: "timestamp" }).notNull(), 45 | updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(), 46 | }); 47 | 48 | export const verifications = sqliteTable("verifications", { 49 | id: text("id").primaryKey(), 50 | identifier: text("identifier").notNull(), 51 | value: text("value").notNull(), 52 | expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), 53 | createdAt: integer("created_at", { mode: "timestamp" }), 54 | updatedAt: integer("updated_at", { mode: "timestamp" }), 55 | }); 56 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raikusy/nextjs-hono-better-auth-d1/6924ca8f8c48e85f8b49144526f19d2860436c78/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": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 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 | } -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | 3 | export default { 4 | schema: "./src/server/db/**.sql.ts", 5 | out: "./src/server/db/migrations", 6 | dialect: "sqlite", 7 | driver: "d1-http", 8 | } satisfies Config; 9 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from "@eslint/eslintrc"; 2 | import { dirname } from "path"; 3 | import { fileURLToPath } from "url"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | { 15 | rules: { 16 | "@typescript-eslint/no-unused-vars": [ 17 | "warn", 18 | { 19 | argsIgnorePattern: "^_", 20 | varsIgnorePattern: "^_", 21 | }, 22 | ], 23 | }, 24 | }, 25 | ]; 26 | 27 | export default eslintConfig; 28 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | // import { setupDevPlatform } from "@cloudflare/next-on-pages/next-dev"; 2 | import type { NextConfig } from "next"; 3 | 4 | const nextConfig: NextConfig = { 5 | /* config options here */ 6 | images: { 7 | remotePatterns: [ 8 | { 9 | hostname: "picsum.photos", 10 | }, 11 | ], 12 | }, 13 | }; 14 | 15 | // if (process.env.NODE_ENV === "development") { 16 | // await setupDevPlatform(); 17 | // } 18 | 19 | export default nextConfig; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-hono-better-auth-d1", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "worker:dev": "wrangler dev", 8 | "pages:build": "npx @cloudflare/next-on-pages", 9 | "preview": "bun run pages:build && wrangler pages dev", 10 | "pages:deploy": "bun run pages:build && wrangler pages deploy", 11 | "worker:deploy": "wrangler deploy", 12 | "build": "next build", 13 | "start": "next start", 14 | "lint": "next lint", 15 | "format": "prettier --write ./src", 16 | "db:generate": "drizzle-kit generate", 17 | "db:migrate": "wrangler d1 migrations apply nextjs-hono-better-auth-d1", 18 | "db:migrate-remote": "wrangler d1 migrations apply nextjs-hono-better-auth-d1 --remote" 19 | }, 20 | "dependencies": { 21 | "@hono/zod-validator": "^0.4.3", 22 | "@hookform/resolvers": "^4.1.3", 23 | "@paralleldrive/cuid2": "^2.2.2", 24 | "@radix-ui/react-alert-dialog": "^1.1.6", 25 | "@radix-ui/react-dialog": "^1.1.6", 26 | "@radix-ui/react-dropdown-menu": "^2.1.6", 27 | "@radix-ui/react-label": "^2.1.2", 28 | "@radix-ui/react-separator": "^1.1.2", 29 | "@radix-ui/react-slot": "^1.1.2", 30 | "@t3-oss/env-nextjs": "^0.12.0", 31 | "@tailwindcss/typography": "^0.5.16", 32 | "@tanstack/react-query": "^5.69.0", 33 | "@types/bun": "^1.2.6", 34 | "better-auth": "^1.2.4", 35 | "better-sqlite3": "^11.9.1", 36 | "boxen": "^8.0.1", 37 | "class-variance-authority": "^0.7.1", 38 | "clsx": "^2.1.1", 39 | "dayjs": "^1.11.13", 40 | "drizzle-orm": "^0.41.0", 41 | "drizzle-zod": "^0.7.0", 42 | "hono": "^4.7.5", 43 | "js-cookie": "^3.0.5", 44 | "lucide-react": "^0.483.0", 45 | "next": "^15.2.4", 46 | "next-themes": "^0.4.6", 47 | "react": "^19.0.0", 48 | "react-dom": "^19.0.0", 49 | "react-hook-form": "^7.54.2", 50 | "slugify": "^1.6.6", 51 | "sonner": "^2.0.1", 52 | "superjson": "^2.2.2", 53 | "tailwind-merge": "^3.0.2", 54 | "tailwindcss-animate": "^1.0.7", 55 | "zod": "^3.24.2" 56 | }, 57 | "devDependencies": { 58 | "@cloudflare/next-on-pages": "^1.13.10", 59 | "@cloudflare/workers-types": "^4.20250321.0", 60 | "@eslint/eslintrc": "^3", 61 | "@tailwindcss/postcss": "^4", 62 | "@trivago/prettier-plugin-sort-imports": "^5.2.2", 63 | "@types/better-sqlite3": "^7.6.12", 64 | "@types/js-cookie": "^3.0.6", 65 | "@types/node": "^22", 66 | "@types/react": "^19", 67 | "@types/react-dom": "^19", 68 | "drizzle-kit": "^0.30.5", 69 | "eslint": "^9", 70 | "eslint-config-next": "15.2.4", 71 | "prettier": "^3.5.3", 72 | "prettier-plugin-tailwindcss": "^0.6.11", 73 | "tailwindcss": "^4.0.15", 74 | "typescript": "^5", 75 | "wrangler": "^4.4.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | const config = { 3 | printWidth: 120, 4 | tabWidth: 2, 5 | useTabs: false, 6 | singleQuote: false, 7 | trailingComma: 'es5', 8 | arrowParens: 'always', 9 | importOrderSeparation: true, 10 | importOrder: ['', '^@/(.*)$', '^[./]'], 11 | importOrderSeparation: true, 12 | importOrderSortSpecifiers: true, 13 | plugins: ['@trivago/prettier-plugin-sort-imports', 'prettier-plugin-tailwindcss'], 14 | }; 15 | 16 | export default config; 17 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/[...route]/route.ts: -------------------------------------------------------------------------------- 1 | import { handle } from "hono/vercel"; 2 | 3 | import routes from "@/server"; 4 | 5 | export const GET = handle(routes); 6 | export const POST = handle(routes); 7 | -------------------------------------------------------------------------------- /src/app/create/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | 3 | import { CreatePostForm } from "@/components/create-post-form"; 4 | import { PageHeader } from "@/components/page-header"; 5 | import { Shell } from "@/components/shell"; 6 | import { requireAuth } from "@/lib/auth-utils"; 7 | 8 | export const metadata: Metadata = { 9 | title: "Create New Post", 10 | description: "Create a new blog post", 11 | }; 12 | 13 | export default async function CreatePostPage() { 14 | // Ensure the user is authenticated 15 | await requireAuth(); 16 | 17 | return ( 18 | 19 | 20 |
21 | 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raikusy/nextjs-hono-better-auth-d1/6924ca8f8c48e85f8b49144526f19d2860436c78/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @plugin "tailwindcss-animate"; 4 | 5 | @plugin "@tailwindcss/typography"; 6 | 7 | @custom-variant dark (&:is(.dark *)); 8 | 9 | @theme inline { 10 | --color-background: var(--background); 11 | --color-foreground: var(--foreground); 12 | --font-sans: var(--font-geist-sans); 13 | --font-mono: var(--font-geist-mono); 14 | --color-sidebar-ring: var(--sidebar-ring); 15 | --color-sidebar-border: var(--sidebar-border); 16 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 17 | --color-sidebar-accent: var(--sidebar-accent); 18 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 19 | --color-sidebar-primary: var(--sidebar-primary); 20 | --color-sidebar-foreground: var(--sidebar-foreground); 21 | --color-sidebar: var(--sidebar); 22 | --color-chart-5: var(--chart-5); 23 | --color-chart-4: var(--chart-4); 24 | --color-chart-3: var(--chart-3); 25 | --color-chart-2: var(--chart-2); 26 | --color-chart-1: var(--chart-1); 27 | --color-ring: var(--ring); 28 | --color-input: var(--input); 29 | --color-border: var(--border); 30 | --color-destructive: var(--destructive); 31 | --color-accent-foreground: var(--accent-foreground); 32 | --color-accent: var(--accent); 33 | --color-muted-foreground: var(--muted-foreground); 34 | --color-muted: var(--muted); 35 | --color-secondary-foreground: var(--secondary-foreground); 36 | --color-secondary: var(--secondary); 37 | --color-primary-foreground: var(--primary-foreground); 38 | --color-primary: var(--primary); 39 | --color-popover-foreground: var(--popover-foreground); 40 | --color-popover: var(--popover); 41 | --color-card-foreground: var(--card-foreground); 42 | --color-card: var(--card); 43 | --radius-sm: calc(var(--radius) - 4px); 44 | --radius-md: calc(var(--radius) - 2px); 45 | --radius-lg: var(--radius); 46 | --radius-xl: calc(var(--radius) + 4px); 47 | } 48 | 49 | :root { 50 | --radius: 0.625rem; 51 | --background: oklch(1 0 0); 52 | --foreground: oklch(0.141 0.005 285.823); 53 | --card: oklch(1 0 0); 54 | --card-foreground: oklch(0.141 0.005 285.823); 55 | --popover: oklch(1 0 0); 56 | --popover-foreground: oklch(0.141 0.005 285.823); 57 | --primary: oklch(0.21 0.006 285.885); 58 | --primary-foreground: oklch(0.985 0 0); 59 | --secondary: oklch(0.967 0.001 286.375); 60 | --secondary-foreground: oklch(0.21 0.006 285.885); 61 | --muted: oklch(0.967 0.001 286.375); 62 | --muted-foreground: oklch(0.552 0.016 285.938); 63 | --accent: oklch(0.967 0.001 286.375); 64 | --accent-foreground: oklch(0.21 0.006 285.885); 65 | --destructive: oklch(0.577 0.245 27.325); 66 | --border: oklch(0.92 0.004 286.32); 67 | --input: oklch(0.92 0.004 286.32); 68 | --ring: oklch(0.705 0.015 286.067); 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.141 0.005 285.823); 76 | --sidebar-primary: oklch(0.21 0.006 285.885); 77 | --sidebar-primary-foreground: oklch(0.985 0 0); 78 | --sidebar-accent: oklch(0.967 0.001 286.375); 79 | --sidebar-accent-foreground: oklch(0.21 0.006 285.885); 80 | --sidebar-border: oklch(0.92 0.004 286.32); 81 | --sidebar-ring: oklch(0.705 0.015 286.067); 82 | } 83 | 84 | .dark { 85 | --background: oklch(0.141 0.005 285.823); 86 | --foreground: oklch(0.985 0 0); 87 | --card: oklch(0.21 0.006 285.885); 88 | --card-foreground: oklch(0.985 0 0); 89 | --popover: oklch(0.21 0.006 285.885); 90 | --popover-foreground: oklch(0.985 0 0); 91 | --primary: oklch(0.92 0.004 286.32); 92 | --primary-foreground: oklch(0.21 0.006 285.885); 93 | --secondary: oklch(0.274 0.006 286.033); 94 | --secondary-foreground: oklch(0.985 0 0); 95 | --muted: oklch(0.274 0.006 286.033); 96 | --muted-foreground: oklch(0.705 0.015 286.067); 97 | --accent: oklch(0.274 0.006 286.033); 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.552 0.016 285.938); 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.21 0.006 285.885); 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.274 0.006 286.033); 113 | --sidebar-accent-foreground: oklch(0.985 0 0); 114 | --sidebar-border: oklch(1 0 0 / 10%); 115 | --sidebar-ring: oklch(0.552 0.016 285.938); 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 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from "next/font/google"; 2 | import type React from "react"; 3 | 4 | import { QueryProvider } from "@/components/query-provider"; 5 | import { SiteFooter } from "@/components/site-footer"; 6 | import { SiteHeader } from "@/components/site-header"; 7 | import { ThemeProvider } from "@/components/theme-provider"; 8 | import { getCurrentUser } from "@/lib/auth-utils"; 9 | 10 | import "./globals.css"; 11 | 12 | const inter = Inter({ subsets: ["latin"] }); 13 | 14 | export const metadata = { 15 | title: "Next.js Blog", 16 | description: "A blog built with Next.js, Tailwind CSS, and shadcn/ui", 17 | }; 18 | 19 | export default async function RootLayout({ children }: { children: React.ReactNode }) { 20 | const currentSession = await getCurrentUser(); 21 | return ( 22 | 23 | 24 | 25 | 26 |
27 | 28 |
{children}
29 | 30 |
31 |
32 |
33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import { LoginForm } from "@/components/login-form"; 5 | import { getCurrentUser } from "@/lib/auth-utils"; 6 | 7 | export const metadata = { 8 | title: "Login", 9 | description: "Login to your account", 10 | }; 11 | 12 | export default async function LoginPage() { 13 | const currentSession = await getCurrentUser(); 14 | if (currentSession) { 15 | redirect("/"); 16 | } 17 | return ( 18 |
19 |
20 |
21 |

Welcome back

22 |

Enter your credentials to sign in to your account

23 |
24 | 25 |

26 | 27 | Don't have an account? Sign Up 28 | 29 |

30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | export default function NotFound() { 4 | return ( 5 |
6 |

Not Found

7 |

The page you are looking for does not exist.

8 | 12 | Return Home 13 | 14 |
15 | ) 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Plus } from "lucide-react"; 2 | import Link from "next/link"; 3 | import { Suspense } from "react"; 4 | 5 | import { BlogList } from "@/components/blog-list"; 6 | import { BlogListSkeleton } from "@/components/blog-list-skeleton"; 7 | import { getCurrentUser } from "@/lib/auth-utils"; 8 | 9 | export default async function HomePage() { 10 | const currentSession = await getCurrentUser(); 11 | return ( 12 |
13 |
14 |
15 |

Blog

16 |

Explore the latest articles and insights from our team.

17 |
18 |
19 | {currentSession ? ( 20 | <> 21 | 25 | 26 | New Post 27 | 28 | 29 | ) : ( 30 | <> 31 | 35 | Login 36 | 37 | 41 | Register 42 | 43 | 44 | )} 45 |
46 |
47 |
48 | }> 49 | 50 | 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/app/post/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | import { Suspense } from "react"; 3 | 4 | import { BlogPost } from "@/components/blog-post"; 5 | import { BlogPostSkeleton } from "@/components/blog-post-skeleton"; 6 | import { getServerRPC } from "@/lib/server-rpc"; 7 | 8 | export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) { 9 | const slug = (await params).slug; 10 | const rpc = await getServerRPC(); 11 | const postResponse = await rpc.api.posts[":slug"].$get({ 12 | param: { slug }, 13 | }); 14 | const post = await postResponse.json(); 15 | 16 | if ("error" in post) { 17 | return { 18 | title: "Post Not Found", 19 | description: "The requested blog post could not be found", 20 | }; 21 | } 22 | 23 | if (!post) { 24 | return { 25 | title: "Post Not Found", 26 | description: "The requested blog post could not be found", 27 | }; 28 | } 29 | 30 | return { 31 | title: post.title, 32 | description: post.excerpt, 33 | }; 34 | } 35 | 36 | export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) { 37 | const slug = (await params).slug; 38 | return ( 39 |
40 | }> 41 | 42 | 43 |
44 | ); 45 | } 46 | 47 | async function BlogPostContent({ slug }: { slug: string }) { 48 | const rpc = await getServerRPC(); 49 | const postResponse = await rpc.api.posts[":slug"].$get({ 50 | param: { slug }, 51 | }); 52 | const post = await postResponse.json(); 53 | 54 | if ("error" in post) { 55 | notFound(); 56 | } 57 | 58 | if (!post) { 59 | notFound(); 60 | } 61 | 62 | return ; 63 | } 64 | -------------------------------------------------------------------------------- /src/app/register/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { RegisterForm } from "@/components/register-form"; 3 | 4 | export const metadata = { 5 | title: "Register", 6 | description: "Create a new account", 7 | }; 8 | 9 | export default function RegisterPage() { 10 | return ( 11 |
12 |
13 |
14 |

15 | Create an account 16 |

17 |

18 | Enter your information to create an account 19 |

20 |
21 | 22 |

23 | 27 | Already have an account? Sign In 28 | 29 |

30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/components/blog-list-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | 3 | export function BlogListSkeleton() { 4 | return ( 5 |
6 | {Array.from({ length: 6 }).map((_, i) => ( 7 |
8 | 9 |
10 | 11 | 12 | 13 | 14 |
15 |
16 | ))} 17 |
18 | ) 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/components/blog-list.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | 4 | import { getServerRPC } from "@/lib/server-rpc"; 5 | import { formatDate } from "@/lib/utils"; 6 | 7 | export async function BlogList() { 8 | const rpc = await getServerRPC(); 9 | const postResponse = await rpc.api.posts.$get(); 10 | const posts = await postResponse.json(); 11 | 12 | console.log(posts); 13 | return ( 14 |
15 | {posts.map((post) => ( 16 | 17 |
18 |
19 | {post.title} 25 |
26 |
27 |

28 | {post.title} 29 |

30 |

{post.excerpt}

31 |

{formatDate(post.createdAt)}

32 |
33 |
34 | 35 | ))} 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/blog-post-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | 3 | export function BlogPostSkeleton() { 4 | return ( 5 |
6 |
7 | 8 |
9 | 10 | 11 |
12 |
13 | 14 |
15 | {Array.from({ length: 5 }).map((_, i) => ( 16 | 17 | ))} 18 |
19 |
20 | ) 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/components/blog-post.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | import { formatDate } from "@/lib/utils"; 4 | import type { Post } from "@/server/validations/post.schema"; 5 | 6 | interface BlogPostProps { 7 | post: Post; 8 | } 9 | 10 | export function BlogPost({ post }: BlogPostProps) { 11 | return ( 12 |
13 |
14 |

{post.title}

15 |
16 | 17 |
18 | 19 | {post.readingTime} min read 20 |
21 |
22 |
23 |
24 | {post.title} 25 |
26 |
27 | {post.content.split("\n\n").map((paragraph, index) => ( 28 |

{paragraph}

29 | ))} 30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/components/create-post-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { useMutation } from "@tanstack/react-query"; 5 | import { Loader2 } from "lucide-react"; 6 | import { useRouter } from "next/navigation"; 7 | import { useForm } from "react-hook-form"; 8 | import { toast } from "sonner"; 9 | 10 | import { Button } from "@/components/ui/button"; 11 | import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; 12 | import { Input } from "@/components/ui/input"; 13 | import { Textarea } from "@/components/ui/textarea"; 14 | import { clientRPC } from "@/lib/client-rpc"; 15 | import { type Post, type PostCreate, postCreateSchema } from "@/server/validations/post.schema"; 16 | 17 | export function CreatePostForm() { 18 | const router = useRouter(); 19 | 20 | const form = useForm({ 21 | resolver: zodResolver(postCreateSchema), 22 | defaultValues: { 23 | title: "", 24 | content: "", 25 | coverImage: "", 26 | }, 27 | }); 28 | 29 | const createPostMutation = useMutation({ 30 | mutationFn: async (data: PostCreate) => { 31 | const res = await clientRPC.api.posts.$post({ json: data }); 32 | return res.json() as Promise; 33 | }, 34 | onSuccess: async (data) => { 35 | try { 36 | toast.success("Post created successfully"); 37 | const slug = data.slug; 38 | 39 | if (slug) { 40 | router.push(`/post/${slug}`); 41 | } else { 42 | router.push("/"); 43 | } 44 | } catch (error) { 45 | console.error("Error processing response:", error); 46 | toast.error("Error processing response"); 47 | } 48 | }, 49 | onError: (error) => { 50 | console.error("Error creating post:", error); 51 | toast.error(error.message || "Failed to create post. Please try again."); 52 | }, 53 | }); 54 | 55 | const onSubmit = async (data: PostCreate) => { 56 | await createPostMutation.mutateAsync(data); 57 | }; 58 | 59 | return ( 60 |
61 | 62 | ( 66 | 67 | Title 68 | 69 | 70 | 71 | 72 | 73 | )} 74 | /> 75 | ( 79 | 80 | Content 81 | 82 |