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();
|
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"]),
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
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"
|
"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")
|
||||||
|
}
|
||||||
|
29
frontend/package-lock.json
generated
29
frontend/package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user