diff --git a/components/ComplexInput.tsx b/components/ComplexInput.tsx new file mode 100644 index 0000000..5d9df92 --- /dev/null +++ b/components/ComplexInput.tsx @@ -0,0 +1,91 @@ +import React, { FC, HTMLProps, useEffect, useState } from "react"; + +interface ComplexInputProps + extends Omit, "value" | "onChange"> { + stringify: (val: T) => string; + parse: (val: string) => T; + value: T; + onChange: (value: T) => void; +} + +function ComplexInput({ + stringify, + parse, + value, + onChange, + ...otherProps +}: ComplexInputProps) { + const [draft, setDraft] = useState(stringify(value)); + + useEffect(() => { + setDraft(stringify(value)); + }, [value]); + + const process = () => { + let shouldNotify = false; + let parsed; + try { + parsed = parse(draft); + shouldNotify = true; + } catch (err) { + setDraft(stringify(value)); + } + if (shouldNotify) { + onChange(parsed); + } + }; + + return ( + process()} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.currentTarget.blur(); + } + otherProps.onKeyDown?.(e); + }} + value={draft} + onChange={(e) => { + setDraft(e.target.value); + }} + /> + ); +} + +const stringify = (value: number[]) => { + return JSON.stringify(value); +}; + +const parse = (value: string) => { + const parsed = JSON.parse(value); + if (!Array.isArray(parsed)) { + throw new Error("Should be array"); + } + if (parsed.some((x) => typeof x !== "number")) { + throw new Error("Should contain only numbers"); + } + return parsed; +}; + +interface NumberArrayInputProps + extends Omit, "value" | "onChange"> { + value: number[]; + onChange: (value: number[]) => void; +} + +export const NumberArrayInput: FC = ({ + value, + onChange, + ...otherProps +}) => { + return ( + + ); +}; diff --git a/components/ContentWidget.tsx b/components/ContentWidget.tsx new file mode 100644 index 0000000..afb0961 --- /dev/null +++ b/components/ContentWidget.tsx @@ -0,0 +1,86 @@ +import { + useLayoutEffect, + useRef, + FC, + ReactNode, + useEffect, + Children, +} from "react"; +import { useMonaco } from "@monaco-editor/react"; +import { useEditor } from "./Editor"; +import ReactDOM from "react-dom"; +import useForceUpdate from "../hooks/useForceUpdate"; + +interface ContentWidgetProps { + children: any; + line: number; + widgetId: string; +} + +/** + * This component pins its children to a specific line in the editor. + * It's good for adding annotations to the editor. + * @param param0 + * @returns + */ +const ContentWidget: FC = ({ + widgetId, + line, + children, +}) => { + const monaco = useMonaco(); + const editor = useEditor(); + const ref = useRef(null); + + useLayoutEffect(() => { + if (editor) { + // Add a content widget (scrolls inline with text) + var contentWidget = { + domNode: null, + allowEditorOverflow: true, + getId: function () { + return widgetId; + }, + getDomNode: function () { + if (!ref.current) { + ref.current = document.createElement("div"); + ref.current.style.position = "relative"; + ref.current.style.width = 0; + ref.current.classList.add("content-widget-portal"); + } + return ref.current; + }, + afterRender: function () { + ReactDOM.render( +
{ + e.preventDefault(); + e.stopPropagation(); + }} + > + {children} +
, + ref.current + ); + }, + getPosition: function () { + return { + position: { + lineNumber: line, + column: 0, + }, + preference: [monaco.editor.ContentWidgetPositionPreference.EXACT], + }; + }, + }; + editor.addContentWidget(contentWidget); + return () => { + editor.removeContentWidget(contentWidget); + }; + } + }, [editor, children]); + + return null; +}; + +export default ContentWidget; diff --git a/components/CreateQuizForm.tsx b/components/CreateQuizForm.tsx index b9fa2cb..5832a97 100644 --- a/components/CreateQuizForm.tsx +++ b/components/CreateQuizForm.tsx @@ -1,10 +1,14 @@ import { Session } from "@supabase/supabase-js"; -import React, { FormEvent, useState } from "react"; -import { SaveQuiz } from "../types"; -import { supabase } from "../utils/supabaseClient"; +import React, { FormEvent, useEffect, useState } from "react"; +import Hashids from "hashids"; +import classNames from "classnames"; +import { Profile, SaveQuiz, Explanation } from "../types"; import Editor from "@monaco-editor/react"; +import { supabase } from "../utils/supabaseClient"; +import PreviewQuiz from "./PreviewQuiz"; +import ExplanationForm from "./ExplanationForm"; +import useTypescript from "../hooks/useTypescript"; -import Hashids from "hashids"; const hashids = new Hashids(); type FormInputProps = { @@ -15,7 +19,7 @@ type FormInputProps = { }; const FormInput = (props: FormInputProps) => ( -
+
@@ -26,25 +30,40 @@ const FormInput = (props: FormInputProps) => ( type CreateQuizFormProps = { session: Session; + profile: Profile; }; -export default function CreateQuizForm({ session }: CreateQuizFormProps) { +enum FormStep { + Task = 0, + Solution = 1, + Explanation = 2, +} + +export default function CreateQuizForm({ + session, + profile, +}: CreateQuizFormProps) { const [description, setDescription] = useState(""); + const [formStep, setFormStep] = useState(FormStep.Task); + const [solution, setSolution] = useState(""); const [startCode, setStartCode] = useState(""); const [output, setOutput] = useState(""); + const [explanation, setExplanation] = useState(); + const { tsClient, tsLoading } = useTypescript(); const [loading, setLoading] = useState(false); const [createdUrl, setCreatedUrl] = useState(null); - const saveQuiz = async (e: FormEvent) => { + async function saveQuiz() { setLoading(true); - e.preventDefault(); const now = new Date(); - const quiz: SaveQuiz = { + const insertQuiz: SaveQuiz = { + language: "typescript", description, + solution, start_code: startCode, target_output: output, - language: "typescript", + explanation, created_at: now, updated_at: now, created_by: session.user.id, @@ -54,7 +73,7 @@ export default function CreateQuizForm({ session }: CreateQuizFormProps) { // Note that friendly ID isn't required, it's just helpful for debugging const { data: insertData, error: insertError } = await supabase .from("quizzes") - .insert(quiz); + .insert(insertQuiz); setLoading(false); if (insertError) throw insertError; @@ -70,99 +89,172 @@ export default function CreateQuizForm({ session }: CreateQuizFormProps) { .from("quizzes") .update({ friendly_id: hashedId }) .eq("id", insertedId); - }; + } - return ( -
-
-
-
- +
+ +