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"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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-alert-dialog": "^1.1.2",
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
"@radix-ui/react-collapsible": "^1.1.1",
|
"@radix-ui/react-collapsible": "^1.1.1",
|
||||||
@ -37,13 +39,15 @@
|
|||||||
"zustand": "^5.0.2"
|
"zustand": "^5.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@shikijs/monaco": "^1.24.2",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"postcss": "^8",
|
|
||||||
"tailwindcss": "^3.4.1",
|
|
||||||
"eslint": "^8",
|
"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 || [];
|
return response.data.tree || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((error as APIError).message) {
|
if ((error as APIError).message) {
|
||||||
console.error("Gitea API Error", error);
|
console.error("Gitea API [getTree] Error", error);
|
||||||
} else {
|
} else {
|
||||||
console.error("Unexpected Error", error);
|
console.error("Unexpected Error", error);
|
||||||
}
|
}
|
||||||
throw new Error("Failed to retrieve tree structure");
|
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 * as actions from "@/actions";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { COriginal, JavaOriginal } from "devicons-react";
|
import { COriginal, JavaOriginal } from "devicons-react";
|
||||||
|
import { useCodeEditorStore } from "@/store/codeEditorStore";
|
||||||
import { ChevronRight, File, Folder, FolderOpen } from "lucide-react";
|
import { ChevronRight, File, Folder, FolderOpen } from "lucide-react";
|
||||||
|
|
||||||
interface FileTree {
|
interface FileTree {
|
||||||
@ -125,7 +126,9 @@ export function PlaygroundSidebar({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Tree({ item }: { item: FileTree }) {
|
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();
|
const fileExtension = name.split(".").pop();
|
||||||
|
|
||||||
let fileIcon = <File />;
|
let fileIcon = <File />;
|
||||||
@ -137,10 +140,37 @@ function Tree({ item }: { item: FileTree }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
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") {
|
if (type === "blob") {
|
||||||
return (
|
return (
|
||||||
<SidebarMenuButton className="data-[active=true]:bg-transparent">
|
<SidebarMenuButton
|
||||||
|
className="data-[active=true]:bg-transparent"
|
||||||
|
onClick={handleFileClick}
|
||||||
|
>
|
||||||
{fileIcon}
|
{fileIcon}
|
||||||
{name}
|
{name}
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
|
import { CodeEditor } from "./components/code-editor";
|
||||||
|
|
||||||
export default function PlaygroundPage() {
|
export default function PlaygroundPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4">
|
<div className="flex flex-1 flex-col">
|
||||||
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
<CodeEditor />
|
||||||
<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>
|
</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