Repository: Elliott-Chong/aideation-yt Branch: main Commit: 02d658dcc045 Files: 40 Total size: 44.6 KB Directory structure: gitextract_i69l0bvo/ ├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── drizzle.config.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── src/ │ ├── app/ │ │ ├── api/ │ │ │ ├── completion/ │ │ │ │ └── route.ts │ │ │ ├── createNoteBook/ │ │ │ │ └── route.ts │ │ │ ├── deleteNote/ │ │ │ │ └── route.ts │ │ │ ├── saveNote/ │ │ │ │ └── route.ts │ │ │ └── uploadToFirebase/ │ │ │ └── route.ts │ │ ├── dashboard/ │ │ │ └── page.tsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── notebook/ │ │ │ └── [noteId]/ │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── sign-in/ │ │ │ └── [[...sign-in]]/ │ │ │ └── page.tsx │ │ └── sign-up/ │ │ └── [[...sign-up]]/ │ │ └── page.tsx │ ├── components/ │ │ ├── CreateNoteDialog.tsx │ │ ├── DeleteButton.tsx │ │ ├── Provider.tsx │ │ ├── TipTapEditor.tsx │ │ ├── TipTapMenuBar.tsx │ │ └── ui/ │ │ ├── TypewriterTitle.tsx │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ ├── input.tsx │ │ └── separator.tsx │ ├── lib/ │ │ ├── clerk-server.ts │ │ ├── db/ │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ ├── firebase.ts │ │ ├── openai.ts │ │ ├── useDebounce.ts │ │ └── utils.ts │ └── middleware.ts ├── tailwind.config.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "extends": "next/core-web-vitals" } ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js .env # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* # local env files .env*.local # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts ================================================ FILE: README.md ================================================ 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). ## Getting Started First, run the development server: ```bash npm run dev # or yarn dev # or pnpm dev # or bun dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. ## Learn More To learn more about Next.js, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! ## Deploy on Vercel 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. Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. ================================================ FILE: components.json ================================================ { "$schema": "https://ui.shadcn.com/schema.json", "style": "default", "rsc": true, "tsx": true, "tailwind": { "config": "tailwind.config.ts", "css": "src/app/globals.css", "baseColor": "slate", "cssVariables": true }, "aliases": { "components": "@/components", "utils": "@/lib/utils" } } ================================================ FILE: drizzle.config.ts ================================================ import type { Config } from "drizzle-kit"; import * as dotenv from "dotenv"; dotenv.config({ path: ".env", }); export default { driver: "pg", schema: "./src/lib/db/schema.ts", dbCredentials: { connectionString: process.env.DATABASE_URL!, }, } satisfies Config; ================================================ FILE: next.config.js ================================================ /** @type {import('next').NextConfig} */ const nextConfig = { images: { domains: ["firebasestorage.googleapis.com"], }, typescript: { ignoreBuildErrors: true, }, eslint: { ignoreDuringBuilds: true, }, }; module.exports = nextConfig; ================================================ FILE: package.json ================================================ { "name": "aideation-yt", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "@clerk/backend": "^0.29.0", "@clerk/nextjs": "^4.24.1", "@neondatabase/serverless": "^0.6.0", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@tanstack/react-query": "^4.35.3", "@tiptap/pm": "^2.1.11", "@tiptap/react": "^2.1.11", "@tiptap/starter-kit": "^2.1.11", "@types/node": "20.6.4", "@types/react": "18.2.22", "@types/react-dom": "18.2.7", "ai": "^2.2.13", "autoprefixer": "10.4.16", "axios": "^1.5.0", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "dotenv": "^16.3.1", "drizzle-kit": "^0.19.13", "drizzle-orm": "^0.28.6", "encoding": "^0.1.13", "eslint": "8.50.0", "eslint-config-next": "13.5.2", "firebase": "^10.4.0", "lucide-react": "^0.279.0", "next": "13.5.2", "openai-edge": "^1.2.2", "pg": "^8.11.3", "postcss": "8.4.30", "react": "18.2.0", "react-dom": "18.2.0", "tailwind-merge": "^1.14.0", "tailwindcss": "3.3.3", "tailwindcss-animate": "^1.0.7", "typescript": "5.2.2", "typewriter-effect": "^2.21.0" }, "devDependencies": { "@tailwindcss/typography": "^0.5.10" } } ================================================ FILE: postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: src/app/api/completion/route.ts ================================================ import { OpenAIApi, Configuration } from "openai-edge"; import { OpenAIStream, StreamingTextResponse } from "ai"; // /api/completion const config = new Configuration({ apiKey: process.env.OPENAI_API_KEY, }); const openai = new OpenAIApi(config); export async function POST(req: Request) { // extract the prompt from the body const { prompt } = await req.json(); const response = await openai.createChatCompletion({ model: "gpt-3.5-turbo", messages: [ { role: "system", content: `You are a helpful AI embedded in a notion text editor app that is used to autocomplete sentences The traits of AI include expert knowledge, helpfulness, cleverness, and articulateness. AI is a well-behaved and well-mannered individual. AI is always friendly, kind, and inspiring, and he is eager to provide vivid and thoughtful responses to the user.`, }, { role: "user", content: ` I am writing a piece of text in a notion text editor app. Help me complete my train of thought here: ##${prompt}## keep the tone of the text consistent with the rest of the text. keep the response short and sweet. `, }, ], stream: true, }); const stream = OpenAIStream(response); return new StreamingTextResponse(stream); } ================================================ FILE: src/app/api/createNoteBook/route.ts ================================================ // /api/createNoteBook import { db } from "@/lib/db"; import { $notes } from "@/lib/db/schema"; import { generateImage, generateImagePrompt } from "@/lib/openai"; import { auth } from "@clerk/nextjs"; import { NextResponse } from "next/server"; export const runtime = "edge"; export async function POST(req: Request) { const { userId } = auth(); if (!userId) { return new NextResponse("unauthorised", { status: 401 }); } const body = await req.json(); const { name } = body; const image_description = await generateImagePrompt(name); if (!image_description) { return new NextResponse("failed to generate image description", { status: 500, }); } const image_url = await generateImage(image_description); if (!image_url) { return new NextResponse("failed to generate image ", { status: 500, }); } const note_ids = await db .insert($notes) .values({ name, userId, imageUrl: image_url, }) .returning({ insertedId: $notes.id, }); return NextResponse.json({ note_id: note_ids[0].insertedId, }); } ================================================ FILE: src/app/api/deleteNote/route.ts ================================================ import { db } from "@/lib/db"; import { $notes } from "@/lib/db/schema"; import { eq } from "drizzle-orm"; import { NextResponse } from "next/server"; export async function POST(req: Request) { const { noteId } = await req.json(); await db.delete($notes).where(eq($notes.id, parseInt(noteId))); return new NextResponse("ok", { status: 200 }); } ================================================ FILE: src/app/api/saveNote/route.ts ================================================ import { db } from "@/lib/db"; import { $notes } from "@/lib/db/schema"; import { eq } from "drizzle-orm"; import { NextResponse } from "next/server"; export async function POST(req: Request) { try { const body = await req.json(); let { noteId, editorState } = body; if (!editorState || !noteId) { return new NextResponse("Missing editorState or noteId", { status: 400 }); } noteId = parseInt(noteId); const notes = await db.select().from($notes).where(eq($notes.id, noteId)); if (notes.length != 1) { return new NextResponse("failed to update", { status: 500 }); } const note = notes[0]; if (note.editorState !== editorState) { await db .update($notes) .set({ editorState, }) .where(eq($notes.id, noteId)); } return NextResponse.json( { success: true, }, { status: 200 } ); } catch (error) { console.error(error); return NextResponse.json( { success: false, }, { status: 500 } ); } } ================================================ FILE: src/app/api/uploadToFirebase/route.ts ================================================ import { db } from "@/lib/db"; import { $notes } from "@/lib/db/schema"; import { uploadFileToFirebase } from "@/lib/firebase"; import { eq } from "drizzle-orm"; import { NextResponse } from "next/server"; export async function POST(req: Request) { try { const { noteId } = await req.json(); // extract out the dalle imageurl // save it to firebase const notes = await db .select() .from($notes) .where(eq($notes.id, parseInt(noteId))); if (!notes[0].imageUrl) { return new NextResponse("no image url", { status: 400 }); } const firebase_url = await uploadFileToFirebase( notes[0].imageUrl, notes[0].name ); // update the note with the firebase url await db .update($notes) .set({ imageUrl: firebase_url, }) .where(eq($notes.id, parseInt(noteId))); return new NextResponse("ok", { status: 200 }); } catch (error) { console.error(error); return new NextResponse("error", { status: 500 }); } } ================================================ FILE: src/app/dashboard/page.tsx ================================================ import CreateNoteDialog from "@/components/CreateNoteDialog"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { db } from "@/lib/db"; import { $notes } from "@/lib/db/schema"; import { UserButton, auth } from "@clerk/nextjs"; import { eq } from "drizzle-orm"; import { ArrowLeft } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import React from "react"; type Props = {}; const DashboardPage = async (props: Props) => { const { userId } = auth(); const notes = await db .select() .from($notes) .where(eq($notes.userId, userId!)); return ( <>

