├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prisma ├── migrations │ ├── 20240628060156_new_database_update │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── hero.gif ├── logo.svg ├── next.svg └── vercel.svg ├── src ├── app │ ├── api │ │ └── document │ │ │ ├── [documentId] │ │ │ └── route.ts │ │ │ └── new │ │ │ └── route.ts │ ├── document │ │ ├── [documentId] │ │ │ ├── _components │ │ │ │ ├── drawer-ai.tsx │ │ │ │ └── editor-block.tsx │ │ │ └── page.tsx │ │ ├── _components │ │ │ ├── dashboard.tsx │ │ │ ├── intro-page.tsx │ │ │ ├── new-document.tsx │ │ │ └── recent-document.tsx │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── editor.tsx │ ├── logo.tsx │ ├── navbar.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── drawer.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── use-toast.ts ├── lib │ └── utils.ts ├── middleware.ts └── utils │ ├── db.ts │ └── open-ai.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | .env 9 | .env.local 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Thumbnail Studio DEV (10)](https://github.com/oliver-gomes/quill-wizards-ai/assets/32399333/be69a15a-f658-4410-9d23-f099cb6ccedf) 2 | 3 | In this tutorial, you will learn how to create full stack Google Doc clone, with AI features to help you suggest storylines, plot twist and even resume ideas while covering all CRUD operations such as adding, editing, deleting docs, authentication, data manipulation using Next.js, TypeScript, React, Clerk, Google Sign in, Prisma, Neon, ShadCN UI, React Hook forms, Zod, OpenAI, TailwindCSS and more. 4 | 5 | ## Getting Started 6 | 7 | First, run the development server: 8 | 9 | ```bash 10 | npm run dev 11 | # or 12 | yarn dev 13 | # or 14 | pnpm dev 15 | # or 16 | bun dev 17 | ``` 18 | 19 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 20 | 21 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 22 | 23 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 24 | 25 | ## Learn More 26 | 27 | To learn more about Next.js, take a look at the following resources: 28 | 29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 31 | 32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 33 | 34 | ## Deploy on Vercel 35 | 36 | 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. 37 | 38 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 39 | # quill-wizards-ai 40 | -------------------------------------------------------------------------------- /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.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quill-wizards", 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 | }, 11 | "dependencies": { 12 | "@clerk/nextjs": "^5.1.6", 13 | "@hookform/resolvers": "^3.6.0", 14 | "@prisma/client": "^5.16.1", 15 | "@radix-ui/react-dialog": "^1.1.1", 16 | "@radix-ui/react-label": "^2.1.0", 17 | "@radix-ui/react-slot": "^1.1.0", 18 | "@radix-ui/react-toast": "^1.2.1", 19 | "axios": "^1.7.2", 20 | "class-variance-authority": "^0.7.0", 21 | "clsx": "^2.1.1", 22 | "lucide-react": "^0.397.0", 23 | "next": "14.2.4", 24 | "openai": "^4.52.1", 25 | "prisma": "^5.16.1", 26 | "react": "^18", 27 | "react-dom": "^18", 28 | "react-hook-form": "^7.52.0", 29 | "react-quill": "^2.0.0", 30 | "tailwind-merge": "^2.3.0", 31 | "tailwindcss-animate": "^1.0.7", 32 | "vaul": "^0.9.1", 33 | "zod": "^3.23.8" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^20", 37 | "@types/react": "^18", 38 | "@types/react-dom": "^18", 39 | "eslint": "^8", 40 | "eslint-config-next": "14.2.4", 41 | "postcss": "^8", 42 | "tailwindcss": "^3.4.1", 43 | "typescript": "^5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240628060156_new_database_update/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Document" ( 3 | "id" TEXT NOT NULL, 4 | "userId" TEXT NOT NULL, 5 | "title" TEXT, 6 | "description" TEXT, 7 | "createAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updateAt" TIMESTAMP(3) NOT NULL, 9 | 10 | CONSTRAINT "Document_pkey" PRIMARY KEY ("id") 11 | ); 12 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model Document { 17 | id String @id @default(uuid()) 18 | userId String 19 | title String? 20 | description String? 21 | createAt DateTime @default(now()) 22 | updateAt DateTime @updatedAt 23 | } 24 | -------------------------------------------------------------------------------- /public/hero.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oliver-gomes/quill-wizards-ai/7d8370e95b5b58c3b66945ab912adbecf713dd83/public/hero.gif -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/document/[documentId]/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/utils/db"; 2 | import { auth } from "@clerk/nextjs/server"; 3 | import { revalidatePath } from "next/cache"; 4 | import { redirect } from "next/navigation"; 5 | import { NextResponse } from "next/server"; 6 | 7 | export async function PUT( 8 | req: Request, 9 | { params }: { params: { documentId: string } } 10 | ) { 11 | try { 12 | const { userId } = auth(); 13 | 14 | if (!userId) { 15 | return new NextResponse("User Not Authenticated", { status: 401 }); 16 | } 17 | 18 | const { title, description } = await req.json(); 19 | 20 | const updateDocument = await db.document.update({ 21 | where: { 22 | id: params.documentId, 23 | userId: userId, 24 | }, 25 | data: { 26 | title: title, 27 | description: description, 28 | }, 29 | }); 30 | 31 | return new NextResponse("Succesfully updated data", { status: 200 }); 32 | } catch (error) { 33 | return new NextResponse("PUT Error", { status: 500 }); 34 | } 35 | } 36 | 37 | export async function DELETE( 38 | req: Request, 39 | { params }: { params: { documentId: string } } 40 | ) { 41 | try { 42 | const { userId } = auth(); 43 | 44 | if (!userId) { 45 | return new NextResponse("User Not Authenticated", { status: 401 }); 46 | } 47 | 48 | const deleteDocument = await db.document.delete({ 49 | where: { 50 | id: params.documentId, 51 | userId: userId, 52 | }, 53 | }); 54 | 55 | redirect("/"); 56 | revalidatePath("/"); 57 | return new NextResponse("Succesfully Deleted data", { status: 200 }); 58 | } catch (error) { 59 | return new NextResponse("DELETE Error", { status: 500 }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/api/document/new/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/utils/db"; 2 | import { auth } from "@clerk/nextjs/server"; 3 | import { revalidatePath } from "next/cache"; 4 | import { NextResponse } from "next/server"; 5 | 6 | export async function POST(req: Request) { 7 | try { 8 | const { userId } = auth(); 9 | 10 | if (!userId) { 11 | return new NextResponse("User Not Authenticated", { status: 401 }); 12 | } 13 | 14 | const { title, description } = await req.json(); 15 | 16 | const createNewDoc = await db.document.create({ 17 | data: { 18 | userId: userId, 19 | title: title, 20 | description: description, 21 | }, 22 | }); 23 | 24 | revalidatePath("/"); 25 | return NextResponse.json(createNewDoc, { status: 200 }); 26 | } catch (error) { 27 | return new NextResponse("POST, NEW DOC ERROR", { status: 500 }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/document/[documentId]/_components/drawer-ai.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { 5 | Drawer, 6 | DrawerContent, 7 | DrawerDescription, 8 | DrawerFooter, 9 | DrawerHeader, 10 | DrawerTitle, 11 | DrawerTrigger, 12 | } from "@/components/ui/drawer"; 13 | import { Button } from "@/components/ui/button"; 14 | import { openAI } from "@/utils/open-ai"; 15 | import { Loader } from "lucide-react"; 16 | 17 | interface DrawerProps { 18 | description: string | null; 19 | } 20 | 21 | const DrawerAI = ({ description }: DrawerProps) => { 22 | const [open, setOpen] = useState(false); 23 | const [wizardSuggestion, setWizardSuggestion] = useState(""); 24 | const [isLoading, setIsLoading] = useState(false); 25 | 26 | const handleWizardSuggestion = async () => { 27 | setIsLoading(true); 28 | try { 29 | const response = (await openAI(description!)) as string; 30 | setWizardSuggestion(response); 31 | setIsLoading(false); 32 | } catch (error) { 33 | console.log(error); 34 | } 35 | }; 36 | 37 | console.log("wizard Suggesstion", wizardSuggestion); 38 | return ( 39 |
40 | 41 | 45 | Ask Your Wizard 🧙‍♂️ 46 | 47 | 48 | 49 | 50 | 🧙‍♂️ Oyyy! Wizard here! am helping you you with your wizarly 51 | storytelling or resume writing 🪄✨Apereciiiuuummm✨? 52 | 53 | {isLoading ? ( 54 | 55 | ) : ( 56 | 57 | {wizardSuggestion.length > 0 &&

{wizardSuggestion}

} 58 |
59 | )} 60 |
61 | 62 |
63 |
64 |
65 | ); 66 | }; 67 | 68 | export default DrawerAI; 69 | -------------------------------------------------------------------------------- /src/app/document/[documentId]/_components/editor-block.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { redirect } from "next/navigation"; 5 | import React from "react"; 6 | import { useForm } from "react-hook-form"; 7 | import { z } from "zod"; 8 | 9 | import { 10 | Form, 11 | FormControl, 12 | FormDescription, 13 | FormField, 14 | FormItem, 15 | FormMessage, 16 | } from "@/components/ui/form"; 17 | import { Input } from "@/components/ui/input"; 18 | import Editor from "@/components/editor"; 19 | import { Button } from "@/components/ui/button"; 20 | import axios from "axios"; 21 | import { revalidatePath } from "next/cache"; 22 | 23 | import { useToast } from "@/components/ui/use-toast"; 24 | import DrawerAI from "./drawer-ai"; 25 | 26 | const FormSchema = z.object({ 27 | title: z.string().min(2).max(50), 28 | description: z.string().min(2), 29 | }); 30 | 31 | interface DocumentProps { 32 | id: string; 33 | userId: string; 34 | title: string | null; 35 | description: string | null; 36 | createAt: Date; 37 | updateAt: Date; 38 | } 39 | 40 | interface EditorBlockProps { 41 | document?: DocumentProps | null; 42 | } 43 | 44 | const EditorBlock: React.FC = ({ document }) => { 45 | const { toast } = useToast(); 46 | if (!document) { 47 | redirect("/"); 48 | } 49 | 50 | const EditorForm = useForm>({ 51 | resolver: zodResolver(FormSchema), 52 | defaultValues: { 53 | title: document.title || "", 54 | description: document.description || "", 55 | }, 56 | }); 57 | 58 | async function onUpdateChange(values: z.infer) { 59 | try { 60 | await axios.put(`/api/document/${document?.id}`, values); 61 | toast({ title: "Document Successfully Updated" }); 62 | revalidatePath("/"); 63 | revalidatePath("/document/" + document?.id); 64 | } catch (error) {} 65 | } 66 | 67 | async function onDocumentDelete() { 68 | try { 69 | await axios.delete(`/api/document/${document?.id}`); 70 | toast({ 71 | title: "Document Delete Successfully", 72 | }); 73 | } catch (error) { 74 | console.log(error); 75 | } 76 | } 77 | 78 | return ( 79 |
80 |
81 | 82 |
83 | 86 |
87 |
88 |
89 | 93 | ( 97 | 98 | 99 | 100 | 101 | 102 | 103 | )} 104 | > 105 | 106 | ( 110 | 111 | 112 | 113 | 114 | 115 | 116 | )} 117 | > 118 | 119 |
120 | 121 |
122 | ); 123 | }; 124 | 125 | export default EditorBlock; 126 | -------------------------------------------------------------------------------- /src/app/document/[documentId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/utils/db"; 2 | import React from "react"; 3 | import EditorBlock from "./_components/editor-block"; 4 | 5 | interface SingleDocumentProps { 6 | documentId: string; 7 | } 8 | const SingelDocumentPage = async ({ 9 | params, 10 | }: { 11 | params: SingleDocumentProps; 12 | }) => { 13 | const getDocument = await db.document.findUnique({ 14 | where: { 15 | id: params.documentId, 16 | }, 17 | }); 18 | 19 | return ( 20 |
21 | 22 |
23 | ); 24 | }; 25 | 26 | export default SingelDocumentPage; 27 | -------------------------------------------------------------------------------- /src/app/document/_components/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from "@clerk/nextjs/server"; 2 | import React, { Suspense } from "react"; 3 | import IntroPage from "./intro-page"; 4 | import { NewDocument } from "./new-document"; 5 | import RecentDocument from "./recent-document"; 6 | import { Loader } from "lucide-react"; 7 | 8 | export const Dashboard = () => { 9 | const { userId } = auth(); 10 | 11 | if (!userId) { 12 | return ; 13 | } 14 | 15 | return ( 16 |
17 | {/* New Document */} 18 | 21 | } 22 | > 23 | 24 | 25 | 26 | {/* Recent Document */} 27 | 30 | } 31 | > 32 | 33 | 34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/app/document/_components/intro-page.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * v0 by Vercel. 3 | * @see https://v0.dev/t/YgbSxkUfKt5 4 | * Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app 5 | */ 6 | import Image from "next/image"; 7 | import Link from "next/link"; 8 | import { SignInButton } from "@clerk/nextjs"; 9 | export default function IntroPage() { 10 | return ( 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |

