chore: format frontend code

This commit is contained in:
Akhilesh Rangani 2024-10-21 13:57:45 -06:00
parent 2897b908fd
commit 6fb1364d6f
64 changed files with 1421 additions and 1272 deletions

View File

@ -1,12 +1,11 @@
import Navbar from "@/components/editor/navbar"
import { Room } from "@/components/editor/live/room" import { Room } from "@/components/editor/live/room"
import Loading from "@/components/editor/loading"
import Navbar from "@/components/editor/navbar"
import { TerminalProvider } from "@/context/TerminalContext"
import { Sandbox, User, UsersToSandboxes } from "@/lib/types" import { Sandbox, User, UsersToSandboxes } from "@/lib/types"
import { currentUser } from "@clerk/nextjs" import { currentUser } from "@clerk/nextjs"
import { notFound, redirect } from "next/navigation"
import Loading from "@/components/editor/loading"
import dynamic from "next/dynamic" import dynamic from "next/dynamic"
import fs from "fs" import { notFound, redirect } from "next/navigation"
import { TerminalProvider } from "@/context/TerminalContext"
export const revalidate = 0 export const revalidate = 0
@ -89,19 +88,20 @@ export default async function CodePage({ params }: { params: { id: string } }) {
return ( return (
<> <>
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background"> <div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
<Room id={sandboxId}> <Room id={sandboxId}>
<TerminalProvider> <TerminalProvider>
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} /> <Navbar
<div className="w-screen flex grow"> userData={userData}
<CodeEditor sandboxData={sandboxData}
userData={userData} shared={shared}
sandboxData={sandboxData} />
/> <div className="w-screen flex grow">
</div> <CodeEditor userData={userData} sandboxData={sandboxData} />
</TerminalProvider> </div>
</Room> </TerminalProvider>
</div> </Room>
</div>
</> </>
) )
} }

View File

