2024-05-26 19:02:47 -07:00
|
|
|
"use client"
|
2024-04-23 01:53:37 -04:00
|
|
|
|
|
|
|
import {
|
|
|
|
Dialog,
|
|
|
|
DialogContent,
|
|
|
|
DialogDescription,
|
|
|
|
DialogHeader,
|
|
|
|
DialogTitle,
|
|
|
|
DialogTrigger,
|
2024-05-26 19:02:47 -07:00
|
|
|
} from "@/components/ui/dialog"
|
|
|
|
import Image from "next/image"
|
2024-10-13 23:34:27 +01:00
|
|
|
import { useState, useCallback, useEffect, useMemo } from "react"
|
2024-05-26 19:02:47 -07:00
|
|
|
import { set, z } from "zod"
|
|
|
|
import { zodResolver } from "@hookform/resolvers/zod"
|
|
|
|
import { useForm } from "react-hook-form"
|
2024-04-23 01:53:37 -04:00
|
|
|
|
|
|
|
import {
|
|
|
|
Form,
|
|
|
|
FormControl,
|
|
|
|
FormDescription,
|
|
|
|
FormField,
|
|
|
|
FormItem,
|
|
|
|
FormLabel,
|
|
|
|
FormMessage,
|
2024-05-26 19:02:47 -07:00
|
|
|
} from "@/components/ui/form"
|
|
|
|
import { Input } from "@/components/ui/input"
|
2024-04-23 01:53:37 -04:00
|
|
|
import {
|
|
|
|
Select,
|
|
|
|
SelectContent,
|
|
|
|
SelectItem,
|
|
|
|
SelectTrigger,
|
|
|
|
SelectValue,
|
2024-05-26 19:02:47 -07:00
|
|
|
} from "@/components/ui/select"
|
|
|
|
import { useUser } from "@clerk/nextjs"
|
|
|
|
import { createSandbox } from "@/lib/actions"
|
|
|
|
import { useRouter } from "next/navigation"
|
2024-10-13 23:34:27 +01:00
|
|
|
import {
|
|
|
|
Loader2,
|
|
|
|
ChevronRight,
|
|
|
|
ChevronLeft,
|
|
|
|
Search,
|
|
|
|
SlashSquare,
|
|
|
|
} from "lucide-react"
|
2024-05-26 19:02:47 -07:00
|
|
|
import { Button } from "../ui/button"
|
2024-09-24 14:10:56 +01:00
|
|
|
import { projectTemplates } from "@/lib/data"
|
2024-04-23 01:53:37 -04:00
|
|
|
|
2024-10-13 23:34:27 +01:00
|
|
|
import useEmblaCarousel from "embla-carousel-react"
|
|
|
|
import type { EmblaCarouselType } from "embla-carousel"
|
|
|
|
import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures"
|
|
|
|
import { cn } from "@/lib/utils"
|
2024-04-23 01:53:37 -04:00
|
|
|
const formSchema = z.object({
|
2024-05-09 12:03:47 -07:00
|
|
|
name: z
|
|
|
|
.string()
|
|
|
|
.min(1)
|
|
|
|
.max(16)
|
|
|
|
.refine(
|
|
|
|
(value) => /^[a-zA-Z0-9_]+$/.test(value),
|
|
|
|
"Name must be alphanumeric and can contain underscores"
|
|
|
|
),
|
2024-04-23 01:53:37 -04:00
|
|
|
visibility: z.enum(["public", "private"]),
|
2024-05-26 19:02:47 -07:00
|
|
|
})
|
2024-04-23 01:53:37 -04:00
|
|
|
|
|
|
|
export default function NewProjectModal({
|
|
|
|
open,
|
|
|
|
setOpen,
|
|
|
|
}: {
|
2024-05-26 19:02:47 -07:00
|
|
|
open: boolean
|
|
|
|
setOpen: (open: boolean) => void
|
2024-04-23 01:53:37 -04:00
|
|
|
}) {
|
2024-05-26 19:02:47 -07:00
|
|
|
const router = useRouter()
|
|
|
|
const user = useUser()
|
2024-10-13 23:34:27 +01:00
|
|
|
const [selected, setSelected] = useState("reactjs")
|
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false }, [
|
|
|
|
WheelGesturesPlugin(),
|
|
|
|
])
|
|
|
|
const {
|
|
|
|
prevBtnDisabled,
|
|
|
|
nextBtnDisabled,
|
|
|
|
onPrevButtonClick,
|
|
|
|
onNextButtonClick,
|
|
|
|
} = usePrevNextButtons(emblaApi)
|
|
|
|
const [search, setSearch] = useState("")
|
2024-04-23 01:53:37 -04:00
|
|
|
|
|
|
|
const form = useForm<z.infer<typeof formSchema>>({
|
|
|
|
resolver: zodResolver(formSchema),
|
|
|
|
defaultValues: {
|
|
|
|
name: "",
|
|
|
|
visibility: "public",
|
|
|
|
},
|
2024-05-26 19:02:47 -07:00
|
|
|
})
|
2024-04-23 01:53:37 -04:00
|
|
|
|
2024-10-13 23:34:27 +01:00
|
|
|
const handleTemplateClick = useCallback(
|
|
|
|
({ id, index }: { id: string; index: number }) => {
|
|
|
|
setSelected(id)
|
|
|
|
emblaApi?.scrollTo(index)
|
|
|
|
},
|
|
|
|
[emblaApi]
|
|
|
|
)
|
|
|
|
const filteredTemplates = useMemo(
|
|
|
|
() =>
|
|
|
|
projectTemplates.filter(
|
|
|
|
(item) =>
|
|
|
|
item.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
|
|
item.description.toLowerCase().includes(search.toLowerCase())
|
|
|
|
),
|
|
|
|
[search, projectTemplates]
|
|
|
|
)
|
|
|
|
const emptyTemplates = useMemo(
|
|
|
|
() => filteredTemplates.length === 0,
|
|
|
|
[filteredTemplates]
|
|
|
|
)
|
2024-04-27 16:22:35 -04:00
|
|
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
2024-05-26 19:02:47 -07:00
|
|
|
if (!user.isSignedIn) return
|
2024-04-27 16:22:35 -04:00
|
|
|
|
2024-05-26 19:02:47 -07:00
|
|
|
const sandboxData = { type: selected, userId: user.user.id, ...values }
|
|
|
|
setLoading(true)
|
2024-04-27 16:22:35 -04:00
|
|
|
|
2024-05-26 19:02:47 -07:00
|
|
|
const id = await createSandbox(sandboxData)
|
|
|
|
router.push(`/code/${id}`)
|
2024-04-23 01:53:37 -04:00
|
|
|
}
|
|
|
|
return (
|
2024-05-09 22:16:56 -07:00
|
|
|
<Dialog
|
|
|
|
open={open}
|
|
|
|
onOpenChange={(open: boolean) => {
|
2024-05-26 19:02:47 -07:00
|
|
|
if (!loading) setOpen(open)
|
2024-05-09 22:16:56 -07:00
|
|
|
}}
|
|
|
|
>
|
2024-09-24 14:01:51 +01:00
|
|
|
<DialogContent className="max-h-[95vh] overflow-y-auto">
|
2024-04-23 01:53:37 -04:00
|
|
|
<DialogHeader>
|
|
|
|
<DialogTitle>Create A Sandbox</DialogTitle>
|
|
|
|
</DialogHeader>
|
2024-10-13 23:34:27 +01:00
|
|
|
<div className="flex flex-col gap-2 max-w-full overflow-hidden">
|
|
|
|
<div className="flex items-center justify-end">
|
|
|
|
<SearchInput
|
|
|
|
{...{
|
|
|
|
value: search,
|
|
|
|
onValueChange: setSearch,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<div className="overflow-hidden relative" ref={emblaRef}>
|
|
|
|
<div
|
|
|
|
className={cn(
|
|
|
|
"grid grid-flow-col gap-x-2 min-h-[97px]",
|
|
|
|
emptyTemplates ? "auto-cols-[100%]" : "auto-cols-[200px]"
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
{filteredTemplates.map((item, i) => (
|
|
|
|
<button
|
|
|
|
disabled={item.disabled || loading}
|
|
|
|
key={item.id}
|
|
|
|
onClick={handleTemplateClick.bind(null, {
|
|
|
|
id: item.id,
|
|
|
|
index: i,
|
|
|
|
})}
|
2024-10-14 12:21:20 +01:00
|
|
|
className={cn(
|
|
|
|
selected === item.id
|
|
|
|
? "shadow-foreground"
|
|
|
|
: "shadow-border",
|
|
|
|
"shadow-[0_0_0_1px_inset] rounded-md border bg-card text-card-foreground text-left p-4 flex flex-col transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
|
)}
|
2024-10-13 23:34:27 +01:00
|
|
|
>
|
|
|
|
<div className="space-x-2 flex items-center justify-start w-full">
|
|
|
|
<Image alt="" src={item.icon} width={20} height={20} />
|
|
|
|
<div className="font-medium">{item.name}</div>
|
|
|
|
</div>
|
|
|
|
<div className="mt-2 text-muted-foreground text-xs line-clamp-2">
|
|
|
|
{item.description}
|
|
|
|
</div>
|
|
|
|
</button>
|
|
|
|
))}
|
|
|
|
{emptyTemplates && (
|
|
|
|
<div className="flex flex-col gap-2 items-center text-center justify-center text-muted-foreground text-sm">
|
|
|
|
<p>No templates found</p>
|
|
|
|
<Button size="xs" asChild>
|
|
|
|
<a
|
|
|
|
href="https://github.com/jamesmurdza/sandbox"
|
|
|
|
target="_blank"
|
|
|
|
>
|
|
|
|
Contribute
|
|
|
|
</a>
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
<div
|
|
|
|
className={cn(
|
|
|
|
"absolute transition-all opacity-100 duration-400 bg-gradient-to-r from-background via-background to-transparent w-14 pl-1 left-0 top-0 -translate-x-1 bottom-0 h-full flex items-center",
|
|
|
|
prevBtnDisabled && "opacity-0 pointer-events-none"
|
|
|
|
)}
|
2024-04-23 01:53:37 -04:00
|
|
|
>
|
2024-10-13 23:34:27 +01:00
|
|
|
<Button
|
|
|
|
size="smIcon"
|
|
|
|
className="rounded-full"
|
|
|
|
onClick={onPrevButtonClick}
|
|
|
|
>
|
|
|
|
<ChevronLeft className="size-5" />
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
<div
|
|
|
|
className={cn(
|
|
|
|
"absolute transition-all opacity-100 duration-400 bg-gradient-to-l from-background via-background to-transparent w-14 pl-1 right-0 top-0 translate-x-1 bottom-0 h-full flex items-center",
|
|
|
|
nextBtnDisabled && "opacity-0 pointer-events-none"
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
<Button
|
|
|
|
size="smIcon"
|
|
|
|
className="rounded-full"
|
|
|
|
onClick={onNextButtonClick}
|
|
|
|
>
|
|
|
|
<ChevronRight className="size-5" />
|
|
|
|
</Button>
|
|
|
|
</div>
|
|
|
|
</div>
|
2024-04-23 01:53:37 -04:00
|
|
|
</div>
|
|
|
|
|
|
|
|
<Form {...form}>
|
2024-05-26 19:02:47 -07:00
|
|
|
<form autoComplete="off" onSubmit={form.handleSubmit(onSubmit)}>
|
2024-04-23 01:53:37 -04:00
|
|
|
<FormField
|
|
|
|
control={form.control}
|
|
|
|
name="name"
|
|
|
|
render={({ field }) => (
|
|
|
|
<FormItem className="mb-4">
|
|
|
|
<FormLabel>Name</FormLabel>
|
|
|
|
<FormControl>
|
2024-05-09 21:05:21 -07:00
|
|
|
<Input
|
|
|
|
disabled={loading}
|
|
|
|
placeholder="My Project"
|
|
|
|
{...field}
|
|
|
|
/>
|
2024-04-23 01:53:37 -04:00
|
|
|
</FormControl>
|
|
|
|
<FormMessage />
|
|
|
|
</FormItem>
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
<FormField
|
|
|
|
control={form.control}
|
|
|
|
name="visibility"
|
|
|
|
render={({ field }) => (
|
|
|
|
<FormItem className="mb-8">
|
|
|
|
<FormLabel>Visibility</FormLabel>
|
|
|
|
<Select
|
2024-05-09 21:05:21 -07:00
|
|
|
disabled={loading}
|
2024-04-23 01:53:37 -04:00
|
|
|
onValueChange={field.onChange}
|
|
|
|
defaultValue={field.value}
|
|
|
|
>
|
|
|
|
<FormControl>
|
|
|
|
<SelectTrigger>
|
|
|
|
<SelectValue />
|
|
|
|
</SelectTrigger>
|
|
|
|
</FormControl>
|
|
|
|
<SelectContent>
|
|
|
|
<SelectItem value="public">Public</SelectItem>
|
|
|
|
<SelectItem value="private">Private</SelectItem>
|
|
|
|
</SelectContent>
|
|
|
|
</Select>
|
2024-05-26 19:02:47 -07:00
|
|
|
<FormDescription>
|
|
|
|
Note: All sandboxes cannot be seen by the public. Private
|
|
|
|
sandboxes cannot be accessed by shared users that you add,
|
|
|
|
while public sandboxes can.
|
|
|
|
</FormDescription>
|
2024-04-23 01:53:37 -04:00
|
|
|
<FormMessage />
|
|
|
|
</FormItem>
|
|
|
|
)}
|
|
|
|
/>
|
2024-04-30 22:48:36 -04:00
|
|
|
<Button disabled={loading} type="submit" className="w-full">
|
2024-04-27 16:22:35 -04:00
|
|
|
{loading ? (
|
|
|
|
<>
|
2024-05-26 21:41:20 -07:00
|
|
|
<Loader2 className="animate-spin mr-2 h-4 w-4" /> Creating
|
|
|
|
project...
|
2024-04-27 16:22:35 -04:00
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
"Submit"
|
|
|
|
)}
|
2024-04-30 22:48:36 -04:00
|
|
|
</Button>
|
2024-04-23 01:53:37 -04:00
|
|
|
</form>
|
|
|
|
</Form>
|
|
|
|
</DialogContent>
|
|
|
|
</Dialog>
|
2024-05-26 19:02:47 -07:00
|
|
|
)
|
2024-04-23 01:53:37 -04:00
|
|
|
}
|
2024-10-13 23:34:27 +01:00
|
|
|
|
|
|
|
function SearchInput({
|
|
|
|
value,
|
|
|
|
onValueChange,
|
|
|
|
}: {
|
|
|
|
value?: string
|
|
|
|
onValueChange?: (value: string) => void
|
|
|
|
}) {
|
|
|
|
const onSubmit = useCallback((e: React.FormEvent) => {
|
|
|
|
e.preventDefault()
|
|
|
|
console.log("searching")
|
|
|
|
}, [])
|
|
|
|
return (
|
|
|
|
<form {...{ onSubmit }} className="w-40 h-8 ">
|
|
|
|
<label
|
|
|
|
htmlFor="template-search"
|
|
|
|
className="flex gap-2 rounded-sm transition-colors bg-[#2e2e2e] border border-[--s-color] [--s-color:hsl(var(--muted-foreground))] focus-within:[--s-color:#fff] h-full items-center px-2"
|
|
|
|
>
|
|
|
|
<Search className="size-4 text-[--s-color] transition-colors" />
|
|
|
|
<input
|
|
|
|
id="template-search"
|
|
|
|
type="text"
|
|
|
|
name="search"
|
|
|
|
placeholder="Search templates"
|
|
|
|
value={value}
|
|
|
|
onChange={(e) => onValueChange?.(e.target.value)}
|
|
|
|
className="bg-transparent placeholder:text-muted-foreground text-white w-full focus:outline-none text-xs"
|
|
|
|
/>
|
|
|
|
</label>
|
|
|
|
</form>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
const usePrevNextButtons = (emblaApi: EmblaCarouselType | undefined) => {
|
|
|
|
const [prevBtnDisabled, setPrevBtnDisabled] = useState(true)
|
|
|
|
const [nextBtnDisabled, setNextBtnDisabled] = useState(true)
|
|
|
|
|
|
|
|
const onPrevButtonClick = useCallback(() => {
|
|
|
|
if (!emblaApi) return
|
|
|
|
emblaApi.scrollPrev()
|
|
|
|
}, [emblaApi])
|
|
|
|
|
|
|
|
const onNextButtonClick = useCallback(() => {
|
|
|
|
if (!emblaApi) return
|
|
|
|
emblaApi.scrollNext()
|
|
|
|
}, [emblaApi])
|
|
|
|
|
|
|
|
const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
|
|
|
|
setPrevBtnDisabled(!emblaApi.canScrollPrev())
|
|
|
|
setNextBtnDisabled(!emblaApi.canScrollNext())
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!emblaApi) return
|
|
|
|
|
|
|
|
onSelect(emblaApi)
|
|
|
|
emblaApi.on("reInit", onSelect).on("select", onSelect)
|
|
|
|
}, [emblaApi, onSelect])
|
|
|
|
|
|
|
|
return {
|
|
|
|
prevBtnDisabled,
|
|
|
|
nextBtnDisabled,
|
|
|
|
onPrevButtonClick,
|
|
|
|
onNextButtonClick,
|
|
|
|
}
|
|
|
|
}
|