From ca0e7f71b3220379b4121f8781e7a54e9c18c856 Mon Sep 17 00:00:00 2001 From: mehedi-hasan Date: Sun, 7 Apr 2024 22:53:52 +0600 Subject: [PATCH] feat: task --- .../(dashboard)/dashboard/(task)/layout.tsx | 19 ++ .../(task)/task/[...taskId]/page.tsx | 170 ++++++++++++++++++ src/components/task/code-editor.tsx | 135 ++++++++++++++ src/components/task/compiler.ts | 27 +++ src/components/task/editor-window.tsx | 150 ++++++++++++++++ src/components/task/header.tsx | 24 +++ src/components/task/languages-dropdown.tsx | 42 +++++ src/components/task/my-components.tsx | 13 ++ src/components/task/output.tsx | 53 ++++++ .../task/preview-error-fallback.tsx | 24 +++ src/components/task/preview.tsx | 23 +++ src/components/task/theme-dropdown.tsx | 44 +++++ 12 files changed, 724 insertions(+) create mode 100644 src/app/(dashboard)/dashboard/(task)/layout.tsx create mode 100644 src/app/(dashboard)/dashboard/(task)/task/[...taskId]/page.tsx create mode 100644 src/components/task/code-editor.tsx create mode 100644 src/components/task/compiler.ts create mode 100644 src/components/task/editor-window.tsx create mode 100644 src/components/task/header.tsx create mode 100644 src/components/task/languages-dropdown.tsx create mode 100644 src/components/task/my-components.tsx create mode 100644 src/components/task/output.tsx create mode 100644 src/components/task/preview-error-fallback.tsx create mode 100644 src/components/task/preview.tsx create mode 100644 src/components/task/theme-dropdown.tsx diff --git a/src/app/(dashboard)/dashboard/(task)/layout.tsx b/src/app/(dashboard)/dashboard/(task)/layout.tsx new file mode 100644 index 0000000..f8aa855 --- /dev/null +++ b/src/app/(dashboard)/dashboard/(task)/layout.tsx @@ -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 ( +
+
+
{children}
+
+ ); +}; + +export default Layout; diff --git a/src/app/(dashboard)/dashboard/(task)/task/[...taskId]/page.tsx b/src/app/(dashboard)/dashboard/(task)/task/[...taskId]/page.tsx new file mode 100644 index 0000000..064425d --- /dev/null +++ b/src/app/(dashboard)/dashboard/(task)/task/[...taskId]/page.tsx @@ -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 ( + <> +
+ + + +
+
+
+
+ {langTutorial?.tutorial[tutorialIndex - 1] && ( + + Prev + + )} +
+ +
+ {langTutorial?.tutorial[tutorialIndex + 1] && ( + + Next + + )} +
+
+
+
+
+ + + + +
+
+
+ + + +
+
+ +
+
+ {langTutorial?.tutorial[tutorialIndex - 1] && ( + + Prev + + )} +
+ +
+ {langTutorial?.tutorial[tutorialIndex + 1] && ( + + Next + + )} +
+
+
+
+
+ + + + +
+
+ + ); +}; + +export default Page; diff --git a/src/components/task/code-editor.tsx b/src/components/task/code-editor.tsx new file mode 100644 index 0000000..12a2d20 --- /dev/null +++ b/src/components/task/code-editor.tsx @@ -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 ( + + ); +} diff --git a/src/components/task/compiler.ts b/src/components/task/compiler.ts new file mode 100644 index 0000000..3813bfb --- /dev/null +++ b/src/components/task/compiler.ts @@ -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
;`); +// } +}; \ No newline at end of file diff --git a/src/components/task/editor-window.tsx b/src/components/task/editor-window.tsx new file mode 100644 index 0000000..6b413a5 --- /dev/null +++ b/src/components/task/editor-window.tsx @@ -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(""); + 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 ( + + +
+
+ + +
+
+ +
+
+
+ + + +
+ {lang === "react" ? ( + + ) : ( + + )} +
+
+
+
+ ); +} + + diff --git a/src/components/task/header.tsx b/src/components/task/header.tsx new file mode 100644 index 0000000..e77f3a9 --- /dev/null +++ b/src/components/task/header.tsx @@ -0,0 +1,24 @@ +import ThemeToggle from "@/components/layout/ThemeToggle/theme-toggle"; +import Link from "next/link"; +import React from "react"; + +const Header = () => { + return ( +
+ +
+ ); +}; + +export default Header; diff --git a/src/components/task/languages-dropdown.tsx b/src/components/task/languages-dropdown.tsx new file mode 100644 index 0000000..9b28b3b --- /dev/null +++ b/src/components/task/languages-dropdown.tsx @@ -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 ( + + ); +}; + +export default LanguagesDropdown; diff --git a/src/components/task/my-components.tsx b/src/components/task/my-components.tsx new file mode 100644 index 0000000..2dc1234 --- /dev/null +++ b/src/components/task/my-components.tsx @@ -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
Error
; + }, + }; \ No newline at end of file diff --git a/src/components/task/output.tsx b/src/components/task/output.tsx new file mode 100644 index 0000000..bbc75ac --- /dev/null +++ b/src/components/task/output.tsx @@ -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(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 ( +
+ {/*

Output

*/} + +
+ {output + ? output.map((line: any, i: number) =>

{line}

) + : 'Click "Run Code" to see the output here'} +
+
+ ); +}; +export default Output; diff --git a/src/components/task/preview-error-fallback.tsx b/src/components/task/preview-error-fallback.tsx new file mode 100644 index 0000000..b0c0164 --- /dev/null +++ b/src/components/task/preview-error-fallback.tsx @@ -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 ( +
+
+

Something went wrong!

+

{error.message}

+
+ +
+ ); +}; + +export default PreviewErrorFallback; diff --git a/src/components/task/preview.tsx b/src/components/task/preview.tsx new file mode 100644 index 0000000..579813b --- /dev/null +++ b/src/components/task/preview.tsx @@ -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 ; +} diff --git a/src/components/task/theme-dropdown.tsx b/src/components/task/theme-dropdown.tsx new file mode 100644 index 0000000..1f72eae --- /dev/null +++ b/src/components/task/theme-dropdown.tsx @@ -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 ( + + ); +}