├── .eslintrc.json
├── prettier.config.js
├── src
├── app
│ ├── ThemeProvider.tsx
│ ├── favicon.ico
│ ├── HomePageThemeToggler.tsx
│ ├── notes
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── NavBar.tsx
│ ├── sign-in
│ │ └── [[...sign-in]]
│ │ │ └── page.tsx
│ ├── sign-up
│ │ └── [[...sign-up]]
│ │ │ └── page.tsx
│ ├── layout.tsx
│ ├── page.tsx
│ ├── globals.css
│ └── api
│ │ ├── chat
│ │ └── route.ts
│ │ └── notes
│ │ └── route.ts
├── assets
│ ├── logo-black.png
│ ├── logo-full.png
│ └── logo-white.png
├── lib
│ ├── utils.ts
│ ├── db
│ │ ├── pinecone.ts
│ │ └── prisma.ts
│ ├── validation
│ │ └── note.ts
│ └── openai.ts
├── middleware.ts
└── components
│ ├── ui
│ ├── loading-button.tsx
│ ├── label.tsx
│ ├── textarea.tsx
│ ├── input.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── dialog.tsx
│ └── form.tsx
│ ├── AIChatButton.tsx
│ ├── ThemeToggleButton.tsx
│ ├── Note.tsx
│ ├── AIChatBox.tsx
│ └── AddEditNoteDialog.tsx
├── postcss.config.js
├── next.config.js
├── .env.template
├── components.json
├── prisma
└── schema.prisma
├── .gitignore
├── tailwind.config.ts
├── public
├── vercel.svg
└── next.svg
├── tsconfig.json
├── README.md
├── package.json
└── tailwind.config.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "prettier"]
3 | }
4 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ["prettier-plugin-tailwindcss"],
3 | };
4 |
--------------------------------------------------------------------------------
/src/app/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | export { ThemeProvider } from "next-themes";
4 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnthonyTlei/nextjs-ai-note-app/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/assets/logo-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnthonyTlei/nextjs-ai-note-app/HEAD/src/assets/logo-black.png
--------------------------------------------------------------------------------
/src/assets/logo-full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnthonyTlei/nextjs-ai-note-app/HEAD/src/assets/logo-full.png
--------------------------------------------------------------------------------
/src/assets/logo-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnthonyTlei/nextjs-ai-note-app/HEAD/src/assets/logo-white.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [{ hostname: "img.clerk.com" }],
5 | },
6 | };
7 |
8 | module.exports = nextConfig;
9 |
--------------------------------------------------------------------------------
/src/app/HomePageThemeToggler.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import ThemeToggleButton from "@/components/ThemeToggleButton";
4 |
5 | export default function HomePageThemeToggler() {
6 | return ;
7 | }
8 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { authMiddleware } from "@clerk/nextjs";
2 |
3 | export default authMiddleware({
4 | publicRoutes: ["/"],
5 | });
6 |
7 | export const config = {
8 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
9 | };
10 |
--------------------------------------------------------------------------------
/src/app/notes/layout.tsx:
--------------------------------------------------------------------------------
1 | import NavBar from "./NavBar";
2 |
3 | export default function Layout({ children }: { children: React.ReactNode }) {
4 | return (
5 | <>
6 |
7 | {children}
8 | >
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/.env.template:
--------------------------------------------------------------------------------
1 | DATABASE_URL=""
2 |
3 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
4 | CLERK_SECRET_KEY=
5 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
6 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
7 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/notes
8 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/notes
9 |
10 | OPENAI_API_KEY=
11 | PINECONE_API_KEY=
--------------------------------------------------------------------------------
/src/lib/db/pinecone.ts:
--------------------------------------------------------------------------------
1 | import { Pinecone } from "@pinecone-database/pinecone";
2 |
3 | const apiKey = process.env.PINECONE_API_KEY;
4 |
5 | if (!apiKey) {
6 | throw new Error("PINECONE_API_KEY is not defined");
7 | }
8 |
9 | const pinecone = new Pinecone({ environment: "gcp-starter", apiKey });
10 |
11 | export const notesIndex = pinecone.Index("nextjs-ai-note-app");
12 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/app/global.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/src/app/sign-in/[[...sign-in]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignIn } from "@clerk/nextjs";
2 |
3 | export const metadata = {
4 | title: "SecondBrain - Sign In",
5 | };
6 |
7 | export default function SignInPage() {
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/sign-up/[[...sign-up]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from "@clerk/nextjs";
2 |
3 | export const metadata = {
4 | title: "SecondBrain - Sign Up",
5 | };
6 |
7 | export default function SignUpPage() {
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "mongodb"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model Note {
11 | id String @id @default(auto()) @map("_id") @db.ObjectId
12 | title String
13 | content String?
14 | userId String
15 | createdAt DateTime @default(now())
16 | updatedAt DateTime @updatedAt
17 |
18 | @@map("notes")
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/validation/note.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const createNoteSchema = z.object({
4 | title: z.string().min(1, { message: "Title is required" }),
5 | content: z.string().optional(),
6 | });
7 |
8 | export type CreateNoteSchema = z.infer;
9 |
10 | export const updateNoteSchema = createNoteSchema.extend({
11 | id: z.string().min(1),
12 | });
13 |
14 | export const deleteNoteSchema = z.object({
15 | id: z.string().min(1),
16 | });
17 |
--------------------------------------------------------------------------------
/src/components/ui/loading-button.tsx:
--------------------------------------------------------------------------------
1 | import { Loader2 } from "lucide-react";
2 | import { Button, ButtonProps } from "./button";
3 |
4 | type LoadingButtonProps = {
5 | loading: boolean;
6 | } & ButtonProps;
7 |
8 | export default function LoadingButton({
9 | children,
10 | loading,
11 | ...props
12 | }: LoadingButtonProps) {
13 | return (
14 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/src/components/AIChatButton.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import AIChatBox from "./AIChatBox";
3 | import { Button } from "./ui/button";
4 | import { Bot } from "lucide-react";
5 |
6 | export default function AIChatButton() {
7 | const [chatBoxOpen, setChatBoxOpen] = useState(false);
8 |
9 | return (
10 | <>
11 |
15 | setChatBoxOpen(false)} />
16 | >
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/lib/db/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | const prismaClientSingleton = () => {
4 | const prisma = new PrismaClient();
5 | return prisma;
6 | };
7 |
8 | type PrismaClientSingleton = ReturnType;
9 |
10 | const globalForPrisma = globalThis as unknown as {
11 | prisma: PrismaClientSingleton | undefined;
12 | };
13 |
14 | const prisma = globalForPrisma.prisma ?? prismaClientSingleton();
15 |
16 | export default prisma;
17 |
18 | if (process.env.NODE_ENV !== "production") {
19 | globalForPrisma.prisma = prisma;
20 | }
21 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config: Config = {
4 | content: [
5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13 | 'gradient-conic':
14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
15 | },
16 | },
17 | },
18 | plugins: [],
19 | }
20 | export default config
21 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lib/openai.ts:
--------------------------------------------------------------------------------
1 | import OpenAi from "openai";
2 |
3 | const apiKey = process.env.OPENAI_API_KEY;
4 |
5 | if (!apiKey) {
6 | throw new Error("OPENAI_API_KEY is not defined");
7 | }
8 |
9 | const openai = new OpenAi({ apiKey });
10 |
11 | export default openai;
12 |
13 | export async function getEmbedding(text: string) {
14 | const response = await openai.embeddings.create({
15 | model: "text-embedding-ada-002",
16 | input: text,
17 | });
18 |
19 | const embedding = response.data[0].embedding;
20 |
21 | if (!embedding) {
22 | throw new Error("Error generating embedding");
23 | }
24 |
25 | return embedding;
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
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 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 | import { ClerkProvider } from "@clerk/nextjs";
5 | import { ThemeProvider } from "@/app/ThemeProvider";
6 |
7 | const inter = Inter({ subsets: ["latin"] });
8 |
9 | export const metadata: Metadata = {
10 | title: "SecondBrain",
11 | description: "Note App powered by AI",
12 | };
13 |
14 | export default function RootLayout({
15 | children,
16 | }: {
17 | children: React.ReactNode;
18 | }) {
19 | return (
20 |
21 |
22 |
23 | {children}
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/notes/page.tsx:
--------------------------------------------------------------------------------
1 | import Note from "@/components/Note";
2 | import prisma from "@/lib/db/prisma";
3 | import { auth } from "@clerk/nextjs";
4 |
5 | export const metadata = {
6 | title: "SecondBrain - Notes",
7 | };
8 |
9 | export default async function NotesPage() {
10 | const { userId } = auth();
11 |
12 | if (!userId) throw Error("userId undefined");
13 |
14 | const allNotes = await prisma.note.findMany({
15 | where: { userId },
16 | });
17 |
18 | return (
19 |
20 | {allNotes.map((note) => (
21 |
22 | ))}
23 | {allNotes.length === 0 &&
{"You don't have any notes yet."}
}
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/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 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/src/components/ThemeToggleButton.tsx:
--------------------------------------------------------------------------------
1 | import { Moon, Sun } from "lucide-react";
2 | import { useTheme } from "next-themes";
3 | import { Button } from "./ui/button";
4 |
5 | export default function ThemeToggleButton() {
6 | const { theme, setTheme } = useTheme();
7 |
8 | return (
9 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Note.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Note as NoteModel } from "@prisma/client";
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardHeader,
9 | CardTitle,
10 | } from "./ui/card";
11 | import { useState } from "react";
12 | import AddEditNoteDialog from "./AddEditNoteDialog";
13 |
14 | interface NoteProps {
15 | note: NoteModel;
16 | }
17 |
18 | export default function Note({ note }: NoteProps) {
19 | const [showEditDialog, setShowEditDialog] = useState(false);
20 | const wasUpdated = note.updatedAt > note.createdAt;
21 | const createdUpdatedAtTimestamp = (
22 | wasUpdated ? note.updatedAt : note.createdAt
23 | ).toDateString();
24 |
25 | return (
26 | <>
27 | setShowEditDialog(true)}
30 | >
31 |
32 | {note.title}
33 |
34 | {createdUpdatedAtTimestamp}
35 | {wasUpdated && " (updated)"}
36 |
37 |
38 |
39 | {note.content}
40 |
41 |
42 |
47 | >
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-ai-note-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "postinstall": "prisma generate"
11 | },
12 | "dependencies": {
13 | "@clerk/nextjs": "^4.29.3",
14 | "@clerk/themes": "^1.7.9",
15 | "@hookform/resolvers": "^3.3.2",
16 | "@pinecone-database/pinecone": "^1.1.2",
17 | "@prisma/client": "^5.5.2",
18 | "@radix-ui/react-dialog": "^1.0.5",
19 | "@radix-ui/react-label": "^2.0.2",
20 | "@radix-ui/react-slot": "^1.0.2",
21 | "ai": "^2.2.20",
22 | "class-variance-authority": "^0.7.0",
23 | "clsx": "^2.0.0",
24 | "eslint-config-prettier": "^9.0.0",
25 | "lucide-react": "^0.292.0",
26 | "next": "14.0.1",
27 | "next-themes": "^0.2.1",
28 | "openai": "^4.15.0",
29 | "prettier": "^3.0.3",
30 | "prettier-plugin-tailwindcss": "^0.5.6",
31 | "prisma": "^5.5.2",
32 | "react": "^18",
33 | "react-dom": "^18",
34 | "react-hook-form": "^7.47.0",
35 | "tailwind-merge": "^2.0.0",
36 | "tailwindcss-animate": "^1.0.7",
37 | "zod": "^3.22.4"
38 | },
39 | "devDependencies": {
40 | "@types/node": "^20",
41 | "@types/react": "^18",
42 | "@types/react-dom": "^18",
43 | "autoprefixer": "^10.0.1",
44 | "eslint": "^8",
45 | "eslint-config-next": "14.0.1",
46 | "postcss": "^8",
47 | "tailwindcss": "^3.3.0",
48 | "typescript": "^5"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import logoBlack from "@/assets/logo-black.png";
3 | import logoWhite from "@/assets/logo-white.png";
4 | import { Button } from "@/components/ui/button";
5 | import Link from "next/link";
6 | import { auth } from "@clerk/nextjs";
7 | import { redirect } from "next/navigation";
8 | import HomePageThemeToggler from "./HomePageThemeToggler";
9 |
10 | export default function Home() {
11 | const { userId } = auth();
12 |
13 | if (userId) {
14 | redirect("/notes");
15 | }
16 |
17 | return (
18 |
19 |
20 |
27 |
34 |
35 | SecondBrain
36 |
37 |
38 |
39 |
40 | An intelligent note taking app built with Clerk, Next.js, OpenAI,
41 | Pinecone, Prisma, Shadcn UI and more.
42 |
43 |
44 |
45 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 222.2 84% 4.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 |
16 | --primary: 222.2 47.4% 11.2%;
17 | --primary-foreground: 210 40% 98%;
18 |
19 | --secondary: 210 40% 96.1%;
20 | --secondary-foreground: 222.2 47.4% 11.2%;
21 |
22 | --muted: 210 40% 96.1%;
23 | --muted-foreground: 215.4 16.3% 46.9%;
24 |
25 | --accent: 210 40% 96.1%;
26 | --accent-foreground: 222.2 47.4% 11.2%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 210 40% 98%;
30 |
31 | --border: 214.3 31.8% 91.4%;
32 | --input: 214.3 31.8% 91.4%;
33 | --ring: 222.2 84% 4.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 222.2 84% 4.9%;
40 | --foreground: 210 40% 98%;
41 |
42 | --card: 222.2 84% 4.9%;
43 | --card-foreground: 210 40% 98%;
44 |
45 | --popover: 222.2 84% 4.9%;
46 | --popover-foreground: 210 40% 98%;
47 |
48 | --primary: 210 40% 98%;
49 | --primary-foreground: 222.2 47.4% 11.2%;
50 |
51 | --secondary: 217.2 32.6% 17.5%;
52 | --secondary-foreground: 210 40% 98%;
53 |
54 | --muted: 217.2 32.6% 17.5%;
55 | --muted-foreground: 215 20.2% 65.1%;
56 |
57 | --accent: 217.2 32.6% 17.5%;
58 | --accent-foreground: 210 40% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 210 40% 98%;
62 |
63 | --border: 217.2 32.6% 17.5%;
64 | --input: 217.2 32.6% 17.5%;
65 | --ring: 212.7 26.8% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
--------------------------------------------------------------------------------
/src/app/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { notesIndex } from "@/lib/db/pinecone";
2 | import prisma from "@/lib/db/prisma";
3 | import openai, { getEmbedding } from "@/lib/openai";
4 | import { auth } from "@clerk/nextjs";
5 | import { OpenAIStream, StreamingTextResponse } from "ai";
6 | import { ChatCompletionMessage } from "openai/resources/index.mjs";
7 |
8 | export async function POST(req: Request) {
9 | try {
10 | const body = await req.json();
11 | const messages: ChatCompletionMessage[] = body.messages;
12 |
13 | const messagesTruncated = messages.slice(-6);
14 |
15 | const embedding = await getEmbedding(
16 | messagesTruncated.map((message) => message.content).join("\n"),
17 | );
18 |
19 | const { userId } = auth();
20 |
21 | const vectorQueryResponse = await notesIndex.query({
22 | vector: embedding,
23 | topK: 4,
24 | filter: { userId },
25 | });
26 |
27 | const relevantNotes = await prisma.note.findMany({
28 | where: {
29 | id: {
30 | in: vectorQueryResponse.matches.map((match) => match.id),
31 | },
32 | },
33 | });
34 |
35 | console.log("Relevant notes found: ", relevantNotes);
36 |
37 | const systemMessage: ChatCompletionMessage = {
38 | role: "system",
39 | content:
40 | "You are an intelligent note-taking app. You answer the user's question based on their existing notes. " +
41 | "The relevant notes for this query are:\n" +
42 | relevantNotes
43 | .map((note) => `Title: ${note.title}\n\nContent:\n${note.content}`)
44 | .join("\n\n"),
45 | };
46 |
47 | const response = await openai.chat.completions.create({
48 | model: "gpt-3.5-turbo",
49 | stream: true,
50 | messages: [systemMessage, ...messagesTruncated],
51 | });
52 |
53 | const stream = OpenAIStream(response);
54 | return new StreamingTextResponse(stream);
55 | } catch (error) {
56 | console.error(error);
57 | return Response.json({ error: "Internal server error" }, { status: 500 });
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | colors: {
20 | border: "hsl(var(--border))",
21 | input: "hsl(var(--input))",
22 | ring: "hsl(var(--ring))",
23 | background: "hsl(var(--background))",
24 | foreground: "hsl(var(--foreground))",
25 | primary: {
26 | DEFAULT: "hsl(var(--primary))",
27 | foreground: "hsl(var(--primary-foreground))",
28 | },
29 | secondary: {
30 | DEFAULT: "hsl(var(--secondary))",
31 | foreground: "hsl(var(--secondary-foreground))",
32 | },
33 | destructive: {
34 | DEFAULT: "hsl(var(--destructive))",
35 | foreground: "hsl(var(--destructive-foreground))",
36 | },
37 | muted: {
38 | DEFAULT: "hsl(var(--muted))",
39 | foreground: "hsl(var(--muted-foreground))",
40 | },
41 | accent: {
42 | DEFAULT: "hsl(var(--accent))",
43 | foreground: "hsl(var(--accent-foreground))",
44 | },
45 | popover: {
46 | DEFAULT: "hsl(var(--popover))",
47 | foreground: "hsl(var(--popover-foreground))",
48 | },
49 | card: {
50 | DEFAULT: "hsl(var(--card))",
51 | foreground: "hsl(var(--card-foreground))",
52 | },
53 | },
54 | borderRadius: {
55 | lg: "var(--radius)",
56 | md: "calc(var(--radius) - 2px)",
57 | sm: "calc(var(--radius) - 4px)",
58 | },
59 | keyframes: {
60 | "accordion-down": {
61 | from: { height: 0 },
62 | to: { height: "var(--radix-accordion-content-height)" },
63 | },
64 | "accordion-up": {
65 | from: { height: "var(--radix-accordion-content-height)" },
66 | to: { height: 0 },
67 | },
68 | },
69 | animation: {
70 | "accordion-down": "accordion-down 0.2s ease-out",
71 | "accordion-up": "accordion-up 0.2s ease-out",
72 | },
73 | },
74 | },
75 | plugins: [require("tailwindcss-animate")],
76 | }
--------------------------------------------------------------------------------
/src/app/notes/NavBar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import Link from "next/link";
5 | import { UserButton } from "@clerk/nextjs";
6 | import { Button } from "@/components/ui/button";
7 | import { Plus } from "lucide-react";
8 | import { useState } from "react";
9 | import { dark } from "@clerk/themes";
10 | import { useTheme } from "next-themes";
11 | import AddEditNoteDialog from "@/components/AddEditNoteDialog";
12 | import ThemeToggleButton from "@/components/ThemeToggleButton";
13 | import logoWhite from "@/assets/logo-white.png";
14 | import logoBlack from "@/assets/logo-black.png";
15 | import AIChatButton from "@/components/AIChatButton";
16 |
17 | export default function NavBar() {
18 | const { theme } = useTheme();
19 | const [showAddEditNoteDialog, setShowAddEditNoteDialog] = useState(false);
20 |
21 | return (
22 | <>
23 |
24 |
25 |
26 |
33 |
40 |
SecondBrain
41 |
42 |
43 |
50 |
51 |
57 |
64 |
65 |
66 |
67 |
68 |
72 | >
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/AIChatBox.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { useChat } from "ai/react";
3 | import { Bot, Trash, XCircle } from "lucide-react";
4 | import { Input } from "./ui/input";
5 | import { Button } from "./ui/button";
6 | import { Message } from "ai";
7 | import { useUser } from "@clerk/nextjs";
8 | import Image from "next/image";
9 | import { useEffect, useRef } from "react";
10 |
11 | interface AIChatBoxProps {
12 | open: boolean;
13 | onClose: () => void;
14 | }
15 |
16 | export default function AIChatBox({ open, onClose }: AIChatBoxProps) {
17 | const {
18 | messages,
19 | input,
20 | handleInputChange,
21 | handleSubmit,
22 | setMessages,
23 | isLoading,
24 | error,
25 | } = useChat();
26 |
27 | const inputRef = useRef(null);
28 | const scrollRef = useRef(null);
29 |
30 | useEffect(() => {
31 | if (scrollRef.current) {
32 | scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
33 | }
34 | }, [messages]);
35 |
36 | useEffect(() => {
37 | if (open) {
38 | inputRef.current?.focus();
39 | }
40 | }, [open]);
41 |
42 | const lastMessageIsUser = messages[messages.length - 1]?.role === "user";
43 |
44 | return (
45 |
51 |
54 |
55 |
56 | {messages.map((message) => (
57 |
58 | ))}
59 | {isLoading && lastMessageIsUser && (
60 |
63 | )}
64 | {error && (
65 |
71 | )}
72 | {!error && messages.length === 0 && (
73 |
74 |
75 | {"Hey there, I'm your AI assistant. Ask me anything!"}
76 |
77 | )}
78 |
79 |
98 |
99 |
100 | );
101 | }
102 |
103 | function ChatMessage({
104 | message: { role, content },
105 | }: {
106 | message: Pick;
107 | }) {
108 | const { user } = useUser();
109 | const isAiMessage = role === "assistant";
110 |
111 | return (
112 |
118 | {isAiMessage &&
}
119 |
125 | {content}
126 |
127 | {!isAiMessage && user?.imageUrl && (
128 |
135 | )}
136 |
137 | );
138 | }
139 |
--------------------------------------------------------------------------------
/src/app/api/notes/route.ts:
--------------------------------------------------------------------------------
1 | import { notesIndex } from "@/lib/db/pinecone";
2 | import prisma from "@/lib/db/prisma";
3 | import { getEmbedding } from "@/lib/openai";
4 | import {
5 | createNoteSchema,
6 | deleteNoteSchema,
7 | updateNoteSchema,
8 | } from "@/lib/validation/note";
9 | import { auth } from "@clerk/nextjs";
10 |
11 | export async function POST(req: Request) {
12 | try {
13 | const body = await req.json();
14 | const parseResult = createNoteSchema.safeParse(body);
15 |
16 | if (!parseResult.success) {
17 | console.log(parseResult.error);
18 | return Response.json({ error: "Invalid Input" }, { status: 400 });
19 | }
20 |
21 | const { title, content } = parseResult.data;
22 | const { userId } = auth();
23 |
24 | if (!userId) {
25 | return Response.json({ error: "Unauthorized" }, { status: 401 });
26 | }
27 |
28 | const embedding = await getEmbeddingForNote(title, content);
29 | const note = await prisma.$transaction(async (tx) => {
30 | const note = await tx.note.create({
31 | data: {
32 | title,
33 | content,
34 | userId,
35 | },
36 | });
37 | await notesIndex.upsert([
38 | { id: note.id, values: embedding, metadata: { userId } },
39 | ]);
40 | return note;
41 | });
42 |
43 | return Response.json({ note }, { status: 201 });
44 | } catch (error) {
45 | console.log(error);
46 | return Response.json({ error: "Internal Server Error" }, { status: 500 });
47 | }
48 | }
49 |
50 | export async function PUT(req: Request) {
51 | try {
52 | const body = await req.json();
53 | const parseResult = updateNoteSchema.safeParse(body);
54 |
55 | if (!parseResult.success) {
56 | console.log(parseResult.error);
57 | return Response.json({ error: "Invalid Input" }, { status: 400 });
58 | }
59 |
60 | const { id, title, content } = parseResult.data;
61 | const note = await prisma.note.findUnique({
62 | where: { id },
63 | });
64 |
65 | if (!note) {
66 | return Response.json({ error: "Note not found" }, { status: 404 });
67 | }
68 |
69 | const { userId } = auth();
70 |
71 | if (!userId || note.userId !== userId) {
72 | return Response.json({ error: "Unauthorized" }, { status: 401 });
73 | }
74 |
75 | const embedding = await getEmbeddingForNote(title, content);
76 | const updatedNote = await prisma.$transaction(async (tx) => {
77 | const updatedNote = await tx.note.update({
78 | where: { id },
79 | data: {
80 | title,
81 | content,
82 | },
83 | });
84 | await notesIndex.upsert([
85 | { id: note.id, values: embedding, metadata: { userId } },
86 | ]);
87 | return updatedNote;
88 | });
89 |
90 | return Response.json({ updatedNote }, { status: 200 });
91 | } catch (error) {
92 | console.log(error);
93 | return Response.json({ error: "Internal Server Error" }, { status: 500 });
94 | }
95 | }
96 |
97 | export async function DELETE(req: Request) {
98 | try {
99 | const body = await req.json();
100 | const parseResult = deleteNoteSchema.safeParse(body);
101 |
102 | if (!parseResult.success) {
103 | console.log(parseResult.error);
104 | return Response.json({ error: "Invalid Input" }, { status: 400 });
105 | }
106 |
107 | const { id } = parseResult.data;
108 | const note = await prisma.note.findUnique({
109 | where: { id },
110 | });
111 |
112 | if (!note) {
113 | return Response.json({ error: "Note not found" }, { status: 404 });
114 | }
115 |
116 | const { userId } = auth();
117 |
118 | if (!userId || note.userId !== userId) {
119 | return Response.json({ error: "Unauthorized" }, { status: 401 });
120 | }
121 |
122 | await prisma.$transaction(async (tx) => {
123 | await tx.note.delete({
124 | where: { id },
125 | });
126 | await notesIndex.deleteOne(id);
127 | });
128 |
129 | return Response.json({ message: "Note deleted" }, { status: 200 });
130 | } catch (error) {
131 | console.log(error);
132 | return Response.json({ error: "Internal Server Error" }, { status: 500 });
133 | }
134 | }
135 |
136 | async function getEmbeddingForNote(title: string, content: string | undefined) {
137 | return getEmbedding(title + "\n\n" + content ?? "");
138 | }
139 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/src/components/AddEditNoteDialog.tsx:
--------------------------------------------------------------------------------
1 | import { CreateNoteSchema, createNoteSchema } from "@/lib/validation/note";
2 | import { set, useForm } from "react-hook-form";
3 | import { zodResolver } from "@hookform/resolvers/zod";
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogFooter,
8 | DialogHeader,
9 | DialogTitle,
10 | } from "./ui/dialog";
11 | import {
12 | Form,
13 | FormControl,
14 | FormField,
15 | FormItem,
16 | FormLabel,
17 | FormMessage,
18 | } from "./ui/form";
19 | import { Input } from "./ui/input";
20 | import { Textarea } from "./ui/textarea";
21 | import LoadingButton from "./ui/loading-button";
22 | import { useRouter } from "next/navigation";
23 | import { Note } from "@prisma/client";
24 | import { useState } from "react";
25 |
26 | interface AddEditNoteDialogProps {
27 | open: boolean;
28 | setOpen: (open: boolean) => void;
29 | noteToEdit?: Note;
30 | }
31 |
32 | export default function AddEditNoteDialog({
33 | open,
34 | setOpen,
35 | noteToEdit,
36 | }: AddEditNoteDialogProps) {
37 | const [deleteInProgress, setDeleteInProgress] = useState(false);
38 | const router = useRouter();
39 |
40 | const form = useForm({
41 | resolver: zodResolver(createNoteSchema),
42 | defaultValues: {
43 | title: noteToEdit?.title || "",
44 | content: noteToEdit?.content || "",
45 | },
46 | });
47 |
48 | async function onSubmit(input: CreateNoteSchema) {
49 | try {
50 | if (noteToEdit) {
51 | const response = await fetch("/api/notes", {
52 | method: "PUT",
53 | body: JSON.stringify({
54 | id: noteToEdit.id,
55 | ...input,
56 | }),
57 | });
58 |
59 | if (!response.ok) {
60 | throw Error("Status code: " + response.status);
61 | }
62 | } else {
63 | const response = await fetch("/api/notes", {
64 | method: "POST",
65 | body: JSON.stringify(input),
66 | });
67 |
68 | if (!response.ok) {
69 | throw Error("Status code: " + response.status);
70 | }
71 |
72 | form.reset();
73 | }
74 |
75 | router.refresh();
76 | setOpen(false);
77 | } catch (error) {
78 | console.error(error);
79 | alert("Something went wrong. Please try again.");
80 | }
81 | }
82 |
83 | async function deleteNote() {
84 | if (!noteToEdit) {
85 | return;
86 | }
87 | setDeleteInProgress(true);
88 | try {
89 | const response = await fetch("/api/notes", {
90 | method: "DELETE",
91 | body: JSON.stringify({
92 | id: noteToEdit.id,
93 | }),
94 | });
95 | if (!response.ok) {
96 | throw Error("Status code: " + response.status);
97 | }
98 | router.refresh();
99 | setOpen(false);
100 | } catch (error) {
101 | console.error(error);
102 | alert("Something went wrong. Please try again.");
103 | } finally {
104 | setDeleteInProgress(false);
105 | }
106 | }
107 |
108 | return (
109 |
166 | );
167 | }
168 |
--------------------------------------------------------------------------------