implement server actions for sandbox data mutation

This commit is contained in:
Ishaan Dey 2024-04-27 21:24:20 -04:00
parent c4e1a894c3
commit 7b7bd6f430
7 changed files with 276 additions and 7 deletions

View File

@ -42,6 +42,27 @@ export default {
const res = await db.select().from(sandbox).all(); const res = await db.select().from(sandbox).all();
return json(res ?? {}); return json(res ?? {});
} }
} else if (method === "DELETE") {
const params = url.searchParams;
if (params.has("id")) {
const id = params.get("id") as string;
const res = await db.delete(sandbox).where(eq(sandbox.id, id)).get();
return success;
} else {
return invalidRequest;
}
} else if (method === "POST") {
const initSchema = z.object({
id: z.string(),
name: z.string().optional(),
visibility: z.enum(["public", "private"]).optional(),
});
const body = await request.json();
const { id, name, visibility } = initSchema.parse(body);
const sb = await db.update(sandbox).set({ name, visibility }).where(eq(sandbox.id, id)).returning().get();
return success;
} else if (method === "PUT") { } else if (method === "PUT") {
const initSchema = z.object({ const initSchema = z.object({
type: z.enum(["react", "node"]), type: z.enum(["react", "node"]),

View File

@ -1,7 +1,7 @@
"use client" "use client"
import { Sandbox } from "@/lib/types" import { Sandbox } from "@/lib/types"
import { Ellipsis, Lock, Trash2 } from "lucide-react" import { Ellipsis, Globe, Lock, Trash2 } from "lucide-react"
import { import {
DropdownMenu, DropdownMenu,
@ -12,7 +12,15 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
export default function ProjectCardDropdown({ sandbox }: { sandbox: Sandbox }) { export default function ProjectCardDropdown({
sandbox,
onVisibilityChange,
onDelete,
}: {
sandbox: Sandbox
onVisibilityChange: (sandbox: Sandbox) => void
onDelete: (sandbox: Sandbox) => void
}) {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
@ -25,12 +33,33 @@ export default function ProjectCardDropdown({ sandbox }: { sandbox: Sandbox }) {
<Ellipsis className="w-4 h-4" /> <Ellipsis className="w-4 h-4" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-40"> <DropdownMenuContent className="w-40">
<DropdownMenuItem className="cursor-pointer"> <DropdownMenuItem
<Lock className="mr-2 h-4 w-4" /> onClick={(e) => {
<span>Make Private</span> e.stopPropagation()
onVisibilityChange(sandbox)
}}
className="cursor-pointer"
>
{sandbox.visibility === "public" ? (
<>
<Lock className="mr-2 h-4 w-4" />
<span>Make Private</span>
</>
) : (
<>
<Globe className="mr-2 h-4 w-4" />
<span>Make Public</span>
</>
)}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem className="!text-destructive cursor-pointer"> <DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
onDelete(sandbox)
}}
className="!text-destructive cursor-pointer"
>
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
<span>Delete Project</span> <span>Delete Project</span>
</DropdownMenuItem> </DropdownMenuItem>

View File

@ -1,3 +1,5 @@
"use client"
import { Sandbox } from "@/lib/types" import { Sandbox } from "@/lib/types"
import ProjectCard from "./projectCard" import ProjectCard from "./projectCard"
import Image from "next/image" import Image from "next/image"
@ -5,12 +7,28 @@ import ProjectCardDropdown from "./projectCard/dropdown"
import { Clock, Globe, Lock } from "lucide-react" import { Clock, Globe, Lock } from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { Card } from "../ui/card" import { Card } from "../ui/card"
import { deleteSandbox, updateSandbox } from "@/lib/actions"
import { toast } from "sonner"
export default function DashboardProjects({ export default function DashboardProjects({
sandboxes, sandboxes,
}: { }: {
sandboxes: Sandbox[] sandboxes: Sandbox[]
}) { }) {
const onDelete = async (sandbox: Sandbox) => {
toast(`Project ${sandbox.name} deleted.`)
const res = await deleteSandbox(sandbox.id)
}
const onVisibilityChange = async (sandbox: Sandbox) => {
const newVisibility = sandbox.visibility === "public" ? "private" : "public"
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
const res = await updateSandbox({
id: sandbox.id,
visibility: newVisibility,
})
}
return ( return (
<div className="grow p-4 flex flex-col"> <div className="grow p-4 flex flex-col">
<div className="text-xl font-medium mb-8">My Projects</div> <div className="text-xl font-medium mb-8">My Projects</div>
@ -38,7 +56,11 @@ export default function DashboardProjects({
<div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden"> <div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden">
{sandbox.name} {sandbox.name}
</div> </div>
<ProjectCardDropdown sandbox={sandbox} /> <ProjectCardDropdown
sandbox={sandbox}
onVisibilityChange={onVisibilityChange}
onDelete={onDelete}
/>
</div> </div>
<div className="flex flex-col text-muted-foreground space-y-0.5 text-sm"> <div className="flex flex-col text-muted-foreground space-y-0.5 text-sm">
<div className="flex items-center"> <div className="flex items-center">

View File

@ -0,0 +1,141 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -1,5 +1,7 @@
"use server" "use server"
import { revalidatePath } from "next/cache"
export async function createSandbox(body: { export async function createSandbox(body: {
type: string type: string
name: string name: string
@ -16,3 +18,27 @@ export async function createSandbox(body: {
return await res.text() return await res.text()
} }
export async function updateSandbox(body: {
id: string
name?: string
visibility?: "public" | "private"
}) {
const res = await fetch("http://localhost:8787/api/sandbox", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
revalidatePath("/dashboard")
}
export async function deleteSandbox(id: string) {
const res = await fetch(`http://localhost:8787/api/sandbox?id=${id}`, {
method: "DELETE",
})
revalidatePath("/dashboard")
}

View File

@ -12,6 +12,7 @@
"@clerk/themes": "^1.7.12", "@clerk/themes": "^1.7.12",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
@ -593,6 +594,34 @@
"@babel/runtime": "^7.13.10" "@babel/runtime": "^7.13.10"
} }
}, },
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.5.tgz",
"integrity": "sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dialog": "1.0.5",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": { "node_modules/@radix-ui/react-arrow": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz",

View File

@ -13,6 +13,7 @@
"@clerk/themes": "^1.7.12", "@clerk/themes": "^1.7.12",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",