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 (
<>
{/* list all the notes */}
{/* if no notes, display this */}
{notes.length === 0 && (
You have no notes yet.
)}
{/* display all the notes */}
>
);
};
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 (
);
};
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"]
}