implement server actions for sandbox data mutation
This commit is contained in:
parent
c4e1a894c3
commit
7b7bd6f430
@ -42,6 +42,27 @@ export default {
|
||||
const res = await db.select().from(sandbox).all();
|
||||
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") {
|
||||
const initSchema = z.object({
|
||||
type: z.enum(["react", "node"]),
|
||||
|
@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { Sandbox } from "@/lib/types"
|
||||
import { Ellipsis, Lock, Trash2 } from "lucide-react"
|
||||
import { Ellipsis, Globe, Lock, Trash2 } from "lucide-react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
@ -12,7 +12,15 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} 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 (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
@ -25,12 +33,33 @@ export default function ProjectCardDropdown({ sandbox }: { sandbox: Sandbox }) {
|
||||
<Ellipsis className="w-4 h-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-40">
|
||||
<DropdownMenuItem className="cursor-pointer">
|
||||
<Lock className="mr-2 h-4 w-4" />
|
||||
<span>Make Private</span>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
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>
|
||||
<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" />
|
||||
<span>Delete Project</span>
|
||||
</DropdownMenuItem>
|
||||
|
@ -1,3 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import { Sandbox } from "@/lib/types"
|
||||
import ProjectCard from "./projectCard"
|
||||
import Image from "next/image"
|
||||
@ -5,12 +7,28 @@ import ProjectCardDropdown from "./projectCard/dropdown"
|
||||
import { Clock, Globe, Lock } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { Card } from "../ui/card"
|
||||
import { deleteSandbox, updateSandbox } from "@/lib/actions"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export default function DashboardProjects({
|
||||
sandboxes,
|
||||
}: {
|
||||
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 (
|
||||
<div className="grow p-4 flex flex-col">
|
||||
<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">
|
||||
{sandbox.name}
|
||||
</div>
|
||||
<ProjectCardDropdown sandbox={sandbox} />
|
||||
<ProjectCardDropdown
|
||||
sandbox={sandbox}
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col text-muted-foreground space-y-0.5 text-sm">
|
||||
<div className="flex items-center">
|
||||
|
141
frontend/components/ui/alert-dialog.tsx
Normal file
141
frontend/components/ui/alert-dialog.tsx
Normal 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,
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
"use server"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
|
||||
export async function createSandbox(body: {
|
||||
type: string
|
||||
name: string
|
||||
@ -16,3 +18,27 @@ export async function createSandbox(body: {
|
||||
|
||||
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")
|
||||
}
|
||||
|
29
frontend/package-lock.json
generated
29
frontend/package-lock.json
generated
@ -12,6 +12,7 @@
|
||||
"@clerk/themes": "^1.7.12",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
@ -593,6 +594,34 @@
|
||||
"@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": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"@clerk/themes": "^1.7.12",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
|
Loading…
x
Reference in New Issue
Block a user