@examplary/cli 1.7.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/preview.d.ts +5 -0
- package/dist/commands/preview.js +174 -0
- package/dist/commands/upload.d.ts +5 -0
- package/dist/commands/upload.js +138 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +37 -0
- package/dist/lib/api.d.ts +5 -0
- package/dist/lib/api.js +47 -0
- package/dist/lib/bundle.d.ts +4 -0
- package/dist/lib/bundle.js +27 -0
- package/dist/lib/jsonata.d.ts +1 -0
- package/dist/lib/jsonata.js +26 -0
- package/dist/lib/read-meta.d.ts +16 -0
- package/dist/lib/read-meta.js +36 -0
- package/dist/lib/styles.d.ts +1 -0
- package/dist/lib/styles.js +24 -0
- package/dist/preview-ui/assets/icon-nodes-Ds7bJID4.js +2 -0
- package/dist/preview-ui/assets/icon-nodes-Ds7bJID4.js.map +1 -0
- package/dist/preview-ui/assets/index-CYESux9p.js +3629 -0
- package/dist/preview-ui/assets/index-CYESux9p.js.map +1 -0
- package/dist/preview-ui/assets/index-DcEcr1Zm.css +1 -0
- package/dist/preview-ui/assets/tags-DZBm0mOh.js +2 -0
- package/dist/preview-ui/assets/tags-DZBm0mOh.js.map +1 -0
- package/{src → dist}/preview-ui/index.html +2 -1
- package/package.json +24 -23
- package/bin/index.js +0 -37
- package/src/commands/preview.js +0 -220
- package/src/commands/upload.js +0 -179
- package/src/lib/api.js +0 -53
- package/src/lib/bundle.js +0 -35
- package/src/lib/jsonata.js +0 -31
- package/src/lib/middleware.js +0 -16
- package/src/lib/read-meta.js +0 -40
- package/src/lib/styles.js +0 -32
- package/src/preview-ui/App.tsx +0 -148
- package/src/preview-ui/components/json-editor.tsx +0 -27
- package/src/preview-ui/components/preview-assessment.tsx +0 -43
- package/src/preview-ui/components/preview-print.tsx +0 -38
- package/src/preview-ui/components/preview-results.tsx +0 -38
- package/src/preview-ui/components/preview-settings-area.tsx +0 -97
- package/src/preview-ui/components/question-component.tsx +0 -75
- package/src/preview-ui/index.css +0 -11
- package/src/preview-ui/lib/demo-api.ts +0 -29
- package/src/preview-ui/lib/i18n.ts +0 -15
- package/src/preview-ui/main.tsx +0 -8
package/src/lib/read-meta.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { readFile } from "fs/promises";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { cwd } from "process";
|
|
4
|
-
|
|
5
|
-
import yaml from "yaml";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Reads and parses the question-type definition file.
|
|
9
|
-
* Prefers question-type.json over question-type.yml.
|
|
10
|
-
*/
|
|
11
|
-
export const readMetaFile = async () => {
|
|
12
|
-
const dir = cwd();
|
|
13
|
-
const jsonPath = join(dir, "question-type.json");
|
|
14
|
-
const ymlPath = join(dir, "question-type.yml");
|
|
15
|
-
|
|
16
|
-
// Try JSON first, then YAML
|
|
17
|
-
for (const { path, parse, format } of [
|
|
18
|
-
{ path: jsonPath, parse: JSON.parse, format: "JSON" },
|
|
19
|
-
{ path: ymlPath, parse: yaml.parse, format: "YAML" },
|
|
20
|
-
]) {
|
|
21
|
-
let contents;
|
|
22
|
-
try {
|
|
23
|
-
contents = await readFile(path, "utf-8");
|
|
24
|
-
} catch {
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const definition = parse(contents);
|
|
30
|
-
if (definition.$schema) delete definition.$schema;
|
|
31
|
-
return { definition, path };
|
|
32
|
-
} catch (error) {
|
|
33
|
-
throw new Error(`Invalid ${format} in ${path}: ${error.message}`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
throw new Error(
|
|
38
|
-
"No question-type.json or question-type.yml found in the current directory",
|
|
39
|
-
);
|
|
40
|
-
};
|
package/src/lib/styles.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "module";
|
|
2
|
-
import path from "path";
|
|
3
|
-
|
|
4
|
-
import tailwindcss from "@tailwindcss/postcss";
|
|
5
|
-
import postcss from "postcss";
|
|
6
|
-
|
|
7
|
-
const require = createRequire(import.meta.url);
|
|
8
|
-
const tailwindDir = path.dirname(require.resolve("tailwindcss/package.json"));
|
|
9
|
-
|
|
10
|
-
export const buildStyles = async () => {
|
|
11
|
-
const absolutePath = path.resolve(tailwindDir);
|
|
12
|
-
|
|
13
|
-
const inputCSS = `
|
|
14
|
-
@import "${absolutePath}/theme.css" layer(theme);
|
|
15
|
-
@import "${absolutePath}/utilities.css" layer(utilities);
|
|
16
|
-
`;
|
|
17
|
-
|
|
18
|
-
const result = await postcss([
|
|
19
|
-
tailwindcss({
|
|
20
|
-
optimize: { minify: true },
|
|
21
|
-
}),
|
|
22
|
-
]).process(inputCSS, { from: "module-css.css" });
|
|
23
|
-
|
|
24
|
-
const utilitiesStyles = result.css.split("@layer utilities")[1].trim();
|
|
25
|
-
if (utilitiesStyles.length < 5) {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const css = result.css.replace(/@layer /g, "@layer modules_");
|
|
30
|
-
|
|
31
|
-
return css;
|
|
32
|
-
};
|
package/src/preview-ui/App.tsx
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { useTranslation } from "react-i18next";
|
|
3
|
-
|
|
4
|
-
import type { QuestionType } from "@examplary/schemas";
|
|
5
|
-
import {
|
|
6
|
-
formatQuestionType,
|
|
7
|
-
Select,
|
|
8
|
-
SelectContent,
|
|
9
|
-
SelectItem,
|
|
10
|
-
SelectTrigger,
|
|
11
|
-
SelectValue,
|
|
12
|
-
} from "@examplary/ui";
|
|
13
|
-
import useLocalStorage from "use-local-storage";
|
|
14
|
-
|
|
15
|
-
import { JsonEditor } from "./components/json-editor";
|
|
16
|
-
import { PreviewAssessment } from "./components/preview-assessment";
|
|
17
|
-
import { PreviewPrint } from "./components/preview-print";
|
|
18
|
-
import { PreviewResults } from "./components/preview-results";
|
|
19
|
-
import { PreviewSettingsArea } from "./components/preview-settings-area";
|
|
20
|
-
|
|
21
|
-
function App() {
|
|
22
|
-
const { i18n } = useTranslation();
|
|
23
|
-
|
|
24
|
-
const rawQuestionType = globalThis._questionType as unknown as QuestionType;
|
|
25
|
-
const questionType = formatQuestionType(rawQuestionType, i18n);
|
|
26
|
-
|
|
27
|
-
const [componentName, setComponentName] = useLocalStorage(
|
|
28
|
-
`component-name-${questionType.id}`,
|
|
29
|
-
Object.keys(questionType.components || {})[0] || "assessment",
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
const availableComponents = Object.keys(questionType.components || {});
|
|
33
|
-
|
|
34
|
-
const [answer, setAnswer] = useState(undefined);
|
|
35
|
-
|
|
36
|
-
const defaultSettings = (questionType.settings || []).reduce(
|
|
37
|
-
(acc, setting) => {
|
|
38
|
-
if ("default" in setting) {
|
|
39
|
-
acc[setting.id] = setting.default;
|
|
40
|
-
}
|
|
41
|
-
return acc;
|
|
42
|
-
},
|
|
43
|
-
{},
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const [question, setQuestion] = useLocalStorage(
|
|
47
|
-
`payload-question-${questionType.id}`,
|
|
48
|
-
{
|
|
49
|
-
id: "q_qQjXQasmUfpWRV5KO5mzkxoy",
|
|
50
|
-
title: "Napoleon's role in the French Revolution",
|
|
51
|
-
type: questionType.id,
|
|
52
|
-
description:
|
|
53
|
-
"When Napoleon Bonaparte rose to power, he was seen as a hero of the French Revolution. However, his actions and policies during his rule have been debated by historians. What was Napoleon's role in the French Revolution?",
|
|
54
|
-
settings: {
|
|
55
|
-
...defaultSettings,
|
|
56
|
-
options: [
|
|
57
|
-
"To establish a centralized legal system and promote the principles of the Revolution.",
|
|
58
|
-
"To overthrow the revolutionary government and establish a dictatorship.",
|
|
59
|
-
"To maintain the status quo and prevent further revolutionary changes.",
|
|
60
|
-
"To support the monarchy and restore the old regime.",
|
|
61
|
-
],
|
|
62
|
-
correctAnswer:
|
|
63
|
-
"To establish a centralized legal system and promote the principles of the Revolution.",
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<div className="min-h-screen flex">
|
|
70
|
-
<aside className="bg-nav w-[360px] text-black border-r-2 border-border p-6 overflow-auto">
|
|
71
|
-
<h1 className="text-xl font-semibold">{questionType.name}</h1>
|
|
72
|
-
<h2 className="text-sm opacity-50">Question type preview</h2>
|
|
73
|
-
|
|
74
|
-
<h3 className="text-sm font-medium mb-1.5 mt-8">Preview options</h3>
|
|
75
|
-
<div className="grid grid-cols-3 items-center gap-3 border border-border-accent rounded-md bg-white p-4">
|
|
76
|
-
<p className="text-gray-800 text-sm">Component</p>
|
|
77
|
-
<Select value={componentName} onValueChange={setComponentName}>
|
|
78
|
-
<SelectTrigger className="text-sm col-span-2">
|
|
79
|
-
<SelectValue />
|
|
80
|
-
</SelectTrigger>
|
|
81
|
-
<SelectContent>
|
|
82
|
-
{availableComponents.map((component) => (
|
|
83
|
-
<SelectItem key={component} value={component}>
|
|
84
|
-
{component}
|
|
85
|
-
</SelectItem>
|
|
86
|
-
))}
|
|
87
|
-
</SelectContent>
|
|
88
|
-
</Select>
|
|
89
|
-
<p className="text-gray-800 text-sm">Locale</p>
|
|
90
|
-
<Select
|
|
91
|
-
value={i18n.language}
|
|
92
|
-
onValueChange={(value) => i18n.changeLanguage(value)}
|
|
93
|
-
>
|
|
94
|
-
<SelectTrigger className="text-sm col-span-2">
|
|
95
|
-
<SelectValue />
|
|
96
|
-
</SelectTrigger>
|
|
97
|
-
<SelectContent>
|
|
98
|
-
<SelectItem value="en">en</SelectItem>
|
|
99
|
-
<SelectItem value="nl">nl</SelectItem>
|
|
100
|
-
</SelectContent>
|
|
101
|
-
</Select>
|
|
102
|
-
</div>
|
|
103
|
-
|
|
104
|
-
<h3 className="text-sm font-medium mb-1.5 mt-8">Question</h3>
|
|
105
|
-
<div className="border border-border-accent rounded-md bg-white overflow-hidden">
|
|
106
|
-
<JsonEditor value={question} setValue={setQuestion} />
|
|
107
|
-
</div>
|
|
108
|
-
|
|
109
|
-
<h3 className="text-sm font-medium mb-1.5 mt-8">Answer</h3>
|
|
110
|
-
<div className="border border-border-accent rounded-md bg-white overflow-hidden">
|
|
111
|
-
<JsonEditor value={answer} setValue={setAnswer} />
|
|
112
|
-
</div>
|
|
113
|
-
</aside>
|
|
114
|
-
|
|
115
|
-
<div className="flex-1 flex items-center justify-center">
|
|
116
|
-
<div className="max-w-3xl mx-auto py-10" key={i18n.language}>
|
|
117
|
-
{componentName === "assessment" && (
|
|
118
|
-
<PreviewAssessment
|
|
119
|
-
questionType={questionType}
|
|
120
|
-
question={question}
|
|
121
|
-
answer={answer}
|
|
122
|
-
saveAnswer={setAnswer}
|
|
123
|
-
/>
|
|
124
|
-
)}
|
|
125
|
-
{componentName === "results" && (
|
|
126
|
-
<PreviewResults
|
|
127
|
-
questionType={questionType}
|
|
128
|
-
question={question}
|
|
129
|
-
answer={answer}
|
|
130
|
-
/>
|
|
131
|
-
)}
|
|
132
|
-
{componentName === "settings-area" && (
|
|
133
|
-
<PreviewSettingsArea
|
|
134
|
-
questionType={questionType}
|
|
135
|
-
question={question}
|
|
136
|
-
setQuestion={setQuestion}
|
|
137
|
-
/>
|
|
138
|
-
)}
|
|
139
|
-
{componentName === "print" && (
|
|
140
|
-
<PreviewPrint questionType={questionType} question={question} />
|
|
141
|
-
)}
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
</div>
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export default App;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
|
|
3
|
-
export const JsonEditor = ({ value, setValue }) => {
|
|
4
|
-
const [stringValue, setStringValue] = useState(() =>
|
|
5
|
-
JSON.stringify(value, null, 2),
|
|
6
|
-
);
|
|
7
|
-
useEffect(() => {
|
|
8
|
-
setStringValue(JSON.stringify(value, null, 2));
|
|
9
|
-
}, [value]);
|
|
10
|
-
|
|
11
|
-
return (
|
|
12
|
-
<textarea
|
|
13
|
-
className="h-[160px] font-mono text-xs p-3 w-full"
|
|
14
|
-
value={stringValue}
|
|
15
|
-
onChange={(e) => {
|
|
16
|
-
const newStringValue = e.target.value;
|
|
17
|
-
setStringValue(newStringValue || "");
|
|
18
|
-
try {
|
|
19
|
-
const parsedValue = JSON.parse(newStringValue || "");
|
|
20
|
-
setValue(parsedValue);
|
|
21
|
-
} catch (error) {
|
|
22
|
-
console.error("Invalid JSON:", error);
|
|
23
|
-
}
|
|
24
|
-
}}
|
|
25
|
-
/>
|
|
26
|
-
);
|
|
27
|
-
};
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { RichTextDisplay } from "@examplary/ui";
|
|
2
|
-
|
|
3
|
-
import { QuestionComponent } from "./question-component";
|
|
4
|
-
import { api } from "../lib/demo-api";
|
|
5
|
-
|
|
6
|
-
export const PreviewAssessment = ({
|
|
7
|
-
questionType,
|
|
8
|
-
question,
|
|
9
|
-
answer,
|
|
10
|
-
saveAnswer,
|
|
11
|
-
}) => {
|
|
12
|
-
return (
|
|
13
|
-
<div className="relative flex flex-col min-h-[calc(100vh-17.5rem)]">
|
|
14
|
-
<div className="rounded-xl border border-border bg-main text-black flex-1 bg-white py-7 px-10 relative z-10">
|
|
15
|
-
<div className="flex justify-between gap-2 mb-3">
|
|
16
|
-
<div className="size-6 bg-bright border text-sm font-bold rounded-full flex items-center justify-center shrink-0">
|
|
17
|
-
1
|
|
18
|
-
</div>
|
|
19
|
-
<div className="flex-1">
|
|
20
|
-
<h3 className="font-semibold text-lg mb-1">
|
|
21
|
-
<RichTextDisplay as="span">{question.title}</RichTextDisplay>
|
|
22
|
-
</h3>
|
|
23
|
-
<RichTextDisplay as="p" className="text-zinc-900 mb-6">
|
|
24
|
-
{question.description}
|
|
25
|
-
</RichTextDisplay>
|
|
26
|
-
|
|
27
|
-
<QuestionComponent
|
|
28
|
-
type={questionType}
|
|
29
|
-
componentName="assessment"
|
|
30
|
-
props={{
|
|
31
|
-
environment: "exam",
|
|
32
|
-
api,
|
|
33
|
-
question,
|
|
34
|
-
answer,
|
|
35
|
-
saveAnswer,
|
|
36
|
-
}}
|
|
37
|
-
/>
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
);
|
|
43
|
-
};
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { RichTextDisplay } from "@examplary/ui";
|
|
2
|
-
|
|
3
|
-
import { QuestionComponent } from "./question-component";
|
|
4
|
-
|
|
5
|
-
export const PreviewPrint = ({ questionType, question }) => {
|
|
6
|
-
return (
|
|
7
|
-
<div className="relative">
|
|
8
|
-
<div className="border-2 border-border border-t-0 border-b-0 bg-main text-black flex-1 bg-white">
|
|
9
|
-
<div className="flex justify-between gap-2 mb-3 py-16 px-12">
|
|
10
|
-
<div className="size-6 border-2 text-sm font-bold rounded-full flex items-center justify-center shrink-0">
|
|
11
|
-
1
|
|
12
|
-
</div>
|
|
13
|
-
<div className="flex-1">
|
|
14
|
-
<h3 className="font-semibold text-lg mb-1">
|
|
15
|
-
<RichTextDisplay as="span">{question.title}</RichTextDisplay>
|
|
16
|
-
</h3>
|
|
17
|
-
<RichTextDisplay as="p" className="text-zinc-900 mb-6">
|
|
18
|
-
{question.description}
|
|
19
|
-
</RichTextDisplay>
|
|
20
|
-
|
|
21
|
-
<QuestionComponent
|
|
22
|
-
type={questionType}
|
|
23
|
-
componentName="print"
|
|
24
|
-
props={{
|
|
25
|
-
environment: "exam",
|
|
26
|
-
question,
|
|
27
|
-
answerBoxes: true,
|
|
28
|
-
}}
|
|
29
|
-
/>
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
</div>
|
|
33
|
-
|
|
34
|
-
<div className="absolute inset-0 bottom-auto h-10 bg-gradient-to-b from-bg to-transparent z-10" />
|
|
35
|
-
<div className="absolute inset-0 top-auto h-10 bg-gradient-to-b to-bg from-transparent z-10" />
|
|
36
|
-
</div>
|
|
37
|
-
);
|
|
38
|
-
};
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { RichTextDisplay } from "@examplary/ui";
|
|
2
|
-
|
|
3
|
-
import { QuestionComponent } from "./question-component";
|
|
4
|
-
import { api } from "../lib/demo-api";
|
|
5
|
-
|
|
6
|
-
export const PreviewResults = ({ questionType, question, answer }) => {
|
|
7
|
-
return (
|
|
8
|
-
<div className="rounded-base border-2 border-border bg-main text-black flex-1 bg-white py-7 px-10 md:mr-5 relative z-10">
|
|
9
|
-
<div className="flex justify-between gap-2 mb-3">
|
|
10
|
-
<div className="size-6 bg-bright border-2 text-sm font-bold rounded-full flex items-center justify-center shrink-0">
|
|
11
|
-
1
|
|
12
|
-
</div>
|
|
13
|
-
<div className="flex-1">
|
|
14
|
-
<h3 className="font-semibold text-lg mb-2.5">
|
|
15
|
-
<RichTextDisplay as="span">{question.title}</RichTextDisplay>
|
|
16
|
-
</h3>
|
|
17
|
-
|
|
18
|
-
{answer ? (
|
|
19
|
-
<QuestionComponent
|
|
20
|
-
type={questionType}
|
|
21
|
-
componentName="results"
|
|
22
|
-
props={{
|
|
23
|
-
environment: "exam",
|
|
24
|
-
api,
|
|
25
|
-
question,
|
|
26
|
-
answer,
|
|
27
|
-
}}
|
|
28
|
-
/>
|
|
29
|
-
) : (
|
|
30
|
-
<span className="text-zinc-500 bg-zinc-100 rounded p-1 px-2 font-medium text-sm">
|
|
31
|
-
Empty answer
|
|
32
|
-
</span>
|
|
33
|
-
)}
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
37
|
-
);
|
|
38
|
-
};
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { cn, MinimalRichTextField } from "@examplary/ui";
|
|
2
|
-
import { FileQuestionIcon } from "lucide-react";
|
|
3
|
-
|
|
4
|
-
import { QuestionComponent } from "./question-component";
|
|
5
|
-
import { api } from "../lib/demo-api";
|
|
6
|
-
|
|
7
|
-
export const PreviewSettingsArea = ({
|
|
8
|
-
questionType,
|
|
9
|
-
question,
|
|
10
|
-
setQuestion,
|
|
11
|
-
}) => {
|
|
12
|
-
return (
|
|
13
|
-
<div
|
|
14
|
-
data-type="question"
|
|
15
|
-
data-question-id={question.id}
|
|
16
|
-
className={cn(
|
|
17
|
-
"rounded-xl border border-border bg-main text-black",
|
|
18
|
-
"mb-3 relative bg-white transition-[border,shadow,opacity] p-6 px-7",
|
|
19
|
-
)}
|
|
20
|
-
>
|
|
21
|
-
<div className="flex gap-3">
|
|
22
|
-
<div
|
|
23
|
-
className={cn(
|
|
24
|
-
"size-6 rounded-full border-2 items-center justify-center flex transition bg-bright",
|
|
25
|
-
)}
|
|
26
|
-
>
|
|
27
|
-
{questionType?.icon ? (
|
|
28
|
-
<img
|
|
29
|
-
src={`/api/icon?path=${questionType.icon}&questionType=${questionType.id}`}
|
|
30
|
-
alt={questionType.name}
|
|
31
|
-
className="size-3.5 object-contain"
|
|
32
|
-
/>
|
|
33
|
-
) : (
|
|
34
|
-
<FileQuestionIcon className="size-3.5" strokeWidth={2.6} />
|
|
35
|
-
)}
|
|
36
|
-
</div>
|
|
37
|
-
<div className="flex flex-1">
|
|
38
|
-
<div className="flex-1 flex flex-col">
|
|
39
|
-
<MinimalRichTextField
|
|
40
|
-
singleLine
|
|
41
|
-
readOnly
|
|
42
|
-
className={cn(
|
|
43
|
-
"w-full font-medium",
|
|
44
|
-
"cursor-not-allowed pointer-events-none",
|
|
45
|
-
)}
|
|
46
|
-
value={question.title}
|
|
47
|
-
placeholder={questionType?.titlePlaceholder}
|
|
48
|
-
/>
|
|
49
|
-
<MinimalRichTextField
|
|
50
|
-
data-type="description"
|
|
51
|
-
className={cn(
|
|
52
|
-
"w-full text-sm mt-1",
|
|
53
|
-
"cursor-not-allowed pointer-events-none",
|
|
54
|
-
)}
|
|
55
|
-
value={question.description}
|
|
56
|
-
readOnly
|
|
57
|
-
showFormattingMenu
|
|
58
|
-
placeholder={questionType?.descriptionPlaceholder}
|
|
59
|
-
/>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
<div className="border-t border-border pt-5 mt-5 ml-9 mb-4">
|
|
64
|
-
<div>
|
|
65
|
-
<QuestionComponent
|
|
66
|
-
type={questionType}
|
|
67
|
-
componentName="settings-area"
|
|
68
|
-
props={{
|
|
69
|
-
api,
|
|
70
|
-
question,
|
|
71
|
-
settings: question.settings,
|
|
72
|
-
environment: "exam",
|
|
73
|
-
setMultipleSettings: (newSettings) => {
|
|
74
|
-
setQuestion({
|
|
75
|
-
...question,
|
|
76
|
-
settings: {
|
|
77
|
-
...question.settings,
|
|
78
|
-
...newSettings,
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
},
|
|
82
|
-
setSetting: (settingId, value) => {
|
|
83
|
-
setQuestion({
|
|
84
|
-
...question,
|
|
85
|
-
settings: {
|
|
86
|
-
...question.settings,
|
|
87
|
-
[settingId]: value,
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
},
|
|
91
|
-
}}
|
|
92
|
-
/>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
);
|
|
97
|
-
};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { ComponentType } from "react";
|
|
2
|
-
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
|
|
3
|
-
import { useTranslation } from "react-i18next";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
fetchComponent,
|
|
7
|
-
FormattedQuestionType,
|
|
8
|
-
FrontendBaseComponentProps,
|
|
9
|
-
} from "@examplary/ui";
|
|
10
|
-
import { Loader2Icon } from "lucide-react";
|
|
11
|
-
import useSWR from "swr";
|
|
12
|
-
|
|
13
|
-
type QuestionComponentProps = {
|
|
14
|
-
type: FormattedQuestionType;
|
|
15
|
-
componentName: "assessment" | "print" | "results" | "settings-area";
|
|
16
|
-
props: Omit<FrontendBaseComponentProps, "api" | "t" | "i18n"> &
|
|
17
|
-
Record<string, any>;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const QuestionComponentContent = ({
|
|
21
|
-
type,
|
|
22
|
-
componentName,
|
|
23
|
-
props,
|
|
24
|
-
}: QuestionComponentProps) => {
|
|
25
|
-
const componentUrl = type?.components?.[componentName];
|
|
26
|
-
|
|
27
|
-
const { t, i18n } = useTranslation(type?.translationNs);
|
|
28
|
-
const { data: Component, error } = useSWR<ComponentType<any>>(
|
|
29
|
-
componentUrl,
|
|
30
|
-
fetchComponent,
|
|
31
|
-
{
|
|
32
|
-
revalidateOnFocus: false,
|
|
33
|
-
revalidateOnReconnect: false,
|
|
34
|
-
},
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
if (!componentUrl) {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (error) {
|
|
42
|
-
return <FallbackComponent error={error} />;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (!Component) {
|
|
46
|
-
return <Loader2Icon className="animate-spin size-5" />;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const finalProps = {
|
|
50
|
-
t,
|
|
51
|
-
i18n,
|
|
52
|
-
...props,
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
return <Component {...finalProps} />;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const FallbackComponent = ({ error }: Partial<FallbackProps>) => {
|
|
59
|
-
return (
|
|
60
|
-
<div className="rounded-md bg-gray-100 p-5">
|
|
61
|
-
<p className="text-red-500 text-sm font-semibold">
|
|
62
|
-
Failed loading question component
|
|
63
|
-
</p>
|
|
64
|
-
<pre className="text-sm font-mono">{error?.message}</pre>
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export const QuestionComponent = (props: QuestionComponentProps) => {
|
|
70
|
-
return (
|
|
71
|
-
<ErrorBoundary FallbackComponent={FallbackComponent}>
|
|
72
|
-
<QuestionComponentContent {...props} />
|
|
73
|
-
</ErrorBoundary>
|
|
74
|
-
);
|
|
75
|
-
};
|
package/src/preview-ui/index.css
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
@layer modules_theme, theme, modules_base, base, modules_components, components, modules_utilities, utilities;
|
|
2
|
-
|
|
3
|
-
@import "tailwindcss";
|
|
4
|
-
@import "@examplary/ui/src/global.css";
|
|
5
|
-
|
|
6
|
-
@layer base {
|
|
7
|
-
:root {
|
|
8
|
-
--radius: 0.5rem;
|
|
9
|
-
--font-sans: "DM Sans", "Inter", "sans-serif";
|
|
10
|
-
}
|
|
11
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export const api = {
|
|
2
|
-
get: async (path: string, options: any) => {
|
|
3
|
-
console.warn("API calls are not supported in the preview UI");
|
|
4
|
-
return {};
|
|
5
|
-
},
|
|
6
|
-
post: async (path: string, body: any, options: any) => {
|
|
7
|
-
console.warn("API calls are not supported in the preview UI");
|
|
8
|
-
return {};
|
|
9
|
-
},
|
|
10
|
-
put: async (path: string, body: any, options: any) => {
|
|
11
|
-
console.warn("API calls are not supported in the preview UI");
|
|
12
|
-
return {};
|
|
13
|
-
},
|
|
14
|
-
delete: async (path: string, options: any) => {
|
|
15
|
-
console.warn("API calls are not supported in the preview UI");
|
|
16
|
-
return {};
|
|
17
|
-
},
|
|
18
|
-
patch: async (path: string, body: any, options: any) => {
|
|
19
|
-
console.warn("API calls are not supported in the preview UI");
|
|
20
|
-
return {};
|
|
21
|
-
},
|
|
22
|
-
uploadFile: async (accept: "*/*") => {
|
|
23
|
-
console.warn("File uploads are not supported in the preview UI");
|
|
24
|
-
return {
|
|
25
|
-
url: `https://placehold.co/600x400?${Math.random()}`,
|
|
26
|
-
name: "image.jpg",
|
|
27
|
-
};
|
|
28
|
-
},
|
|
29
|
-
};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { initReactI18next } from "react-i18next";
|
|
2
|
-
|
|
3
|
-
import I18n from "i18next";
|
|
4
|
-
import LanguageDetector from "i18next-browser-languagedetector";
|
|
5
|
-
|
|
6
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
7
|
-
export const i18n = I18n.use(initReactI18next)
|
|
8
|
-
.use(LanguageDetector)
|
|
9
|
-
.init({
|
|
10
|
-
interpolation: {
|
|
11
|
-
escapeValue: false,
|
|
12
|
-
},
|
|
13
|
-
supportedLngs: ["en", "nl"],
|
|
14
|
-
fallbackLng: "en",
|
|
15
|
-
});
|