My Notes

{/* list all the notes */} {/* if no notes, display this */} {notes.length === 0 && (

You have no notes yet.

)} {/* display all the notes */}
{notes.map((note) => { return (
{note.name}

{note.name}

{new Date(note.createdAt).toLocaleDateString()}

); })}
); }; export default DashboardPage; ================================================ FILE: src/app/globals.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; --radius: 0.5rem; } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } } .grainy { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAElBMVEUAAAD8/vz08vT09vT8+vzs7uxH16TeAAAAAXRSTlMAQObYZgAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAuFJREFUOI0Vk+3NLiEIRG1B8ClAYAsQ2AIEt4D9ePtv5Xp/mZgYJ2fOFJKEfInkVWY2aglmQFkimRTV7MblYyVqD7HXyhKsSuPX12MeDhRHLtGvRG+P+B/S0Vu4OswR9tmvwNPyhdCDbVayJGads/WiUWcjCvCnruTBNHS9gmX2VzVbk7ZvB1gb1hkWFGl+A/n+/FowcO34U/XvKqZ/fHY+6vgRfU92XrOBUbGeeDfQmjWjdrK+frc6FdGReQhfSF5JvR29O2QrfNw1huTwlgsyXLo0u+5So82sgv7tsFZR2nxB6lXiquHrfD8nfYZ9SeT0LiuvSoVrxGY16pCNRZKqvwWsn5OHypPBELzohMCaRaa0ceTHYqe7X/gfJEEtKFbJpWoNqO+aS1cuTykGPpK5Ga48m6L3NefTr013KqYBQu929iP1oQ/7UwSR+i3zqruUmT84qmhzLpxyj7pr9kg7LKvqaXxZmdpn+6o8sHqSqojy02gU3U8q9PnpidiaLks0mbMYz+q2uVXsoBQ8bfURULYxRgZVYCHMv9F4OA7qxT2NPPpvGQ/sTDH2yznKh7E2AcErfcNsaIoN1izzbJiaY63x4QjUFdBSvDCvugPpu5xDny0jzEeuUQbcP1aGT9V90uixngTRLYNEIIZ6yOF1H8tm7rj2JxiefsVy53zGVy3ag5uuPsdufYOzYxLRxngKe7nhx3VAq54pmz/DK9/Q3aDam2Yt3hNXB4HuU87jKNd/CKZn77Qdn5QkXPfqSkhk7hGOXXB+7v09KbBbqdvxGqa0AqfK/atIrL2WXdAgXAJ43Wtwe/aIoacXezeGPMlhDOHDbSfHnaXsL2QzbT82GRwZuezdwcoWzx5pnOnGMUdHuiY7lhdyWzWiHnucLZQxYStMJbtcydHaQ6vtMbe0AcDbxG+QG14AL94xry4297xpy9Cpf1OoxZ740gHDfrK+gtsy0xabwJmfgtCeii79B6aj0SJeLbd7AAAAAElFTkSuQmCC); } .tiptap.ProseMirror { outline: none; } .is-active { background-color: #dadada; border-radius: 2px; } ================================================ FILE: src/app/layout.tsx ================================================ import "./globals.css"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; import { ClerkProvider } from "@clerk/nextjs"; import Provider from "@/components/Provider"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "AIdeation YT", }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ================================================ FILE: src/app/notebook/[noteId]/page.tsx ================================================ import DeleteButton from "@/components/DeleteButton"; import TipTapEditor from "@/components/TipTapEditor"; import { Button } from "@/components/ui/button"; import { clerk } from "@/lib/clerk-server"; import { db } from "@/lib/db"; import { $notes } from "@/lib/db/schema"; import { auth } from "@clerk/nextjs"; import { and, eq } from "drizzle-orm"; import Link from "next/link"; import { redirect } from "next/navigation"; import React from "react"; type Props = { params: { noteId: string; }; }; const NotebookPage = async ({ params: { noteId } }: Props) => { const { userId } = await auth(); if (!userId) { return redirect("/dashboard"); } const user = await clerk.users.getUser(userId); const notes = await db .select() .from($notes) .where(and(eq($notes.id, parseInt(noteId)), eq($notes.userId, userId))); if (notes.length != 1) { return redirect("/dashboard"); } const note = notes[0]; return (
{user.firstName} {user.lastName} / {note.name}
); }; export default NotebookPage; ================================================ FILE: src/app/page.tsx ================================================ import TypewriterTitle from "@/components/ui/TypewriterTitle"; import { Button } from "@/components/ui/button"; import Link from "next/link"; import { ArrowRight } from "lucide-react"; export default function Home() { return (

AI note taking{" "} assistant.

); } ================================================ FILE: src/app/sign-in/[[...sign-in]]/page.tsx ================================================ import { SignIn } from "@clerk/nextjs"; export default function Page() { return (
); } ================================================ FILE: src/app/sign-up/[[...sign-up]]/page.tsx ================================================ import { SignUp } from "@clerk/nextjs"; export default function Page() { return (
); } ================================================ FILE: src/components/CreateNoteDialog.tsx ================================================ "use client"; import React from "react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "./ui/dialog"; import { Loader2, Plus } from "lucide-react"; import { Input } from "./ui/input"; import axios from "axios"; import { Button } from "./ui/button"; import { useMutation } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; type Props = {}; const CreateNoteDialog = (props: Props) => { const router = useRouter(); const [input, setInput] = React.useState(""); const uploadToFirebase = useMutation({ mutationFn: async (noteId: string) => { const response = await axios.post("/api/uploadToFirebase", { noteId, }); return response.data; }, }); const createNotebook = useMutation({ mutationFn: async () => { const response = await axios.post("/api/createNoteBook", { name: input, }); return response.data; }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (input === "") { window.alert("Please enter a name for your notebook"); return; } createNotebook.mutate(undefined, { onSuccess: ({ note_id }) => { console.log("created new note:", { note_id }); // hit another endpoint to uplod the temp dalle url to permanent firebase url uploadToFirebase.mutate(note_id); router.push(`/notebook/${note_id}`); }, onError: (error) => { console.error(error); window.alert("Failed to create new notebook"); }, }); }; return (

