From eabc9fa2f632cf3a156cf5e6e63114acc11ea242 Mon Sep 17 00:00:00 2001 From: Hamzat Victor Date: Sun, 13 Oct 2024 23:34:27 +0100 Subject: [PATCH 1/7] feat: update create new project dialog --- frontend/app/globals.css | 21 ++ frontend/components/dashboard/newProject.tsx | 212 ++++++++++++++++--- frontend/package-lock.json | 49 +++++ frontend/package.json | 2 + 4 files changed, 260 insertions(+), 24 deletions(-) diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 762a30d..9392370 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -152,3 +152,24 @@ .tab-scroll::-webkit-scrollbar { display: none; } + +.fade-r { + --mask-gradient: linear-gradient( + to right, + white 0%, + white calc(100% - var(--fade-size)), + transparent + ); + -webkit-mask-image: var(--mask-gradient); + mask-image: var(--mask-gradient); +} +.fade-l { + --mask-gradient: linear-gradient( + to left, + white var(--fade-size), + white 100%, + transparent + ); + -webkit-mask-image: var(--mask-gradient); + mask-image: var(--mask-gradient); +} diff --git a/frontend/components/dashboard/newProject.tsx b/frontend/components/dashboard/newProject.tsx index 334d232..228fa59 100644 --- a/frontend/components/dashboard/newProject.tsx +++ b/frontend/components/dashboard/newProject.tsx @@ -9,7 +9,7 @@ import { DialogTrigger, } from "@/components/ui/dialog" import Image from "next/image" -import { useState } from "react" +import { useState, useCallback, useEffect, useMemo } from "react" import { set, z } from "zod" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" @@ -34,10 +34,20 @@ import { import { useUser } from "@clerk/nextjs" import { createSandbox } from "@/lib/actions" import { useRouter } from "next/navigation" -import { Loader2 } from "lucide-react" +import { + Loader2, + ChevronRight, + ChevronLeft, + Search, + SlashSquare, +} from "lucide-react" import { Button } from "../ui/button" import { projectTemplates } from "@/lib/data" +import useEmblaCarousel from "embla-carousel-react" +import type { EmblaCarouselType } from "embla-carousel" +import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures" +import { cn } from "@/lib/utils" const formSchema = z.object({ name: z .string() @@ -57,11 +67,20 @@ export default function NewProjectModal({ open: boolean setOpen: (open: boolean) => void }) { + const router = useRouter() + const user = useUser() const [selected, setSelected] = useState("reactjs") const [loading, setLoading] = useState(false) - const router = useRouter() - - const user = useUser() + const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false }, [ + WheelGesturesPlugin(), + ]) + const { + prevBtnDisabled, + nextBtnDisabled, + onPrevButtonClick, + onNextButtonClick, + } = usePrevNextButtons(emblaApi) + const [search, setSearch] = useState("") const form = useForm>({ resolver: zodResolver(formSchema), @@ -71,6 +90,26 @@ export default function NewProjectModal({ }, }) + const handleTemplateClick = useCallback( + ({ id, index }: { id: string; index: number }) => { + setSelected(id) + emblaApi?.scrollTo(index) + }, + [emblaApi] + ) + const filteredTemplates = useMemo( + () => + projectTemplates.filter( + (item) => + item.name.toLowerCase().includes(search.toLowerCase()) || + item.description.toLowerCase().includes(search.toLowerCase()) + ), + [search, projectTemplates] + ) + const emptyTemplates = useMemo( + () => filteredTemplates.length === 0, + [filteredTemplates] + ) async function onSubmit(values: z.infer) { if (!user.isSignedIn) return @@ -80,7 +119,6 @@ export default function NewProjectModal({ const id = await createSandbox(sandboxData) router.push(`/code/${id}`) } - return ( Create A Sandbox -
- {projectTemplates.map((item) => ( - - ))} + {filteredTemplates.map((item, i) => ( + + ))} + {emptyTemplates && ( +
+

No templates found

+ +
+ )} +
+
+ +
+
+ +
+
@@ -178,3 +277,68 @@ export default function NewProjectModal({
) } + +function SearchInput({ + value, + onValueChange, +}: { + value?: string + onValueChange?: (value: string) => void +}) { + const onSubmit = useCallback((e: React.FormEvent) => { + e.preventDefault() + console.log("searching") + }, []) + return ( + + + + ) +} +const usePrevNextButtons = (emblaApi: EmblaCarouselType | undefined) => { + const [prevBtnDisabled, setPrevBtnDisabled] = useState(true) + const [nextBtnDisabled, setNextBtnDisabled] = useState(true) + + const onPrevButtonClick = useCallback(() => { + if (!emblaApi) return + emblaApi.scrollPrev() + }, [emblaApi]) + + const onNextButtonClick = useCallback(() => { + if (!emblaApi) return + emblaApi.scrollNext() + }, [emblaApi]) + + const onSelect = useCallback((emblaApi: EmblaCarouselType) => { + setPrevBtnDisabled(!emblaApi.canScrollPrev()) + setNextBtnDisabled(!emblaApi.canScrollNext()) + }, []) + + useEffect(() => { + if (!emblaApi) return + + onSelect(emblaApi) + emblaApi.on("reInit", onSelect).on("select", onSelect) + }, [emblaApi, onSelect]) + + return { + prevBtnDisabled, + nextBtnDisabled, + onPrevButtonClick, + onNextButtonClick, + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6f3df42..5f6d175 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,6 +34,8 @@ "@xterm/xterm": "^5.5.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "embla-carousel-react": "^8.3.0", + "embla-carousel-wheel-gestures": "^8.0.1", "framer-motion": "^11.2.3", "fs": "^0.0.1-security", "geist": "^1.3.0", @@ -2690,6 +2692,45 @@ "integrity": "sha512-w+9yAVHoHhysCa+gln7AzbO9CdjFcL/wN/5dd+XW/Msl2d/4+WisEaCF1nty0xbAKaxdaJfgLB2296U7zZB7BA==", "dev": true }, + "node_modules/embla-carousel": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.3.0.tgz", + "integrity": "sha512-Ve8dhI4w28qBqR8J+aMtv7rLK89r1ZA5HocwFz6uMB/i5EiC7bGI7y+AM80yAVUJw3qqaZYK7clmZMUR8kM3UA==" + }, + "node_modules/embla-carousel-react": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.3.0.tgz", + "integrity": "sha512-P1FlinFDcIvggcErRjNuVqnUR8anyo8vLMIH8Rthgofw7Nj8qTguCa2QjFAbzxAUTQTPNNjNL7yt0BGGinVdFw==", + "dependencies": { + "embla-carousel": "8.3.0", + "embla-carousel-reactive-utils": "8.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.3.0.tgz", + "integrity": "sha512-EYdhhJ302SC4Lmkx8GRsp0sjUhEN4WyFXPOk0kGu9OXZSRMmcBlRgTvHcq8eKJE1bXWBsOi1T83B+BSSVZSmwQ==", + "peerDependencies": { + "embla-carousel": "8.3.0" + } + }, + "node_modules/embla-carousel-wheel-gestures": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/embla-carousel-wheel-gestures/-/embla-carousel-wheel-gestures-8.0.1.tgz", + "integrity": "sha512-LMAnruDqDmsjL6UoQD65aLotpmfO49Fsr3H0bMi7I+BH6jbv9OJiE61kN56daKsVtCQEt0SU1MrJslbhtgF3yQ==", + "dependencies": { + "wheel-gestures": "^2.2.5" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "embla-carousel": "^8.0.0 || ~8.0.0-rc03" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -4553,6 +4594,14 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/wheel-gestures": { + "version": "2.2.48", + "resolved": "https://registry.npmjs.org/wheel-gestures/-/wheel-gestures-2.2.48.tgz", + "integrity": "sha512-f+Gy33Oa5Z14XY9679Zze+7VFhbsQfBFXodnU2x589l4kxGM9L5Y8zETTmcMR5pWOPQyRv4Z0lNax6xCO0NSlA==", + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index e82a344..34bdbfe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,6 +35,8 @@ "@xterm/xterm": "^5.5.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "embla-carousel-react": "^8.3.0", + "embla-carousel-wheel-gestures": "^8.0.1", "framer-motion": "^11.2.3", "fs": "^0.0.1-security", "geist": "^1.3.0", From a0183451ad02dae555d401afab452230b769dcbf Mon Sep 17 00:00:00 2001 From: Hamzat Victor Date: Sun, 13 Oct 2024 23:48:36 +0100 Subject: [PATCH 2/7] feat: update project image and description --- frontend/lib/data/index.ts | 6 +++--- frontend/public/project-icons/next-js.svg | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 frontend/public/project-icons/next-js.svg diff --git a/frontend/lib/data/index.ts b/frontend/lib/data/index.ts index 0965589..3877c03 100644 --- a/frontend/lib/data/index.ts +++ b/frontend/lib/data/index.ts @@ -22,15 +22,15 @@ export const projectTemplates: { { id: "nextjs", name: "NextJS", - icon: "/project-icons/node.svg", - description: "A JavaScript runtime built on the V8 JavaScript engine", + icon: "/project-icons/next-js.svg", + description: "a React framework for building full-stack web applications", disabled: false, }, { id: "streamlit", name: "Streamlit", icon: "/project-icons/python.svg", - description: "A JavaScript runtime built on the V8 JavaScript engine", + description: "A faster way to build and share data apps", disabled: false, }, ] diff --git a/frontend/public/project-icons/next-js.svg b/frontend/public/project-icons/next-js.svg new file mode 100644 index 0000000..596bf80 --- /dev/null +++ b/frontend/public/project-icons/next-js.svg @@ -0,0 +1 @@ + \ No newline at end of file From 33fc0822175bb481d5ef27dd34835ccc93c8f3fa Mon Sep 17 00:00:00 2001 From: Hamzat Victor Date: Mon, 14 Oct 2024 10:10:47 +0100 Subject: [PATCH 3/7] feat: terminal now resize appropriately --- .../components/editor/terminals/terminal.tsx | 124 +++++++++++------- 1 file changed, 75 insertions(+), 49 deletions(-) diff --git a/frontend/components/editor/terminals/terminal.tsx b/frontend/components/editor/terminals/terminal.tsx index 1187acb..83d6195 100644 --- a/frontend/components/editor/terminals/terminal.tsx +++ b/frontend/components/editor/terminals/terminal.tsx @@ -1,12 +1,13 @@ -"use client"; +"use client" -import { Terminal } from "@xterm/xterm"; -import { FitAddon } from "@xterm/addon-fit"; -import "./xterm.css"; +import { Terminal } from "@xterm/xterm" +import { FitAddon } from "@xterm/addon-fit" +import "./xterm.css" -import { useEffect, useRef, useState } from "react"; -import { Socket } from "socket.io-client"; -import { Loader2 } from "lucide-react"; +import { ElementRef, useEffect, useRef, useState } from "react" +import { Socket } from "socket.io-client" +import { Loader2 } from "lucide-react" +import { debounce } from "@/lib/utils" export default function EditorTerminal({ socket, @@ -15,16 +16,17 @@ export default function EditorTerminal({ setTerm, visible, }: { - socket: Socket; - id: string; - term: Terminal | null; - setTerm: (term: Terminal) => void; - visible: boolean; + socket: Socket + id: string + term: Terminal | null + setTerm: (term: Terminal) => void + visible: boolean }) { - const terminalRef = useRef(null); + const terminalRef = useRef>(null) + const fitAddonRef = useRef(null) useEffect(() => { - if (!terminalRef.current) return; + if (!terminalRef.current) return // console.log("new terminal", id, term ? "reusing" : "creating"); const terminal = new Terminal({ @@ -36,56 +38,80 @@ export default function EditorTerminal({ fontSize: 14, lineHeight: 1.5, letterSpacing: 0, - }); + }) - setTerm(terminal); - - return () => { - if (terminal) terminal.dispose(); - }; - }, []); + setTerm(terminal) + const dispose = () => { + terminal.dispose() + } + return dispose + }, []) useEffect(() => { - if (!term) return; + if (!term) return - if (!terminalRef.current) return; - const fitAddon = new FitAddon(); - term.loadAddon(fitAddon); - term.open(terminalRef.current); - fitAddon.fit(); + if (!terminalRef.current) return + if (!fitAddonRef.current) { + const fitAddon = new FitAddon() + term.loadAddon(fitAddon) + term.open(terminalRef.current) + fitAddon.fit() + fitAddonRef.current = fitAddon + } const disposableOnData = term.onData((data) => { - socket.emit("terminalData", id, data); - }); + socket.emit("terminalData", id, data) + }) const disposableOnResize = term.onResize((dimensions) => { - // const terminal_size = { - // width: dimensions.cols, - // height: dimensions.rows, - // }; - fitAddon.fit(); - socket.emit("terminalResize", dimensions); - }); + fitAddonRef.current?.fit() + socket.emit("terminalResize", dimensions) + }) + const resizeObserver = new ResizeObserver( + debounce((entries) => { + if (!fitAddonRef.current || !terminalRef.current) return + const entry = entries[0] + if (!entry) return + + const { width, height } = entry.contentRect + + // Only call fit if the size has actually changed + if ( + width !== terminalRef.current.offsetWidth || + height !== terminalRef.current.offsetHeight + ) { + try { + fitAddonRef.current.fit() + } catch (err) { + console.error("Error during fit:", err) + } + } + }, 50) // Debounce for 50ms + ) + + // start observing for resize + resizeObserver.observe(terminalRef.current) return () => { - disposableOnData.dispose(); - disposableOnResize.dispose(); - }; - }, [term, terminalRef.current]); + disposableOnData.dispose() + disposableOnResize.dispose() + resizeObserver.disconnect() + } + }, [term, terminalRef.current]) useEffect(() => { - if (!term) return; + if (!term) return const handleTerminalResponse = (response: { id: string; data: string }) => { if (response.id === id) { - term.write(response.data); + term.write(response.data) } - }; - socket.on("terminalResponse", handleTerminalResponse); - + } + socket.on("terminalResponse", handleTerminalResponse) + return () => { - socket.off("terminalResponse", handleTerminalResponse); - }; - }, [term, id, socket]); + socket.off("terminalResponse", handleTerminalResponse) + } + }, [term, id, socket]) return ( <> @@ -102,5 +128,5 @@ export default function EditorTerminal({ ) : null} - ); + ) } From fa5d1e9a57674f855f45399aeb464a335823d2cc Mon Sep 17 00:00:00 2001 From: Hamzat Victor Date: Mon, 14 Oct 2024 12:06:54 +0100 Subject: [PATCH 4/7] feat: add skeleton loader to file explorer --- frontend/components/editor/index.tsx | 203 ++++++++++--------- frontend/components/editor/loading/index.tsx | 6 +- frontend/components/editor/sidebar/index.tsx | 99 ++++----- 3 files changed, 159 insertions(+), 149 deletions(-) diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index 415b552..eabbe4d 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -36,7 +36,7 @@ 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 { cn, deepMerge } from "@/lib/utils" export default function CodeEditor({ userData, @@ -62,9 +62,9 @@ export default function CodeEditor({ // This heartbeat is critical to preventing the E2B sandbox from timing out useEffect(() => { // 10000 ms = 10 seconds - const interval = setInterval(() => socket?.emit("heartbeat"), 10000); - return () => clearInterval(interval); - }, [socket]); + const interval = setInterval(() => socket?.emit("heartbeat"), 10000) + return () => clearInterval(interval) + }, [socket]) //Preview Button state const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true) @@ -80,7 +80,7 @@ export default function CodeEditor({ const [activeFileContent, setActiveFileContent] = useState("") const [deletingFolderId, setDeletingFolderId] = useState("") // Added this state to track the most recent content for each file - const [fileContents, setFileContents] = useState>({}); + const [fileContents, setFileContents] = useState>({}) // Editor state const [editorLanguage, setEditorLanguage] = useState("plaintext") @@ -416,7 +416,7 @@ export default function CodeEditor({ }) } }, [generate.show]) - + // Suggestion widget effect useEffect(() => { if (!suggestionRef.current || !editorRef) return @@ -462,16 +462,17 @@ export default function CodeEditor({ const model = editorRef?.getModel() // added this because it was giving client side exception - Illegal value for lineNumber when opening an empty file 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. - 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. - 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). // 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. if (line.trim() !== "") { - decorations.instance?.clear(); + decorations.instance?.clear() return } } @@ -497,28 +498,28 @@ export default function CodeEditor({ debounce((activeFileId: string | undefined) => { if (activeFileId) { // Get the current content of the file - const content = fileContents[activeFileId]; + const content = fileContents[activeFileId] // Mark the file as saved in the tabs setTabs((prev) => prev.map((tab) => tab.id === activeFileId ? { ...tab, saved: true } : tab ) - ); - console.log(`Saving file...${activeFileId}`); - console.log(`Saving file...${content}`); - socket?.emit("saveFile", activeFileId, content); + ) + console.log(`Saving file...${activeFileId}`) + console.log(`Saving file...${content}`) + socket?.emit("saveFile", activeFileId, content) } }, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000), [socket, fileContents] - ); + ) // Keydown event listener to trigger file save on Ctrl+S or Cmd+S useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === "s" && (e.metaKey || e.ctrlKey)) { e.preventDefault() - debouncedSaveData(activeFileId); + debouncedSaveData(activeFileId) } } document.addEventListener("keydown", down) @@ -615,7 +616,7 @@ export default function CodeEditor({ // Socket event listener effect useEffect(() => { - const onConnect = () => { } + const onConnect = () => {} const onDisconnect = () => { setTerminals([]) @@ -685,46 +686,49 @@ export default function CodeEditor({ } // 300ms debounce delay, adjust as needed const selectFile = (tab: TTab) => { - if (tab.id === activeFileId) return; - - setGenerate((prev) => ({ ...prev, show: false })); - + if (tab.id === activeFileId) return + + setGenerate((prev) => ({ ...prev, show: false })) + // 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) => { if (exists) { // If the tab exists, make it the active tab - setActiveFileId(exists.id); - return prev; + setActiveFileId(exists.id) + return prev } // 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 (fileContents[tab.id]) { - setActiveFileContent(fileContents[tab.id]); + setActiveFileContent(fileContents[tab.id]) } else { // Otherwise, fetch the content of the file and cache it debouncedGetFile(tab.id, (response: string) => { - setFileContents(prev => ({ ...prev, [tab.id]: response })); - setActiveFileContent(response); - }); + setFileContents((prev) => ({ ...prev, [tab.id]: response })) + setActiveFileContent(response) + }) } - + // 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 - setActiveFileId(tab.id); - }; + setActiveFileId(tab.id) + } // Added this effect to update fileContents when the editor content changes useEffect(() => { if (activeFileId) { // 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 const closeTab = (id: string) => { @@ -740,8 +744,8 @@ export default function CodeEditor({ ? numTabs === 1 ? null : index < numTabs - 1 - ? tabs[index + 1].id - : tabs[index - 1].id + ? tabs[index + 1].id + : tabs[index - 1].id : activeFileId setTabs((prev) => prev.filter((t) => t.id !== id)) @@ -834,7 +838,7 @@ export default function CodeEditor({ { }} + setOpen={() => {}} /> @@ -862,7 +866,10 @@ export default function CodeEditor({ )} -
+
{generate.show ? ( ) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643 - clerk.loaded ? ( - <> - {provider && userInfo ? ( - - ) : null} - { - // If the new content is different from the cached content, update it - if (value !== fileContents[activeFileId]) { - setActiveFileContent(value ?? ""); // Update the active file content - // Mark the file as unsaved by setting 'saved' to false - setTabs((prev) => - prev.map((tab) => - tab.id === activeFileId - ? { ...tab, saved: false } - : tab - ) + clerk.loaded ? ( + <> + {provider && userInfo ? ( + + ) : null} + { + // If the new content is different from the cached content, update it + if (value !== fileContents[activeFileId]) { + setActiveFileContent(value ?? "") // Update the active file content + // Mark the file as unsaved by setting 'saved' to false + setTabs((prev) => + prev.map((tab) => + tab.id === activeFileId + ? { ...tab, saved: false } + : tab ) - } else { - // If the content matches the cached content, mark the file as saved - setTabs((prev) => - prev.map((tab) => - tab.id === activeFileId - ? { ...tab, saved: true } - : tab - ) + ) + } else { + // If the content matches the cached content, mark the file as saved + setTabs((prev) => + prev.map((tab) => + tab.id === activeFileId + ? { ...tab, saved: true } + : tab ) - } - }} - options={{ - tabSize: 2, - minimap: { - enabled: false, - }, - padding: { - bottom: 4, - top: 4, - }, - scrollBeyondLastLine: false, - fixedOverflowWidgets: true, - fontFamily: "var(--font-geist-mono)", - }} - theme="vs-dark" - value={activeFileContent} - /> - - ) : ( -
- - Waiting for Clerk to load... -
- )} + ) + } + }} + options={{ + tabSize: 2, + minimap: { + enabled: false, + }, + padding: { + bottom: 4, + top: 4, + }, + scrollBeyondLastLine: false, + fixedOverflowWidgets: true, + fontFamily: "var(--font-geist-mono)", + }} + theme="vs-dark" + value={activeFileContent} + /> + + ) : ( +
+ + Waiting for Clerk to load... +
+ )}
diff --git a/frontend/components/editor/loading/index.tsx b/frontend/components/editor/loading/index.tsx index eebabc2..34f8378 100644 --- a/frontend/components/editor/loading/index.tsx +++ b/frontend/components/editor/loading/index.tsx @@ -84,8 +84,10 @@ export default function Loading({
-
- +
+ {new Array(6).fill(0).map((_, i) => ( + + ))}
diff --git a/frontend/components/editor/sidebar/index.tsx b/frontend/components/editor/sidebar/index.tsx index 45fa643..a5dca5f 100644 --- a/frontend/components/editor/sidebar/index.tsx +++ b/frontend/components/editor/sidebar/index.tsx @@ -1,4 +1,4 @@ -"use client"; +"use client" import { FilePlus, @@ -7,20 +7,21 @@ import { MonitorPlay, Search, Sparkles, -} from "lucide-react"; -import SidebarFile from "./file"; -import SidebarFolder from "./folder"; -import { Sandbox, TFile, TFolder, TTab } from "@/lib/types"; -import { useEffect, useRef, useState } from "react"; -import New from "./new"; -import { Socket } from "socket.io-client"; -import { Switch } from "@/components/ui/switch"; +} from "lucide-react" +import SidebarFile from "./file" +import SidebarFolder from "./folder" +import { Sandbox, TFile, TFolder, TTab } from "@/lib/types" +import { useEffect, useRef, useState } from "react" +import New from "./new" +import { Socket } from "socket.io-client" +import { Switch } from "@/components/ui/switch" import { dropTargetForElements, monitorForElements, -} from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; -import Button from "@/components/ui/customButton"; +} from "@atlaskit/pragmatic-drag-and-drop/element/adapter" +import Button from "@/components/ui/customButton" +import { Skeleton } from "@/components/ui/skeleton" export default function Sidebar({ sandboxData, @@ -34,75 +35,73 @@ export default function Sidebar({ addNew, deletingFolderId, }: { - sandboxData: Sandbox; - files: (TFile | TFolder)[]; - selectFile: (tab: TTab) => void; + sandboxData: Sandbox + files: (TFile | TFolder)[] + selectFile: (tab: TTab) => void handleRename: ( id: string, newName: string, oldName: string, type: "file" | "folder" - ) => boolean; - handleDeleteFile: (file: TFile) => void; - handleDeleteFolder: (folder: TFolder) => void; - socket: Socket; - setFiles: (files: (TFile | TFolder)[]) => void; - addNew: (name: string, type: "file" | "folder") => void; - deletingFolderId: string; + ) => boolean + handleDeleteFile: (file: TFile) => void + handleDeleteFolder: (folder: TFolder) => void + socket: Socket + setFiles: (files: (TFile | TFolder)[]) => void + addNew: (name: string, type: "file" | "folder") => void + deletingFolderId: string }) { - const ref = useRef(null); // drop target - - const [creatingNew, setCreatingNew] = useState<"file" | "folder" | null>( - null - ); - const [movingId, setMovingId] = useState(""); + const ref = useRef(null) // drop target + const [creatingNew, setCreatingNew] = useState<"file" | "folder" | null>(null) + const [movingId, setMovingId] = useState("") + console.log(files) useEffect(() => { - const el = ref.current; + const el = ref.current if (el) { return dropTargetForElements({ element: el, getData: () => ({ id: `projects/${sandboxData.id}` }), canDrop: ({ source }) => { - const file = files.find((child) => child.id === source.data.id); - return !file; + const file = files.find((child) => child.id === source.data.id) + return !file }, - }); + }) } - }, [files]); + }, [files]) useEffect(() => { return monitorForElements({ onDrop({ source, location }) { - const destination = location.current.dropTargets[0]; + const destination = location.current.dropTargets[0] if (!destination) { - return; + return } - const fileId = source.data.id as string; - const folderId = destination.data.id as string; + const fileId = source.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) { - return; + return } - console.log("move file", fileId, "to folder", folderId); + console.log("move file", fileId, "to folder", folderId) - setMovingId(fileId); + setMovingId(fileId) socket.emit( "moveFile", fileId, folderId, (response: (TFolder | TFile)[]) => { - setFiles(response); - setMovingId(""); + setFiles(response) + setMovingId("") } - ); + ) }, - }); - }, []); + }) + }, []) return (
@@ -138,8 +137,10 @@ export default function Sidebar({ } rounded-sm w-full mt-1 flex flex-col`} > */} {files.length === 0 ? ( -
- +
+ {new Array(6).fill(0).map((_, i) => ( + + ))}
) : ( <> @@ -172,7 +173,7 @@ export default function Sidebar({ socket={socket} type={creatingNew} stopEditing={() => { - setCreatingNew(null); + setCreatingNew(null) }} addNew={addNew} /> @@ -187,5 +188,5 @@ export default function Sidebar({ */}
- ); + ) } From 6e14f676cf5a0722298c88dac7eab0346fd2ef4a Mon Sep 17 00:00:00 2001 From: Hamzat Victor Date: Mon, 14 Oct 2024 12:09:17 +0100 Subject: [PATCH 5/7] feat: sort files in explorer --- frontend/components/editor/sidebar/index.tsx | 11 +++++---- frontend/lib/utils.ts | 25 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/frontend/components/editor/sidebar/index.tsx b/frontend/components/editor/sidebar/index.tsx index a5dca5f..e0b8c86 100644 --- a/frontend/components/editor/sidebar/index.tsx +++ b/frontend/components/editor/sidebar/index.tsx @@ -11,7 +11,7 @@ import { import SidebarFile from "./file" import SidebarFolder from "./folder" import { Sandbox, TFile, TFolder, TTab } from "@/lib/types" -import { useEffect, useRef, useState } from "react" +import { useEffect, useMemo, useRef, useState } from "react" import New from "./new" import { Socket } from "socket.io-client" import { Switch } from "@/components/ui/switch" @@ -22,6 +22,7 @@ import { } from "@atlaskit/pragmatic-drag-and-drop/element/adapter" import Button from "@/components/ui/customButton" import { Skeleton } from "@/components/ui/skeleton" +import { sortFileExplorer } from "@/lib/utils" export default function Sidebar({ sandboxData, @@ -55,7 +56,9 @@ export default function Sidebar({ const [creatingNew, setCreatingNew] = useState<"file" | "folder" | null>(null) const [movingId, setMovingId] = useState("") - console.log(files) + const sortedFiles = useMemo(() => { + return sortFileExplorer(files) + }, [files]) useEffect(() => { const el = ref.current @@ -136,7 +139,7 @@ export default function Sidebar({ isDraggedOver ? "bg-secondary/50" : "" } rounded-sm w-full mt-1 flex flex-col`} > */} - {files.length === 0 ? ( + {sortedFiles.length === 0 ? (
{new Array(6).fill(0).map((_, i) => ( @@ -144,7 +147,7 @@ export default function Sidebar({
) : ( <> - {files.map((child) => + {sortedFiles.map((child) => child.type === "file" ? ( { const isObject = (item: any) => { return item && typeof item === "object" && !Array.isArray(item) } + +export function sortFileExplorer( + items: (TFile | TFolder)[] +): (TFile | TFolder)[] { + return items + .sort((a, b) => { + // First, sort by type (folders before files) + if (a.type !== b.type) { + return a.type === "folder" ? -1 : 1 + } + + // Then, sort alphabetically by name + return a.name.localeCompare(b.name, undefined, { sensitivity: "base" }) + }) + .map((item) => { + // If it's a folder, recursively sort its children + if (item.type === "folder") { + return { + ...item, + children: sortFileExplorer(item.children), + } + } + return item + }) +} From 82c6a1288308bea4bdd6653eb72bcdc621631541 Mon Sep 17 00:00:00 2001 From: Hamzat Victor Date: Mon, 14 Oct 2024 12:15:46 +0100 Subject: [PATCH 6/7] chore: remove unsused styles --- frontend/app/globals.css | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 9392370..762a30d 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -152,24 +152,3 @@ .tab-scroll::-webkit-scrollbar { display: none; } - -.fade-r { - --mask-gradient: linear-gradient( - to right, - white 0%, - white calc(100% - var(--fade-size)), - transparent - ); - -webkit-mask-image: var(--mask-gradient); - mask-image: var(--mask-gradient); -} -.fade-l { - --mask-gradient: linear-gradient( - to left, - white var(--fade-size), - white 100%, - transparent - ); - -webkit-mask-image: var(--mask-gradient); - mask-image: var(--mask-gradient); -} From 3c4e8ec2de5c1196287f7bfbc191da60cf9b69a3 Mon Sep 17 00:00:00 2001 From: Hamzat Victor Date: Mon, 14 Oct 2024 12:21:20 +0100 Subject: [PATCH 7/7] fix: resolve border clipping issue --- frontend/components/dashboard/newProject.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/components/dashboard/newProject.tsx b/frontend/components/dashboard/newProject.tsx index 228fa59..b793fc2 100644 --- a/frontend/components/dashboard/newProject.tsx +++ b/frontend/components/dashboard/newProject.tsx @@ -154,9 +154,12 @@ export default function NewProjectModal({ id: item.id, index: i, })} - className={`${ - selected === item.id ? "border-foreground" : "border-border" - } rounded-md border bg-card text-card-foreground shadow text-left p-4 flex flex-col transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed`} + className={cn( + selected === item.id + ? "shadow-foreground" + : "shadow-border", + "shadow-[0_0_0_1px_inset] rounded-md border bg-card text-card-foreground text-left p-4 flex flex-col transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 disabled:cursor-not-allowed" + )} >