19 | Unlock your Writing Potential 🪶 20 |

21 |

22 | Discover how our cutting-edge products and services can 23 | tranform your writing with the power of AI 24 |

25 |
26 |
27 | 32 | Get Started 33 | 34 |
35 |
36 | Hero 43 |
44 |
45 |
46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/app/document/_components/new-document.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Card, 5 | CardContent, 6 | CardFooter, 7 | CardHeader, 8 | } from "@/components/ui/card"; 9 | import { useToast } from "@/components/ui/use-toast"; 10 | import axios from "axios"; 11 | import { Plus } from "lucide-react"; 12 | import { useRouter } from "next/navigation"; 13 | import React from "react"; 14 | 15 | export const NewDocument = () => { 16 | const router = useRouter(); 17 | const { toast } = useToast(); 18 | 19 | const createNewDoc = async ( 20 | title: string = "Untitled Document", 21 | description: string = "" 22 | ) => { 23 | try { 24 | const response = await axios.post("/api/document/new", { 25 | title: title, 26 | description: description, 27 | }); 28 | 29 | toast({ 30 | title: "Document Successfully Created!", 31 | }); 32 | router.push(`/document/${response.data.id}`); 33 | } catch (error) {} 34 | }; 35 | 36 | const TemplateMap = [ 37 | { 38 | component: ( 39 | 48 | ), 49 | footer: "Blank Document", 50 | }, 51 | { 52 | component: ( 53 | 82 | ), 83 | footer: "Wizardly Template", 84 | }, 85 | { 86 | component: ( 87 | 116 | ), 117 | footer: "Resume Template", 118 | }, 119 | ]; 120 | 121 | return ( 122 |
123 |
124 |

Start a new document

125 |
126 | {TemplateMap.map((template) => ( 127 |
128 | {template.component} 129 |

{template.footer}

130 |
131 | ))} 132 |
133 |
134 |
135 | ); 136 | }; 137 | -------------------------------------------------------------------------------- /src/app/document/_components/recent-document.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/utils/db"; 2 | import { auth } from "@clerk/nextjs/server"; 3 | import Link from "next/link"; 4 | import { redirect } from "next/navigation"; 5 | 6 | import { 7 | Card, 8 | CardContent, 9 | CardFooter, 10 | CardHeader, 11 | } from "@/components/ui/card"; 12 | import { BookText } from "lucide-react"; 13 | 14 | const RecentDocument = async () => { 15 | const { userId } = auth(); 16 | 17 | if (!userId) { 18 | redirect("/"); 19 | } 20 | 21 | const userDocuments = await db.document.findMany({ 22 | where: { 23 | userId: userId, 24 | }, 25 | orderBy: { 26 | createAt: "asc", 27 | }, 28 | }); 29 | 30 | return ( 31 |
32 |

