From d941e2c056039af8a101596664c8f54971d89653 Mon Sep 17 00:00:00 2001 From: Ishaan Dey Date: Sat, 27 Apr 2024 16:22:35 -0400 Subject: [PATCH] add sonner + project creation working --- .../database/drizzle/0002_rare_beyonder.sql | 1 + .../database/drizzle/meta/0002_snapshot.json | 118 ++++++++++++++++++ .../database/drizzle/meta/0003_snapshot.json | 118 ++++++++++++++++++ backend/database/drizzle/meta/_journal.json | 14 +++ backend/database/src/index.ts | 67 +++++----- backend/database/src/schema.ts | 1 + backend/server/src/types.ts | 1 + frontend/app/layout.tsx | 2 + frontend/components/dashboard/newProject.tsx | 30 ++++- .../dashboard/projectCard/dropdown.tsx | 2 +- frontend/components/dashboard/projects.tsx | 80 +++++++----- frontend/components/editor/index.tsx | 13 +- frontend/components/ui/customButton.tsx | 5 +- frontend/components/ui/sonner.tsx | 31 +++++ frontend/lib/actions.ts | 7 +- frontend/lib/types.ts | 1 + frontend/package-lock.json | 10 ++ frontend/package.json | 1 + 18 files changed, 427 insertions(+), 75 deletions(-) create mode 100644 backend/database/drizzle/0002_rare_beyonder.sql create mode 100644 backend/database/drizzle/meta/0002_snapshot.json create mode 100644 backend/database/drizzle/meta/0003_snapshot.json create mode 100644 frontend/components/ui/sonner.tsx diff --git a/backend/database/drizzle/0002_rare_beyonder.sql b/backend/database/drizzle/0002_rare_beyonder.sql new file mode 100644 index 0000000..2bae800 --- /dev/null +++ b/backend/database/drizzle/0002_rare_beyonder.sql @@ -0,0 +1 @@ +ALTER TABLE sandbox ADD `visibility` text; \ No newline at end of file diff --git a/backend/database/drizzle/meta/0002_snapshot.json b/backend/database/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..786aa32 --- /dev/null +++ b/backend/database/drizzle/meta/0002_snapshot.json @@ -0,0 +1,118 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "a2239d60-e4b8-4375-979f-bbca5539a825", + "prevId": "739c5df4-de1b-408b-bc8c-5783688c5297", + "tables": { + "sandbox": { + "name": "sandbox", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_id_unique": { + "name": "sandbox_id_unique", + "columns": [ + "id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "sandbox_user_id_user_id_fk": { + "name": "sandbox_user_id_user_id_fk", + "tableFrom": "sandbox", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_unique": { + "name": "user_id_unique", + "columns": [ + "id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/backend/database/drizzle/meta/0003_snapshot.json b/backend/database/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..6f7eb1c --- /dev/null +++ b/backend/database/drizzle/meta/0003_snapshot.json @@ -0,0 +1,118 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "913932e8-65cf-40b9-a978-8818396234f6", + "prevId": "a2239d60-e4b8-4375-979f-bbca5539a825", + "tables": { + "sandbox": { + "name": "sandbox", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sandbox_id_unique": { + "name": "sandbox_id_unique", + "columns": [ + "id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "sandbox_user_id_user_id_fk": { + "name": "sandbox_user_id_user_id_fk", + "tableFrom": "sandbox", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_unique": { + "name": "user_id_unique", + "columns": [ + "id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/backend/database/drizzle/meta/_journal.json b/backend/database/drizzle/meta/_journal.json index 12e8317..af099fd 100644 --- a/backend/database/drizzle/meta/_journal.json +++ b/backend/database/drizzle/meta/_journal.json @@ -15,6 +15,20 @@ "when": 1713937589365, "tag": "0001_magenta_tenebrous", "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1714247208074, + "tag": "0002_rare_beyonder", + "breakpoints": true + }, + { + "idx": 3, + "version": "5", + "when": 1714247272878, + "tag": "0003_silky_talos", + "breakpoints": true } ] } \ No newline at end of file diff --git a/backend/database/src/index.ts b/backend/database/src/index.ts index 8630cf7..867f466 100644 --- a/backend/database/src/index.ts +++ b/backend/database/src/index.ts @@ -13,6 +13,9 @@ export interface Env { // https://github.com/drizzle-team/drizzle-orm/tree/main/examples/cloudflare-d1 +// npm run generate +// npx wrangler d1 execute d1-sandbox --local --file=./drizzle/ + export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const success = new Response("Success", { status: 200 }); @@ -26,37 +29,43 @@ export default { const db = drizzle(env.DB, { schema }); - if (path === "/api/sandbox/create" && method === "POST") { - const initSchema = z.object({ - type: z.enum(["react", "node"]), - name: z.string(), - userId: z.string(), - }); - - const body = await request.json(); - const { type, name, userId } = initSchema.parse(body); - - const sb = await db.insert(sandbox).values({ type, name, userId }).returning().get(); - - console.log("sb:", sb); - await fetch("https://storage.ishaan1013.workers.dev/api/init", { - method: "POST", - body: JSON.stringify({ sandboxId: sb.id, type }), - headers: { "Content-Type": "application/json" }, - }); - - return success; - } else if (path === "/api/sandbox" && method === "GET") { - const params = url.searchParams; - if (params.has("id")) { - const id = params.get("id") as string; - const res = await db.query.sandbox.findFirst({ - where: (sandbox, { eq }) => eq(sandbox.id, id), + if (path === "/api/sandbox") { + if (method === "GET") { + const params = url.searchParams; + if (params.has("id")) { + const id = params.get("id") as string; + const res = await db.query.sandbox.findFirst({ + where: (sandbox, { eq }) => eq(sandbox.id, id), + }); + return json(res ?? {}); + } else { + const res = await db.select().from(sandbox).all(); + return json(res ?? {}); + } + } else if (method === "PUT") { + const initSchema = z.object({ + type: z.enum(["react", "node"]), + name: z.string(), + userId: z.string(), + visibility: z.enum(["public", "private"]), }); - return json(res ?? {}); + + const body = await request.json(); + const { type, name, userId, visibility } = initSchema.parse(body); + + const sb = await db.insert(sandbox).values({ type, name, userId, visibility }).returning().get(); + + // console.log("sb:", sb); + await fetch("https://storage.ishaan1013.workers.dev/api/init", { + method: "POST", + body: JSON.stringify({ sandboxId: sb.id, type }), + headers: { "Content-Type": "application/json" }, + }); + + return new Response(sb.id, { status: 200 }); } else { - const res = await db.select().from(sandbox).all(); - return json(res ?? {}); + console.log(method); + return methodNotAllowed; } } else if (path === "/api/user") { if (method === "GET") { diff --git a/backend/database/src/schema.ts b/backend/database/src/schema.ts index 8bf466c..2d8d668 100644 --- a/backend/database/src/schema.ts +++ b/backend/database/src/schema.ts @@ -24,6 +24,7 @@ export const sandbox = sqliteTable("sandbox", { .unique(), name: text("name").notNull(), type: text("type", { enum: ["react", "node"] }).notNull(), + visibility: text("visibility", { enum: ["public", "private"] }), userId: text("user_id") .notNull() .references(() => user.id), diff --git a/backend/server/src/types.ts b/backend/server/src/types.ts index 88a7c8b..be0d21f 100644 --- a/backend/server/src/types.ts +++ b/backend/server/src/types.ts @@ -11,6 +11,7 @@ export type Sandbox = { id: string name: string type: "react" | "node" + visibility: "public" | "private" userId: string } diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 1ef3eee..beaf5bd 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -4,6 +4,7 @@ import { GeistMono } from "geist/font/mono" import "./globals.css" import { ThemeProvider } from "@/components/layout/themeProvider" import { ClerkProvider } from "@clerk/nextjs" +import { Toaster } from "@/components/ui/sonner" export const metadata: Metadata = { title: "Sandbox", @@ -26,6 +27,7 @@ export default function RootLayout({ disableTransitionOnChange > {children} + diff --git a/frontend/components/dashboard/newProject.tsx b/frontend/components/dashboard/newProject.tsx index 0a00b13..cbac721 100644 --- a/frontend/components/dashboard/newProject.tsx +++ b/frontend/components/dashboard/newProject.tsx @@ -10,7 +10,7 @@ import { } from "@/components/ui/dialog" import Image from "next/image" import { useState } from "react" -import { z } from "zod" +import { set, z } from "zod" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" @@ -32,6 +32,10 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" +import { useUser } from "@clerk/nextjs" +import { createSandbox } from "@/lib/actions" +import { useRouter } from "next/navigation" +import { Loader2 } from "lucide-react" type TOptions = "react" | "node" @@ -68,6 +72,10 @@ export default function NewProjectModal({ setOpen: (open: boolean) => void }) { const [selected, setSelected] = useState("react") + const [loading, setLoading] = useState(false) + const router = useRouter() + + const user = useUser() const form = useForm>({ resolver: zodResolver(formSchema), @@ -77,8 +85,14 @@ export default function NewProjectModal({ }, }) - function onSubmit(values: z.infer) { - const sandboxData = { type: selected, ...values } + async function onSubmit(values: z.infer) { + if (!user.isSignedIn) return + + const sandboxData = { type: selected, userId: user.user.id, ...values } + setLoading(true) + + const id = await createSandbox(sandboxData) + router.push(`/code/${id}`) } return ( @@ -146,8 +160,14 @@ export default function NewProjectModal({ )} /> - - Submit + + {loading ? ( + <> + Loading... + + ) : ( + "Submit" + )} diff --git a/frontend/components/dashboard/projectCard/dropdown.tsx b/frontend/components/dashboard/projectCard/dropdown.tsx index bd45df8..89288d1 100644 --- a/frontend/components/dashboard/projectCard/dropdown.tsx +++ b/frontend/components/dashboard/projectCard/dropdown.tsx @@ -20,7 +20,7 @@ export default function ProjectCardDropdown({ sandbox }: { sandbox: Sandbox }) { e.preventDefault() e.stopPropagation() }} - className="h-6 w-6 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 rounded-sm" + className="h-6 w-6 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 rounded-sm outline-foreground" > diff --git a/frontend/components/dashboard/projects.tsx b/frontend/components/dashboard/projects.tsx index 4819590..729ebbc 100644 --- a/frontend/components/dashboard/projects.tsx +++ b/frontend/components/dashboard/projects.tsx @@ -2,7 +2,9 @@ import { Sandbox } from "@/lib/types" import ProjectCard from "./projectCard" import Image from "next/image" import ProjectCardDropdown from "./projectCard/dropdown" -import { Clock, Globe } from "lucide-react" +import { Clock, Globe, Lock } from "lucide-react" +import Link from "next/link" +import { Card } from "../ui/card" export default function DashboardProjects({ sandboxes, @@ -12,35 +14,53 @@ export default function DashboardProjects({ return (
My Projects
-
- {sandboxes.map((sandbox) => ( - -
- -
- {sandbox.name} -
- -
-
-
- Public -
-
- 3d ago -
-
-
- ))} +
+
+ {sandboxes.map((sandbox) => ( + + + {/* */} +
+ +
+ {sandbox.name} +
+ +
+
+
+ {sandbox.visibility === "private" ? ( + <> + Private + + ) : ( + <> + Public + + )} +
+
+ 3d ago +
+
+ {/*
*/} +
+ + ))} +
) diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index a327aa8..29c695c 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -23,8 +23,8 @@ import { useClerk } from "@clerk/nextjs" import { TFile, TFileData, TFolder, TTab } from "./sidebar/types" import { io } from "socket.io-client" -import { set } from "zod" import { processFileType } from "@/lib/utils" +import { toast } from "sonner" export default function CodeEditor({ userId, @@ -117,11 +117,6 @@ export default function CodeEditor({ setTabs((prev) => prev.filter((t) => t.id !== tab.id)) } - // Note: add renaming validation: - // In general: must not contain / or \ or whitespace, not empty, no duplicates - // Files: must contain dot - // Folders: must not contain dot - const handleRename = ( id: string, newName: string, @@ -129,14 +124,18 @@ export default function CodeEditor({ type: "file" | "folder" ) => { // Validation + if (newName === oldName) { + return false + } + if ( - newName === oldName || newName.includes("/") || newName.includes("\\") || newName.includes(" ") || (type === "file" && !newName.includes(".")) || (type === "folder" && newName.includes(".")) ) { + toast.error("Invalid file name.") return false } diff --git a/frontend/components/ui/customButton.tsx b/frontend/components/ui/customButton.tsx index 241657e..cd1e888 100644 --- a/frontend/components/ui/customButton.tsx +++ b/frontend/components/ui/customButton.tsx @@ -7,19 +7,22 @@ const Button = ({ className, onClick, type, + disabled = false, }: { children: React.ReactNode className?: string onClick?: () => void type?: "button" | "submit" | "reset" + disabled?: boolean }) => { return (