ui + shared user improvements
This commit is contained in:
@ -11,12 +11,7 @@ import * as Y from "yjs"
|
||||
import LiveblocksProvider from "@liveblocks/yjs"
|
||||
import { MonacoBinding } from "y-monaco"
|
||||
import { Awareness } from "y-protocols/awareness"
|
||||
import {
|
||||
TypedLiveblocksProvider,
|
||||
useRoom,
|
||||
AwarenessList,
|
||||
useSelf,
|
||||
} from "@/liveblocks.config"
|
||||
import { TypedLiveblocksProvider, useRoom } from "@/liveblocks.config"
|
||||
|
||||
import {
|
||||
ResizableHandle,
|
||||
@ -27,6 +22,7 @@ import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
FileJson,
|
||||
Loader2,
|
||||
Plus,
|
||||
RotateCw,
|
||||
Shell,
|
||||
@ -355,15 +351,20 @@ export default function CodeEditor({
|
||||
setFiles(files)
|
||||
}
|
||||
|
||||
socket.on("connect", onConnect)
|
||||
const onRateLimit = (message: string) => {
|
||||
toast.error(message)
|
||||
}
|
||||
|
||||
socket.on("connect", onConnect)
|
||||
socket.on("disconnect", onDisconnect)
|
||||
socket.on("loaded", onLoadedEvent)
|
||||
socket.on("rateLimit", onRateLimit)
|
||||
|
||||
return () => {
|
||||
socket.off("connect", onConnect)
|
||||
socket.off("disconnect", onDisconnect)
|
||||
socket.off("loaded", onLoadedEvent)
|
||||
socket.off("rateLimit", onRateLimit)
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -547,7 +548,7 @@ export default function CodeEditor({
|
||||
>
|
||||
{!activeId ? (
|
||||
<>
|
||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-secondary select-none">
|
||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
||||
<FileJson className="w-6 h-6 mr-3" />
|
||||
No file selected.
|
||||
</div>
|
||||
@ -591,7 +592,12 @@ export default function CodeEditor({
|
||||
value={activeFile ?? ""}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
) : (
|
||||
<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" />
|
||||
Waiting for Clerk to load...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
@ -629,27 +635,36 @@ export default function CodeEditor({
|
||||
minSize={20}
|
||||
className="p-2 flex flex-col"
|
||||
>
|
||||
<div className="h-10 w-full flex gap-2 shrink-0">
|
||||
<Tab selected>
|
||||
<SquareTerminal className="w-4 h-4 mr-2" />
|
||||
Shell
|
||||
</Tab>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (terminals.length >= 4) {
|
||||
toast.error("You reached the maximum # of terminals.")
|
||||
}
|
||||
}}
|
||||
size="smIcon"
|
||||
variant={"secondary"}
|
||||
className={`font-normal select-none text-muted-foreground`}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full relative grow h-full overflow-hidden rounded-md bg-secondary">
|
||||
{socket ? <EditorTerminal socket={socket} /> : null}
|
||||
</div>
|
||||
{isOwner ? (
|
||||
<>
|
||||
<div className="h-10 w-full flex gap-2 shrink-0">
|
||||
<Tab selected>
|
||||
<SquareTerminal className="w-4 h-4 mr-2" />
|
||||
Shell
|
||||
</Tab>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (terminals.length >= 4) {
|
||||
toast.error("You reached the maximum # of terminals.")
|
||||
}
|
||||
}}
|
||||
size="smIcon"
|
||||
variant={"secondary"}
|
||||
className={`font-normal select-none text-muted-foreground`}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full relative grow h-full overflow-hidden rounded-md bg-secondary">
|
||||
{socket ? <EditorTerminal socket={socket} /> : null}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-lg font-medium text-muted-foreground/50 select-none">
|
||||
<TerminalSquare className="w-4 h-4 mr-2" />
|
||||
No terminal access.
|
||||
</div>
|
||||
)}
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</ResizablePanel>
|
||||
|
@ -1,33 +1,39 @@
|
||||
"use client"
|
||||
|
||||
import { colorClasses, colors } from "@/lib/colors"
|
||||
import { useOthers } from "@/liveblocks.config"
|
||||
import { useState } from "react"
|
||||
|
||||
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",
|
||||
orange:
|
||||
"w-8 h-8 leading-none font-mono rounded-full ring-1 ring-orange-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-orange-950 to-orange-600 flex items-center justify-center text-xs font-medium",
|
||||
yellow:
|
||||
"w-8 h-8 leading-none font-mono rounded-full ring-1 ring-yellow-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-yellow-950 to-yellow-600 flex items-center justify-center text-xs font-medium",
|
||||
green:
|
||||
"w-8 h-8 leading-none font-mono rounded-full ring-1 ring-green-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-green-950 to-green-600 flex items-center justify-center text-xs font-medium",
|
||||
blue: "w-8 h-8 leading-none font-mono rounded-full ring-1 ring-blue-700 ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr from-blue-950 to-blue-600 flex items-center justify-center text-xs font-medium",
|
||||
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",
|
||||
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() {
|
||||
const users = useOthers()
|
||||
|
||||
const colorNames = Object.keys(colors)
|
||||
const [activeColors, setActiveColors] = useState([])
|
||||
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
{users.map(({ connectionId, info }) => {
|
||||
const c = colorNames[
|
||||
connectionId % colorNames.length
|
||||
] as keyof typeof colors
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-8 h-8 font-mono rounded-full ring-1 ${colorClasses[c].ring} ring-offset-2 ring-offset-background overflow-hidden bg-gradient-to-tr ${colorClasses[c].bg} flex items-center justify-center text-xs font-medium`}
|
||||
>
|
||||
{info.name
|
||||
.split(" ")
|
||||
.slice(0, 2)
|
||||
.map((letter) => letter[0].toUpperCase())}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<>
|
||||
<div className="flex space-x-2">
|
||||
{users.map(({ connectionId, info }) => {
|
||||
return (
|
||||
<div className={classNames[info.color]}>
|
||||
{info.name
|
||||
.split(" ")
|
||||
.slice(0, 2)
|
||||
.map((letter) => letter[0].toUpperCase())}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="h-full w-[1px] bg-border mx-2" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -5,11 +5,14 @@ import {
|
||||
UserAwareness,
|
||||
useSelf,
|
||||
} from "@/liveblocks.config"
|
||||
import { colors } from "@/lib/colors"
|
||||
|
||||
export function Cursors({ yProvider }: { yProvider: TypedLiveblocksProvider }) {
|
||||
// Get user info from Liveblocks authentication endpoint
|
||||
const userInfo = useSelf((me) => me.info)
|
||||
|
||||
if (!userInfo) return null
|
||||
|
||||
const [awarenessUsers, setAwarenessUsers] = useState<AwarenessList>([])
|
||||
|
||||
useEffect(() => {
|
||||
@ -40,7 +43,7 @@ export function Cursors({ yProvider }: { yProvider: TypedLiveblocksProvider }) {
|
||||
cursorStyles += `
|
||||
.yRemoteSelection-${clientId},
|
||||
.yRemoteSelectionHead-${clientId} {
|
||||
--user-color: ${"#FF0000"};
|
||||
--user-color: ${colors[client.user.color]};
|
||||
}
|
||||
|
||||
.yRemoteSelectionHead-${clientId}::after {
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import { RoomProvider } from "@/liveblocks.config"
|
||||
import { ClientSideSuspense } from "@liveblocks/react"
|
||||
import Loading from "../loading"
|
||||
|
||||
export function Room({
|
||||
id,
|
||||
@ -17,9 +18,9 @@ export function Room({
|
||||
cursor: null,
|
||||
}}
|
||||
>
|
||||
<ClientSideSuspense fallback={<div>Loading!!!!</div>}>
|
||||
{() => children}
|
||||
</ClientSideSuspense>
|
||||
{/* <ClientSideSuspense fallback={<Loading />}> */}
|
||||
{children}
|
||||
{/* </ClientSideSuspense> */}
|
||||
</RoomProvider>
|
||||
)
|
||||
}
|
||||
|
41
frontend/components/editor/loading.tsx
Normal file
41
frontend/components/editor/loading.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import Image from "next/image"
|
||||
import Logo from "@/assets/logo.svg"
|
||||
import { Skeleton } from "../ui/skeleton"
|
||||
import { Loader, Loader2 } from "lucide-react"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
|
||||
<div className="h-14 px-2 w-full flex items-center justify-between border-b border-border">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Image src={Logo} alt="Logo" width={36} height={36} />
|
||||
<Skeleton className="w-[100px] h-[24px] rounded-md" />
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Skeleton className="w-[64px] h-[36px] rounded-md" />
|
||||
<Skeleton className="w-[36px] h-[36px] rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grow flex w-screen">
|
||||
<div className="h-full w-56 select-none flex flex-col text-sm items-start p-2">
|
||||
<div className="flex w-full items-center justify-between h-8 mb-1 ">
|
||||
<div className="text-muted-foreground">Explorer</div>
|
||||
<div className="flex space-x-1">
|
||||
<Skeleton className="w-6 h-6 rounded-md" />
|
||||
<Skeleton className="w-6 h-6 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full mt-1 flex flex-col">
|
||||
<div className="w-full flex justify-center">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-secondary select-none">
|
||||
<Loader2 className="w-6 h-6 mr-3 animate-spin" />
|
||||
Loading...
|
||||
</div>{" "}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -62,7 +62,7 @@ export default function Navbar({
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center h-full space-x-4">
|
||||
<Avatars />
|
||||
|
||||
{isOwner ? (
|
||||
|
@ -8,7 +8,7 @@ import { useState } from "react"
|
||||
import New from "./new"
|
||||
import { Socket } from "socket.io-client"
|
||||
import Button from "@/components/ui/customButton"
|
||||
import Toggle from "@/components/ui/customToggle"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
|
||||
export default function Sidebar({
|
||||
files,
|
||||
@ -104,12 +104,20 @@ export default function Sidebar({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex items-center"> */}
|
||||
<Toggle value={ai} setValue={setAi} className="w-full">
|
||||
<Sparkles className="h-3 w-3 mr-2" />
|
||||
AI Copilot
|
||||
</Toggle>
|
||||
{/* </div> */}
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center">
|
||||
<Sparkles
|
||||
className={`h-4 w-4 mr-2 ${
|
||||
ai ? "text-indigo-500" : "text-muted-foreground"
|
||||
}`}
|
||||
/>
|
||||
Copilot{" "}
|
||||
<span className="font-mono text-muted-foreground inline-block ml-1.5 text-xs leading-none border border-b-2 border-muted-foreground py-1 px-1.5 rounded-md">
|
||||
⌘G
|
||||
</span>
|
||||
</div>
|
||||
<Switch checked={ai} onCheckedChange={setAi} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,43 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { Plus } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "./button"
|
||||
|
||||
const Toggle = ({
|
||||
children,
|
||||
className,
|
||||
value,
|
||||
setValue,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
value: boolean
|
||||
setValue: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}) => {
|
||||
if (value)
|
||||
return (
|
||||
<button
|
||||
onClick={() => setValue(false)}
|
||||
className={cn(
|
||||
className,
|
||||
`gradient-button-bg p-[1px] inline-flex group rounded-md text-sm font-medium focus-visible:ring-offset-2 h-9 focus-visible:ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50`
|
||||
)}
|
||||
>
|
||||
<div className="rounded-[6px] w-full gradient-button flex items-center justify-center whitespace-nowrap px-4 py-2 h-full">
|
||||
{children}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
else
|
||||
return (
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
onClick={() => setValue(true)}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default Toggle
|
15
frontend/components/ui/skeleton.tsx
Normal file
15
frontend/components/ui/skeleton.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
29
frontend/components/ui/switch.tsx
Normal file
29
frontend/components/ui/switch.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
Reference in New Issue
Block a user