Recent Document

33 |
34 | {userDocuments.length > 0 ? ( 35 | userDocuments.map((document) => ( 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |

{document.title}

47 |
48 | )) 49 | ) : ( 50 |

51 | Once you start writing your recent document will go here... 52 |

53 | )} 54 |
55 |
56 | ); 57 | }; 58 | 59 | export default RecentDocument; 60 | -------------------------------------------------------------------------------- /src/app/document/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Dashboard } from "./_components/dashboard"; 3 | 4 | const DocumentPage = () => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | 12 | export default DocumentPage; 13 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oliver-gomes/quill-wizards-ai/7d8370e95b5b58c3b66945ab912adbecf713dd83/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | import Navbar from "@/components/navbar"; 6 | import { Toaster } from "@/components/ui/toaster"; 7 | 8 | import { ClerkProvider } from "@clerk/nextjs"; 9 | 10 | const inter = Inter({ subsets: ["latin"] }); 11 | 12 | export const metadata: Metadata = { 13 | title: "Create Next App", 14 | description: "Generated by create next app", 15 | }; 16 | 17 | export default function RootLayout({ 18 | children, 19 | }: Readonly<{ 20 | children: React.ReactNode; 21 | }>) { 22 | return ( 23 | 24 | 25 | 26 | 27 | {children} 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { redirect } from "next/navigation"; 3 | 4 | export default function Home() { 5 | redirect("/document"); 6 | return
; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/editor.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import dynamic from "next/dynamic"; 4 | import { useMemo } from "react"; 5 | import "react-quill/dist/quill.snow.css"; 6 | 7 | interface EditorProps { 8 | onChange: (value: string) => void; 9 | value: string; 10 | } 11 | 12 | const modules = { 13 | toolbar: [ 14 | [{ header: [1, 2, false] }], 15 | ["bold", "italic", "underline", "strike", "blockquote"], 16 | [ 17 | { list: "ordered" }, 18 | { list: "bullet" }, 19 | { indent: "-1" }, 20 | { indent: "+1" }, 21 | ], 22 | ["link", "image"], 23 | [{ font: [] }], 24 | ["clean"], 25 | [{ align: [] }], 26 | ], 27 | }; 28 | const formats = [ 29 | "header", 30 | "bold", 31 | "italic", 32 | "underline", 33 | "strike", 34 | "blockquote", 35 | "list", 36 | "bullet", 37 | "indent", 38 | "link", 39 | "image", 40 | ]; 41 | const Editor = ({ onChange, value }: EditorProps) => { 42 | const ReactQuill = useMemo( 43 | () => dynamic(() => import("react-quill"), { ssr: false }), 44 | [] 45 | ); 46 | return ( 47 |
48 | 56 |
57 | ); 58 | }; 59 | 60 | export default Editor; 61 | -------------------------------------------------------------------------------- /src/components/logo.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import { Karla } from "next/font/google"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | const karla = Karla({ subsets: ["latin"], weight: "500" }); 7 | 8 | export const Logo = () => { 9 | return ( 10 |
11 | 12 | logo 13 | 14 |

Quill Wizards

15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { SignInButton, SignedOut, SignedIn, UserButton } from "@clerk/nextjs"; 2 | import { Logo } from "./logo"; 3 | import { Button } from "./ui/button"; 4 | 5 | const Navbar = () => { 6 | return ( 7 |
8 | {/* Logo */} 9 | 10 | {/* Auth */} 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default Navbar; 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { Drawer as DrawerPrimitive } from "vaul" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Drawer = ({ 9 | shouldScaleBackground = true, 10 | ...props 11 | }: React.ComponentProps) => ( 12 | 16 | ) 17 | Drawer.displayName = "Drawer" 18 | 19 | const DrawerTrigger = DrawerPrimitive.Trigger 20 | 21 | const DrawerPortal = DrawerPrimitive.Portal 22 | 23 | const DrawerClose = DrawerPrimitive.Close 24 | 25 | const DrawerOverlay = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 34 | )) 35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName 36 | 37 | const DrawerContent = React.forwardRef< 38 | React.ElementRef, 39 | React.ComponentPropsWithoutRef 40 | >(({ className, children, ...props }, ref) => ( 41 | 42 | 43 | 51 |
52 | {children} 53 | 54 | 55 | )) 56 | DrawerContent.displayName = "DrawerContent" 57 | 58 | const DrawerHeader = ({ 59 | className, 60 | ...props 61 | }: React.HTMLAttributes) => ( 62 |
66 | ) 67 | DrawerHeader.displayName = "DrawerHeader" 68 | 69 | const DrawerFooter = ({ 70 | className, 71 | ...props 72 | }: React.HTMLAttributes) => ( 73 |
77 | ) 78 | DrawerFooter.displayName = "DrawerFooter" 79 | 80 | const DrawerTitle = React.forwardRef< 81 | React.ElementRef, 82 | React.ComponentPropsWithoutRef 83 | >(({ className, ...props }, ref) => ( 84 | 92 | )) 93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName 94 | 95 | const DrawerDescription = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, ...props }, ref) => ( 99 | 104 | )) 105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName 106 | 107 | export { 108 | Drawer, 109 | DrawerPortal, 110 | DrawerOverlay, 111 | DrawerTrigger, 112 | DrawerClose, 113 | DrawerContent, 114 | DrawerHeader, 115 | DrawerFooter, 116 | DrawerTitle, 117 | DrawerDescription, 118 | } 119 | -------------------------------------------------------------------------------- /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 |