ui + shared user improvements
This commit is contained in:
parent
dd400b1d2a
commit
09ead6073b
128
backend/server/dist/index.js
vendored
128
backend/server/dist/index.js
vendored
@ -22,6 +22,7 @@ const socket_io_1 = require("socket.io");
|
|||||||
const zod_1 = require("zod");
|
const zod_1 = require("zod");
|
||||||
const utils_1 = require("./utils");
|
const utils_1 = require("./utils");
|
||||||
const node_pty_1 = require("node-pty");
|
const node_pty_1 = require("node-pty");
|
||||||
|
const ratelimit_1 = require("./ratelimit");
|
||||||
dotenv_1.default.config();
|
dotenv_1.default.config();
|
||||||
const app = (0, express_1.default)();
|
const app = (0, express_1.default)();
|
||||||
const port = process.env.PORT || 4000;
|
const port = process.env.PORT || 4000;
|
||||||
@ -89,60 +90,97 @@ io.on("connection", (socket) => __awaiter(void 0, void 0, void 0, function* () {
|
|||||||
});
|
});
|
||||||
// todo: send diffs + debounce for efficiency
|
// todo: send diffs + debounce for efficiency
|
||||||
socket.on("saveFile", (fileId, body) => __awaiter(void 0, void 0, void 0, function* () {
|
socket.on("saveFile", (fileId, body) => __awaiter(void 0, void 0, void 0, function* () {
|
||||||
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
try {
|
||||||
if (!file)
|
yield ratelimit_1.saveFileRL.consume(data.userId, 1);
|
||||||
return;
|
if (Buffer.byteLength(body, "utf-8") > ratelimit_1.MAX_BODY_SIZE) {
|
||||||
file.data = body;
|
socket.emit("rateLimit", "Rate limited: file size too large. Please reduce the file size.");
|
||||||
fs_1.default.writeFile(path_1.default.join(dirName, file.id), body, function (err) {
|
return;
|
||||||
if (err)
|
}
|
||||||
throw err;
|
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
||||||
});
|
if (!file)
|
||||||
yield (0, utils_1.saveFile)(fileId, body);
|
return;
|
||||||
|
file.data = body;
|
||||||
|
fs_1.default.writeFile(path_1.default.join(dirName, file.id), body, function (err) {
|
||||||
|
if (err)
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
yield (0, utils_1.saveFile)(fileId, body);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
socket.emit("rateLimit", "Rate limited: file saving. Please slow down.");
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
socket.on("createFile", (name) => __awaiter(void 0, void 0, void 0, function* () {
|
socket.on("createFile", (name) => __awaiter(void 0, void 0, void 0, function* () {
|
||||||
const id = `projects/${data.id}/${name}`;
|
try {
|
||||||
fs_1.default.writeFile(path_1.default.join(dirName, id), "", function (err) {
|
yield ratelimit_1.createFileRL.consume(data.userId, 1);
|
||||||
if (err)
|
const id = `projects/${data.id}/${name}`;
|
||||||
throw err;
|
fs_1.default.writeFile(path_1.default.join(dirName, id), "", function (err) {
|
||||||
});
|
if (err)
|
||||||
sandboxFiles.files.push({
|
throw err;
|
||||||
id,
|
});
|
||||||
name,
|
sandboxFiles.files.push({
|
||||||
type: "file",
|
id,
|
||||||
});
|
name,
|
||||||
sandboxFiles.fileData.push({
|
type: "file",
|
||||||
id,
|
});
|
||||||
data: "",
|
sandboxFiles.fileData.push({
|
||||||
});
|
id,
|
||||||
yield (0, utils_1.createFile)(id);
|
data: "",
|
||||||
|
});
|
||||||
|
yield (0, utils_1.createFile)(id);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
socket.emit("rateLimit", "Rate limited: file creation. Please slow down.");
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
socket.on("renameFile", (fileId, newName) => __awaiter(void 0, void 0, void 0, function* () {
|
socket.on("renameFile", (fileId, newName) => __awaiter(void 0, void 0, void 0, function* () {
|
||||||
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
try {
|
||||||
if (!file)
|
yield ratelimit_1.renameFileRL.consume(data.userId, 1);
|
||||||
|
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
||||||
|
if (!file)
|
||||||
|
return;
|
||||||
|
file.id = newName;
|
||||||
|
const parts = fileId.split("/");
|
||||||
|
const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName;
|
||||||
|
fs_1.default.rename(path_1.default.join(dirName, fileId), path_1.default.join(dirName, newFileId), function (err) {
|
||||||
|
if (err)
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
yield (0, utils_1.renameFile)(fileId, newFileId, file.data);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
socket.emit("rateLimit", "Rate limited: file renaming. Please slow down.");
|
||||||
return;
|
return;
|
||||||
file.id = newName;
|
}
|
||||||
const parts = fileId.split("/");
|
|
||||||
const newFileId = parts.slice(0, parts.length - 1).join("/") + "/" + newName;
|
|
||||||
fs_1.default.rename(path_1.default.join(dirName, fileId), path_1.default.join(dirName, newFileId), function (err) {
|
|
||||||
if (err)
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
yield (0, utils_1.renameFile)(fileId, newFileId, file.data);
|
|
||||||
}));
|
}));
|
||||||
socket.on("deleteFile", (fileId, callback) => __awaiter(void 0, void 0, void 0, function* () {
|
socket.on("deleteFile", (fileId, callback) => __awaiter(void 0, void 0, void 0, function* () {
|
||||||
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
try {
|
||||||
if (!file)
|
yield ratelimit_1.deleteFileRL.consume(data.userId, 1);
|
||||||
return;
|
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
|
||||||
fs_1.default.unlink(path_1.default.join(dirName, fileId), function (err) {
|
if (!file)
|
||||||
if (err)
|
return;
|
||||||
throw err;
|
fs_1.default.unlink(path_1.default.join(dirName, fileId), function (err) {
|
||||||
});
|
if (err)
|
||||||
sandboxFiles.fileData = sandboxFiles.fileData.filter((f) => f.id !== fileId);
|
throw err;
|
||||||
yield (0, utils_1.deleteFile)(fileId);
|
});
|
||||||
const newFiles = yield (0, utils_1.getSandboxFiles)(data.id);
|
sandboxFiles.fileData = sandboxFiles.fileData.filter((f) => f.id !== fileId);
|
||||||
callback(newFiles.files);
|
yield (0, utils_1.deleteFile)(fileId);
|
||||||
|
const newFiles = yield (0, utils_1.getSandboxFiles)(data.id);
|
||||||
|
callback(newFiles.files);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
socket.emit("rateLimit", "Rate limited: file deletion. Please slow down.");
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
socket.on("createTerminal", ({ id }) => {
|
socket.on("createTerminal", ({ id }) => {
|
||||||
|
if (terminals[id]) {
|
||||||
|
console.log("Terminal already exists.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Object.keys(terminals).length >= 4) {
|
||||||
|
console.log("Too many terminals.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pty = (0, node_pty_1.spawn)(os_1.default.platform() === "win32" ? "cmd.exe" : "bash", [], {
|
const pty = (0, node_pty_1.spawn)(os_1.default.platform() === "win32" ? "cmd.exe" : "bash", [], {
|
||||||
name: "xterm",
|
name: "xterm",
|
||||||
cols: 100,
|
cols: 100,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { colors } from "@/lib/colors"
|
||||||
import { User } from "@/lib/types"
|
import { User } from "@/lib/types"
|
||||||
import { currentUser } from "@clerk/nextjs"
|
import { currentUser } from "@clerk/nextjs"
|
||||||
import { Liveblocks } from "@liveblocks/node"
|
import { Liveblocks } from "@liveblocks/node"
|
||||||
@ -19,12 +20,19 @@ export async function POST(request: NextRequest) {
|
|||||||
const res = await fetch(`http://localhost:8787/api/user?id=${clerkUser.id}`)
|
const res = await fetch(`http://localhost:8787/api/user?id=${clerkUser.id}`)
|
||||||
const user = (await res.json()) as User
|
const user = (await res.json()) as User
|
||||||
|
|
||||||
|
const colorNames = Object.keys(colors)
|
||||||
|
const randomColor = colorNames[
|
||||||
|
Math.floor(Math.random() * colorNames.length)
|
||||||
|
] as keyof typeof colors
|
||||||
|
const code = colors[randomColor]
|
||||||
|
|
||||||
// Create a session for the current user
|
// Create a session for the current user
|
||||||
// userInfo is made available in Liveblocks presence hooks, e.g. useOthers
|
// userInfo is made available in Liveblocks presence hooks, e.g. useOthers
|
||||||
const session = liveblocks.prepareSession(user.id, {
|
const session = liveblocks.prepareSession(user.id, {
|
||||||
userInfo: {
|
userInfo: {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
color: randomColor,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -99,13 +99,23 @@
|
|||||||
background: radial-gradient(circle at bottom right, #312e81 -75%, hsl(0 0% 3.9%) 60%); /* violet 900 -> bg */
|
background: radial-gradient(circle at bottom right, #312e81 -75%, hsl(0 0% 3.9%) 60%); /* violet 900 -> bg */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-decoration::before {
|
||||||
|
content: 'Generate';
|
||||||
|
color: #525252;
|
||||||
|
/* border: 1px solid #525252; */
|
||||||
|
/* padding: 2px 4px; */
|
||||||
|
/* border-radius: 4px; */
|
||||||
|
margin-left: 36px;
|
||||||
|
}
|
||||||
.inline-decoration::after {
|
.inline-decoration::after {
|
||||||
content: 'Generate ⌘G';
|
content: '⌘G';
|
||||||
color: #525252;
|
color: #525252;
|
||||||
border: 1px solid #525252;
|
border: 1px solid #525252;
|
||||||
padding: 2px 4px;
|
border-bottom-width: 2px;
|
||||||
|
padding: 0 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-left: 56px;
|
margin-left: 6px;
|
||||||
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.yRemoteSelection {
|
.yRemoteSelection {
|
||||||
|
@ -11,12 +11,7 @@ import * as Y from "yjs"
|
|||||||
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 {
|
import { TypedLiveblocksProvider, useRoom } from "@/liveblocks.config"
|
||||||
TypedLiveblocksProvider,
|
|
||||||
useRoom,
|
|
||||||
AwarenessList,
|
|
||||||
useSelf,
|
|
||||||
} from "@/liveblocks.config"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ResizableHandle,
|
ResizableHandle,
|
||||||
@ -27,6 +22,7 @@ import {
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
FileJson,
|
FileJson,
|
||||||
|
Loader2,
|
||||||
Plus,
|
Plus,
|
||||||
RotateCw,
|
RotateCw,
|
||||||
Shell,
|
Shell,
|
||||||
@ -355,15 +351,20 @@ export default function CodeEditor({
|
|||||||
setFiles(files)
|
setFiles(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on("connect", onConnect)
|
const onRateLimit = (message: string) => {
|
||||||
|
toast.error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on("connect", onConnect)
|
||||||
socket.on("disconnect", onDisconnect)
|
socket.on("disconnect", onDisconnect)
|
||||||
socket.on("loaded", onLoadedEvent)
|
socket.on("loaded", onLoadedEvent)
|
||||||
|
socket.on("rateLimit", onRateLimit)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off("connect", onConnect)
|
socket.off("connect", onConnect)
|
||||||
socket.off("disconnect", onDisconnect)
|
socket.off("disconnect", onDisconnect)
|
||||||
socket.off("loaded", onLoadedEvent)
|
socket.off("loaded", onLoadedEvent)
|
||||||
|
socket.off("rateLimit", onRateLimit)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -547,7 +548,7 @@ export default function CodeEditor({
|
|||||||
>
|
>
|
||||||
{!activeId ? (
|
{!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" />
|
<FileJson className="w-6 h-6 mr-3" />
|
||||||
No file selected.
|
No file selected.
|
||||||
</div>
|
</div>
|
||||||
@ -591,7 +592,12 @@ export default function CodeEditor({
|
|||||||
value={activeFile ?? ""}
|
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>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle />
|
<ResizableHandle />
|
||||||
@ -629,27 +635,36 @@ export default function CodeEditor({
|
|||||||
minSize={20}
|
minSize={20}
|
||||||
className="p-2 flex flex-col"
|
className="p-2 flex flex-col"
|
||||||
>
|
>
|
||||||
<div className="h-10 w-full flex gap-2 shrink-0">
|
{isOwner ? (
|
||||||
<Tab selected>
|
<>
|
||||||
<SquareTerminal className="w-4 h-4 mr-2" />
|
<div className="h-10 w-full flex gap-2 shrink-0">
|
||||||
Shell
|
<Tab selected>
|
||||||
</Tab>
|
<SquareTerminal className="w-4 h-4 mr-2" />
|
||||||
<Button
|
Shell
|
||||||
onClick={() => {
|
</Tab>
|
||||||
if (terminals.length >= 4) {
|
<Button
|
||||||
toast.error("You reached the maximum # of terminals.")
|
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`}
|
size="smIcon"
|
||||||
>
|
variant={"secondary"}
|
||||||
<Plus className="w-4 h-4" />
|
className={`font-normal select-none text-muted-foreground`}
|
||||||
</Button>
|
>
|
||||||
</div>
|
<Plus className="w-4 h-4" />
|
||||||
<div className="w-full relative grow h-full overflow-hidden rounded-md bg-secondary">
|
</Button>
|
||||||
{socket ? <EditorTerminal socket={socket} /> : null}
|
</div>
|
||||||
</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>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
@ -1,33 +1,39 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { colorClasses, colors } from "@/lib/colors"
|
|
||||||
import { useOthers } from "@/liveblocks.config"
|
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() {
|
export function Avatars() {
|
||||||
const users = useOthers()
|
const users = useOthers()
|
||||||
|
|
||||||
const colorNames = Object.keys(colors)
|
|
||||||
const [activeColors, setActiveColors] = useState([])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex space-x-2">
|
<>
|
||||||
{users.map(({ connectionId, info }) => {
|
<div className="flex space-x-2">
|
||||||
const c = colorNames[
|
{users.map(({ connectionId, info }) => {
|
||||||
connectionId % colorNames.length
|
return (
|
||||||
] as keyof typeof colors
|
<div className={classNames[info.color]}>
|
||||||
|
{info.name
|
||||||
return (
|
.split(" ")
|
||||||
<div
|
.slice(0, 2)
|
||||||
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`}
|
.map((letter) => letter[0].toUpperCase())}
|
||||||
>
|
</div>
|
||||||
{info.name
|
)
|
||||||
.split(" ")
|
})}
|
||||||
.slice(0, 2)
|
</div>
|
||||||
.map((letter) => letter[0].toUpperCase())}
|
<div className="h-full w-[1px] bg-border mx-2" />
|
||||||
</div>
|
</>
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,14 @@ import {
|
|||||||
UserAwareness,
|
UserAwareness,
|
||||||
useSelf,
|
useSelf,
|
||||||
} from "@/liveblocks.config"
|
} from "@/liveblocks.config"
|
||||||
|
import { colors } from "@/lib/colors"
|
||||||
|
|
||||||
export function Cursors({ yProvider }: { yProvider: TypedLiveblocksProvider }) {
|
export function Cursors({ yProvider }: { yProvider: TypedLiveblocksProvider }) {
|
||||||
// Get user info from Liveblocks authentication endpoint
|
// Get user info from Liveblocks authentication endpoint
|
||||||
const userInfo = useSelf((me) => me.info)
|
const userInfo = useSelf((me) => me.info)
|
||||||
|
|
||||||
|
if (!userInfo) return null
|
||||||
|
|
||||||
const [awarenessUsers, setAwarenessUsers] = useState<AwarenessList>([])
|
const [awarenessUsers, setAwarenessUsers] = useState<AwarenessList>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -40,7 +43,7 @@ export function Cursors({ yProvider }: { yProvider: TypedLiveblocksProvider }) {
|
|||||||
cursorStyles += `
|
cursorStyles += `
|
||||||
.yRemoteSelection-${clientId},
|
.yRemoteSelection-${clientId},
|
||||||
.yRemoteSelectionHead-${clientId} {
|
.yRemoteSelectionHead-${clientId} {
|
||||||
--user-color: ${"#FF0000"};
|
--user-color: ${colors[client.user.color]};
|
||||||
}
|
}
|
||||||
|
|
||||||
.yRemoteSelectionHead-${clientId}::after {
|
.yRemoteSelectionHead-${clientId}::after {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { RoomProvider } from "@/liveblocks.config"
|
import { RoomProvider } from "@/liveblocks.config"
|
||||||
import { ClientSideSuspense } from "@liveblocks/react"
|
import { ClientSideSuspense } from "@liveblocks/react"
|
||||||
|
import Loading from "../loading"
|
||||||
|
|
||||||
export function Room({
|
export function Room({
|
||||||
id,
|
id,
|
||||||
@ -17,9 +18,9 @@ export function Room({
|
|||||||
cursor: null,
|
cursor: null,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ClientSideSuspense fallback={<div>Loading!!!!</div>}>
|
{/* <ClientSideSuspense fallback={<Loading />}> */}
|
||||||
{() => children}
|
{children}
|
||||||
</ClientSideSuspense>
|
{/* </ClientSideSuspense> */}
|
||||||
</RoomProvider>
|
</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}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center h-full space-x-4">
|
||||||
<Avatars />
|
<Avatars />
|
||||||
|
|
||||||
{isOwner ? (
|
{isOwner ? (
|
||||||
|
@ -8,7 +8,7 @@ import { useState } from "react"
|
|||||||
import New from "./new"
|
import New from "./new"
|
||||||
import { Socket } from "socket.io-client"
|
import { Socket } from "socket.io-client"
|
||||||
import Button from "@/components/ui/customButton"
|
import Button from "@/components/ui/customButton"
|
||||||
import Toggle from "@/components/ui/customToggle"
|
import { Switch } from "@/components/ui/switch"
|
||||||
|
|
||||||
export default function Sidebar({
|
export default function Sidebar({
|
||||||
files,
|
files,
|
||||||
@ -104,12 +104,20 @@ export default function Sidebar({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="flex items-center"> */}
|
<div className="flex items-center justify-between w-full">
|
||||||
<Toggle value={ai} setValue={setAi} className="w-full">
|
<div className="flex items-center">
|
||||||
<Sparkles className="h-3 w-3 mr-2" />
|
<Sparkles
|
||||||
AI Copilot
|
className={`h-4 w-4 mr-2 ${
|
||||||
</Toggle>
|
ai ? "text-indigo-500" : "text-muted-foreground"
|
||||||
{/* </div> */}
|
}`}
|
||||||
|
/>
|
||||||
|
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>
|
</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 }
|
@ -7,34 +7,3 @@ export const colors = {
|
|||||||
purple: "#7c3aed",
|
purple: "#7c3aed",
|
||||||
pink: "#db2777",
|
pink: "#db2777",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const colorClasses = {
|
|
||||||
red: {
|
|
||||||
ring: "ring-red-700",
|
|
||||||
bg: "from-red-950 to-red-600",
|
|
||||||
},
|
|
||||||
orange: {
|
|
||||||
ring: "ring-orange-700",
|
|
||||||
bg: "from-orange-950 to-orange-600",
|
|
||||||
},
|
|
||||||
yellow: {
|
|
||||||
ring: "ring-yellow-700",
|
|
||||||
bg: "from-yellow-950 to-yellow-600",
|
|
||||||
},
|
|
||||||
green: {
|
|
||||||
ring: "ring-green-700",
|
|
||||||
bg: "from-green-950 to-green-600",
|
|
||||||
},
|
|
||||||
blue: {
|
|
||||||
ring: "ring-blue-700",
|
|
||||||
bg: "from-blue-950 to-blue-600",
|
|
||||||
},
|
|
||||||
purple: {
|
|
||||||
ring: "ring-purple-700",
|
|
||||||
bg: "from-purple-950 to-purple-600",
|
|
||||||
},
|
|
||||||
pink: {
|
|
||||||
ring: "ring-pink-700",
|
|
||||||
bg: "from-pink-950 to-pink-600",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { createClient } from "@liveblocks/client"
|
import { createClient } from "@liveblocks/client"
|
||||||
import { createRoomContext, createLiveblocksContext } from "@liveblocks/react"
|
import { createRoomContext, createLiveblocksContext } from "@liveblocks/react"
|
||||||
import YLiveblocksProvider from "@liveblocks/yjs"
|
import YLiveblocksProvider from "@liveblocks/yjs"
|
||||||
|
import { colors } from "./lib/colors"
|
||||||
|
|
||||||
const client = createClient({
|
const client = createClient({
|
||||||
// publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY!,
|
// publicApiKey: process.env.NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY!,
|
||||||
@ -73,6 +74,7 @@ type UserMeta = {
|
|||||||
info: {
|
info: {
|
||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
|
color: keyof typeof colors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,60 +101,56 @@ export type AwarenessList = [number, UserAwareness][]
|
|||||||
|
|
||||||
// Room-level hooks, use inside `RoomProvider`
|
// Room-level hooks, use inside `RoomProvider`
|
||||||
export const {
|
export const {
|
||||||
suspense: {
|
RoomProvider,
|
||||||
RoomProvider,
|
useRoom,
|
||||||
useRoom,
|
useMyPresence,
|
||||||
useMyPresence,
|
useUpdateMyPresence,
|
||||||
useUpdateMyPresence,
|
useSelf,
|
||||||
useSelf,
|
useOthers,
|
||||||
useOthers,
|
useOthersMapped,
|
||||||
useOthersMapped,
|
useOthersListener,
|
||||||
useOthersListener,
|
useOthersConnectionIds,
|
||||||
useOthersConnectionIds,
|
useOther,
|
||||||
useOther,
|
useBroadcastEvent,
|
||||||
useBroadcastEvent,
|
useEventListener,
|
||||||
useEventListener,
|
useErrorListener,
|
||||||
useErrorListener,
|
useStorage,
|
||||||
useStorage,
|
useBatch,
|
||||||
useBatch,
|
useHistory,
|
||||||
useHistory,
|
useUndo,
|
||||||
useUndo,
|
useRedo,
|
||||||
useRedo,
|
useCanUndo,
|
||||||
useCanUndo,
|
useCanRedo,
|
||||||
useCanRedo,
|
useMutation,
|
||||||
useMutation,
|
useStatus,
|
||||||
useStatus,
|
useLostConnectionListener,
|
||||||
useLostConnectionListener,
|
useThreads,
|
||||||
useThreads,
|
useCreateThread,
|
||||||
useCreateThread,
|
useEditThreadMetadata,
|
||||||
useEditThreadMetadata,
|
useCreateComment,
|
||||||
useCreateComment,
|
useEditComment,
|
||||||
useEditComment,
|
useDeleteComment,
|
||||||
useDeleteComment,
|
useAddReaction,
|
||||||
useAddReaction,
|
useRemoveReaction,
|
||||||
useRemoveReaction,
|
useThreadSubscription,
|
||||||
useThreadSubscription,
|
useMarkThreadAsRead,
|
||||||
useMarkThreadAsRead,
|
useRoomNotificationSettings,
|
||||||
useRoomNotificationSettings,
|
useUpdateRoomNotificationSettings,
|
||||||
useUpdateRoomNotificationSettings,
|
|
||||||
|
|
||||||
// These hooks can be exported from either context
|
// These hooks can be exported from either context
|
||||||
useUser,
|
useUser,
|
||||||
useRoomInfo,
|
useRoomInfo,
|
||||||
},
|
|
||||||
} = createRoomContext<Presence, Storage, UserMeta, RoomEvent, ThreadMetadata>(
|
} = createRoomContext<Presence, Storage, UserMeta, RoomEvent, ThreadMetadata>(
|
||||||
client
|
client
|
||||||
)
|
)
|
||||||
|
|
||||||
// Project-level hooks, use inside `LiveblocksProvider`
|
// Project-level hooks, use inside `LiveblocksProvider`
|
||||||
export const {
|
export const {
|
||||||
suspense: {
|
LiveblocksProvider,
|
||||||
LiveblocksProvider,
|
useMarkInboxNotificationAsRead,
|
||||||
useMarkInboxNotificationAsRead,
|
useMarkAllInboxNotificationsAsRead,
|
||||||
useMarkAllInboxNotificationsAsRead,
|
useInboxNotifications,
|
||||||
useInboxNotifications,
|
useUnreadInboxNotificationsCount,
|
||||||
useUnreadInboxNotificationsCount,
|
|
||||||
},
|
|
||||||
} = createLiveblocksContext<UserMeta, ThreadMetadata>(client)
|
} = createLiveblocksContext<UserMeta, ThreadMetadata>(client)
|
||||||
|
|
||||||
export type TypedLiveblocksProvider = YLiveblocksProvider<
|
export type TypedLiveblocksProvider = YLiveblocksProvider<
|
||||||
|
30
frontend/package-lock.json
generated
30
frontend/package-lock.json
generated
@ -24,6 +24,7 @@
|
|||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
@ -1228,6 +1229,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-switch": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1",
|
||||||
|
"@radix-ui/react-use-previous": "1.0.1",
|
||||||
|
"@radix-ui/react-use-size": "1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user