New Note Book

New Note Book You can create a new note by clicking the button below.
setInput(e.target.value)} placeholder="Name..." />
); }; export default CreateNoteDialog; ================================================ FILE: src/components/DeleteButton.tsx ================================================ "use client"; import React from "react"; import { Button } from "./ui/button"; import { Trash } from "lucide-react"; import { useMutation } from "@tanstack/react-query"; import axios from "axios"; import { useRouter } from "next/navigation"; type Props = { noteId: number; }; const DeleteButton = ({ noteId }: Props) => { const router = useRouter(); const deleteNote = useMutation({ mutationFn: async () => { const response = await axios.post("/api/deleteNote", { noteId, }); return response.data; }, }); return ( ); }; export default DeleteButton; ================================================ FILE: src/components/Provider.tsx ================================================ "use client"; import React from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; type Props = { children: React.ReactNode; }; const queryClient = new QueryClient(); const Provider = ({ children }: Props) => { return ( {children} ); }; export default Provider; ================================================ FILE: src/components/TipTapEditor.tsx ================================================ "use client"; import React from "react"; import { EditorContent, useEditor } from "@tiptap/react"; import { StarterKit } from "@tiptap/starter-kit"; import TipTapMenuBar from "./TipTapMenuBar"; import { Button } from "./ui/button"; import { useDebounce } from "@/lib/useDebounce"; import { useMutation } from "@tanstack/react-query"; import Text from "@tiptap/extension-text"; import axios from "axios"; import { NoteType } from "@/lib/db/schema"; import { useCompletion } from "ai/react"; type Props = { note: NoteType }; const TipTapEditor = ({ note }: Props) => { const [editorState, setEditorState] = React.useState( note.editorState || `

${note.name}

` ); const { complete, completion } = useCompletion({ api: "/api/completion", }); const saveNote = useMutation({ mutationFn: async () => { const response = await axios.post("/api/saveNote", { noteId: note.id, editorState, }); return response.data; }, }); const customText = Text.extend({ addKeyboardShortcuts() { return { "Shift-a": () => { // take the last 30 words const prompt = this.editor.getText().split(" ").slice(-30).join(" "); complete(prompt); return true; }, }; }, }); const editor = useEditor({ autofocus: true, extensions: [StarterKit, customText], content: editorState, onUpdate: ({ editor }) => { setEditorState(editor.getHTML()); }, }); const lastCompletion = React.useRef(""); React.useEffect(() => { if (!completion || !editor) return; const diff = completion.slice(lastCompletion.current.length); lastCompletion.current = completion; editor.commands.insertContent(diff); }, [completion, editor]); const debouncedEditorState = useDebounce(editorState, 500); React.useEffect(() => { // save to db if (debouncedEditorState === "") return; saveNote.mutate(undefined, { onSuccess: (data) => { console.log("success update!", data); }, onError: (err) => { console.error(err); }, }); }, [debouncedEditorState]); return ( <>
{editor && }
Tip: Press{" "} Shift + A {" "} for AI autocomplete ); }; export default TipTapEditor; ================================================ FILE: src/components/TipTapMenuBar.tsx ================================================ import { Editor } from "@tiptap/react"; import { Bold, Code, CodepenIcon, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, Italic, List, ListOrdered, Quote, Redo, Strikethrough, Undo, } from "lucide-react"; const TipTapMenuBar = ({ editor }: { editor: Editor }) => { return (
); }; export default TipTapMenuBar; ================================================ FILE: src/components/ui/TypewriterTitle.tsx ================================================ "use client"; import React from "react"; import Typewriter from "typewriter-effect"; type Props = {}; const TypewriterTitle = (props: Props) => { return ( { typewriter .typeString("🚀 Supercharged Productivity.") .pauseFor(1000) .deleteAll() .typeString("🤖 AI-Powered Insights.") .start(); }} /> ); }; export default TypewriterTitle; ================================================ FILE: src/components/ui/button.tsx ================================================ import * as React from "react" import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( "inline-flex items-center justify-center 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", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", }, }, defaultVariants: { variant: "default", size: "default", }, } ) export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button" return ( ) } ) Button.displayName = "Button" export { Button, buttonVariants } ================================================ FILE: src/components/ui/dialog.tsx ================================================ "use client" import * as React from "react" import * as DialogPrimitive from "@radix-ui/react-dialog" import { X } from "lucide-react" import { cn } from "@/lib/utils" const Dialog = DialogPrimitive.Root const DialogTrigger = DialogPrimitive.Trigger const DialogPortal = ({ className, ...props }: DialogPrimitive.DialogPortalProps) => ( ) DialogPortal.displayName = DialogPrimitive.Portal.displayName const DialogOverlay = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DialogOverlay.displayName = DialogPrimitive.Overlay.displayName const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( {children} Close )) DialogContent.displayName = DialogPrimitive.Content.displayName const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
) DialogHeader.displayName = "DialogHeader" const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
) DialogFooter.displayName = "DialogFooter" const DialogTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DialogTitle.displayName = DialogPrimitive.Title.displayName const DialogDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) DialogDescription.displayName = DialogPrimitive.Description.displayName export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, } ================================================ FILE: src/components/ui/input.tsx ================================================ import * as React from "react" import { cn } from "@/lib/utils" export interface InputProps extends React.InputHTMLAttributes {} const Input = React.forwardRef( ({ className, type, ...props }, ref) => { return ( ) } ) Input.displayName = "Input" export { Input } ================================================ FILE: src/components/ui/separator.tsx ================================================ "use client" import * as React from "react" import * as SeparatorPrimitive from "@radix-ui/react-separator" import { cn } from "@/lib/utils" const Separator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >( ( { className, orientation = "horizontal", decorative = true, ...props }, ref ) => ( ) ) Separator.displayName = SeparatorPrimitive.Root.displayName export { Separator } ================================================ FILE: src/lib/clerk-server.ts ================================================ import { Clerk } from "@clerk/backend"; export const clerk = Clerk({ apiKey: process.env.CLERK_SECRET_KEY, }); ================================================ FILE: src/lib/db/index.ts ================================================ import { neon, neonConfig } from "@neondatabase/serverless"; import { drizzle } from "drizzle-orm/neon-http"; neonConfig.fetchConnectionCache = true; if (!process.env.DATABASE_URL) { throw new Error("DATABASE_URL is not defined"); } const sql = neon(process.env.DATABASE_URL); export const db = drizzle(sql); ================================================ FILE: src/lib/db/schema.ts ================================================ import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; export const $notes = pgTable("notes", { id: serial("id").primaryKey(), name: text("name").notNull(), createdAt: timestamp("created_at").notNull().defaultNow(), imageUrl: text("imageUrl"), userId: text("user_id").notNull(), editorState: text("editor_state"), }); export type NoteType = typeof $notes.$inferInsert; // drizzle-orm // drizzle-kit ================================================ FILE: src/lib/firebase.ts ================================================ // Import the functions you need from the SDKs you need import { initializeApp } from "firebase/app"; import { getDownloadURL, getStorage, ref, uploadBytes } from "firebase/storage"; // TODO: Add SDKs for Firebase products that you want to use // https://firebase.google.com/docs/web/setup#available-libraries // Your web app's Firebase configuration const firebaseConfig = { apiKey: process.env.FIREBASE_API_KEY, authDomain: "aideation-yt.firebaseapp.com", projectId: "aideation-yt", storageBucket: "aideation-yt.appspot.com", messagingSenderId: "962348384448", appId: "1:962348384448:web:e02758407aba3258d5ad25", }; // Initialize Firebase const app = initializeApp(firebaseConfig); export const storage = getStorage(app); export async function uploadFileToFirebase(image_url: string, name: string) { try { const response = await fetch(image_url); const buffer = await response.arrayBuffer(); const file_name = name.replace(" ", "") + Date.now + ".jpeg"; const storageRef = ref(storage, file_name); await uploadBytes(storageRef, buffer, { contentType: "image/jpeg", }); const firebase_url = await getDownloadURL(storageRef); return firebase_url; } catch (error) { console.error(error); } } ================================================ FILE: src/lib/openai.ts ================================================ import { Configuration, OpenAIApi } from "openai-edge"; const config = new Configuration({ apiKey: process.env.OPENAI_API_KEY, }); const openai = new OpenAIApi(config); export async function generateImagePrompt(name: string) { try { const response = await openai.createChatCompletion({ model: "gpt-3.5-turbo", messages: [ { role: "system", content: "You are an creative and helpful AI assistance capable of generating interesting thumbnail descriptions for my notes. Your output will be fed into the DALLE API to generate a thumbnail. The description should be minimalistic and flat styled", }, { role: "user", content: `Please generate a thumbnail description for my notebook titles ${name}`, }, ], }); const data = await response.json(); const image_description = data.choices[0].message.content; return image_description as string; } catch (error) { console.log(error); throw error; } } export async function generateImage(image_description: string) { try { const response = await openai.createImage({ prompt: image_description, n: 1, size: "256x256", }); const data = await response.json(); const image_url = data.data[0].url; return image_url as string; } catch (error) { console.error(error); } } ================================================ FILE: src/lib/useDebounce.ts ================================================ import React from "react"; export function useDebounce(value: string, delay: number) { const [debouncedValue, setDebouncedValue] = React.useState(value); React.useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(handler); }; }, [value, delay]); return debouncedValue; } ================================================ FILE: src/lib/utils.ts ================================================ import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } ================================================ FILE: src/middleware.ts ================================================ import { authMiddleware } from "@clerk/nextjs"; // This example protects all routes including api/trpc routes // Please edit this to allow other routes to be public as needed. // See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your middleware export default authMiddleware({ publicRoutes: ["/"], }); export const config = { matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], }; ================================================ FILE: tailwind.config.ts ================================================ /** @type {import('tailwindcss').Config} */ module.exports = { darkMode: ["class"], content: [ "./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}", ], theme: { container: { center: true, padding: "2rem", screens: { "2xl": "1400px", }, }, extend: { colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))", }, secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))", }, destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))", }, muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))", }, accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))", }, popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))", }, card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, keyframes: { "accordion-down": { from: { height: 0 }, to: { height: "var(--radix-accordion-content-height)" }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: 0 }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", }, }, }, plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], }; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "es6", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [ { "name": "next" } ], "paths": { "@/*": ["./src/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] }