feat(playground): add code editor component and integrate file retrieval functionality
This commit is contained in:
parent
ac7e34dfc5
commit
668145a9c7
12
package.json
12
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
52
src/app/playground/components/code-editor.tsx
Normal file
52
src/app/playground/components/code-editor.tsx
Normal 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);
|
||||
})();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
23
src/store/codeEditorStore.ts
Normal file
23
src/store/codeEditorStore.ts
Normal 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 }),
|
||||
}));
|
Loading…
Reference in New Issue
Block a user