@ -1,8 +1,8 @@
import { UserButton, currentUser } from "@clerk/nextjs"
import { redirect } from "next/navigation"
import Dashboard from "@/components/dashboard" import Dashboard from "@/components/dashboard"
import Navbar from "@/components/dashboard/navbar" import Navbar from "@/components/dashboard/navbar"
import { Sandbox, User } from "@/lib/types" import { User } from "@/lib/types"
import { currentUser } from "@clerk/nextjs"
import { redirect } from "next/navigation"
export default async function DashboardPage() { export default async function DashboardPage() {
const user = await currentUser() const user = await currentUser()

View File

@ -1,13 +1,13 @@
import type { Metadata } from "next"
import { GeistSans } from "geist/font/sans"
import { GeistMono } from "geist/font/mono"
import "./globals.css"
import { ThemeProvider } from "@/components/layout/themeProvider" import { ThemeProvider } from "@/components/layout/themeProvider"
import { ClerkProvider } from "@clerk/nextjs"
import { Toaster } from "@/components/ui/sonner" import { Toaster } from "@/components/ui/sonner"
import { PreviewProvider } from "@/context/PreviewContext"
import { SocketProvider } from "@/context/SocketContext"
import { ClerkProvider } from "@clerk/nextjs"
import { Analytics } from "@vercel/analytics/react" import { Analytics } from "@vercel/analytics/react"
import { PreviewProvider } from "@/context/PreviewContext"; import { GeistMono } from "geist/font/mono"
import { SocketProvider } from '@/context/SocketContext' import { GeistSans } from "geist/font/sans"
import type { Metadata } from "next"
import "./globals.css"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Sandbox", title: "Sandbox",
@ -15,7 +15,7 @@ export const metadata: Metadata = {
} }
export default function RootLayout({ export default function RootLayout({
children children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode children: React.ReactNode
}>) { }>) {
@ -30,9 +30,7 @@ export default function RootLayout({
disableTransitionOnChange disableTransitionOnChange
> >
<SocketProvider> <SocketProvider>
<PreviewProvider> <PreviewProvider>{children}</PreviewProvider>
{children}
</PreviewProvider>
</SocketProvider> </SocketProvider>
<Analytics /> <Analytics />
<Toaster position="bottom-left" richColors /> <Toaster position="bottom-left" richColors />
@ -41,4 +39,4 @@ export default function RootLayout({
</html> </html>
</ClerkProvider> </ClerkProvider>
) )
} }

View File

@ -1,13 +1,13 @@
import { currentUser } from "@clerk/nextjs"; import Landing from "@/components/landing"
import { redirect } from "next/navigation"; import { currentUser } from "@clerk/nextjs"
import Landing from "@/components/landing"; import { redirect } from "next/navigation"
export default async function Home() { export default async function Home() {
const user = await currentUser(); const user = await currentUser()
if (user) { if (user) {
redirect("/dashboard"); redirect("/dashboard")
} }
return <Landing />; return <Landing />
} }

View File

@ -3,16 +3,9 @@
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import Image from "next/image"
import { useState } from "react"
import { Button } from "../ui/button"
import { ChevronRight } from "lucide-react"
export default function AboutModal({ export default function AboutModal({
open, open,

View File

@ -1,24 +1,16 @@
"use client" "use client"
import CustomButton from "@/components/ui/customButton"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import CustomButton from "@/components/ui/customButton"
Code2,
FolderDot,
HelpCircle,
Plus,
Settings,
Users,
} from "lucide-react"
import { useEffect, useState } from "react"
import { Sandbox } from "@/lib/types" import { Sandbox } from "@/lib/types"
import { Code2, FolderDot, HelpCircle, Plus, Users } from "lucide-react"
import { useRouter, useSearchParams } from "next/navigation"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import AboutModal from "./about"
import NewProjectModal from "./newProject"
import DashboardProjects from "./projects" import DashboardProjects from "./projects"
import DashboardSharedWithMe from "./shared" import DashboardSharedWithMe from "./shared"
import NewProjectModal from "./newProject"
import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation"
import AboutModal from "./about"
import { toast } from "sonner"
type TScreen = "projects" | "shared" | "settings" | "search" type TScreen = "projects" | "shared" | "settings" | "search"
@ -49,8 +41,9 @@ export default function Dashboard({
const q = searchParams.get("q") const q = searchParams.get("q")
const router = useRouter() const router = useRouter()
useEffect(() => { // update the dashboard to show a new project useEffect(() => {
router.refresh() // update the dashboard to show a new project
router.refresh()
}, []) }, [])
return ( return (

View File

@ -1,9 +1,9 @@
import Logo from "@/assets/logo.svg"
import { User } from "@/lib/types"
import Image from "next/image" import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import Logo from "@/assets/logo.svg"
import DashboardNavbarSearch from "./search"
import UserButton from "../../ui/userButton" import UserButton from "../../ui/userButton"
import { User } from "@/lib/types" import DashboardNavbarSearch from "./search"
export default function DashboardNavbar({ userData }: { userData: User }) { export default function DashboardNavbar({ userData }: { userData: User }) {
return ( return (

View File

@ -1,13 +1,12 @@
"use client"; "use client"
import { Input } from "../../ui/input"; import { Search } from "lucide-react"
import { Search } from "lucide-react"; import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"; import { Input } from "../../ui/input"
import { useRouter } from "next/navigation";
export default function DashboardNavbarSearch() { export default function DashboardNavbarSearch() {
// const [search, setSearch] = useState(""); // const [search, setSearch] = useState("");
const router = useRouter(); const router = useRouter()
// useEffect(() => { // useEffect(() => {
// const delayDebounceFn = setTimeout(() => { // const delayDebounceFn = setTimeout(() => {
@ -29,14 +28,14 @@ export default function DashboardNavbarSearch() {
// onChange={(e) => setSearch(e.target.value)} // onChange={(e) => setSearch(e.target.value)}
onChange={(e) => { onChange={(e) => {
if (e.target.value === "") { if (e.target.value === "") {
router.push(`/dashboard`); router.push(`/dashboard`)
return; return
} }
router.push(`/dashboard?q=${e.target.value}`); router.push(`/dashboard?q=${e.target.value}`)
}} }}
placeholder="Search projects..." placeholder="Search projects..."
className="pl-8" className="pl-8"
/> />
</div> </div>
); )
} }

View File

@ -3,16 +3,14 @@
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { zodResolver } from "@hookform/resolvers/zod"
import Image from "next/image" import Image from "next/image"
import { useState } from "react" import { useState } from "react"
import { set, z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { z } from "zod"
import { import {
Form, Form,
@ -31,12 +29,12 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select"
import { useUser } from "@clerk/nextjs"
import { createSandbox } from "@/lib/actions" import { createSandbox } from "@/lib/actions"
import { useRouter } from "next/navigation"
import { Loader2 } from "lucide-react"
import { Button } from "../ui/button"
import { projectTemplates } from "@/lib/data" import { projectTemplates } from "@/lib/data"
import { useUser } from "@clerk/nextjs"
import { Loader2 } from "lucide-react"
import { useRouter } from "next/navigation"
import { Button } from "../ui/button"
const formSchema = z.object({ const formSchema = z.object({
name: z name: z

View File

@ -1,30 +1,30 @@
"use client"; "use client"
import { Sandbox } from "@/lib/types"; import { Sandbox } from "@/lib/types"
import { Ellipsis, Globe, Lock, Trash2 } from "lucide-react"; import { Ellipsis, Globe, Lock, Trash2 } from "lucide-react"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu"
export default function ProjectCardDropdown({ export default function ProjectCardDropdown({
sandbox, sandbox,
onVisibilityChange, onVisibilityChange,
onDelete, onDelete,
}: { }: {
sandbox: Sandbox; sandbox: Sandbox
onVisibilityChange: (sandbox: Sandbox) => void; onVisibilityChange: (sandbox: Sandbox) => void
onDelete: (sandbox: Sandbox) => void; onDelete: (sandbox: Sandbox) => void
}) { }) {
return ( return (
<DropdownMenu modal={false}> <DropdownMenu modal={false}>
<DropdownMenuTrigger <DropdownMenuTrigger
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault()
e.stopPropagation(); e.stopPropagation()
}} }}
className="h-6 w-6 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 rounded-sm outline-foreground" className="h-6 w-6 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 rounded-sm outline-foreground"
> >
@ -33,8 +33,8 @@ export default function ProjectCardDropdown({
<DropdownMenuContent className="w-40"> <DropdownMenuContent className="w-40">
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
onVisibilityChange(sandbox); onVisibilityChange(sandbox)
}} }}
className="cursor-pointer" className="cursor-pointer"
> >
@ -52,8 +52,8 @@ export default function ProjectCardDropdown({
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation()
onDelete(sandbox); onDelete(sandbox)
}} }}
className="!text-destructive cursor-pointer" className="!text-destructive cursor-pointer"
> >
@ -62,5 +62,5 @@ export default function ProjectCardDropdown({
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); )
} }

View File

@ -1,14 +1,14 @@
"use client" "use client"
import { Card } from "@/components/ui/card"
import { projectTemplates } from "@/lib/data"
import { Sandbox } from "@/lib/types"
import { AnimatePresence, motion } from "framer-motion" import { AnimatePresence, motion } from "framer-motion"
import { Clock, Globe, Lock } from "lucide-react"
import Image from "next/image" import Image from "next/image"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import ProjectCardDropdown from "./dropdown" import ProjectCardDropdown from "./dropdown"
import { Clock, Globe, Lock } from "lucide-react"
import { Sandbox } from "@/lib/types"
import { Card } from "@/components/ui/card"
import { useRouter } from "next/navigation"
import { projectTemplates } from "@/lib/data"
export default function ProjectCard({ export default function ProjectCard({
children, children,

View File

@ -1,8 +1,8 @@
"use client"; "use client"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
import { Canvas, useFrame, useThree } from "@react-three/fiber"; import { Canvas, useFrame, useThree } from "@react-three/fiber"
import React, { useMemo, useRef } from "react"; import React, { useMemo, useRef } from "react"
import * as THREE from "three"; import * as THREE from "three"
export const CanvasRevealEffect = ({ export const CanvasRevealEffect = ({
animationSpeed = 0.4, animationSpeed = 0.4,
@ -12,12 +12,12 @@ export const CanvasRevealEffect = ({
dotSize, dotSize,
showGradient = true, showGradient = true,
}: { }: {
animationSpeed?: number; animationSpeed?: number
opacities?: number[]; opacities?: number[]
colors?: number[][]; colors?: number[][]
containerClassName?: string; containerClassName?: string
dotSize?: number; dotSize?: number
showGradient?: boolean; showGradient?: boolean
}) => { }) => {
return ( return (
<div className={cn("h-full relative bg-white w-full", containerClassName)}> <div className={cn("h-full relative bg-white w-full", containerClassName)}>
@ -41,16 +41,16 @@ export const CanvasRevealEffect = ({
<div className="absolute inset-0 bg-gradient-to-t from-background to-[100%]" /> <div className="absolute inset-0 bg-gradient-to-t from-background to-[100%]" />
)} )}
</div> </div>
); )
}; }
interface DotMatrixProps { interface DotMatrixProps {
colors?: number[][]; colors?: number[][]
opacities?: number[]; opacities?: number[]
totalSize?: number; totalSize?: number
dotSize?: number; dotSize?: number
shader?: string; shader?: string
center?: ("x" | "y")[]; center?: ("x" | "y")[]
} }
const DotMatrix: React.FC<DotMatrixProps> = ({ const DotMatrix: React.FC<DotMatrixProps> = ({
@ -69,7 +69,7 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
colors[0], colors[0],
colors[0], colors[0],
colors[0], colors[0],
]; ]
if (colors.length === 2) { if (colors.length === 2) {
colorsArray = [ colorsArray = [
colors[0], colors[0],
@ -78,7 +78,7 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
colors[1], colors[1],
colors[1], colors[1],
colors[1], colors[1],
]; ]
} else if (colors.length === 3) { } else if (colors.length === 3) {
colorsArray = [ colorsArray = [
colors[0], colors[0],
@ -87,7 +87,7 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
colors[1], colors[1],
colors[2], colors[2],
colors[2], colors[2],
]; ]
} }
return { return {
@ -111,8 +111,8 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
value: dotSize, value: dotSize,
type: "uniform1f", type: "uniform1f",
}, },
}; }
}, [colors, opacities, totalSize, dotSize]); }, [colors, opacities, totalSize, dotSize])
return ( return (
<Shader <Shader
@ -168,87 +168,87 @@ const DotMatrix: React.FC<DotMatrixProps> = ({
uniforms={uniforms} uniforms={uniforms}
maxFps={60} maxFps={60}
/> />
); )
}; }
type Uniforms = { type Uniforms = {
[key: string]: { [key: string]: {
value: number[] | number[][] | number; value: number[] | number[][] | number
type: string; type: string
}; }
}; }
const ShaderMaterial = ({ const ShaderMaterial = ({
source, source,
uniforms, uniforms,
maxFps = 60, maxFps = 60,
}: { }: {
source: string; source: string
hovered?: boolean; hovered?: boolean
maxFps?: number; maxFps?: number
uniforms: Uniforms; uniforms: Uniforms
}) => { }) => {
const { size } = useThree(); const { size } = useThree()
const ref = useRef<THREE.Mesh>(); const ref = useRef<THREE.Mesh>()
let lastFrameTime = 0; let lastFrameTime = 0
useFrame(({ clock }) => { useFrame(({ clock }) => {
if (!ref.current) return; if (!ref.current) return
const timestamp = clock.getElapsedTime(); const timestamp = clock.getElapsedTime()
if (timestamp - lastFrameTime < 1 / maxFps) { if (timestamp - lastFrameTime < 1 / maxFps) {
return; return
} }
lastFrameTime = timestamp; lastFrameTime = timestamp
const material: any = ref.current.material; const material: any = ref.current.material
const timeLocation = material.uniforms.u_time; const timeLocation = material.uniforms.u_time
timeLocation.value = timestamp; timeLocation.value = timestamp
}); })
const getUniforms = () => { const getUniforms = () => {
const preparedUniforms: any = {}; const preparedUniforms: any = {}
for (const uniformName in uniforms) { for (const uniformName in uniforms) {
const uniform: any = uniforms[uniformName]; const uniform: any = uniforms[uniformName]
switch (uniform.type) { switch (uniform.type) {
case "uniform1f": case "uniform1f":
preparedUniforms[uniformName] = { value: uniform.value, type: "1f" }; preparedUniforms[uniformName] = { value: uniform.value, type: "1f" }
break; break
case "uniform3f": case "uniform3f":
preparedUniforms[uniformName] = { preparedUniforms[uniformName] = {
value: new THREE.Vector3().fromArray(uniform.value), value: new THREE.Vector3().fromArray(uniform.value),
type: "3f", type: "3f",
}; }
break; break
case "uniform1fv": case "uniform1fv":
preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" }; preparedUniforms[uniformName] = { value: uniform.value, type: "1fv" }
break; break
case "uniform3fv": case "uniform3fv":
preparedUniforms[uniformName] = { preparedUniforms[uniformName] = {
value: uniform.value.map((v: number[]) => value: uniform.value.map((v: number[]) =>
new THREE.Vector3().fromArray(v) new THREE.Vector3().fromArray(v)
), ),
type: "3fv", type: "3fv",
}; }
break; break
case "uniform2f": case "uniform2f":
preparedUniforms[uniformName] = { preparedUniforms[uniformName] = {
value: new THREE.Vector2().fromArray(uniform.value), value: new THREE.Vector2().fromArray(uniform.value),
type: "2f", type: "2f",
}; }
break; break
default: default:
console.error(`Invalid uniform type for '${uniformName}'.`); console.error(`Invalid uniform type for '${uniformName}'.`)
break; break
} }
} }
preparedUniforms["u_time"] = { value: 0, type: "1f" }; preparedUniforms["u_time"] = { value: 0, type: "1f" }
preparedUniforms["u_resolution"] = { preparedUniforms["u_resolution"] = {
value: new THREE.Vector2(size.width * 2, size.height * 2), value: new THREE.Vector2(size.width * 2, size.height * 2),
}; // Initialize u_resolution } // Initialize u_resolution
return preparedUniforms; return preparedUniforms
}; }
// Shader material // Shader material
const material = useMemo(() => { const material = useMemo(() => {
@ -272,33 +272,33 @@ const ShaderMaterial = ({
blending: THREE.CustomBlending, blending: THREE.CustomBlending,
blendSrc: THREE.SrcAlphaFactor, blendSrc: THREE.SrcAlphaFactor,
blendDst: THREE.OneFactor, blendDst: THREE.OneFactor,
}); })
return materialObject; return materialObject
}, [size.width, size.height, source]); }, [size.width, size.height, source])
return ( return (
<mesh ref={ref as any}> <mesh ref={ref as any}>
<planeGeometry args={[2, 2]} /> <planeGeometry args={[2, 2]} />
<primitive object={material} attach="material" /> <primitive object={material} attach="material" />
</mesh> </mesh>
); )
}; }
const Shader: React.FC<ShaderProps> = ({ source, uniforms, maxFps = 60 }) => { const Shader: React.FC<ShaderProps> = ({ source, uniforms, maxFps = 60 }) => {
return ( return (
<Canvas className="absolute inset-0 h-full w-full"> <Canvas className="absolute inset-0 h-full w-full">
<ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} /> <ShaderMaterial source={source} uniforms={uniforms} maxFps={maxFps} />
</Canvas> </Canvas>
); )
}; }
interface ShaderProps { interface ShaderProps {
source: string; source: string
uniforms: { uniforms: {
[key: string]: { [key: string]: {
value: number[] | number[][] | number; value: number[] | number[][] | number
type: string; type: string
}; }
}; }
maxFps?: number; maxFps?: number
} }

View File

@ -1,16 +1,12 @@
"use client"; "use client"
import { Sandbox } from "@/lib/types"; import { deleteSandbox, updateSandbox } from "@/lib/actions"
import ProjectCard from "./projectCard"; import { Sandbox } from "@/lib/types"
import Image from "next/image"; import Link from "next/link"
import ProjectCardDropdown from "./projectCard/dropdown"; import { useEffect, useState } from "react"
import { Clock, Globe, Lock } from "lucide-react"; import { toast } from "sonner"
import Link from "next/link"; import ProjectCard from "./projectCard"
import { Card } from "../ui/card"; import { CanvasRevealEffect } from "./projectCard/revealEffect"
import { deleteSandbox, updateSandbox } from "@/lib/actions";
import { toast } from "sonner";
import { useEffect, useState } from "react";
import { CanvasRevealEffect } from "./projectCard/revealEffect";
const colors: { [key: string]: number[][] } = { const colors: { [key: string]: number[][] } = {
react: [ react: [
@ -21,38 +17,37 @@ const colors: { [key: string]: number[][] } = {
[86, 184, 72], [86, 184, 72],
[59, 112, 52], [59, 112, 52],
], ],
}; }
export default function DashboardProjects({ export default function DashboardProjects({
sandboxes, sandboxes,
q, q,
}: { }: {
sandboxes: Sandbox[]; sandboxes: Sandbox[]
q: string | null; q: string | null
}) { }) {
const [deletingId, setDeletingId] = useState<string>(""); const [deletingId, setDeletingId] = useState<string>("")
const onDelete = async (sandbox: Sandbox) => { const onDelete = async (sandbox: Sandbox) => {
setDeletingId(sandbox.id); setDeletingId(sandbox.id)
toast(`Project ${sandbox.name} deleted.`); toast(`Project ${sandbox.name} deleted.`)
await deleteSandbox(sandbox.id); await deleteSandbox(sandbox.id)
}; }
useEffect(() => { useEffect(() => {
if (deletingId) { if (deletingId) {
setDeletingId(""); setDeletingId("")
} }
}, [sandboxes]); }, [sandboxes])
const onVisibilityChange = async (sandbox: Sandbox) => { const onVisibilityChange = async (sandbox: Sandbox) => {
const newVisibility = const newVisibility = sandbox.visibility === "public" ? "private" : "public"
sandbox.visibility === "public" ? "private" : "public"; toast(`Project ${sandbox.name} is now ${newVisibility}.`)
toast(`Project ${sandbox.name} is now ${newVisibility}.`);
await updateSandbox({ await updateSandbox({
id: sandbox.id, id: sandbox.id,
visibility: newVisibility, visibility: newVisibility,
}); })
}; }
return ( return (
<div className="grow p-4 flex flex-col"> <div className="grow p-4 flex flex-col">
@ -65,7 +60,7 @@ export default function DashboardProjects({
{sandboxes.map((sandbox) => { {sandboxes.map((sandbox) => {
if (q && q.length > 0) { if (q && q.length > 0) {
if (!sandbox.name.toLowerCase().includes(q.toLowerCase())) { if (!sandbox.name.toLowerCase().includes(q.toLowerCase())) {
return null; return null
} }
} }
return ( return (
@ -93,7 +88,7 @@ export default function DashboardProjects({
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" /> <div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
</ProjectCard> </ProjectCard>
</Link> </Link>
); )
})} })}
</div> </div>
) : ( ) : (
@ -103,5 +98,5 @@ export default function DashboardProjects({
)} )}
</div> </div>
</div> </div>
); )
} }

View File

@ -1,29 +1,27 @@
import { Sandbox } from "@/lib/types";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption,
TableCell, TableCell,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table"
import Image from "next/image"; import { ChevronRight } from "lucide-react"
import Button from "../ui/customButton"; import Image from "next/image"
import { ChevronRight } from "lucide-react"; import Link from "next/link"
import Avatar from "../ui/avatar"; import Avatar from "../ui/avatar"
import Link from "next/link"; import Button from "../ui/customButton"
export default function DashboardSharedWithMe({ export default function DashboardSharedWithMe({
shared, shared,
}: { }: {
shared: { shared: {
id: string; id: string
name: string; name: string
type: "react" | "node"; type: "react" | "node"
author: string; author: string
sharedOn: Date; sharedOn: Date
}[]; }[]
}) { }) {
return ( return (
<div className="grow p-4 flex flex-col"> <div className="grow p-4 flex flex-col">
@ -86,5 +84,5 @@ export default function DashboardSharedWithMe({
</div> </div>
)} )}
</div> </div>
); )
} }

View File

@ -1,36 +1,51 @@
import React from 'react'; import { Send, StopCircle } from "lucide-react"
import { Button } from '../../ui/button'; import { Button } from "../../ui/button"
import { Send, StopCircle } from 'lucide-react';
interface ChatInputProps { interface ChatInputProps {
input: string; input: string
setInput: (input: string) => void; setInput: (input: string) => void
isGenerating: boolean; isGenerating: boolean
handleSend: () => void; handleSend: () => void
handleStopGeneration: () => void; handleStopGeneration: () => void
} }
export default function ChatInput({ input, setInput, isGenerating, handleSend, handleStopGeneration }: ChatInputProps) { export default function ChatInput({
input,
setInput,
isGenerating,
handleSend,
handleStopGeneration,
}: ChatInputProps) {
return ( return (
<div className="flex space-x-2 min-w-0"> <div className="flex space-x-2 min-w-0">
<input <input
type="text" type="text"
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && !isGenerating && handleSend()} onKeyPress={(e) => e.key === "Enter" && !isGenerating && handleSend()}
className="flex-grow p-2 border rounded-lg min-w-0 bg-input" className="flex-grow p-2 border rounded-lg min-w-0 bg-input"
placeholder="Type your message..." placeholder="Type your message..."
disabled={isGenerating} disabled={isGenerating}
/> />
{isGenerating ? ( {isGenerating ? (
<Button onClick={handleStopGeneration} variant="destructive" size="icon" className="h-10 w-10"> <Button
onClick={handleStopGeneration}
variant="destructive"
size="icon"
className="h-10 w-10"
>
<StopCircle className="w-4 h-4" /> <StopCircle className="w-4 h-4" />
</Button> </Button>
) : ( ) : (
<Button onClick={handleSend} disabled={isGenerating} size="icon" className="h-10 w-10"> <Button
onClick={handleSend}
disabled={isGenerating}
size="icon"
className="h-10 w-10"
>
<Send className="w-4 h-4" /> <Send className="w-4 h-4" />
</Button> </Button>
)} )}
</div> </div>
); )
} }

View File

@ -1,10 +1,10 @@
import { Check, ChevronDown, ChevronUp, Copy, CornerUpLeft } from 'lucide-react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Button } from '../../ui/button';
import { ChevronUp, ChevronDown, Copy, Check, CornerUpLeft } from 'lucide-react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import { Button } from '../../ui/button';
import { copyToClipboard, stringifyContent } from './lib/chatUtils'; import { copyToClipboard, stringifyContent } from './lib/chatUtils';
interface MessageProps { interface MessageProps {

View File

@ -1,5 +1,4 @@
import React from 'react'; import { ChevronDown, ChevronUp, X } from 'lucide-react';
import { ChevronUp, ChevronDown, X } from 'lucide-react';
interface ContextDisplayProps { interface ContextDisplayProps {
context: string | null; context: string | null;

View File

@ -1,48 +1,58 @@
import React, { useState, useEffect, useRef } from 'react'; import { X } from "lucide-react"
import LoadingDots from '../../ui/LoadingDots'; import { useEffect, useRef, useState } from "react"
import ChatMessage from './ChatMessage'; import LoadingDots from "../../ui/LoadingDots"
import ChatInput from './ChatInput'; import ChatInput from "./ChatInput"
import ContextDisplay from './ContextDisplay'; import ChatMessage from "./ChatMessage"
import { handleSend, handleStopGeneration } from './lib/chatUtils'; import ContextDisplay from "./ContextDisplay"
import { X } from 'lucide-react'; import { handleSend, handleStopGeneration } from "./lib/chatUtils"
interface Message { interface Message {
role: 'user' | 'assistant'; role: "user" | "assistant"
content: string; content: string
context?: string; context?: string
} }
export default function AIChat({ activeFileContent, activeFileName, onClose }: { activeFileContent: string, activeFileName: string, onClose: () => void }) { export default function AIChat({
const [messages, setMessages] = useState<Message[]>([]); activeFileContent,
const [input, setInput] = useState(''); activeFileName,
const [isGenerating, setIsGenerating] = useState(false); onClose,
const chatContainerRef = useRef<HTMLDivElement>(null); }: {
const abortControllerRef = useRef<AbortController | null>(null); activeFileContent: string
const [context, setContext] = useState<string | null>(null); activeFileName: string
const [isContextExpanded, setIsContextExpanded] = useState(false); onClose: () => void
const [isLoading, setIsLoading] = useState(false); }) {
const [messages, setMessages] = useState<Message[]>([])
const [input, setInput] = useState("")
const [isGenerating, setIsGenerating] = useState(false)
const chatContainerRef = useRef<HTMLDivElement>(null)
const abortControllerRef = useRef<AbortController | null>(null)
const [context, setContext] = useState<string | null>(null)
const [isContextExpanded, setIsContextExpanded] = useState(false)
const [isLoading, setIsLoading] = useState(false)
useEffect(() => { useEffect(() => {
scrollToBottom(); scrollToBottom()
}, [messages]); }, [messages])
const scrollToBottom = () => { const scrollToBottom = () => {
if (chatContainerRef.current) { if (chatContainerRef.current) {
setTimeout(() => { setTimeout(() => {
chatContainerRef.current?.scrollTo({ chatContainerRef.current?.scrollTo({
top: chatContainerRef.current.scrollHeight, top: chatContainerRef.current.scrollHeight,
behavior: 'smooth' behavior: "smooth",
}); })
}, 100); }, 100)
} }
}; }
return ( return (
<div className="flex flex-col h-screen w-full"> <div className="flex flex-col h-screen w-full">
<div className="flex justify-between items-center p-2 border-b"> <div className="flex justify-between items-center p-2 border-b">
<span className="text-muted-foreground/50 font-medium">CHAT</span> <span className="text-muted-foreground/50 font-medium">CHAT</span>
<div className="flex items-center h-full"> <div className="flex items-center h-full">
<span className="text-muted-foreground/50 font-medium">{activeFileName}</span> <span className="text-muted-foreground/50 font-medium">
{activeFileName}
</span>
<div className="mx-2 h-full w-px bg-muted-foreground/20"></div> <div className="mx-2 h-full w-px bg-muted-foreground/20"></div>
<button <button
onClick={onClose} onClick={onClose}
@ -53,11 +63,14 @@ export default function AIChat({ activeFileContent, activeFileName, onClose }: {
</button> </button>
</div> </div>
</div> </div>
<div ref={chatContainerRef} className="flex-grow overflow-y-auto p-4 space-y-4"> <div
ref={chatContainerRef}
className="flex-grow overflow-y-auto p-4 space-y-4"
>
{messages.map((message, messageIndex) => ( {messages.map((message, messageIndex) => (
<ChatMessage <ChatMessage
key={messageIndex} key={messageIndex}
message={message} message={message}
setContext={setContext} setContext={setContext}
setIsContextExpanded={setIsContextExpanded} setIsContextExpanded={setIsContextExpanded}
/> />
@ -65,20 +78,33 @@ export default function AIChat({ activeFileContent, activeFileName, onClose }: {
{isLoading && <LoadingDots />} {isLoading && <LoadingDots />}
</div> </div>
<div className="p-4 border-t mb-14"> <div className="p-4 border-t mb-14">
<ContextDisplay <ContextDisplay
context={context} context={context}
isContextExpanded={isContextExpanded} isContextExpanded={isContextExpanded}
setIsContextExpanded={setIsContextExpanded} setIsContextExpanded={setIsContextExpanded}
setContext={setContext} setContext={setContext}
/> />
<ChatInput <ChatInput
input={input} input={input}
setInput={setInput} setInput={setInput}
isGenerating={isGenerating} isGenerating={isGenerating}
handleSend={() => handleSend(input, context, messages, setMessages, setInput, setIsContextExpanded, setIsGenerating, setIsLoading, abortControllerRef, activeFileContent)} handleSend={() =>
handleSend(
input,
context,
messages,
setMessages,
setInput,
setIsContextExpanded,
setIsGenerating,
setIsLoading,
abortControllerRef,
activeFileContent
)
}
handleStopGeneration={() => handleStopGeneration(abortControllerRef)} handleStopGeneration={() => handleStopGeneration(abortControllerRef)}
/> />
</div> </div>
</div> </div>
); )
} }

View File

@ -1,58 +1,68 @@
import React from 'react'; import React from "react"
export const stringifyContent = (content: any, seen = new WeakSet()): string => { export const stringifyContent = (
if (typeof content === 'string') { content: any,
return content; seen = new WeakSet()
): string => {
if (typeof content === "string") {
return content
} }
if (content === null) { if (content === null) {
return 'null'; return "null"
} }
if (content === undefined) { if (content === undefined) {
return 'undefined'; return "undefined"
} }
if (typeof content === 'number' || typeof content === 'boolean') { if (typeof content === "number" || typeof content === "boolean") {
return content.toString(); return content.toString()
} }
if (typeof content === 'function') { if (typeof content === "function") {
return content.toString(); return content.toString()
} }
if (typeof content === 'symbol') { if (typeof content === "symbol") {
return content.toString(); return content.toString()
} }
if (typeof content === 'bigint') { if (typeof content === "bigint") {
return content.toString() + 'n'; return content.toString() + "n"
} }
if (React.isValidElement(content)) { if (React.isValidElement(content)) {
return React.Children.toArray((content as React.ReactElement).props.children) return React.Children.toArray(
.map(child => stringifyContent(child, seen)) (content as React.ReactElement).props.children
.join(''); )
.map((child) => stringifyContent(child, seen))
.join("")
} }
if (Array.isArray(content)) { if (Array.isArray(content)) {
return '[' + content.map(item => stringifyContent(item, seen)).join(', ') + ']'; return (
"[" + content.map((item) => stringifyContent(item, seen)).join(", ") + "]"
)
} }
if (typeof content === 'object') { if (typeof content === "object") {
if (seen.has(content)) { if (seen.has(content)) {
return '[Circular]'; return "[Circular]"
} }
seen.add(content); seen.add(content)
try { try {
const pairs = Object.entries(content).map( const pairs = Object.entries(content).map(
([key, value]) => `${key}: ${stringifyContent(value, seen)}` ([key, value]) => `${key}: ${stringifyContent(value, seen)}`
); )
return '{' + pairs.join(', ') + '}'; return "{" + pairs.join(", ") + "}"
} catch (error) { } catch (error) {
return Object.prototype.toString.call(content); return Object.prototype.toString.call(content)
} }
} }
return String(content); return String(content)
}; }
export const copyToClipboard = (text: string, setCopiedText: (text: string | null) => void) => { export const copyToClipboard = (
text: string,
setCopiedText: (text: string | null) => void
) => {
navigator.clipboard.writeText(text).then(() => { navigator.clipboard.writeText(text).then(() => {
setCopiedText(text); setCopiedText(text)
setTimeout(() => setCopiedText(null), 2000); setTimeout(() => setCopiedText(null), 2000)
}); })
}; }
export const handleSend = async ( export const handleSend = async (
input: string, input: string,
@ -66,97 +76,105 @@ export const handleSend = async (
abortControllerRef: React.MutableRefObject<AbortController | null>, abortControllerRef: React.MutableRefObject<AbortController | null>,
activeFileContent: string activeFileContent: string
) => { ) => {
if (input.trim() === '' && !context) return; if (input.trim() === "" && !context) return
const newMessage = { const newMessage = {
role: 'user' as const, role: "user" as const,
content: input, content: input,
context: context || undefined context: context || undefined,
}; }
const updatedMessages = [...messages, newMessage]; const updatedMessages = [...messages, newMessage]
setMessages(updatedMessages); setMessages(updatedMessages)
setInput(''); setInput("")
setIsContextExpanded(false); setIsContextExpanded(false)
setIsGenerating(true); setIsGenerating(true)
setIsLoading(true); setIsLoading(true)
abortControllerRef.current = new AbortController(); abortControllerRef.current = new AbortController()
try { try {
const anthropicMessages = updatedMessages.map(msg => ({ const anthropicMessages = updatedMessages.map((msg) => ({
role: msg.role === 'user' ? 'human' : 'assistant', role: msg.role === "user" ? "human" : "assistant",
content: msg.content content: msg.content,
})); }))
const response = await fetch(`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`, { const response = await fetch(
method: 'POST', `${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`,
headers: { {
'Content-Type': 'application/json', method: "POST",
}, headers: {
body: JSON.stringify({ "Content-Type": "application/json",
messages: anthropicMessages, },
context: context || undefined, body: JSON.stringify({
activeFileContent: activeFileContent, messages: anthropicMessages,
}), context: context || undefined,
signal: abortControllerRef.current.signal, activeFileContent: activeFileContent,
}); }),
signal: abortControllerRef.current.signal,
}
)
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to get AI response'); throw new Error("Failed to get AI response")
} }
const reader = response.body?.getReader(); const reader = response.body?.getReader()
const decoder = new TextDecoder(); const decoder = new TextDecoder()
const assistantMessage = { role: 'assistant' as const, content: '' }; const assistantMessage = { role: "assistant" as const, content: "" }
setMessages([...updatedMessages, assistantMessage]); setMessages([...updatedMessages, assistantMessage])
setIsLoading(false); setIsLoading(false)
let buffer = ''; let buffer = ""
const updateInterval = 100; const updateInterval = 100
let lastUpdateTime = Date.now(); let lastUpdateTime = Date.now()
if (reader) { if (reader) {
while (true) { while (true) {
const { done, value } = await reader.read(); const { done, value } = await reader.read()
if (done) break; if (done) break
buffer += decoder.decode(value, { stream: true }); buffer += decoder.decode(value, { stream: true })
const currentTime = Date.now(); const currentTime = Date.now()
if (currentTime - lastUpdateTime > updateInterval) { if (currentTime - lastUpdateTime > updateInterval) {
setMessages(prev => { setMessages((prev) => {
const updatedMessages = [...prev]; const updatedMessages = [...prev]
const lastMessage = updatedMessages[updatedMessages.length - 1]; const lastMessage = updatedMessages[updatedMessages.length - 1]
lastMessage.content = buffer; lastMessage.content = buffer
return updatedMessages; return updatedMessages
}); })
lastUpdateTime = currentTime; lastUpdateTime = currentTime
} }
} }
setMessages(prev => { setMessages((prev) => {
const updatedMessages = [...prev]; const updatedMessages = [...prev]
const lastMessage = updatedMessages[updatedMessages.length - 1]; const lastMessage = updatedMessages[updatedMessages.length - 1]
lastMessage.content = buffer; lastMessage.content = buffer
return updatedMessages; return updatedMessages
}); })
} }
} catch (error: any) { } catch (error: any) {
if (error.name === 'AbortError') { if (error.name === "AbortError") {
console.log('Generation aborted'); console.log("Generation aborted")
} else { } else {
console.error('Error fetching AI response:', error); console.error("Error fetching AI response:", error)
const errorMessage = { role: 'assistant' as const, content: 'Sorry, I encountered an error. Please try again.' }; const errorMessage = {
setMessages(prev => [...prev, errorMessage]); role: "assistant" as const,
content: "Sorry, I encountered an error. Please try again.",
}
setMessages((prev) => [...prev, errorMessage])
} }
} finally { } finally {
setIsGenerating(false); setIsGenerating(false)
setIsLoading(false); setIsLoading(false)
abortControllerRef.current = null; abortControllerRef.current = null
} }
}; }
export const handleStopGeneration = (abortControllerRef: React.MutableRefObject<AbortController | null>) => { export const handleStopGeneration = (
abortControllerRef: React.MutableRefObject<AbortController | null>
) => {
if (abortControllerRef.current) { if (abortControllerRef.current) {
abortControllerRef.current.abort(); abortControllerRef.current.abort()
} }
}; }

View File

@ -1,13 +1,13 @@
"use client" "use client"
import { useCallback, useEffect, useRef, useState } from "react"
import { Button } from "../ui/button"
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
import { Socket } from "socket.io-client"
import { Editor } from "@monaco-editor/react"
import { User } from "@/lib/types" import { User } from "@/lib/types"
import { toast } from "sonner" import { Editor } from "@monaco-editor/react"
import { Check, Loader2, RotateCw, Sparkles, X } from "lucide-react"
import { usePathname, useRouter } from "next/navigation" import { usePathname, useRouter } from "next/navigation"
import { useCallback, useEffect, useRef, useState } from "react"
import { Socket } from "socket.io-client"
import { toast } from "sonner"
import { Button } from "../ui/button"
// import monaco from "monaco-editor" // import monaco from "monaco-editor"
export default function GenerateInput({ export default function GenerateInput({

View File

@ -1,43 +1,55 @@
"use client" "use client"
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react"
import * as monaco from "monaco-editor"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { toast } from "sonner"
import { useClerk } from "@clerk/nextjs" import { useClerk } from "@clerk/nextjs"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { AnimatePresence, motion } from "framer-motion" import { AnimatePresence, motion } from "framer-motion"
import * as monaco from "monaco-editor"
import { useCallback, useEffect, useRef, useState } from "react"
import { toast } from "sonner"
import * as Y from "yjs" import { TypedLiveblocksProvider, useRoom, useSelf } from "@/liveblocks.config"
import LiveblocksProvider from "@liveblocks/yjs" import LiveblocksProvider from "@liveblocks/yjs"
import { MonacoBinding } from "y-monaco" import { MonacoBinding } from "y-monaco"
import { Awareness } from "y-protocols/awareness" import { Awareness } from "y-protocols/awareness"
import { TypedLiveblocksProvider, useRoom, useSelf } from "@/liveblocks.config" import * as Y from "yjs"
import { import {
ResizableHandle, ResizableHandle,
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from "@/components/ui/resizable" } from "@/components/ui/resizable"
import { FileJson, Loader2, Sparkles, TerminalSquare, ArrowDownToLine, ArrowRightToLine } from "lucide-react" import { PreviewProvider, usePreview } from "@/context/PreviewContext"
import Tab from "../ui/tab" import { useSocket } from "@/context/SocketContext"
import Sidebar from "./sidebar" import { parseTSConfigToMonacoOptions } from "@/lib/tsconfig"
import GenerateInput from "./generate" import { Sandbox, TFile, TFolder, TTab, User } from "@/lib/types"
import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types" import {
import { addNew, processFileType, validateName, debounce } from "@/lib/utils" addNew,
import { Cursors } from "./live/cursors" debounce,
deepMerge,
processFileType,
validateName,
} from "@/lib/utils"
import { Terminal } from "@xterm/xterm" import { Terminal } from "@xterm/xterm"
import {
ArrowDownToLine,
ArrowRightToLine,
FileJson,
Loader2,
Sparkles,
TerminalSquare,
} from "lucide-react"
import React from "react"
import { ImperativePanelHandle } from "react-resizable-panels"
import { Button } from "../ui/button"
import Tab from "../ui/tab"
import AIChat from "./AIChat"
import GenerateInput from "./generate"
import { Cursors } from "./live/cursors"
import DisableAccessModal from "./live/disableModal" import DisableAccessModal from "./live/disableModal"
import Loading from "./loading" import Loading from "./loading"
import PreviewWindow from "./preview" import PreviewWindow from "./preview"
import Sidebar from "./sidebar"
import Terminals from "./terminals" import Terminals from "./terminals"
import { ImperativePanelHandle } from "react-resizable-panels"
import { PreviewProvider, usePreview } from "@/context/PreviewContext"
import { useSocket } from "@/context/SocketContext"
import { Button } from "../ui/button"
import React from "react"
import { parseTSConfigToMonacoOptions } from "@/lib/tsconfig"
import { deepMerge } from "@/lib/utils"
import AIChat from "./AIChat"
export default function CodeEditor({ export default function CodeEditor({
userData, userData,
@ -63,9 +75,9 @@ export default function CodeEditor({
// This heartbeat is critical to preventing the E2B sandbox from timing out // This heartbeat is critical to preventing the E2B sandbox from timing out
useEffect(() => { useEffect(() => {
// 10000 ms = 10 seconds // 10000 ms = 10 seconds
const interval = setInterval(() => socket?.emit("heartbeat"), 10000); const interval = setInterval(() => socket?.emit("heartbeat"), 10000)
return () => clearInterval(interval); return () => clearInterval(interval)
}, [socket]); }, [socket])
//Preview Button state //Preview Button state
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true) const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
@ -75,11 +87,11 @@ export default function CodeEditor({
}) })
// Layout state // Layout state
const [isHorizontalLayout, setIsHorizontalLayout] = useState(false); const [isHorizontalLayout, setIsHorizontalLayout] = useState(false)
const [previousLayout, setPreviousLayout] = useState(false); const [previousLayout, setPreviousLayout] = useState(false)
// AI Chat state // AI Chat state
const [isAIChatOpen, setIsAIChatOpen] = useState(false); const [isAIChatOpen, setIsAIChatOpen] = useState(false)
// File state // File state
const [files, setFiles] = useState<(TFolder | TFile)[]>([]) const [files, setFiles] = useState<(TFolder | TFile)[]>([])
@ -88,7 +100,7 @@ export default function CodeEditor({
const [activeFileContent, setActiveFileContent] = useState("") const [activeFileContent, setActiveFileContent] = useState("")
const [deletingFolderId, setDeletingFolderId] = useState("") const [deletingFolderId, setDeletingFolderId] = useState("")
// Added this state to track the most recent content for each file // Added this state to track the most recent content for each file
const [fileContents, setFileContents] = useState<Record<string, string>>({}); const [fileContents, setFileContents] = useState<Record<string, string>>({})
// Editor state // Editor state
const [editorLanguage, setEditorLanguage] = useState("plaintext") const [editorLanguage, setEditorLanguage] = useState("plaintext")
@ -153,7 +165,7 @@ export default function CodeEditor({
const generateRef = useRef<HTMLDivElement>(null) const generateRef = useRef<HTMLDivElement>(null)
const suggestionRef = useRef<HTMLDivElement>(null) const suggestionRef = useRef<HTMLDivElement>(null)
const generateWidgetRef = useRef<HTMLDivElement>(null) const generateWidgetRef = useRef<HTMLDivElement>(null)
const { previewPanelRef } = usePreview(); const { previewPanelRef } = usePreview()
const editorPanelRef = useRef<ImperativePanelHandle>(null) const editorPanelRef = useRef<ImperativePanelHandle>(null)
const previewWindowRef = useRef<{ refreshIframe: () => void }>(null) const previewWindowRef = useRef<{ refreshIframe: () => void }>(null)
@ -470,16 +482,17 @@ export default function CodeEditor({
const model = editorRef?.getModel() const model = editorRef?.getModel()
// added this because it was giving client side exception - Illegal value for lineNumber when opening an empty file // added this because it was giving client side exception - Illegal value for lineNumber when opening an empty file
if (model) { if (model) {
const totalLines = model.getLineCount(); const totalLines = model.getLineCount()
// Check if the cursorLine is a valid number, If cursorLine is out of bounds, we fall back to 1 (the first line) as a default safe value. // Check if the cursorLine is a valid number, If cursorLine is out of bounds, we fall back to 1 (the first line) as a default safe value.
const lineNumber = cursorLine > 0 && cursorLine <= totalLines ? cursorLine : 1; // fallback to a valid line number const lineNumber =
cursorLine > 0 && cursorLine <= totalLines ? cursorLine : 1 // fallback to a valid line number
// If for some reason the content doesn't exist, we use an empty string as a fallback. // If for some reason the content doesn't exist, we use an empty string as a fallback.
const line = model.getLineContent(lineNumber) ?? ""; const line = model.getLineContent(lineNumber) ?? ""
// Check if the line is not empty or only whitespace (i.e., `.trim()` removes spaces). // Check if the line is not empty or only whitespace (i.e., `.trim()` removes spaces).
// If the line has content, we clear any decorations using the instance of the `decorations` object. // If the line has content, we clear any decorations using the instance of the `decorations` object.
// Decorations refer to editor highlights, underlines, or markers, so this clears those if conditions are met. // Decorations refer to editor highlights, underlines, or markers, so this clears those if conditions are met.
if (line.trim() !== "") { if (line.trim() !== "") {
decorations.instance?.clear(); decorations.instance?.clear()
return return
} }
} }
@ -505,40 +518,40 @@ export default function CodeEditor({
debounce((activeFileId: string | undefined) => { debounce((activeFileId: string | undefined) => {
if (activeFileId) { if (activeFileId) {
// Get the current content of the file // Get the current content of the file
const content = fileContents[activeFileId]; const content = fileContents[activeFileId]
// Mark the file as saved in the tabs // Mark the file as saved in the tabs
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => prev.map((tab) =>
tab.id === activeFileId ? { ...tab, saved: true } : tab tab.id === activeFileId ? { ...tab, saved: true } : tab
) )
); )
console.log(`Saving file...${activeFileId}`); console.log(`Saving file...${activeFileId}`)
console.log(`Saving file...${content}`); console.log(`Saving file...${content}`)
socket?.emit("saveFile", activeFileId, content); socket?.emit("saveFile", activeFileId, content)
} }
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000), }, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
[socket, fileContents] [socket, fileContents]
); )
// Keydown event listener to trigger file save on Ctrl+S or Cmd+S, and toggle AI chat on Ctrl+L or Cmd+L // Keydown event listener to trigger file save on Ctrl+S or Cmd+S, and toggle AI chat on Ctrl+L or Cmd+L
useEffect(() => { useEffect(() => {
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
if (e.key === "s" && (e.metaKey || e.ctrlKey)) { if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
e.preventDefault() e.preventDefault()
debouncedSaveData(activeFileId); debouncedSaveData(activeFileId)
} else if (e.key === "l" && (e.metaKey || e.ctrlKey)) { } else if (e.key === "l" && (e.metaKey || e.ctrlKey)) {
e.preventDefault() e.preventDefault()
setIsAIChatOpen(prev => !prev); setIsAIChatOpen((prev) => !prev)
} }
}; }
document.addEventListener("keydown", down); document.addEventListener("keydown", down)
// Added this line to prevent Monaco editor from handling Cmd/Ctrl+L // Added this line to prevent Monaco editor from handling Cmd/Ctrl+L
editorRef?.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL, () => { editorRef?.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL, () => {
setIsAIChatOpen(prev => !prev); setIsAIChatOpen((prev) => !prev)
}); })
return () => { return () => {
document.removeEventListener("keydown", down) document.removeEventListener("keydown", down)
@ -632,7 +645,7 @@ export default function CodeEditor({
// Socket event listener effect // Socket event listener effect
useEffect(() => { useEffect(() => {
const onConnect = () => { } const onConnect = () => {}
const onDisconnect = () => { const onDisconnect = () => {
setTerminals([]) setTerminals([])
@ -702,46 +715,49 @@ export default function CodeEditor({
} // 300ms debounce delay, adjust as needed } // 300ms debounce delay, adjust as needed
const selectFile = (tab: TTab) => { const selectFile = (tab: TTab) => {
if (tab.id === activeFileId) return; if (tab.id === activeFileId) return
setGenerate((prev) => ({ ...prev, show: false })); setGenerate((prev) => ({ ...prev, show: false }))
// Check if the tab already exists in the list of open tabs // Check if the tab already exists in the list of open tabs
const exists = tabs.find((t) => t.id === tab.id); const exists = tabs.find((t) => t.id === tab.id)
setTabs((prev) => { setTabs((prev) => {
if (exists) { if (exists) {
// If the tab exists, make it the active tab // If the tab exists, make it the active tab
setActiveFileId(exists.id); setActiveFileId(exists.id)
return prev; return prev
} }
// If the tab doesn't exist, add it to the list of tabs and make it active // If the tab doesn't exist, add it to the list of tabs and make it active
return [...prev, tab]; return [...prev, tab]
}); })
// If the file's content is already cached, set it as the active content // If the file's content is already cached, set it as the active content
if (fileContents[tab.id]) { if (fileContents[tab.id]) {
setActiveFileContent(fileContents[tab.id]); setActiveFileContent(fileContents[tab.id])
} else { } else {
// Otherwise, fetch the content of the file and cache it // Otherwise, fetch the content of the file and cache it
debouncedGetFile(tab.id, (response: string) => { debouncedGetFile(tab.id, (response: string) => {
setFileContents(prev => ({ ...prev, [tab.id]: response })); setFileContents((prev) => ({ ...prev, [tab.id]: response }))
setActiveFileContent(response); setActiveFileContent(response)
}); })
} }
// Set the editor language based on the file type // Set the editor language based on the file type
setEditorLanguage(processFileType(tab.name)); setEditorLanguage(processFileType(tab.name))
// Set the active file ID to the new tab // Set the active file ID to the new tab
setActiveFileId(tab.id); setActiveFileId(tab.id)
}; }
// Added this effect to update fileContents when the editor content changes // Added this effect to update fileContents when the editor content changes
useEffect(() => { useEffect(() => {
if (activeFileId) { if (activeFileId) {
// Cache the current active file content using the file ID as the key // Cache the current active file content using the file ID as the key
setFileContents(prev => ({ ...prev, [activeFileId]: activeFileContent })); setFileContents((prev) => ({
...prev,
[activeFileId]: activeFileContent,
}))
} }
}, [activeFileContent, activeFileId]); }, [activeFileContent, activeFileId])
// Close tab and remove from tabs // Close tab and remove from tabs
const closeTab = (id: string) => { const closeTab = (id: string) => {
@ -757,8 +773,8 @@ export default function CodeEditor({
? numTabs === 1 ? numTabs === 1
? null ? null
: index < numTabs - 1 : index < numTabs - 1
? tabs[index + 1].id ? tabs[index + 1].id
: tabs[index - 1].id : tabs[index - 1].id
: activeFileId : activeFileId
setTabs((prev) => prev.filter((t) => t.id !== id)) setTabs((prev) => prev.filter((t) => t.id !== id))
@ -846,34 +862,34 @@ export default function CodeEditor({
const togglePreviewPanel = () => { const togglePreviewPanel = () => {
if (isPreviewCollapsed) { if (isPreviewCollapsed) {
previewPanelRef.current?.expand(); previewPanelRef.current?.expand()
setIsPreviewCollapsed(false); setIsPreviewCollapsed(false)
} else { } else {
previewPanelRef.current?.collapse(); previewPanelRef.current?.collapse()
setIsPreviewCollapsed(true); setIsPreviewCollapsed(true)
} }
}; }
const toggleLayout = () => { const toggleLayout = () => {
if (!isAIChatOpen) { if (!isAIChatOpen) {
setIsHorizontalLayout(prev => !prev); setIsHorizontalLayout((prev) => !prev)
} }
}; }
// Add an effect to handle layout changes when AI chat is opened/closed // Add an effect to handle layout changes when AI chat is opened/closed
useEffect(() => { useEffect(() => {
if (isAIChatOpen) { if (isAIChatOpen) {
setPreviousLayout(isHorizontalLayout); setPreviousLayout(isHorizontalLayout)
setIsHorizontalLayout(true); setIsHorizontalLayout(true)
} else { } else {
setIsHorizontalLayout(previousLayout); setIsHorizontalLayout(previousLayout)
} }
}, [isAIChatOpen]); }, [isAIChatOpen])
// Modify the toggleAIChat function // Modify the toggleAIChat function
const toggleAIChat = () => { const toggleAIChat = () => {
setIsAIChatOpen(prev => !prev); setIsAIChatOpen((prev) => !prev)
}; }
// On disabled access for shared users, show un-interactable loading placeholder + info modal // On disabled access for shared users, show un-interactable loading placeholder + info modal
if (disableAccess.isDisabled) if (disableAccess.isDisabled)
@ -882,7 +898,7 @@ export default function CodeEditor({
<DisableAccessModal <DisableAccessModal
message={disableAccess.message} message={disableAccess.message}
open={disableAccess.isDisabled} open={disableAccess.isDisabled}
setOpen={() => { }} setOpen={() => {}}
/> />
<Loading /> <Loading />
</> </>
@ -921,8 +937,8 @@ export default function CodeEditor({
code: code:
(isSelected && editorRef?.getSelection() (isSelected && editorRef?.getSelection()
? editorRef ? editorRef
?.getModel() ?.getModel()
?.getValueInRange(editorRef?.getSelection()!) ?.getValueInRange(editorRef?.getSelection()!)
: editorRef?.getValue()) ?? "", : editorRef?.getValue()) ?? "",
line: generate.line, line: generate.line,
}} }}
@ -1008,10 +1024,14 @@ export default function CodeEditor({
deletingFolderId={deletingFolderId} deletingFolderId={deletingFolderId}
/> />
{/* Outer ResizablePanelGroup for main layout */} {/* Outer ResizablePanelGroup for main layout */}
<ResizablePanelGroup direction={isHorizontalLayout ? "horizontal" : "vertical"}> <ResizablePanelGroup
direction={isHorizontalLayout ? "horizontal" : "vertical"}
>
{/* Left side: Editor and Preview/Terminal */} {/* Left side: Editor and Preview/Terminal */}
<ResizablePanel defaultSize={isAIChatOpen ? 80 : 100} minSize={50}> <ResizablePanel defaultSize={isAIChatOpen ? 80 : 100} minSize={50}>
<ResizablePanelGroup direction={isHorizontalLayout ? "vertical" : "horizontal"}> <ResizablePanelGroup
direction={isHorizontalLayout ? "vertical" : "horizontal"}
>
<ResizablePanel <ResizablePanel
className="p-2 flex flex-col" className="p-2 flex flex-col"
maxSize={80} maxSize={80}
@ -1048,72 +1068,77 @@ export default function CodeEditor({
</div> </div>
</> </>
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643 ) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
clerk.loaded ? ( clerk.loaded ? (
<> <>
{provider && userInfo ? ( {provider && userInfo ? (
<Cursors yProvider={provider} userInfo={userInfo} /> <Cursors yProvider={provider} userInfo={userInfo} />
) : null} ) : null}
<Editor <Editor
height="100%" height="100%"
language={editorLanguage} language={editorLanguage}
beforeMount={handleEditorWillMount} beforeMount={handleEditorWillMount}
onMount={handleEditorMount} onMount={handleEditorMount}
onChange={(value) => { onChange={(value) => {
// If the new content is different from the cached content, update it // If the new content is different from the cached content, update it
if (value !== fileContents[activeFileId]) { if (value !== fileContents[activeFileId]) {
setActiveFileContent(value ?? ""); // Update the active file content setActiveFileContent(value ?? "") // Update the active file content
// Mark the file as unsaved by setting 'saved' to false // Mark the file as unsaved by setting 'saved' to false
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => prev.map((tab) =>
tab.id === activeFileId tab.id === activeFileId
? { ...tab, saved: false } ? { ...tab, saved: false }
: tab : tab
)
) )
} else { )
// If the content matches the cached content, mark the file as saved } else {
setTabs((prev) => // If the content matches the cached content, mark the file as saved
prev.map((tab) => setTabs((prev) =>
tab.id === activeFileId prev.map((tab) =>
? { ...tab, saved: true } tab.id === activeFileId
: tab ? { ...tab, saved: true }
) : tab
) )
} )
}} }
options={{ }}
tabSize: 2, options={{
minimap: { tabSize: 2,
enabled: false, minimap: {
}, enabled: false,
padding: { },
bottom: 4, padding: {
top: 4, bottom: 4,
}, top: 4,
scrollBeyondLastLine: false, },
fixedOverflowWidgets: true, scrollBeyondLastLine: false,
fontFamily: "var(--font-geist-mono)", fixedOverflowWidgets: true,
}} fontFamily: "var(--font-geist-mono)",
theme="vs-dark" }}
value={activeFileContent} theme="vs-dark"
/> value={activeFileContent}
</> />
) : ( </>
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none"> ) : (
<Loader2 className="animate-spin w-6 h-6 mr-3" /> <div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
Waiting for Clerk to load... <Loader2 className="animate-spin w-6 h-6 mr-3" />
</div> Waiting for Clerk to load...
)} </div>
)}
</div> </div>
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> <ResizableHandle />
<ResizablePanel defaultSize={30}> <ResizablePanel defaultSize={30}>
<ResizablePanelGroup direction={ <ResizablePanelGroup
isAIChatOpen && isHorizontalLayout ? "horizontal" : direction={
isAIChatOpen ? "vertical" : isAIChatOpen && isHorizontalLayout
isHorizontalLayout ? "horizontal" : ? "horizontal"
"vertical" : isAIChatOpen
}> ? "vertical"
: isHorizontalLayout
? "horizontal"
: "vertical"
}
>
<ResizablePanel <ResizablePanel
ref={previewPanelRef} ref={previewPanelRef}
defaultSize={isPreviewCollapsed ? 4 : 20} defaultSize={isPreviewCollapsed ? 4 : 20}
@ -1131,8 +1156,12 @@ export default function CodeEditor({
variant="ghost" variant="ghost"
className="mr-2 border" className="mr-2 border"
disabled={isAIChatOpen} disabled={isAIChatOpen}
> >
{isHorizontalLayout ? <ArrowRightToLine className="w-4 h-4" /> : <ArrowDownToLine className="w-4 h-4" />} {isHorizontalLayout ? (
<ArrowRightToLine className="w-4 h-4" />
) : (
<ArrowDownToLine className="w-4 h-4" />
)}
</Button> </Button>
<PreviewWindow <PreviewWindow
open={togglePreviewPanel} open={togglePreviewPanel}
@ -1175,9 +1204,12 @@ export default function CodeEditor({
<> <>
<ResizableHandle /> <ResizableHandle />
<ResizablePanel defaultSize={30} minSize={15}> <ResizablePanel defaultSize={30} minSize={15}>
<AIChat <AIChat
activeFileContent={activeFileContent} activeFileContent={activeFileContent}
activeFileName={tabs.find(tab => tab.id === activeFileId)?.name || 'No file selected'} activeFileName={
tabs.find((tab) => tab.id === activeFileId)?.name ||
"No file selected"
}
onClose={toggleAIChat} onClose={toggleAIChat}
/> />
</ResizablePanel> </ResizablePanel>

View File

@ -1,6 +1,6 @@
"use client"; "use client"
import { useOthers } from "@/liveblocks.config"; import { useOthers } from "@/liveblocks.config"
const classNames = { const classNames = {
red: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-red-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-red-950 to-red-600 flex items-center justify-center text-xs font-medium", red: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-red-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-red-950 to-red-600 flex items-center justify-center text-xs font-medium",
@ -14,10 +14,10 @@ const classNames = {
purple: purple:
"w-8 h-8 leading-none font-mono rounded-full ring-1 ring-purple-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-purple-950 to-purple-600 flex items-center justify-center text-xs font-medium", "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-purple-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-purple-950 to-purple-600 flex items-center justify-center text-xs font-medium",
pink: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-pink-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-pink-950 to-pink-600 flex items-center justify-center text-xs font-medium", pink: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-pink-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-pink-950 to-pink-600 flex items-center justify-center text-xs font-medium",
}; }
export function Avatars() { export function Avatars() {
const users = useOthers(); const users = useOthers()
return ( return (
<> <>
@ -30,12 +30,12 @@ export function Avatars() {
.slice(0, 2) .slice(0, 2)
.map((letter) => letter[0].toUpperCase())} .map((letter) => letter[0].toUpperCase())}
</div> </div>
); )
})} })}
</div> </div>
{users.length > 0 ? ( {users.length > 0 ? (
<div className="h-full w-[1px] bg-border mx-2" /> <div className="h-full w-[1px] bg-border mx-2" />
) : null} ) : null}
</> </>
); )
} }

View File

@ -1,11 +1,10 @@
import { useEffect, useMemo, useState } from "react" import { colors } from "@/lib/colors"
import { import {
AwarenessList, AwarenessList,
TypedLiveblocksProvider, TypedLiveblocksProvider,
UserAwareness, UserAwareness,
useSelf,
} from "@/liveblocks.config" } from "@/liveblocks.config"
import { colors } from "@/lib/colors" import { useEffect, useMemo, useState } from "react"
export function Cursors({ export function Cursors({
yProvider, yProvider,

View File

@ -1,43 +1,35 @@
"use client"; "use client"
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, } from "@/components/ui/dialog"
} from "@/components/ui/dialog";
import { import { Loader2 } from "lucide-react"
ChevronRight, import { useRouter } from "next/navigation"
FileStack, import { useEffect } from "react"
Globe,
Loader2,
TextCursor,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function DisableAccessModal({ export default function DisableAccessModal({
open, open,
setOpen, setOpen,
message, message,
}: { }: {
open: boolean; open: boolean
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void
message: string; message: string
}) { }) {
const router = useRouter(); const router = useRouter()
useEffect(() => { useEffect(() => {
if (open) { if (open) {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
router.push("/dashboard"); router.push("/dashboard")
}, 5000); }, 5000)
return () => clearTimeout(timeout); return () => clearTimeout(timeout)
} }
}, []); }, [])
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
@ -54,5 +46,5 @@ export default function DisableAccessModal({
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); )
} }

View File

@ -1,14 +1,13 @@
"use client"; "use client"
import { RoomProvider } from "@/liveblocks.config"; import { RoomProvider } from "@/liveblocks.config"
import { ClientSideSuspense } from "@liveblocks/react";
export function Room({ export function Room({
id, id,
children, children,
}: { }: {
id: string; id: string
children: React.ReactNode; children: React.ReactNode
}) { }) {
return ( return (
<RoomProvider <RoomProvider
@ -21,5 +20,5 @@ export function Room({
{children} {children}
{/* </ClientSideSuspense> */} {/* </ClientSideSuspense> */}
</RoomProvider> </RoomProvider>
); )
} }

View File

@ -1,9 +1,6 @@
"use client" "use client"
import Image from "next/image"
import Logo from "@/assets/logo.svg" import Logo from "@/assets/logo.svg"
import { Skeleton } from "@/components/ui/skeleton"
import { Loader2, X } from "lucide-react"
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -11,6 +8,9 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Skeleton } from "@/components/ui/skeleton"
import { Loader2, X } from "lucide-react"
import Image from "next/image"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
export default function Loading({ export default function Loading({

View File

@ -1,34 +1,38 @@
"use client"; "use client"
import { useState } from "react"; import { Button } from "@/components/ui/button"
import { Button } from "@/components/ui/button"; import {
import { useTerminal } from "@/context/TerminalContext"; Popover,
import { Play, Pause, Globe, Globe2 } from "lucide-react"; PopoverContent,
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; PopoverTrigger,
import { Sandbox, User } from "@/lib/types"; } from "@/components/ui/popover"
import { useTerminal } from "@/context/TerminalContext"
import { Sandbox, User } from "@/lib/types"
import { Globe } from "lucide-react"
import { useState } from "react"
export default function DeployButtonModal({ export default function DeployButtonModal({
userData, userData,
data, data,
}: { }: {
userData: User; userData: User
data: Sandbox; data: Sandbox
}) { }) {
const { deploy } = useTerminal(); const { deploy } = useTerminal()
const [isDeploying, setIsDeploying] = useState(false); const [isDeploying, setIsDeploying] = useState(false)
const handleDeploy = () => { const handleDeploy = () => {
if (isDeploying) { if (isDeploying) {
console.log("Stopping deployment..."); console.log("Stopping deployment...")
setIsDeploying(false); setIsDeploying(false)
} else { } else {
console.log("Starting deployment..."); console.log("Starting deployment...")
setIsDeploying(true); setIsDeploying(true)
deploy(() => { deploy(() => {
setIsDeploying(false); setIsDeploying(false)
}); })
} }
}; }
return ( return (
<> <>
@ -39,7 +43,10 @@ export default function DeployButtonModal({
Deploy Deploy
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-4 w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl rounded-lg shadow-lg" style={{ backgroundColor: 'rgb(10,10,10)', color: 'white' }}> <PopoverContent
className="p-4 w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl rounded-lg shadow-lg"
style={{ backgroundColor: "rgb(10,10,10)", color: "white" }}
>
<h3 className="font-semibold text-gray-300 mb-2">Domains</h3> <h3 className="font-semibold text-gray-300 mb-2">Domains</h3>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<DeploymentOption <DeploymentOption
@ -49,16 +56,30 @@ export default function DeployButtonModal({
user={userData.name} user={userData.name}
/> />
</div> </div>
<Button variant="outline" className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]" onClick={handleDeploy}> <Button
{isDeploying ? "Deploying..." : "Update"} variant="outline"
className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]"
onClick={handleDeploy}
>
{isDeploying ? "Deploying..." : "Update"}
</Button> </Button>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</> </>
); )
} }
function DeploymentOption({ icon, domain, timestamp, user }: { icon: React.ReactNode; domain: string; timestamp: string; user: string }) { function DeploymentOption({
icon,
domain,
timestamp,
user,
}: {
icon: React.ReactNode
domain: string
timestamp: string
user: string
}) {
return ( return (
<div className="flex flex-col gap-2 w-full text-left p-2 rounded-md border border-gray-700 bg-gray-900"> <div className="flex flex-col gap-2 w-full text-left p-2 rounded-md border border-gray-700 bg-gray-900">
<div className="flex items-start gap-2 relative"> <div className="flex items-start gap-2 relative">
@ -72,7 +93,9 @@ function DeploymentOption({ icon, domain, timestamp, user }: { icon: React.React
{domain} {domain}
</a> </a>
</div> </div>
<p className="text-sm text-gray-400 mt-0 ml-7">{timestamp} {user}</p> <p className="text-sm text-gray-400 mt-0 ml-7">
{timestamp} {user}
</p>
</div> </div>
); )
} }

View File

@ -1,60 +1,57 @@
"use client"; "use client"
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, } from "@/components/ui/dialog"
} from "@/components/ui/dialog"; import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"; import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"
import { useForm } from "react-hook-form";
import { Button } from "@/components/ui/button"
import { import {
Form, Form,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form"
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input"
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select"
import { Loader2 } from "lucide-react"; import { deleteSandbox, updateSandbox } from "@/lib/actions"
import { useState } from "react"; import { Sandbox } from "@/lib/types"
import { Sandbox } from "@/lib/types"; import { Loader2 } from "lucide-react"
import { Button } from "@/components/ui/button"; import { useRouter } from "next/navigation"
import { deleteSandbox, updateSandbox } from "@/lib/actions"; import { useState } from "react"
import { useRouter } from "next/navigation"; import { toast } from "sonner"
import { toast } from "sonner";
const formSchema = z.object({ const formSchema = z.object({
name: z.string().min(1).max(16), name: z.string().min(1).max(16),
visibility: z.enum(["public", "private"]), visibility: z.enum(["public", "private"]),
}); })
export default function EditSandboxModal({ export default function EditSandboxModal({
open, open,
setOpen, setOpen,
data, data,
}: { }: {
open: boolean; open: boolean
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void
data: Sandbox; data: Sandbox
}) { }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false)
const [loadingDelete, setLoadingDelete] = useState(false); const [loadingDelete, setLoadingDelete] = useState(false)
const router = useRouter(); const router = useRouter()
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
@ -62,22 +59,22 @@ export default function EditSandboxModal({
name: data.name, name: data.name,
visibility: data.visibility, visibility: data.visibility,
}, },
}); })
async function onSubmit(values: z.infer<typeof formSchema>) { async function onSubmit(values: z.infer<typeof formSchema>) {
setLoading(true); setLoading(true)
await updateSandbox({ id: data.id, ...values }); await updateSandbox({ id: data.id, ...values })
toast.success("Sandbox updated successfully"); toast.success("Sandbox updated successfully")
setLoading(false); setLoading(false)
} }
async function onDelete() { async function onDelete() {
setLoadingDelete(true); setLoadingDelete(true)
await deleteSandbox(data.id); await deleteSandbox(data.id)
router.push("/dashboard"); router.push("/dashboard")
} }
return ( return (
@ -153,5 +150,5 @@ export default function EditSandboxModal({
</Button> </Button>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); )
} }

View File

@ -1,33 +1,33 @@
"use client"; "use client"
import Image from "next/image"; import Logo from "@/assets/logo.svg"
import Logo from "@/assets/logo.svg"; import { Button } from "@/components/ui/button"
import { Pencil, Users } from "lucide-react"; import UserButton from "@/components/ui/userButton"
import Link from "next/link"; import { Sandbox, User } from "@/lib/types"
import { Sandbox, User } from "@/lib/types"; import { Pencil, Users } from "lucide-react"
import UserButton from "@/components/ui/userButton"; import Image from "next/image"
import { Button } from "@/components/ui/button"; import Link from "next/link"
import { useState } from "react"; import { useState } from "react"
import EditSandboxModal from "./edit"; import { Avatars } from "../live/avatars"
import ShareSandboxModal from "./share"; import DeployButtonModal from "./deploy"
import { Avatars } from "../live/avatars"; import EditSandboxModal from "./edit"
import RunButtonModal from "./run"; import RunButtonModal from "./run"
import DeployButtonModal from "./deploy"; import ShareSandboxModal from "./share"
export default function Navbar({ export default function Navbar({
userData, userData,
sandboxData, sandboxData,
shared, shared,
}: { }: {
userData: User; userData: User
sandboxData: Sandbox; sandboxData: Sandbox
shared: { id: string; name: string }[]; shared: { id: string; name: string }[]
}) { }) {
const [isEditOpen, setIsEditOpen] = useState(false); const [isEditOpen, setIsEditOpen] = useState(false)
const [isShareOpen, setIsShareOpen] = useState(false); const [isShareOpen, setIsShareOpen] = useState(false)
const [isRunning, setIsRunning] = useState(false); const [isRunning, setIsRunning] = useState(false)
const isOwner = sandboxData.userId === userData.id;; const isOwner = sandboxData.userId === userData.id
return ( return (
<> <>
@ -72,19 +72,16 @@ export default function Navbar({
{isOwner ? ( {isOwner ? (
<> <>
<DeployButtonModal <DeployButtonModal data={sandboxData} userData={userData} />
data={sandboxData} <Button variant="outline" onClick={() => setIsShareOpen(true)}>
userData={userData} <Users className="w-4 h-4 mr-2" />
/> Share
<Button variant="outline" onClick={() => setIsShareOpen(true)}> </Button>
<Users className="w-4 h-4 mr-2" />
Share
</Button>
</> </>
) : null} ) : null}
<UserButton userData={userData} /> <UserButton userData={userData} />
</div> </div>
</div> </div>
</> </>
); )
} }

View File

@ -1,73 +1,78 @@
"use client"; "use client"
import React, { useEffect, useRef } from 'react'; import { Button } from "@/components/ui/button"
import { Play, StopCircle } from "lucide-react"; import { usePreview } from "@/context/PreviewContext"
import { Button } from "@/components/ui/button"; import { useTerminal } from "@/context/TerminalContext"
import { useTerminal } from "@/context/TerminalContext"; import { Sandbox } from "@/lib/types"
import { usePreview } from "@/context/PreviewContext"; import { Play, StopCircle } from "lucide-react"
import { toast } from "sonner"; import { useEffect, useRef } from "react"
import { Sandbox } from "@/lib/types"; import { toast } from "sonner"
export default function RunButtonModal({ export default function RunButtonModal({
isRunning, isRunning,
setIsRunning, setIsRunning,
sandboxData, sandboxData,
}: { }: {
isRunning: boolean; isRunning: boolean
setIsRunning: (running: boolean) => void; setIsRunning: (running: boolean) => void
sandboxData: Sandbox; sandboxData: Sandbox
}) { }) {
const { createNewTerminal, closeTerminal, terminals } = useTerminal(); const { createNewTerminal, closeTerminal, terminals } = useTerminal()
const { setIsPreviewCollapsed, previewPanelRef } = usePreview(); const { setIsPreviewCollapsed, previewPanelRef } = usePreview()
// Ref to keep track of the last created terminal's ID // Ref to keep track of the last created terminal's ID
const lastCreatedTerminalRef = useRef<string | null>(null); const lastCreatedTerminalRef = useRef<string | null>(null)
// Effect to update the lastCreatedTerminalRef when a new terminal is added // Effect to update the lastCreatedTerminalRef when a new terminal is added
useEffect(() => { useEffect(() => {
if (terminals.length > 0 && !isRunning) { if (terminals.length > 0 && !isRunning) {
const latestTerminal = terminals[terminals.length - 1]; const latestTerminal = terminals[terminals.length - 1]
if (latestTerminal && latestTerminal.id !== lastCreatedTerminalRef.current) { if (
lastCreatedTerminalRef.current = latestTerminal.id; latestTerminal &&
latestTerminal.id !== lastCreatedTerminalRef.current
) {
lastCreatedTerminalRef.current = latestTerminal.id
} }
} }
}, [terminals, isRunning]); }, [terminals, isRunning])
const handleRun = async () => { const handleRun = async () => {
if (isRunning && lastCreatedTerminalRef.current) if (isRunning && lastCreatedTerminalRef.current) {
{ await closeTerminal(lastCreatedTerminalRef.current)
await closeTerminal(lastCreatedTerminalRef.current); lastCreatedTerminalRef.current = null
lastCreatedTerminalRef.current = null; setIsPreviewCollapsed(true)
setIsPreviewCollapsed(true); previewPanelRef.current?.collapse()
previewPanelRef.current?.collapse(); } else if (!isRunning && terminals.length < 4) {
} const command =
else if (!isRunning && terminals.length < 4) sandboxData.type === "streamlit"
{ ? "pip install -r requirements.txt && streamlit run main.py --server.runOnSave true"
const command = sandboxData.type === "streamlit" : "yarn install && yarn dev"
? "pip install -r requirements.txt && streamlit run main.py --server.runOnSave true"
: "yarn install && yarn dev";
try { try {
// Create a new terminal with the appropriate command // Create a new terminal with the appropriate command
await createNewTerminal(command); await createNewTerminal(command)
setIsPreviewCollapsed(false); setIsPreviewCollapsed(false)
previewPanelRef.current?.expand(); previewPanelRef.current?.expand()
} catch (error) { } catch (error) {
toast.error("Failed to create new terminal."); toast.error("Failed to create new terminal.")
console.error("Error creating new terminal:", error); console.error("Error creating new terminal:", error)
return; return
} }
} else if (!isRunning) { } else if (!isRunning) {
toast.error("You've reached the maximum number of terminals."); toast.error("You've reached the maximum number of terminals.")
return; return
} }
setIsRunning(!isRunning); setIsRunning(!isRunning)
}; }
return ( return (
<Button variant="outline" onClick={handleRun}> <Button variant="outline" onClick={handleRun}>
{isRunning ? <StopCircle className="w-4 h-4 mr-2" /> : <Play className="w-4 h-4 mr-2" />} {isRunning ? (
{isRunning ? 'Stop' : 'Run'} <StopCircle className="w-4 h-4 mr-2" />
) : (
<Play className="w-4 h-4 mr-2" />
)}
{isRunning ? "Stop" : "Run"}
</Button> </Button>
); )
} }

View File

@ -6,10 +6,11 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { import {
Form, Form,
FormControl, FormControl,
@ -18,14 +19,13 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Link, Loader2, UserPlus, X } from "lucide-react"
import { useState } from "react"
import { Sandbox } from "@/lib/types"
import { Button } from "@/components/ui/button"
import { shareSandbox } from "@/lib/actions" import { shareSandbox } from "@/lib/actions"
import { Sandbox } from "@/lib/types"
import { DialogDescription } from "@radix-ui/react-dialog"
import { Link, Loader2, UserPlus } from "lucide-react"
import { useState } from "react"
import { toast } from "sonner" import { toast } from "sonner"
import SharedUser from "./sharedUser" import SharedUser from "./sharedUser"
import { DialogDescription } from "@radix-ui/react-dialog"
const formSchema = z.object({ const formSchema = z.object({
email: z.string().email(), email: z.string().email(),

View File

@ -1,66 +1,69 @@
"use client" "use client"
import { Link, RotateCw, UnfoldVertical } from "lucide-react"
import { import {
Link, forwardRef,
RotateCw, useEffect,
TerminalSquare, useImperativeHandle,
UnfoldVertical, useRef,
} from "lucide-react" useState,
import { useEffect, useRef, useState, useImperativeHandle, forwardRef } from "react" } from "react"
import { toast } from "sonner" import { toast } from "sonner"
export default forwardRef(function PreviewWindow({ export default forwardRef(function PreviewWindow(
collapsed, {
open, collapsed,
src open,
}: { src,
collapsed: boolean }: {
open: () => void collapsed: boolean
src: string open: () => void
}, src: string
ref: React.Ref<{ },
refreshIframe: () => void ref: React.Ref<{
}>) { refreshIframe: () => void
}>
) {
const frameRef = useRef<HTMLIFrameElement>(null) const frameRef = useRef<HTMLIFrameElement>(null)
const [iframeKey, setIframeKey] = useState(0) const [iframeKey, setIframeKey] = useState(0)
const refreshIframe = () => { const refreshIframe = () => {
setIframeKey(prev => prev + 1) setIframeKey((prev) => prev + 1)
} }
// Refresh the preview when the URL changes. // Refresh the preview when the URL changes.
useEffect(refreshIframe, [src]) useEffect(refreshIframe, [src])
// Expose refreshIframe method to the parent. // Expose refreshIframe method to the parent.
useImperativeHandle(ref, () => ({ refreshIframe })) useImperativeHandle(ref, () => ({ refreshIframe }))
return ( return (
<> <>
<div className="h-8 rounded-md px-3 bg-secondary flex items-center w-full justify-between"> <div className="h-8 rounded-md px-3 bg-secondary flex items-center w-full justify-between">
<div className="text-xs">Preview</div> <div className="text-xs">Preview</div>
<div className="flex space-x-1 translate-x-1"> <div className="flex space-x-1 translate-x-1">
{collapsed ? ( {collapsed ? (
<PreviewButton onClick={open}>
<UnfoldVertical className="w-4 h-4" />
</PreviewButton>
) : (
<>
<PreviewButton onClick={open}> <PreviewButton onClick={open}>
<UnfoldVertical className="w-4 h-4" /> <UnfoldVertical className="w-4 h-4" />
</PreviewButton> </PreviewButton>
) : (
<>
<PreviewButton onClick={open}>
<UnfoldVertical className="w-4 h-4" />
</PreviewButton>
<PreviewButton <PreviewButton
onClick={() => { onClick={() => {
navigator.clipboard.writeText(src) navigator.clipboard.writeText(src)
toast.info("Copied preview link to clipboard") toast.info("Copied preview link to clipboard")
}} }}
> >
<Link className="w-4 h-4" /> <Link className="w-4 h-4" />
</PreviewButton> </PreviewButton>
<PreviewButton onClick={refreshIframe}> <PreviewButton onClick={refreshIframe}>
<RotateCw className="w-3 h-3" /> <RotateCw className="w-3 h-3" />
</PreviewButton> </PreviewButton>
</> </>
)} )}
</div>
</div> </div>
</div>
</> </>
) )
}) })
@ -76,8 +79,9 @@ function PreviewButton({
}) { }) {
return ( return (
<div <div
className={`${disabled ? "pointer-events-none opacity-50" : "" className={`${
} p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm`} disabled ? "pointer-events-none opacity-50" : ""
} p-0.5 h-5 w-5 ml-0.5 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 cursor-pointer rounded-sm`}
onClick={onClick} onClick={onClick}
> >
{children} {children}

View File

@ -1,18 +1,18 @@
"use client"; "use client"
import Image from "next/image";
import { getIconForFile } from "vscode-icons-js";
import { TFile, TTab } from "@/lib/types";
import { useEffect, useRef, useState } from "react";
import { import {
ContextMenu, ContextMenu,
ContextMenuContent, ContextMenuContent,
ContextMenuItem, ContextMenuItem,
ContextMenuTrigger, ContextMenuTrigger,
} from "@/components/ui/context-menu"; } from "@/components/ui/context-menu"
import { Loader2, Pencil, Trash2 } from "lucide-react"; import { TFile, TTab } from "@/lib/types"
import { Loader2, Pencil, Trash2 } from "lucide-react"
import Image from "next/image"
import { useEffect, useRef, useState } from "react"
import { getIconForFile } from "vscode-icons-js"
import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
export default function SidebarFile({ export default function SidebarFile({
data, data,
@ -22,36 +22,36 @@ export default function SidebarFile({
movingId, movingId,
deletingFolderId, deletingFolderId,
}: { }: {
data: TFile; data: TFile
selectFile: (file: TTab) => void; selectFile: (file: TTab) => void
handleRename: ( handleRename: (
id: string, id: string,
newName: string, newName: string,
oldName: string, oldName: string,
type: "file" | "folder" type: "file" | "folder"
) => boolean; ) => boolean
handleDeleteFile: (file: TFile) => void; handleDeleteFile: (file: TFile) => void
movingId: string; movingId: string
deletingFolderId: string; deletingFolderId: string
}) { }) {
const isMoving = movingId === data.id; const isMoving = movingId === data.id
const isDeleting = const isDeleting =
deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId); deletingFolderId.length > 0 && data.id.startsWith(deletingFolderId)
const ref = useRef(null); // for draggable const ref = useRef(null) // for draggable
const [dragging, setDragging] = useState(false); const [dragging, setDragging] = useState(false)
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null)
const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`); const [imgSrc, setImgSrc] = useState(`/icons/${getIconForFile(data.name)}`)
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false)
const [pendingDelete, setPendingDelete] = useState(isDeleting); const [pendingDelete, setPendingDelete] = useState(isDeleting)
useEffect(() => { useEffect(() => {
setPendingDelete(isDeleting); setPendingDelete(isDeleting)
}, [isDeleting]); }, [isDeleting])
useEffect(() => { useEffect(() => {
const el = ref.current; const el = ref.current
if (el) if (el)
return draggable({ return draggable({
@ -59,14 +59,14 @@ export default function SidebarFile({
onDragStart: () => setDragging(true), onDragStart: () => setDragging(true),
onDrop: () => setDragging(false), onDrop: () => setDragging(false),
getInitialData: () => ({ id: data.id }), getInitialData: () => ({ id: data.id }),
}); })
}, []); }, [])
useEffect(() => { useEffect(() => {
if (editing) { if (editing) {
setTimeout(() => inputRef.current?.focus(), 0); setTimeout(() => inputRef.current?.focus(), 0)
} }
}, [editing, inputRef.current]); }, [editing, inputRef.current])
const renameFile = () => { const renameFile = () => {
const renamed = handleRename( const renamed = handleRename(
@ -74,12 +74,12 @@ export default function SidebarFile({
inputRef.current?.value ?? data.name, inputRef.current?.value ?? data.name,
data.name, data.name,
"file" "file"
); )
if (!renamed && inputRef.current) { if (!renamed && inputRef.current) {
inputRef.current.value = data.name; inputRef.current.value = data.name
} }
setEditing(false); setEditing(false)
}; }
return ( return (
<ContextMenu> <ContextMenu>
@ -88,7 +88,7 @@ export default function SidebarFile({
disabled={pendingDelete || dragging || isMoving} disabled={pendingDelete || dragging || isMoving}
onClick={() => { onClick={() => {
if (!editing && !pendingDelete && !isMoving) if (!editing && !pendingDelete && !isMoving)
selectFile({ ...data, saved: true }); selectFile({ ...data, saved: true })
}} }}
onDoubleClick={() => { onDoubleClick={() => {
setEditing(true) setEditing(true)
@ -119,8 +119,8 @@ export default function SidebarFile({
) : ( ) : (
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault()
renameFile(); renameFile()
}} }}
> >
<input <input
@ -138,8 +138,8 @@ export default function SidebarFile({
<ContextMenuContent> <ContextMenuContent>
<ContextMenuItem <ContextMenuItem
onClick={() => { onClick={() => {
console.log("rename"); console.log("rename")
setEditing(true); setEditing(true)
}} }}
> >
<Pencil className="w-4 h-4 mr-2" /> <Pencil className="w-4 h-4 mr-2" />
@ -148,9 +148,9 @@ export default function SidebarFile({
<ContextMenuItem <ContextMenuItem
disabled={pendingDelete} disabled={pendingDelete}
onClick={() => { onClick={() => {
console.log("delete"); console.log("delete")
setPendingDelete(true); setPendingDelete(true)
handleDeleteFile(data); handleDeleteFile(data)
}} }}
> >
<Trash2 className="w-4 h-4 mr-2" /> <Trash2 className="w-4 h-4 mr-2" />
@ -158,5 +158,5 @@ export default function SidebarFile({
</ContextMenuItem> </ContextMenuItem>
</ContextMenuContent> </ContextMenuContent>
</ContextMenu> </ContextMenu>
); )
} }

View File

@ -1,20 +1,20 @@
"use client" "use client"
import Image from "next/image"
import { useEffect, useRef, useState } from "react"
import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js"
import { TFile, TFolder, TTab } from "@/lib/types"
import SidebarFile from "./file"
import { import {
ContextMenu, ContextMenu,
ContextMenuContent, ContextMenuContent,
ContextMenuItem, ContextMenuItem,
ContextMenuTrigger, ContextMenuTrigger,
} from "@/components/ui/context-menu" } from "@/components/ui/context-menu"
import { ChevronRight, Loader2, Pencil, Trash2 } from "lucide-react" import { TFile, TFolder, TTab } from "@/lib/types"
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { motion, AnimatePresence } from "framer-motion" import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
import { AnimatePresence, motion } from "framer-motion"
import { ChevronRight, Pencil, Trash2 } from "lucide-react"
import Image from "next/image"
import { useEffect, useRef, useState } from "react"
import { getIconForFolder, getIconForOpenFolder } from "vscode-icons-js"
import SidebarFile from "./file"
// Note: Renaming has not been implemented in the backend yet, so UI relating to renaming is commented out // Note: Renaming has not been implemented in the backend yet, so UI relating to renaming is commented out

View File

@ -1,25 +1,25 @@
"use client"; "use client"
import { Button } from "@/components/ui/button"
import { Sandbox, TFile, TFolder, TTab } from "@/lib/types"
import { import {
FilePlus, FilePlus,
FolderPlus, FolderPlus,
Loader2, Loader2,
Sparkles,
MessageSquareMore, MessageSquareMore,
} from "lucide-react"; Sparkles,
import SidebarFile from "./file"; } from "lucide-react"
import SidebarFolder from "./folder"; import { useEffect, useRef, useState } from "react"
import { Sandbox, TFile, TFolder, TTab } from "@/lib/types"; import { Socket } from "socket.io-client"
import { useEffect, useRef, useState } from "react"; import SidebarFile from "./file"
import New from "./new"; import SidebarFolder from "./folder"
import { Socket } from "socket.io-client"; import New from "./new"
import { Button } from "@/components/ui/button";
import { import {
dropTargetForElements, dropTargetForElements,
monitorForElements, monitorForElements,
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"
export default function Sidebar({ export default function Sidebar({
sandboxData, sandboxData,
files, files,
@ -32,75 +32,73 @@ export default function Sidebar({
addNew, addNew,
deletingFolderId, deletingFolderId,
}: { }: {
sandboxData: Sandbox; sandboxData: Sandbox
files: (TFile | TFolder)[]; files: (TFile | TFolder)[]
selectFile: (tab: TTab) => void; selectFile: (tab: TTab) => void
handleRename: ( handleRename: (
id: string, id: string,
newName: string, newName: string,
oldName: string, oldName: string,
type: "file" | "folder" type: "file" | "folder"
) => boolean; ) => boolean
handleDeleteFile: (file: TFile) => void; handleDeleteFile: (file: TFile) => void
handleDeleteFolder: (folder: TFolder) => void; handleDeleteFolder: (folder: TFolder) => void
socket: Socket; socket: Socket
setFiles: (files: (TFile | TFolder)[]) => void; setFiles: (files: (TFile | TFolder)[]) => void
addNew: (name: string, type: "file" | "folder") => void; addNew: (name: string, type: "file" | "folder") => void
deletingFolderId: string; deletingFolderId: string
}) { }) {
const ref = useRef(null); // drop target const ref = useRef(null) // drop target
const [creatingNew, setCreatingNew] = useState<"file" | "folder" | null>( const [creatingNew, setCreatingNew] = useState<"file" | "folder" | null>(null)
null const [movingId, setMovingId] = useState("")
);
const [movingId, setMovingId] = useState("");
useEffect(() => { useEffect(() => {
const el = ref.current; const el = ref.current
if (el) { if (el) {
return dropTargetForElements({ return dropTargetForElements({
element: el, element: el,
getData: () => ({ id: `projects/${sandboxData.id}` }), getData: () => ({ id: `projects/${sandboxData.id}` }),
canDrop: ({ source }) => { canDrop: ({ source }) => {
const file = files.find((child) => child.id === source.data.id); const file = files.find((child) => child.id === source.data.id)
return !file; return !file
}, },
}); })
} }
}, [files]); }, [files])
useEffect(() => { useEffect(() => {
return monitorForElements({ return monitorForElements({
onDrop({ source, location }) { onDrop({ source, location }) {
const destination = location.current.dropTargets[0]; const destination = location.current.dropTargets[0]
if (!destination) { if (!destination) {
return; return
} }
const fileId = source.data.id as string; const fileId = source.data.id as string
const folderId = destination.data.id as string; const folderId = destination.data.id as string
const fileFolder = fileId.split("/").slice(0, -1).join("/"); const fileFolder = fileId.split("/").slice(0, -1).join("/")
if (fileFolder === folderId) { if (fileFolder === folderId) {
return; return
} }
console.log("move file", fileId, "to folder", folderId); console.log("move file", fileId, "to folder", folderId)
setMovingId(fileId); setMovingId(fileId)
socket.emit( socket.emit(
"moveFile", "moveFile",
fileId, fileId,
folderId, folderId,
(response: (TFolder | TFile)[]) => { (response: (TFolder | TFile)[]) => {
setFiles(response); setFiles(response)
setMovingId(""); setMovingId("")
} }
); )
}, },
}); })
}, []); }, [])
return ( return (
<div className="h-full w-56 select-none flex flex-col text-sm"> <div className="h-full w-56 select-none flex flex-col text-sm">
@ -170,7 +168,7 @@ export default function Sidebar({
socket={socket} socket={socket}
type={creatingNew} type={creatingNew}
stopEditing={() => { stopEditing={() => {
setCreatingNew(null); setCreatingNew(null)
}} }}
addNew={addNew} addNew={addNew}
/> />
@ -180,7 +178,13 @@ export default function Sidebar({
</div> </div>
</div> </div>
<div className="fixed bottom-0 w-48 flex flex-col p-2 bg-background"> <div className="fixed bottom-0 w-48 flex flex-col p-2 bg-background">
<Button variant="ghost" className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2" disabled aria-disabled="true" style={{ opacity: 1}}> <Button
variant="ghost"
className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2"
disabled
aria-disabled="true"
style={{ opacity: 1 }}
>
<Sparkles className="h-4 w-4 mr-2 text-indigo-500 opacity-70" /> <Sparkles className="h-4 w-4 mr-2 text-indigo-500 opacity-70" />
Copilot Copilot
<div className="ml-auto"> <div className="ml-auto">
@ -189,16 +193,22 @@ export default function Sidebar({
</kbd> </kbd>
</div> </div>
</Button> </Button>
<Button variant="ghost" className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2" disabled aria-disabled="true" style={{ opacity: 1 }}> <Button
variant="ghost"
className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2"
disabled
aria-disabled="true"
style={{ opacity: 1 }}
>
<MessageSquareMore className="h-4 w-4 mr-2 text-indigo-500 opacity-70" /> <MessageSquareMore className="h-4 w-4 mr-2 text-indigo-500 opacity-70" />
AI Chat AI Chat
<div className="ml-auto"> <div className="ml-auto">
<kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground"> <kbd className="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
<span className="text-xs"></span>L <span className="text-xs"></span>L
</kbd> </kbd>
</div> </div>
</Button> </Button>
</div> </div>
</div> </div>
); )
} }

View File

@ -1,9 +1,9 @@
"use client"; "use client"
import { validateName } from "@/lib/utils"; import { validateName } from "@/lib/utils"
import Image from "next/image"; import Image from "next/image"
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react"
import { Socket } from "socket.io-client"; import { Socket } from "socket.io-client"
export default function New({ export default function New({
socket, socket,
@ -11,18 +11,18 @@ export default function New({
stopEditing, stopEditing,
addNew, addNew,
}: { }: {
socket: Socket; socket: Socket
type: "file" | "folder"; type: "file" | "folder"
stopEditing: () => void; stopEditing: () => void
addNew: (name: string, type: "file" | "folder") => void; addNew: (name: string, type: "file" | "folder") => void
}) { }) {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null)
function createNew() { function createNew() {
const name = inputRef.current?.value; const name = inputRef.current?.value
if (name) { if (name) {
const valid = validateName(name, "", type); const valid = validateName(name, "", type)
if (valid.status) { if (valid.status) {
if (type === "file") { if (type === "file") {
socket.emit( socket.emit(
@ -30,23 +30,23 @@ export default function New({
name, name,
({ success }: { success: boolean }) => { ({ success }: { success: boolean }) => {
if (success) { if (success) {
addNew(name, type); addNew(name, type)
} }
} }
); )
} else { } else {
socket.emit("createFolder", name, () => { socket.emit("createFolder", name, () => {
addNew(name, type); addNew(name, type)
}); })
} }
} }
} }
stopEditing(); stopEditing()
} }
useEffect(() => { useEffect(() => {
inputRef.current?.focus(); inputRef.current?.focus()
}, []); }, [])
return ( return (
<div className="w-full flex items-center h-7 px-1 hover:bg-secondary rounded-sm cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"> <div className="w-full flex items-center h-7 px-1 hover:bg-secondary rounded-sm cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring">
@ -63,8 +63,8 @@ export default function New({
/> />
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault()
createNew(); createNew()
}} }}
> >
<input <input
@ -74,5 +74,5 @@ export default function New({
/> />
</form> </form>
</div> </div>
); )
} }

View File

@ -1,18 +1,17 @@
"use client"; "use client"
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button"
import Tab from "@/components/ui/tab"; import Tab from "@/components/ui/tab"
import { Terminal } from "@xterm/xterm";
import { Loader2, Plus, SquareTerminal, TerminalSquare } from "lucide-react";
import { toast } from "sonner";
import EditorTerminal from "./terminal";
import { useTerminal } from "@/context/TerminalContext";
import { useEffect } from "react";
import { useSocket } from "@/context/SocketContext" import { useSocket } from "@/context/SocketContext"
import { useTerminal } from "@/context/TerminalContext"
import { Terminal } from "@xterm/xterm"
import { Loader2, Plus, SquareTerminal, TerminalSquare } from "lucide-react"
import { useEffect } from "react"
import { toast } from "sonner"
import EditorTerminal from "./terminal"
export default function Terminals() { export default function Terminals() {
const { socket } = useSocket()
const { socket } = useSocket();
const { const {
terminals, terminals,
@ -22,24 +21,24 @@ export default function Terminals() {
activeTerminalId, activeTerminalId,
setActiveTerminalId, setActiveTerminalId,
creatingTerminal, creatingTerminal,
} = useTerminal(); } = useTerminal()
const activeTerminal = terminals.find((t) => t.id === activeTerminalId); const activeTerminal = terminals.find((t) => t.id === activeTerminalId)
// Effect to set the active terminal when a new one is created // Effect to set the active terminal when a new one is created
useEffect(() => { useEffect(() => {
if (terminals.length > 0 && !activeTerminalId) { if (terminals.length > 0 && !activeTerminalId) {
setActiveTerminalId(terminals[terminals.length - 1].id); setActiveTerminalId(terminals[terminals.length - 1].id)
} }
}, [terminals, activeTerminalId, setActiveTerminalId]); }, [terminals, activeTerminalId, setActiveTerminalId])
const handleCreateTerminal = () => { const handleCreateTerminal = () => {
if (terminals.length >= 4) { if (terminals.length >= 4) {
toast.error("You reached the maximum # of terminals."); toast.error("You reached the maximum # of terminals.")
return; return
} }
createNewTerminal(); createNewTerminal()
}; }
return ( return (
<> <>
@ -85,7 +84,7 @@ export default function Terminals() {
? { ...term, terminal: t } ? { ...term, terminal: t }
: term : term
) )
); )
}} }}
visible={activeTerminalId === term.id} visible={activeTerminalId === term.id}
/> />
@ -98,5 +97,5 @@ export default function Terminals() {
</div> </div>
)} )}
</> </>
); )
} }

View File

@ -1,12 +1,12 @@
"use client"; "use client"
import { Terminal } from "@xterm/xterm"; import { FitAddon } from "@xterm/addon-fit"
import { FitAddon } from "@xterm/addon-fit"; import { Terminal } from "@xterm/xterm"
import "./xterm.css"; import "./xterm.css"
import { useEffect, useRef, useState } from "react"; import { Loader2 } from "lucide-react"
import { Socket } from "socket.io-client"; import { useEffect, useRef } from "react"
import { Loader2 } from "lucide-react"; import { Socket } from "socket.io-client"
export default function EditorTerminal({ export default function EditorTerminal({
socket, socket,
@ -15,16 +15,16 @@ export default function EditorTerminal({
setTerm, setTerm,
visible, visible,
}: { }: {
socket: Socket; socket: Socket
id: string; id: string
term: Terminal | null; term: Terminal | null
setTerm: (term: Terminal) => void; setTerm: (term: Terminal) => void
visible: boolean; visible: boolean
}) { }) {
const terminalRef = useRef(null); const terminalRef = useRef(null)
useEffect(() => { useEffect(() => {
if (!terminalRef.current) return; if (!terminalRef.current) return
// console.log("new terminal", id, term ? "reusing" : "creating"); // console.log("new terminal", id, term ? "reusing" : "creating");
const terminal = new Terminal({ const terminal = new Terminal({
@ -36,56 +36,56 @@ export default function EditorTerminal({
fontSize: 14, fontSize: 14,
lineHeight: 1.5, lineHeight: 1.5,
letterSpacing: 0, letterSpacing: 0,
}); })
setTerm(terminal); setTerm(terminal)
return () => { return () => {
if (terminal) terminal.dispose(); if (terminal) terminal.dispose()
}; }
}, []); }, [])
useEffect(() => { useEffect(() => {
if (!term) return; if (!term) return
if (!terminalRef.current) return; if (!terminalRef.current) return
const fitAddon = new FitAddon(); const fitAddon = new FitAddon()
term.loadAddon(fitAddon); term.loadAddon(fitAddon)
term.open(terminalRef.current); term.open(terminalRef.current)
fitAddon.fit(); fitAddon.fit()
const disposableOnData = term.onData((data) => { const disposableOnData = term.onData((data) => {
socket.emit("terminalData", id, data); socket.emit("terminalData", id, data)
}); })
const disposableOnResize = term.onResize((dimensions) => { const disposableOnResize = term.onResize((dimensions) => {
// const terminal_size = { // const terminal_size = {
// width: dimensions.cols, // width: dimensions.cols,
// height: dimensions.rows, // height: dimensions.rows,
// }; // };
fitAddon.fit(); fitAddon.fit()
socket.emit("terminalResize", dimensions); socket.emit("terminalResize", dimensions)
}); })
return () => { return () => {
disposableOnData.dispose(); disposableOnData.dispose()
disposableOnResize.dispose(); disposableOnResize.dispose()
}; }
}, [term, terminalRef.current]); }, [term, terminalRef.current])
useEffect(() => { useEffect(() => {
if (!term) return; if (!term) return
const handleTerminalResponse = (response: { id: string; data: string }) => { const handleTerminalResponse = (response: { id: string; data: string }) => {
if (response.id === id) { if (response.id === id) {
term.write(response.data); term.write(response.data)
} }
}; }
socket.on("terminalResponse", handleTerminalResponse); socket.on("terminalResponse", handleTerminalResponse)
return () => { return () => {
socket.off("terminalResponse", handleTerminalResponse); socket.off("terminalResponse", handleTerminalResponse)
}; }
}, [term, id, socket]); }, [term, id, socket])
return ( return (
<> <>
@ -102,5 +102,5 @@ export default function EditorTerminal({
) : null} ) : null}
</div> </div>
</> </>
); )
} }

View File

@ -35,7 +35,7 @@
* Default styles for xterm.js * Default styles for xterm.js
*/ */
.xterm { .xterm {
cursor: text; cursor: text;
position: relative; position: relative;
user-select: none; user-select: none;
@ -80,7 +80,7 @@
.xterm .composition-view { .xterm .composition-view {
/* TODO: Composition position got messed up somewhere */ /* TODO: Composition position got messed up somewhere */
background: transparent; background: transparent;
color: #FFF; color: #fff;
display: none; display: none;
position: absolute; position: absolute;
white-space: nowrap; white-space: nowrap;
@ -154,12 +154,12 @@
} }
.xterm .xterm-accessibility-tree:not(.debug) *::selection { .xterm .xterm-accessibility-tree:not(.debug) *::selection {
color: transparent; color: transparent;
} }
.xterm .xterm-accessibility-tree { .xterm .xterm-accessibility-tree {
user-select: text; user-select: text;
white-space: pre; white-space: pre;
} }
.xterm .live-region { .xterm .live-region {
@ -176,33 +176,55 @@ white-space: pre;
opacity: 1 !important; opacity: 1 !important;
} }
.xterm-underline-1 { text-decoration: underline; } .xterm-underline-1 {
.xterm-underline-2 { text-decoration: double underline; } text-decoration: underline;
.xterm-underline-3 { text-decoration: wavy underline; } }
.xterm-underline-4 { text-decoration: dotted underline; } .xterm-underline-2 {
.xterm-underline-5 { text-decoration: dashed underline; } text-decoration: double underline;
}
.xterm-underline-3 {
text-decoration: wavy underline;
}
.xterm-underline-4 {
text-decoration: dotted underline;
}
.xterm-underline-5 {
text-decoration: dashed underline;
}
.xterm-overline { .xterm-overline {
text-decoration: overline; text-decoration: overline;
} }
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; } .xterm-overline.xterm-underline-1 {
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; } text-decoration: overline underline;
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; } }
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; } .xterm-overline.xterm-underline-2 {
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; } text-decoration: overline double underline;
}
.xterm-overline.xterm-underline-3 {
text-decoration: overline wavy underline;
}
.xterm-overline.xterm-underline-4 {
text-decoration: overline dotted underline;
}
.xterm-overline.xterm-underline-5 {
text-decoration: overline dashed underline;
}
.xterm-strikethrough { .xterm-strikethrough {
text-decoration: line-through; text-decoration: line-through;
} }
.xterm-screen .xterm-decoration-container .xterm-decoration { .xterm-screen .xterm-decoration-container .xterm-decoration {
z-index: 6; z-index: 6;
position: absolute; position: absolute;
} }
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer { .xterm-screen
z-index: 7; .xterm-decoration-container
.xterm-decoration.xterm-decoration-top-layer {
z-index: 7;
} }
.xterm-decoration-overview-ruler { .xterm-decoration-overview-ruler {
@ -216,4 +238,4 @@ z-index: 7;
.xterm-decoration-top { .xterm-decoration-top {
z-index: 2; z-index: 2;
position: relative; position: relative;
} }

View File

@ -1,8 +1,8 @@
import Image from "next/image"
import Logo from "@/assets/logo.svg" import Logo from "@/assets/logo.svg"
import XLogo from "@/assets/x.svg" import XLogo from "@/assets/x.svg"
import Button from "@/components/ui/customButton" import Button from "@/components/ui/customButton"
import { ChevronRight } from "lucide-react" import { ChevronRight } from "lucide-react"
import Image from "next/image"
import Link from "next/link" import Link from "next/link"
export default function Landing() { export default function Landing() {

View File

@ -1,6 +1,5 @@
"use client" "use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes" import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types" import { type ThemeProviderProps } from "next-themes/dist/types"

View File

@ -1,4 +1,4 @@
import React from 'react'; import React from "react"
const LoadingDots: React.FC = () => { const LoadingDots: React.FC = () => {
return ( return (
@ -9,24 +9,35 @@ const LoadingDots: React.FC = () => {
<style jsx>{` <style jsx>{`
.loading-dots { .loading-dots {
display: inline-block; display: inline-block;
font-size: 24px; font-size: 24px;
} }
.dot { .dot {
opacity: 0; opacity: 0;
animation: showHideDot 1.5s ease-in-out infinite; animation: showHideDot 1.5s ease-in-out infinite;
} }
.dot:nth-child(1) { animation-delay: 0s; } .dot:nth-child(1) {
.dot:nth-child(2) { animation-delay: 0.5s; } animation-delay: 0s;
.dot:nth-child(3) { animation-delay: 1s; } }
.dot:nth-child(2) {
animation-delay: 0.5s;
}
.dot:nth-child(3) {
animation-delay: 1s;
}
@keyframes showHideDot { @keyframes showHideDot {
0% { opacity: 0; } 0% {
50% { opacity: 1; } opacity: 0;
100% { opacity: 0; } }
50% {
opacity: 1;
}
100% {
opacity: 0;
}
} }
`}</style> `}</style>
</span> </span>
); )
}; }
export default LoadingDots;
export default LoadingDots

View File

@ -1,10 +1,10 @@
"use client" "use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import * as React from "react"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button" import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
const AlertDialog = AlertDialogPrimitive.Root const AlertDialog = AlertDialogPrimitive.Root
@ -128,14 +128,14 @@ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export { export {
AlertDialog, AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction, AlertDialogAction,
AlertDialogCancel, AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogTitle,
AlertDialogTrigger,
} }

View File

@ -1,6 +1,6 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"

View File

@ -73,4 +73,4 @@ const CardFooter = React.forwardRef<
)) ))
CardFooter.displayName = "CardFooter" CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }

View File

@ -1,12 +1,12 @@
"use client" "use client"
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { import {
CheckIcon, CheckIcon,
ChevronRightIcon, ChevronRightIcon,
DotFilledIcon, DotFilledIcon,
} from "@radix-ui/react-icons" } from "@radix-ui/react-icons"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -187,18 +187,18 @@ ContextMenuShortcut.displayName = "ContextMenuShortcut"
export { export {
ContextMenu, ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem, ContextMenuCheckboxItem,
ContextMenuRadioItem, ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel, ContextMenuLabel,
ContextMenuPortal,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator, ContextMenuSeparator,
ContextMenuShortcut, ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub, ContextMenuSub,
ContextMenuSubContent, ContextMenuSubContent,
ContextMenuSubTrigger, ContextMenuSubTrigger,
ContextMenuRadioGroup, ContextMenuTrigger,
} }

View File

@ -1,6 +1,5 @@
import * as React from "react"
import { Plus } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as React from "react"
const Button = ({ const Button = ({
children, children,

View File

@ -1,18 +1,18 @@
"use client"; "use client"
import * as React from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"
import * as DialogPrimitive from "@radix-ui/react-dialog"; import { Cross2Icon } from "@radix-ui/react-icons"
import { Cross2Icon } from "@radix-ui/react-icons"; import * as React from "react"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root; const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger; const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal; const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close; const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
@ -26,8 +26,8 @@ const DialogOverlay = React.forwardRef<
)} )}
{...props} {...props}
/> />
)); ))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)); ))
DialogContent.displayName = DialogPrimitive.Content.displayName; DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogContentNoClose = React.forwardRef< const DialogContentNoClose = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
@ -70,9 +70,9 @@ const DialogContentNoClose = React.forwardRef<
{children} {children}
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
)); ))
DialogContentNoClose.displayName = DialogContentNoClose.displayName =
DialogPrimitive.Content.displayName + "NoClose"; DialogPrimitive.Content.displayName + "NoClose"
const DialogHeader = ({ const DialogHeader = ({
className, className,
@ -85,8 +85,8 @@ const DialogHeader = ({
)} )}
{...props} {...props}
/> />
); )
DialogHeader.displayName = "DialogHeader"; DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({ const DialogFooter = ({
className, className,
@ -99,8 +99,8 @@ const DialogFooter = ({
)} )}
{...props} {...props}
/> />
); )
DialogFooter.displayName = "DialogFooter"; DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, React.ElementRef<typeof DialogPrimitive.Title>,
@ -114,8 +114,8 @@ const DialogTitle = React.forwardRef<
)} )}
{...props} {...props}
/> />
)); ))
DialogTitle.displayName = DialogPrimitive.Title.displayName; DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef< const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ElementRef<typeof DialogPrimitive.Description>,
@ -126,19 +126,19 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
)); ))
DialogDescription.displayName = DialogPrimitive.Description.displayName; DialogDescription.displayName = DialogPrimitive.Description.displayName
export { export {
Dialog, Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose, DialogClose,
DialogContent, DialogContent,
DialogContentNoClose, DialogContentNoClose,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription, DialogDescription,
}; DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@ -1,12 +1,12 @@
"use client" "use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { import {
CheckIcon, CheckIcon,
ChevronRightIcon, ChevronRightIcon,
DotFilledIcon, DotFilledIcon,
} from "@radix-ui/react-icons" } from "@radix-ui/react-icons"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -188,18 +188,18 @@ DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export { export {
DropdownMenu, DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuRadioItem, DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuShortcut, DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuRadioGroup, DropdownMenuTrigger,
} }

View File

@ -1,6 +1,6 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import * as React from "react"
import { import {
Controller, Controller,
ControllerProps, ControllerProps,
@ -10,8 +10,8 @@ import {
useFormContext, useFormContext,
} from "react-hook-form" } from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { cn } from "@/lib/utils"
const Form = FormProvider const Form = FormProvider
@ -165,12 +165,12 @@ const FormMessage = React.forwardRef<
FormMessage.displayName = "FormMessage" FormMessage.displayName = "FormMessage"
export { export {
useFormField,
Form, Form,
FormItem,
FormLabel,
FormControl, FormControl,
FormDescription, FormDescription,
FormMessage,
FormField, FormField,
FormItem,
FormLabel,
FormMessage,
useFormField,
} }

View File

@ -1,8 +1,8 @@
"use client" "use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"

View File

@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover" import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -30,4 +30,4 @@ const PopoverContent = React.forwardRef<
)) ))
PopoverContent.displayName = PopoverPrimitive.Content.displayName PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }

View File

@ -42,4 +42,4 @@ const ResizableHandle = ({
</ResizablePrimitive.PanelResizeHandle> </ResizablePrimitive.PanelResizeHandle>
) )
export { ResizablePanelGroup, ResizablePanel, ResizableHandle } export { ResizableHandle, ResizablePanel, ResizablePanelGroup }

View File

@ -1,6 +1,5 @@
"use client" "use client"
import * as React from "react"
import { import {
CaretSortIcon, CaretSortIcon,
CheckIcon, CheckIcon,
@ -8,6 +7,7 @@ import {
ChevronUpIcon, ChevronUpIcon,
} from "@radix-ui/react-icons" } from "@radix-ui/react-icons"
import * as SelectPrimitive from "@radix-ui/react-select" import * as SelectPrimitive from "@radix-ui/react-select"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -152,13 +152,13 @@ SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export { export {
Select, Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent, SelectContent,
SelectLabel, SelectGroup,
SelectItem, SelectItem,
SelectSeparator, SelectLabel,
SelectScrollUpButton,
SelectScrollDownButton, SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
} }

View File

@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch" import * as SwitchPrimitives from "@radix-ui/react-switch"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"

View File

@ -1,8 +1,8 @@
"use client"; "use client"
import { Loader2, X } from "lucide-react"; import { Loader2, X } from "lucide-react"
import { Button } from "./button"; import { MouseEventHandler } from "react"
import { MouseEvent, MouseEventHandler, useEffect } from "react"; import { Button } from "./button"
export default function Tab({ export default function Tab({
children, children,
@ -13,13 +13,13 @@ export default function Tab({
onClose, onClose,
closing = false, closing = false,
}: { }: {
children: React.ReactNode; children: React.ReactNode
creating?: boolean; creating?: boolean
saved?: boolean; saved?: boolean
selected?: boolean; selected?: boolean
onClick?: MouseEventHandler<HTMLButtonElement>; onClick?: MouseEventHandler<HTMLButtonElement>
onClose?: () => void; onClose?: () => void
closing?: boolean; closing?: boolean
}) { }) {
return ( return (
<Button <Button
@ -37,9 +37,9 @@ export default function Tab({
onClick={ onClick={
onClose && !closing onClose && !closing
? (e) => { ? (e) => {
e.stopPropagation(); e.stopPropagation()
e.preventDefault(); e.preventDefault()
onClose(); onClose()
} }
: undefined : undefined
} }
@ -57,5 +57,5 @@ export default function Tab({
)} )}
</div> </div>
</Button> </Button>
); )
} }

View File

@ -110,11 +110,11 @@ TableCaption.displayName = "TableCaption"
export { export {
Table, Table,
TableHeader,
TableBody, TableBody,
TableCaption,
TableCell,
TableFooter, TableFooter,
TableHead, TableHead,
TableHeader,
TableRow, TableRow,
TableCell,
TableCaption,
} }

View File

@ -1,23 +1,22 @@
"use client"; "use client"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu"
import { User } from "@/lib/types"; import { User } from "@/lib/types"
import { useClerk } from "@clerk/nextjs"; import { useClerk } from "@clerk/nextjs"
import { LogOut, Pencil, Sparkles } from "lucide-react"; import { LogOut, Sparkles } from "lucide-react"
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation"
export default function UserButton({ userData }: { userData: User }) { export default function UserButton({ userData }: { userData: User }) {
if (!userData) return null; if (!userData) return null
const { signOut } = useClerk(); const { signOut } = useClerk()
const router = useRouter(); const router = useRouter()
return ( return (
<DropdownMenu> <DropdownMenu>
@ -68,5 +67,5 @@ export default function UserButton({ userData }: { userData: User }) {
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); )
} }

View File

@ -1,34 +1,44 @@
"use client" "use client"
import React, { createContext, useContext, useState, useRef } from 'react'; import React, { createContext, useContext, useRef, useState } from "react"
import { ImperativePanelHandle } from "react-resizable-panels"; import { ImperativePanelHandle } from "react-resizable-panels"
interface PreviewContextType { interface PreviewContextType {
isPreviewCollapsed: boolean; isPreviewCollapsed: boolean
setIsPreviewCollapsed: React.Dispatch<React.SetStateAction<boolean>>; setIsPreviewCollapsed: React.Dispatch<React.SetStateAction<boolean>>
previewURL: string; previewURL: string
setPreviewURL: React.Dispatch<React.SetStateAction<string>>; setPreviewURL: React.Dispatch<React.SetStateAction<string>>
previewPanelRef: React.RefObject<ImperativePanelHandle>; previewPanelRef: React.RefObject<ImperativePanelHandle>
} }
const PreviewContext = createContext<PreviewContextType | undefined>(undefined); const PreviewContext = createContext<PreviewContextType | undefined>(undefined)
export const PreviewProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const PreviewProvider: React.FC<{ children: React.ReactNode }> = ({
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true); children,
const [previewURL, setPreviewURL] = useState<string>(""); }) => {
const previewPanelRef = useRef<ImperativePanelHandle>(null); const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
const [previewURL, setPreviewURL] = useState<string>("")
const previewPanelRef = useRef<ImperativePanelHandle>(null)
return ( return (
<PreviewContext.Provider value={{ isPreviewCollapsed, setIsPreviewCollapsed, previewURL, setPreviewURL, previewPanelRef }}> <PreviewContext.Provider
value={{
isPreviewCollapsed,
setIsPreviewCollapsed,
previewURL,
setPreviewURL,
previewPanelRef,
}}
>
{children} {children}
</PreviewContext.Provider> </PreviewContext.Provider>
); )
}; }
export const usePreview = () => { export const usePreview = () => {
const context = useContext(PreviewContext); const context = useContext(PreviewContext)
if (context === undefined) { if (context === undefined) {
throw new Error('usePreview must be used within a PreviewProvider'); throw new Error("usePreview must be used within a PreviewProvider")
} }
return context; return context
}; }

View File

@ -1,63 +1,65 @@
"use client"; "use client"
import React, { createContext, useContext, useEffect, useState } from 'react'; import React, { createContext, useContext, useEffect, useState } from "react"
import { io, Socket } from 'socket.io-client'; import { io, Socket } from "socket.io-client"
interface SocketContextType { interface SocketContextType {
socket: Socket | null; socket: Socket | null
setUserAndSandboxId: (userId: string, sandboxId: string) => void; setUserAndSandboxId: (userId: string, sandboxId: string) => void
} }
const SocketContext = createContext<SocketContextType | undefined>(undefined); const SocketContext = createContext<SocketContextType | undefined>(undefined)
export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({
const [socket, setSocket] = useState<Socket | null>(null); children,
const [userId, setUserId] = useState<string | null>(null); }) => {
const [sandboxId, setSandboxId] = useState<string | null>(null); const [socket, setSocket] = useState<Socket | null>(null)
const [userId, setUserId] = useState<string | null>(null)
const [sandboxId, setSandboxId] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
if (userId && sandboxId) { if (userId && sandboxId) {
console.log("Initializing socket connection..."); console.log("Initializing socket connection...")
const newSocket = io(`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`); const newSocket = io(
console.log("Socket instance:", newSocket); `${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`
setSocket(newSocket); )
console.log("Socket instance:", newSocket)
setSocket(newSocket)
newSocket.on('connect', () => { newSocket.on("connect", () => {
console.log("Socket connected:", newSocket.id); console.log("Socket connected:", newSocket.id)
}); })
newSocket.on('disconnect', () => { newSocket.on("disconnect", () => {
console.log("Socket disconnected"); console.log("Socket disconnected")
}); })
return () => { return () => {
console.log("Disconnecting socket..."); console.log("Disconnecting socket...")
newSocket.disconnect(); newSocket.disconnect()
}; }
} }
}, [userId, sandboxId]); }, [userId, sandboxId])
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => { const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
setUserId(newUserId); setUserId(newUserId)
setSandboxId(newSandboxId); setSandboxId(newSandboxId)
}; }
const value = { const value = {
socket, socket,
setUserAndSandboxId, setUserAndSandboxId,
}; }
return ( return (
<SocketContext.Provider value={ value }> <SocketContext.Provider value={value}>{children}</SocketContext.Provider>
{children} )
</SocketContext.Provider> }
);
};
export const useSocket = (): SocketContextType => { export const useSocket = (): SocketContextType => {
const context = useContext(SocketContext); const context = useContext(SocketContext)
if (!context) { if (!context) {
throw new Error('useSocket must be used within a SocketProvider'); throw new Error("useSocket must be used within a SocketProvider")
} }
return context; return context
}; }

View File

@ -1,33 +1,44 @@
"use client"; "use client"
import React, { createContext, useContext, useState } from 'react'; import { useSocket } from "@/context/SocketContext"
import { Terminal } from '@xterm/xterm'; import {
import { createTerminal as createTerminalHelper, closeTerminal as closeTerminalHelper } from '@/lib/terminal'; closeTerminal as closeTerminalHelper,
import { useSocket } from '@/context/SocketContext'; createTerminal as createTerminalHelper,
} from "@/lib/terminal"
import { Terminal } from "@xterm/xterm"
import React, { createContext, useContext, useState } from "react"
interface TerminalContextType { interface TerminalContextType {
terminals: { id: string; terminal: Terminal | null }[]; terminals: { id: string; terminal: Terminal | null }[]
setTerminals: React.Dispatch<React.SetStateAction<{ id: string; terminal: Terminal | null }[]>>; setTerminals: React.Dispatch<
activeTerminalId: string; React.SetStateAction<{ id: string; terminal: Terminal | null }[]>
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>; >
creatingTerminal: boolean; activeTerminalId: string
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>; setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
createNewTerminal: (command?: string) => Promise<void>; creatingTerminal: boolean
closeTerminal: (id: string) => void; setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>
deploy: (callback: () => void) => void; createNewTerminal: (command?: string) => Promise<void>
closeTerminal: (id: string) => void
deploy: (callback: () => void) => void
} }
const TerminalContext = createContext<TerminalContextType | undefined>(undefined); const TerminalContext = createContext<TerminalContextType | undefined>(
undefined
)
export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
const { socket } = useSocket(); children,
const [terminals, setTerminals] = useState<{ id: string; terminal: Terminal | null }[]>([]); }) => {
const [activeTerminalId, setActiveTerminalId] = useState<string>(''); const { socket } = useSocket()
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false); const [terminals, setTerminals] = useState<
{ id: string; terminal: Terminal | null }[]
>([])
const [activeTerminalId, setActiveTerminalId] = useState<string>("")
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false)
const createNewTerminal = async (command?: string): Promise<void> => { const createNewTerminal = async (command?: string): Promise<void> => {
if (!socket) return; if (!socket) return
setCreatingTerminal(true); setCreatingTerminal(true)
try { try {
createTerminalHelper({ createTerminalHelper({
setTerminals, setTerminals,
@ -35,36 +46,36 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setCreatingTerminal, setCreatingTerminal,
command, command,
socket, socket,
}); })
} catch (error) { } catch (error) {
console.error("Error creating terminal:", error); console.error("Error creating terminal:", error)
} finally { } finally {
setCreatingTerminal(false); setCreatingTerminal(false)
} }
}; }
const closeTerminal = (id: string) => { const closeTerminal = (id: string) => {
if (!socket) return; if (!socket) return
const terminalToClose = terminals.find(term => term.id === id); const terminalToClose = terminals.find((term) => term.id === id)
if (terminalToClose) { if (terminalToClose) {
closeTerminalHelper({ closeTerminalHelper({
term: terminalToClose, term: terminalToClose,
terminals, terminals,
setTerminals, setTerminals,
setActiveTerminalId, setActiveTerminalId,
setClosingTerminal: () => {}, setClosingTerminal: () => {},
socket, socket,
activeTerminalId, activeTerminalId,
}); })
} }
}; }
const deploy = (callback: () => void) => { const deploy = (callback: () => void) => {
if (!socket) console.error("Couldn't deploy: No socket"); if (!socket) console.error("Couldn't deploy: No socket")
console.log("Deploying...") console.log("Deploying...")
socket?.emit("deploy", () => { socket?.emit("deploy", () => {
callback(); callback()
}); })
} }
const value = { const value = {
@ -76,20 +87,20 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setCreatingTerminal, setCreatingTerminal,
createNewTerminal, createNewTerminal,
closeTerminal, closeTerminal,
deploy deploy,
}; }
return ( return (
<TerminalContext.Provider value={value}> <TerminalContext.Provider value={value}>
{children} {children}
</TerminalContext.Provider> </TerminalContext.Provider>
); )
}; }
export const useTerminal = (): TerminalContextType => { export const useTerminal = (): TerminalContextType => {
const context = useContext(TerminalContext); const context = useContext(TerminalContext)
if (!context) { if (!context) {
throw new Error('useTerminal must be used within a TerminalProvider'); throw new Error("useTerminal must be used within a TerminalProvider")
} }
return context; return context
}; }

View File

@ -1,8 +1,8 @@
// Helper functions for terminal instances // Helper functions for terminal instances
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2"
import { Terminal } from "@xterm/xterm"; import { Terminal } from "@xterm/xterm"
import { Socket } from "socket.io-client"; import { Socket } from "socket.io-client"
export const createTerminal = ({ export const createTerminal = ({
setTerminals, setTerminals,
@ -11,30 +11,33 @@ export const createTerminal = ({
command, command,
socket, socket,
}: { }: {
setTerminals: React.Dispatch<React.SetStateAction<{ setTerminals: React.Dispatch<
id: string; React.SetStateAction<
terminal: Terminal | null; {
}[]>>; id: string
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>; terminal: Terminal | null
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>; }[]
command?: string; >
socket: Socket; >
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>
command?: string
socket: Socket
}) => { }) => {
setCreatingTerminal(true); setCreatingTerminal(true)
const id = createId(); const id = createId()
console.log("creating terminal, id:", id); console.log("creating terminal, id:", id)
setTerminals((prev) => [...prev, { id, terminal: null }]); setTerminals((prev) => [...prev, { id, terminal: null }])
setActiveTerminalId(id); setActiveTerminalId(id)
setTimeout(() => { setTimeout(() => {
socket.emit("createTerminal", id, () => { socket.emit("createTerminal", id, () => {
setCreatingTerminal(false); setCreatingTerminal(false)
if (command) socket.emit("terminalData", id, command + "\n"); if (command) socket.emit("terminalData", id, command + "\n")
}); })
}, 1000); }, 1000)
}; }
export const closeTerminal = ({ export const closeTerminal = ({
term, term,
@ -44,32 +47,36 @@ export const closeTerminal = ({
setClosingTerminal, setClosingTerminal,
socket, socket,
activeTerminalId, activeTerminalId,
} : { }: {
term: { term: {
id: string; id: string
terminal: Terminal | null terminal: Terminal | null
} }
terminals: { terminals: {
id: string; id: string
terminal: Terminal | null terminal: Terminal | null
}[] }[]
setTerminals: React.Dispatch<React.SetStateAction<{ setTerminals: React.Dispatch<
id: string; React.SetStateAction<
terminal: Terminal | null {
}[]>> id: string
terminal: Terminal | null
}[]
>
>
setActiveTerminalId: React.Dispatch<React.SetStateAction<string>> setActiveTerminalId: React.Dispatch<React.SetStateAction<string>>
setClosingTerminal: React.Dispatch<React.SetStateAction<string>> setClosingTerminal: React.Dispatch<React.SetStateAction<string>>
socket: Socket socket: Socket
activeTerminalId: string activeTerminalId: string
}) => { }) => {
const numTerminals = terminals.length; const numTerminals = terminals.length
const index = terminals.findIndex((t) => t.id === term.id); const index = terminals.findIndex((t) => t.id === term.id)
if (index === -1) return; if (index === -1) return
setClosingTerminal(term.id); setClosingTerminal(term.id)
socket.emit("closeTerminal", term.id, () => { socket.emit("closeTerminal", term.id, () => {
setClosingTerminal(""); setClosingTerminal("")
const nextId = const nextId =
activeTerminalId === term.id activeTerminalId === term.id
@ -78,17 +85,17 @@ export const closeTerminal = ({
: index < numTerminals - 1 : index < numTerminals - 1
? terminals[index + 1].id ? terminals[index + 1].id
: terminals[index - 1].id : terminals[index - 1].id
: activeTerminalId; : activeTerminalId
setTerminals((prev) => prev.filter((t) => t.id !== term.id)); setTerminals((prev) => prev.filter((t) => t.id !== term.id))
if (!nextId) { if (!nextId) {
setActiveTerminalId(""); setActiveTerminalId("")
} else { } else {
const nextTerminal = terminals.find((t) => t.id === nextId); const nextTerminal = terminals.find((t) => t.id === nextId)
if (nextTerminal) { if (nextTerminal) {
setActiveTerminalId(nextTerminal.id); setActiveTerminalId(nextTerminal.id)
} }
} }
}); })
}; }

View File

@ -1,65 +1,65 @@
// DB Types // DB Types
export type User = { export type User = {
id: string; id: string
name: string; name: string
email: string; email: string
generations: number; generations: number
sandbox: Sandbox[]; sandbox: Sandbox[]
usersToSandboxes: UsersToSandboxes[]; usersToSandboxes: UsersToSandboxes[]
}; }
export type Sandbox = { export type Sandbox = {
id: string; id: string
name: string; name: string
type: string; type: string
visibility: "public" | "private"; visibility: "public" | "private"
createdAt: Date; createdAt: Date
userId: string; userId: string
usersToSandboxes: UsersToSandboxes[]; usersToSandboxes: UsersToSandboxes[]
}; }
export type UsersToSandboxes = { export type UsersToSandboxes = {
userId: string; userId: string
sandboxId: string; sandboxId: string
sharedOn: Date; sharedOn: Date
}; }
export type R2Files = { export type R2Files = {
objects: R2FileData[]; objects: R2FileData[]
truncated: boolean; truncated: boolean
delimitedPrefixes: any[]; delimitedPrefixes: any[]
}; }
export type R2FileData = { export type R2FileData = {
storageClass: string; storageClass: string
uploaded: string; uploaded: string
checksums: any; checksums: any
httpEtag: string; httpEtag: string
etag: string; etag: string
size: number; size: number
version: string; version: string
key: string; key: string
}; }
export type TFolder = { export type TFolder = {
id: string; id: string
type: "folder"; type: "folder"
name: string; name: string
children: (TFile | TFolder)[]; children: (TFile | TFolder)[]
}; }
export type TFile = { export type TFile = {
id: string; id: string
type: "file"; type: "file"
name: string; name: string
}; }
export type TTab = TFile & { export type TTab = TFile & {
saved: boolean; saved: boolean
}; }
export type TFileData = { export type TFileData = {
id: string; id: string
data: string; data: string
}; }

View File

@ -1,8 +1,8 @@
import { type ClassValue, clsx } from "clsx" import { type ClassValue, clsx } from "clsx"
// import { toast } from "sonner" // import { toast } from "sonner"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
import { Sandbox, TFile, TFolder } from "./types"
import fileExtToLang from "./file-extension-to-language.json" import fileExtToLang from "./file-extension-to-language.json"
import { Sandbox, TFile, TFolder } from "./types"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))