feat: task
This commit is contained in:
parent
f7ad1ac2ab
commit
ca0e7f71b3
|
@ -0,0 +1,19 @@
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import React from "react";
|
||||||
|
import Header from "@/components/task/header";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Task | Skilled Ai",
|
||||||
|
description: "Skilled Ai",
|
||||||
|
};
|
||||||
|
|
||||||
|
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-rows-[53px_1fr] min-h-screen">
|
||||||
|
<Header/>
|
||||||
|
<main className="">{children}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
|
@ -0,0 +1,170 @@
|
||||||
|
import React from "react";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import {
|
||||||
|
ResizableHandle,
|
||||||
|
ResizablePanel,
|
||||||
|
ResizablePanelGroup,
|
||||||
|
} from "@/components/ui/resizable";
|
||||||
|
import { unified } from "unified";
|
||||||
|
import remarkParse from "remark-parse";
|
||||||
|
import remarkRehype from "remark-rehype";
|
||||||
|
import rehypeStringify from "rehype-stringify";
|
||||||
|
import rehypePrettyCode from "rehype-pretty-code";
|
||||||
|
import markdownStyles from "@/styles/markdown-styles.module.css";
|
||||||
|
import { tutorialData } from "@/constants/tutorial-data";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import EditorWindow from "@/components/task/editor-window";
|
||||||
|
import { LANGUAGE_VERSIONS } from "@/constants/language-options";
|
||||||
|
|
||||||
|
async function main(tutorialCode: string) {
|
||||||
|
const file = await unified()
|
||||||
|
.use(remarkParse)
|
||||||
|
.use(remarkRehype)
|
||||||
|
.use(rehypePrettyCode, {})
|
||||||
|
.use(rehypeStringify)
|
||||||
|
.process(tutorialCode);
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
const languages = Object.keys(LANGUAGE_VERSIONS);
|
||||||
|
|
||||||
|
const Page = async ({ params }: { params: { taskId: string[] } }) => {
|
||||||
|
if (!params.taskId[0] || !languages.includes(params.taskId[0])) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const langTutorial = tutorialData.find(
|
||||||
|
(t) => t.language === params.taskId[0],
|
||||||
|
)!;
|
||||||
|
|
||||||
|
const tutorial = langTutorial.tutorial.find(
|
||||||
|
(t) => t.id === (Number(params.taskId[1]) || 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
const tutorialIndex = langTutorial.tutorial.findIndex(
|
||||||
|
(t) => t.id === (Number(params.taskId[1]) || 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!tutorial || tutorialIndex === -1) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = await main(tutorial?.content || "");
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="md:hidden w-full h-full">
|
||||||
|
<ResizablePanelGroup
|
||||||
|
direction="vertical"
|
||||||
|
className="max-w-[1440px] mx-auto h-full"
|
||||||
|
>
|
||||||
|
<ResizablePanel defaultSize={50}>
|
||||||
|
<ScrollArea className="h-[calc(100vh-55px)] border-b">
|
||||||
|
<div className="px-4 py-6 flex flex-col gap-4 h-full mb-[50vh]">
|
||||||
|
<div
|
||||||
|
className={cn("grow", markdownStyles["markdown"])}
|
||||||
|
dangerouslySetInnerHTML={{ __html: String(code) }}
|
||||||
|
></div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="mb-6">
|
||||||
|
{langTutorial?.tutorial[tutorialIndex - 1] && (
|
||||||
|
<Link
|
||||||
|
href={`/dashboard/task/${params.taskId[0]}/${
|
||||||
|
Number(params.taskId[1]) - 1
|
||||||
|
}`}
|
||||||
|
className={buttonVariants({
|
||||||
|
variant: "outline",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Prev
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
{langTutorial?.tutorial[tutorialIndex + 1] && (
|
||||||
|
<Link
|
||||||
|
href={`/dashboard/task/${params.taskId[0]}/${
|
||||||
|
Number(params.taskId[1]) + 1
|
||||||
|
}`}
|
||||||
|
className={buttonVariants({ variant: "outline" })}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</ResizablePanel>
|
||||||
|
<ResizableHandle withHandle />
|
||||||
|
<ResizablePanel defaultSize={50}>
|
||||||
|
<EditorWindow
|
||||||
|
language={params.taskId[0]}
|
||||||
|
step={Number(params.taskId[1]) || 1}
|
||||||
|
/>
|
||||||
|
</ResizablePanel>
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:block w-full h-full">
|
||||||
|
<ResizablePanelGroup
|
||||||
|
direction="horizontal"
|
||||||
|
className="max-w-[1440px] mx-auto h-full"
|
||||||
|
>
|
||||||
|
<ResizablePanel defaultSize={50}>
|
||||||
|
<ScrollArea className="h-[calc(100vh-55px)]">
|
||||||
|
<div className="px-4 py-6 flex flex-col gap-4 h-full">
|
||||||
|
<div
|
||||||
|
className={cn("grow", markdownStyles["markdown"])}
|
||||||
|
dangerouslySetInnerHTML={{ __html: String(code) }}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="mb-6">
|
||||||
|
{langTutorial?.tutorial[tutorialIndex - 1] && (
|
||||||
|
<Link
|
||||||
|
href={`/dashboard/task/${params.taskId[0]}/${
|
||||||
|
Number(params.taskId[1]) - 1
|
||||||
|
}`}
|
||||||
|
className={buttonVariants({
|
||||||
|
variant: "outline",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Prev
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
{langTutorial?.tutorial[tutorialIndex + 1] && (
|
||||||
|
<Link
|
||||||
|
href={`/dashboard/task/${params.taskId[0]}/${
|
||||||
|
Number(params.taskId[1]) + 1
|
||||||
|
}`}
|
||||||
|
className={buttonVariants({ variant: "outline" })}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</ResizablePanel>
|
||||||
|
<ResizableHandle withHandle />
|
||||||
|
<ResizablePanel defaultSize={50}>
|
||||||
|
<EditorWindow
|
||||||
|
language={params.taskId[0]}
|
||||||
|
step={Number(params.taskId[1]) || 1}
|
||||||
|
/>
|
||||||
|
</ResizablePanel>
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -0,0 +1,135 @@
|
||||||
|
import React, { useCallback } from "react";
|
||||||
|
import Editor from "@monaco-editor/react";
|
||||||
|
|
||||||
|
const activateMonacoJSXHighlighter = async (monacoEditor: any, monaco: any) => {
|
||||||
|
// monaco-jsx-highlighter depends on these in addition to Monaco and an instance of a Monaco Editor.
|
||||||
|
const { default: traverse } = await import("@babel/traverse");
|
||||||
|
const { parse } = await import("@babel/parser");
|
||||||
|
// >>> The star of the show =P >>>
|
||||||
|
const {
|
||||||
|
default: MonacoJSXHighlighter,
|
||||||
|
JSXTypes,
|
||||||
|
makeBabelParse, //By @HaimCandiTech
|
||||||
|
} = await import(
|
||||||
|
// @ts-ignore
|
||||||
|
"monaco-jsx-highlighter" // Note: there is a polyfilled version alongside the regular version.
|
||||||
|
); // For example, starting with 2.0.2, 2.0.2-polyfilled is also available.
|
||||||
|
|
||||||
|
const parseJSX = makeBabelParse(parse, true); // param0:Babel's parse, param1: default config for JSX syntax (false), TSX (true).
|
||||||
|
// Instantiate the highlighter
|
||||||
|
const monacoJSXHighlighter = new MonacoJSXHighlighter(
|
||||||
|
monaco, // references Range and other APIs
|
||||||
|
parseJSX, // obtains an AST, internally passes to parse options: {...options, sourceType: "module",plugins: ["jsx"],errorRecovery: true}
|
||||||
|
traverse, // helps collecting the JSX expressions within the AST
|
||||||
|
monacoEditor, // highlights the content of that editor via decorations
|
||||||
|
);
|
||||||
|
// Start the JSX highlighting and get the dispose function
|
||||||
|
let disposeJSXHighlighting =
|
||||||
|
monacoJSXHighlighter.highlightOnDidChangeModelContent();
|
||||||
|
// Enhance monaco's editor.action.commentLine with JSX commenting and get its disposer
|
||||||
|
let disposeJSXCommenting = monacoJSXHighlighter.addJSXCommentCommand();
|
||||||
|
// <<< You are all set. >>>
|
||||||
|
|
||||||
|
// Optional: customize the color font in JSX texts (style class JSXElement.JSXText.tastyPizza from ./index.css)
|
||||||
|
JSXTypes.JSXText.options.inlineClassName = "JSXElement.JSXText.tastyPizza";
|
||||||
|
// more details here: https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IModelDecorationOptions.html
|
||||||
|
// console.log(
|
||||||
|
// "Customize each JSX expression type's options, they must match monaco.editor.IModelDecorationOptions:",
|
||||||
|
// JSXTypes,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// This example's shorthands for toggling actions
|
||||||
|
const toggleJSXHighlighting = () => {
|
||||||
|
if (disposeJSXHighlighting) {
|
||||||
|
disposeJSXHighlighting();
|
||||||
|
disposeJSXHighlighting = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
disposeJSXHighlighting =
|
||||||
|
monacoJSXHighlighter.highlightOnDidChangeModelContent();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleJSXCommenting = () => {
|
||||||
|
if (disposeJSXCommenting) {
|
||||||
|
disposeJSXCommenting();
|
||||||
|
disposeJSXCommenting = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
disposeJSXCommenting = monacoJSXHighlighter.addJSXCommentCommand();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isToggleJSXHighlightingOn = () => !!disposeJSXHighlighting;
|
||||||
|
const isToggleJSXCommentingOn = () => !!disposeJSXCommenting;
|
||||||
|
|
||||||
|
return {
|
||||||
|
monacoJSXHighlighter,
|
||||||
|
toggleJSXHighlighting,
|
||||||
|
toggleJSXCommenting,
|
||||||
|
isToggleJSXHighlightingOn,
|
||||||
|
isToggleJSXCommentingOn,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type CodeEditorProps = {
|
||||||
|
// code: string;
|
||||||
|
defaultCode?: string;
|
||||||
|
onChange: (value: string | undefined) => void;
|
||||||
|
theme: string;
|
||||||
|
language: string;
|
||||||
|
codeValue: string;
|
||||||
|
};
|
||||||
|
export function CodeEditor({
|
||||||
|
defaultCode,
|
||||||
|
onChange,
|
||||||
|
theme,
|
||||||
|
language,
|
||||||
|
codeValue,
|
||||||
|
}: CodeEditorProps) {
|
||||||
|
// const [value, setValue] = useState("");
|
||||||
|
|
||||||
|
const handleEditorDidMount = useCallback((monacoEditor: any, monaco: any) => {
|
||||||
|
// monacoEditor.updateOptions({ wordWrap: "on" });
|
||||||
|
activateMonacoJSXHighlighter(monacoEditor, monaco)
|
||||||
|
.then((monacoJSXHighlighterRefCurrent) => {
|
||||||
|
// monacoJSXHighlighterRef.current = monacoJSXHighlighterRefCurrent;
|
||||||
|
// setIsEditorReady(!!monacoEditor);
|
||||||
|
// setIsJSXHighlightingOn(
|
||||||
|
// monacoJSXHighlighterRefCurrent.isToggleJSXHighlightingOn()
|
||||||
|
// );
|
||||||
|
// setIsJSXCommentingOn(
|
||||||
|
// monacoJSXHighlighterRefCurrent.isToggleJSXCommentingOn()
|
||||||
|
// );
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
// console.log(e)
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// const handleEditorChange = (value: string | undefined) => {
|
||||||
|
// setValue(value || "");
|
||||||
|
// onChange(value || "");
|
||||||
|
// };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
// height="50vh" // By default, it fully fits with its parent
|
||||||
|
options={{
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
wordWrap: "on"
|
||||||
|
}}
|
||||||
|
theme={theme}
|
||||||
|
language={language === "react" ? "javascript" : language}
|
||||||
|
value={codeValue}
|
||||||
|
defaultValue={defaultCode}
|
||||||
|
onMount={handleEditorDidMount}
|
||||||
|
onChange={onChange}
|
||||||
|
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
//@ts-ignore
|
||||||
|
import { transform } from "@babel/standalone";
|
||||||
|
|
||||||
|
const compile = (input: string) =>
|
||||||
|
transform(input, {
|
||||||
|
filename: "random.tsx",
|
||||||
|
presets: ["react", "es2017"]
|
||||||
|
})?.code;
|
||||||
|
|
||||||
|
export const compileCode = (code: string) => {
|
||||||
|
// try {
|
||||||
|
|
||||||
|
|
||||||
|
const compiled = compile(
|
||||||
|
code.replace(
|
||||||
|
"import * as React from 'react'; //don't change this line\n",
|
||||||
|
""
|
||||||
|
)
|
||||||
|
)?.replace('"use strict";\n', "");
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
return new Function("React", "", `return ${compiled};`);
|
||||||
|
// } catch (error) {
|
||||||
|
// console.log(error)
|
||||||
|
// // return new Function("React", "", `return <div></div>;`);
|
||||||
|
// }
|
||||||
|
};
|
|
@ -0,0 +1,150 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { compileCode } from "./compiler";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { CodeEditor } from "./code-editor";
|
||||||
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
|
import PreviewErrorFallback from "./preview-error-fallback";
|
||||||
|
import {
|
||||||
|
ResizableHandle,
|
||||||
|
ResizablePanel,
|
||||||
|
ResizablePanelGroup,
|
||||||
|
} from "@/components/ui/resizable";
|
||||||
|
import { ThemeDropdown } from "./theme-dropdown";
|
||||||
|
import LanguagesDropdown from "./languages-dropdown";
|
||||||
|
import Output from "./output";
|
||||||
|
import { tutorialData } from "@/constants/tutorial-data";
|
||||||
|
import { notFound, useRouter } from "next/navigation";
|
||||||
|
import { defineTheme } from "@/utils/define-theme";
|
||||||
|
import Preview from "./preview";
|
||||||
|
import { MyComponents } from "./my-components";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default function EditorWindow({
|
||||||
|
language: lang,
|
||||||
|
step,
|
||||||
|
}: {
|
||||||
|
language: string;
|
||||||
|
step: number;
|
||||||
|
}) {
|
||||||
|
const [componentName, setComponentName] = useState<any>("");
|
||||||
|
const [theme, setTheme] = useState("cobalt");
|
||||||
|
// const [language, setLanguage] = useState(languageOptions[0]);
|
||||||
|
// const [language, setLanguage] = useState("react");
|
||||||
|
// const [defaultCode, setDefaultCode] = useState("");
|
||||||
|
const [codeValue, setCodeValue] = useState("");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleCodeChange = (code: string | undefined) => {
|
||||||
|
try {
|
||||||
|
setCodeValue(code || "");
|
||||||
|
const func = compileCode(code || "");
|
||||||
|
const id = uuid();
|
||||||
|
MyComponents[id] = func(React);
|
||||||
|
setComponentName(id);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
// setComponentName("error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
defineTheme("oceanic-next").then((_) => setTheme("oceanic-next"));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function handleThemeChange(th: any) {
|
||||||
|
const theme = th;
|
||||||
|
// console.log("theme...", theme);
|
||||||
|
|
||||||
|
if (["light", "vs-dark"].includes(theme)) {
|
||||||
|
setTheme(theme);
|
||||||
|
} else {
|
||||||
|
defineTheme(theme).then((_) => setTheme(theme));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLanguageChange = (lang: string) => {
|
||||||
|
router.push(`/dashboard/task/${lang}/1`);
|
||||||
|
// try {
|
||||||
|
// if (lang === "react") {
|
||||||
|
// const func = compileCode(CODE_SNIPPETS[lang]);
|
||||||
|
// const id = uuid();
|
||||||
|
// MyComponents[id] = func(React);
|
||||||
|
// setComponentName(id);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// setLanguage(lang);
|
||||||
|
// // @ts-ignore
|
||||||
|
// setCodeValue(CODE_SNIPPETS[lang]);
|
||||||
|
// // @ts-ignore
|
||||||
|
// // setDefaultCode(CODE_SNIPPETS[language]);
|
||||||
|
// } catch (error) {
|
||||||
|
// // console.log(error);
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
const tutorial = tutorialData
|
||||||
|
.find((t) => t.language === lang)
|
||||||
|
?.tutorial.find((t) => t.id === step);
|
||||||
|
|
||||||
|
if (!tutorial) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
// console.log(tutorial);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResizablePanelGroup direction="vertical" className="border-r">
|
||||||
|
<ResizablePanel defaultSize={50}>
|
||||||
|
<div className="h-full ">
|
||||||
|
<div className="p-2 flex gap-4">
|
||||||
|
<ThemeDropdown
|
||||||
|
handleThemeChange={handleThemeChange}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
<LanguagesDropdown
|
||||||
|
handleLanguageChange={handleLanguageChange}
|
||||||
|
language={lang}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="h-full">
|
||||||
|
<CodeEditor
|
||||||
|
codeValue={codeValue}
|
||||||
|
// @ts-ignore
|
||||||
|
// defaultCode={CODE_SNIPPETS[language]}
|
||||||
|
defaultCode={tutorial?.code || ""}
|
||||||
|
onChange={handleCodeChange}
|
||||||
|
theme={theme}
|
||||||
|
// language={language}
|
||||||
|
language={lang}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ResizablePanel>
|
||||||
|
<ResizableHandle withHandle />
|
||||||
|
<ResizablePanel defaultSize={50}>
|
||||||
|
<ErrorBoundary FallbackComponent={PreviewErrorFallback}>
|
||||||
|
<div className="bg-white text-black h-full p-4">
|
||||||
|
{lang === "react" ? (
|
||||||
|
<Preview
|
||||||
|
componentName={componentName}
|
||||||
|
tutorialCode={tutorial?.code || ""}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Output
|
||||||
|
// @ts-ignore
|
||||||
|
codeValue={codeValue || tutorial?.code || ""}
|
||||||
|
// codeValue={codeValue || CODE_SNIPPETS[language]}
|
||||||
|
// language={language}
|
||||||
|
language={lang}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</ResizablePanel>
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import ThemeToggle from "@/components/layout/ThemeToggle/theme-toggle";
|
||||||
|
import Link from "next/link";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
return (
|
||||||
|
<header className="border-b px-4 h-min">
|
||||||
|
<nav className="flex justify-between items-center max-w-[1440px] mx-auto py-2">
|
||||||
|
<Link href="/">
|
||||||
|
<h2>Skilled Ai</h2>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-8">
|
||||||
|
{/* <Link href="/">Home</Link> */}
|
||||||
|
<Link href="/dashboard">Dashboard</Link>
|
||||||
|
{/* <Link href="/login">Login</Link> */}
|
||||||
|
<ThemeToggle/>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { LANGUAGE_VERSIONS } from "@/constants/language-options";
|
||||||
|
// import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
// import Link from "next/link";
|
||||||
|
|
||||||
|
const languages = Object.entries(LANGUAGE_VERSIONS);
|
||||||
|
|
||||||
|
const LanguagesDropdown = ({ handleLanguageChange, language }: any) => {
|
||||||
|
return (
|
||||||
|
<Select onValueChange={handleLanguageChange} value={language}>
|
||||||
|
<SelectTrigger className="w-[150px]">
|
||||||
|
<SelectValue placeholder="Select Language" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{/* <ScrollArea className="h-96"> */}
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Languages</SelectLabel>
|
||||||
|
{languages.map(([lang, version]) => (
|
||||||
|
// <Link href={`/dashboard/task/${lang}`} key={lang}>
|
||||||
|
<SelectItem className="cursor-pointer" value={lang} key={lang}>
|
||||||
|
{lang} {version}
|
||||||
|
</SelectItem>
|
||||||
|
// </Link>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
{/* </ScrollArea> */}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LanguagesDropdown;
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from "react";
|
||||||
|
import { compileCode } from "./compiler";
|
||||||
|
import { CODE_SNIPPETS } from "@/constants/code-snippets";
|
||||||
|
|
||||||
|
export const MyComponents: { [key: string]: any } = {
|
||||||
|
default: (() => {
|
||||||
|
const func = compileCode(CODE_SNIPPETS["react"]);
|
||||||
|
return func(React);
|
||||||
|
})(),
|
||||||
|
error: function () {
|
||||||
|
return <div>Error</div>;
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
import { executeCode } from "@/lib/api";
|
||||||
|
|
||||||
|
const Output = ({ codeValue, language }: any) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [output, setOutput] = useState<any>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
||||||
|
const runCode = async () => {
|
||||||
|
if (!codeValue) return;
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const { run: result } = await executeCode(language, codeValue);
|
||||||
|
setOutput(result.output.split("\n"));
|
||||||
|
result.stderr ? setIsError(true) : setIsError(false);
|
||||||
|
} catch (error) {
|
||||||
|
// console.log(error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "An error occurred.",
|
||||||
|
description: (error as Error).message || "Unable to run code",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
{/* <p>Output</p> */}
|
||||||
|
<Button variant="outline" onClick={runCode} disabled={isLoading}>
|
||||||
|
{isLoading ? "Running..." : "Run Code"}
|
||||||
|
</Button>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mt-4 p-4 rounded-md",
|
||||||
|
isError ? "text-red-700 border border-red-700 bg-red-50" : "border",
|
||||||
|
)}
|
||||||
|
// color={isError ? "red.400" : ""}
|
||||||
|
// borderColor={isError ? "red.500" : "#333"}
|
||||||
|
>
|
||||||
|
{output
|
||||||
|
? output.map((line: any, i: number) => <p key={i}>{line}</p>)
|
||||||
|
: 'Click "Run Code" to see the output here'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Output;
|
|
@ -0,0 +1,24 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import React from "react";
|
||||||
|
// import { useErrorBoundary } from "react-error-boundary";
|
||||||
|
|
||||||
|
const PreviewErrorFallback = ({ error, resetErrorBoundary }: any) => {
|
||||||
|
// const { resetBoundary } = useErrorBoundary();
|
||||||
|
|
||||||
|
// console.log(error);
|
||||||
|
return (
|
||||||
|
<div className="bg-white text-black p-4 h-full">
|
||||||
|
<div className="border border-red-700 text-red-700 bg-red-50 mb-4 p-4 rounded-md">
|
||||||
|
<p className="mb-2 font-semibold">Something went wrong!</p>
|
||||||
|
<p>{error.message}</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" onClick={resetErrorBoundary}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PreviewErrorFallback;
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React from "react";
|
||||||
|
import { compileCode } from "./compiler";
|
||||||
|
import { MyComponents } from "./my-components";
|
||||||
|
|
||||||
|
export default function Preview({
|
||||||
|
componentName,
|
||||||
|
tutorialCode,
|
||||||
|
}: {
|
||||||
|
componentName: string;
|
||||||
|
tutorialCode: string;
|
||||||
|
}) {
|
||||||
|
// const Component =
|
||||||
|
// componentName && MyComponents[componentName] ? (
|
||||||
|
// MyComponents[componentName]
|
||||||
|
// ) : (
|
||||||
|
// <></>
|
||||||
|
// );
|
||||||
|
const Component =
|
||||||
|
componentName !== ""
|
||||||
|
? MyComponents[componentName]
|
||||||
|
: compileCode(tutorialCode)(React);
|
||||||
|
return <Component />;
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
// @ts-ignore
|
||||||
|
import monacoThemes from "monaco-themes/themes/themelist";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
|
||||||
|
export function ThemeDropdown({ handleThemeChange, theme }: any) {
|
||||||
|
const themes = Object.entries(monacoThemes)
|
||||||
|
// .filter(([themeId, themeName]) => themeId !== "cobalt2")
|
||||||
|
.map(([themeId, themeName]) => ({
|
||||||
|
label: themeName,
|
||||||
|
value: themeId,
|
||||||
|
key: themeId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select onValueChange={handleThemeChange} value={theme}>
|
||||||
|
<SelectTrigger className="w-[150px]">
|
||||||
|
<SelectValue placeholder="Select theme" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<ScrollArea className="h-96">
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Theme</SelectLabel>
|
||||||
|
{themes.map(({ label, value, key }) => (
|
||||||
|
<SelectItem className="cursor-pointer" key={key} value={value}>
|
||||||
|
{label as string}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</ScrollArea>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue