feat(playground): add code editor component and integrate file retrieval functionality

This commit is contained in:
ngc2207 2024-12-15 12:33:39 +08:00
parent ac7e34dfc5
commit 668145a9c7
6 changed files with 139 additions and 14 deletions

View File

@ -9,6 +9,8 @@
"lint": "next lint"
},
"dependencies": {
"@fontsource-variable/fira-code": "^5.1.0",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.1",
@ -37,13 +39,15 @@
"zustand": "^5.0.2"
},
"devDependencies": {
"typescript": "^5",
"@shikijs/monaco": "^1.24.2",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8",
"eslint-config-next": "15.0.4"
"eslint-config-next": "15.0.4",
"postcss": "^8",
"shiki": "^1.24.2",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}

View File

@ -15,10 +15,29 @@ export async function retrieveTreeStructure(
return response.data.tree || [];
} catch (error) {
if ((error as APIError).message) {
console.error("Gitea API Error", error);
console.error("Gitea API [getTree] Error", error);
} else {
console.error("Unexpected Error", error);
}
throw new Error("Failed to retrieve tree structure");
}
}
export async function retrieveFileContent(
owner: string,
repo: string,
path: string
): Promise<{ encoding: string; content: string }> {
try {
const response = await api.repos.repoGetContents(owner, repo, path);
const { encoding, content } = response.data;
return { encoding: encoding || "", content: content || "" };
} catch (error) {
if ((error as APIError).message) {
console.error("Gitea API [repoGetContents] Error", error);
} else {
console.error("Unexpected Error", error);
}
throw new Error("Failed to retrieve file contents");
}
}

View File

@ -0,0 +1,52 @@
"use client";
import { useRef } from "react";
import "@fontsource-variable/fira-code";
import { createHighlighter } from "shiki";
import type { editor } from "monaco-editor";
import { shikiToMonaco } from "@shikijs/monaco";
import { useCodeEditorStore } from "@/store/codeEditorStore";
import MonacoEditor, { type Monaco } from "@monaco-editor/react";
export function CodeEditor() {
const monacoRef = useRef<Monaco | null>(null);
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const { lang, theme, value, isLigature } = useCodeEditorStore();
return (
<MonacoEditor
language={lang}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 14,
fontFamily: "Fira Code Variable, monospace",
tabSize: 4,
showFoldingControls: "always",
fontLigatures: isLigature,
automaticLayout: true,
}}
value={value}
onMount={(editor, monaco) => {
editorRef.current = editor;
monacoRef.current = monaco;
void (async () => {
const ADDITIONAL_LANGUAGES = [
"c",
"java",
] as const satisfies Parameters<typeof createHighlighter>[0]["langs"];
for (const lang of ADDITIONAL_LANGUAGES) {
monacoRef.current?.languages.register({ id: lang });
}
const highlighter = await createHighlighter({
themes: [theme],
langs: ADDITIONAL_LANGUAGES,
});
shikiToMonaco(highlighter, monacoRef.current);
})();
}}
/>
);
}

View File

@ -22,6 +22,7 @@ import {
import * as actions from "@/actions";
import { useSession } from "next-auth/react";
import { COriginal, JavaOriginal } from "devicons-react";
import { useCodeEditorStore } from "@/store/codeEditorStore";
import { ChevronRight, File, Folder, FolderOpen } from "lucide-react";
interface FileTree {
@ -125,7 +126,9 @@ export function PlaygroundSidebar({
}
function Tree({ item }: { item: FileTree }) {
const { name, type, children } = item;
const { data: session } = useSession();
const username = session?.user?.name ?? "";
const { name, path, type, children } = item;
const fileExtension = name.split(".").pop();
let fileIcon = <File />;
@ -137,10 +140,37 @@ function Tree({ item }: { item: FileTree }) {
}
const [isOpen, setIsOpen] = React.useState(false);
const { setLang, setValue } = useCodeEditorStore();
const langMap: { [key: string]: string } = {
c: "c",
java: "java",
};
const handleFileClick = async () => {
if (type === "blob") {
const fileContentsResponse = await actions.retrieveFileContent(
username,
"playground",
path
);
const { encoding, content } = fileContentsResponse;
const decodedContent = Buffer.from(
content,
encoding as BufferEncoding
).toString("utf-8");
const lang = langMap[fileExtension ?? ""] || "plaintext";
setLang(lang);
setValue(decodedContent);
}
};
if (type === "blob") {
return (
<SidebarMenuButton className="data-[active=true]:bg-transparent">
<SidebarMenuButton
className="data-[active=true]:bg-transparent"
onClick={handleFileClick}
>
{fileIcon}
{name}
</SidebarMenuButton>

View File

@ -1,12 +1,9 @@
import { CodeEditor } from "./components/code-editor";
export default function PlaygroundPage() {
return (
<div className="flex flex-1 flex-col gap-4 p-4">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
</div>
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
<div className="flex flex-1 flex-col">
<CodeEditor />
</div>
);
}

View File

@ -0,0 +1,23 @@
import { create } from "zustand";
interface CodeEditorState {
lang: string;
theme: string;
value: string;
isLigature: boolean;
setLang: (lang: string) => void;
setTheme: (theme: string) => void;
setValue: (value: string) => void;
setIsLigature: (isLigature: boolean) => void;
}
export const useCodeEditorStore = create<CodeEditorState>((set) => ({
lang: "c",
theme: "one-dark-pro",
value: "",
isLigature: false,
setLang: (lang) => set({ lang }),
setTheme: (theme) => set({ theme }),
setValue: (value) => set({ value }),
setIsLigature: (isLigature) => set({ isLigature }),
}));