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