├── .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 | 
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 |
87 |
88 |
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 |
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 |
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 |
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/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 |
--------------------------------------------------------------------------------
/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/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToastPrimitives from "@radix-ui/react-toast"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | destructive:
34 | "destructive group border-destructive bg-destructive text-destructive-foreground",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware } from "@clerk/nextjs/server";
2 |
3 | export default clerkMiddleware();
4 |
5 | export const config = {
6 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
7 | };
8 |
--------------------------------------------------------------------------------
/src/utils/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 | export const db = new PrismaClient();
3 |
--------------------------------------------------------------------------------
/src/utils/open-ai.ts:
--------------------------------------------------------------------------------
1 | import OpenAI from "openai";
2 |
3 | const openai = new OpenAI({
4 | apiKey: process.env.OPENAI_API_KEY,
5 | dangerouslyAllowBrowser: true,
6 | });
7 |
8 | export async function openAI(description: string) {
9 | const response = await openai.chat.completions.create({
10 | messages: [
11 | {
12 | role: "system",
13 | content:
14 | "I need help writing about the description I provided, sugges me some ideas about the story unfolding or resume writing if resume details are provided",
15 | },
16 |
17 | {
18 | role: "user",
19 | content: JSON.stringify({
20 | description: [description],
21 | }),
22 | },
23 | ],
24 | model: "gpt-3.5-turbo",
25 | });
26 |
27 | const messageContent = response.choices[0].message?.content;
28 |
29 | if (messageContent) {
30 | return messageContent;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------