Code snippet, better UX in questionnaire

This commit is contained in:
Rotimi Best
2021-07-03 19:29:41 +00:00
parent b76d1c9647
commit fbdfd3321b
17 changed files with 348 additions and 92 deletions
+9
View File
@@ -0,0 +1,9 @@
<script>
import marked from "marked";
export let code = "";
</script>
<div class="bg-gray-200 p-5 rounded-md mb-3">
{@html marked("```js\n" + code + "\n```")}
</div>
@@ -2,9 +2,9 @@
import {
questionnaire,
handleAddOption,
handleAddQuestion,
handleRemoveOption,
handleRemoveQuestion,
handleCode,
} from "./store/index";
import Delete24 from "carbon-icons-svelte/lib/Delete24";
import AddFilled24 from "carbon-icons-svelte/lib/AddFilled24";
@@ -14,9 +14,10 @@
import Checkbox from "../../../../Form/Checkbox.svelte";
import RadioItem from "../../../../Form/RadioItem.svelte";
import IconButton from "../../../../IconButton/index.svelte";
import Select from "../../../../Form/Select.svelte";
import PrimaryButton from "../../../../PrimaryButton/index.svelte";
import Dropdown from "../../../../Dropdown/index.svelte";
import { VARIANTS } from "../../../../PrimaryButton/constants";
import Select from "../../../../Form/Select.svelte";
import QuestionContainer from "../../../../QuestionContainer/index.svelte";
import {
QUESTION_TYPE,
@@ -26,39 +27,83 @@
// import readme from "../../readme.js";
// let value = readme;
const extraActions = ["Code Snippets", "Upload image"];
const initialQuestionsLength = $questionnaire.questions.length;
function shouldScrollToLast(index, questions) {
const currentQuestionsLength = questions.length;
const isLast = index + 1 === currentQuestionsLength;
return isLast && initialQuestionsLength !== currentQuestionsLength;
}
function handleOptionClick(questionIndex) {
return (actionIndex) => {
switch (actionIndex) {
case 0:
handleCode(questionIndex, true);
break;
case 1:
// uploadImage(questionIndex);
break;
default:
console.log("No option");
}
};
}
</script>
<div class="root relative">
<div class="head flex bg-white items-center justify-between sticky right-0">
<h3>Home task</h3>
<PrimaryButton
variant={VARIANTS.OUTLINED}
onClick={handleAddQuestion}
label="Add Question"
mnb
v
/>
</div>
<div class="w-auto m-auto">
<QuestionContainer isTitle={true}>
<TextField placeholder="Title" bind:value={$questionnaire.title} />
<TextField
placeholder="Title"
bind:value={$questionnaire.title}
className="mb-2"
/>
<TextArea
placeholder="Description and Rules"
bind:value={$questionnaire.description}
/>
</QuestionContainer>
{#each $questionnaire.questions as question}
<QuestionContainer onClose={handleRemoveQuestion(question.id)}>
<div class="flex justify-between">
{#each $questionnaire.questions as question, index}
<QuestionContainer
onClose={handleRemoveQuestion(question.id)}
scrollToQuestion={shouldScrollToLast(index, $questionnaire.questions)}
>
<div class="flex justify-between items-center">
<div class="mr-2 w-3/5">
<TextField placeholder="Question" bind:value={question.title} />
</div>
<Dropdown
options={extraActions}
handleOptionClick={handleOptionClick(question.id)}
>
<AddFilled24 class="carbon-icon" />
<p class="ml-2 text-gray-600">Add</p>
</Dropdown>
<Select bind:value={question.type} options={QUESTION_TYPES} />
</div>
<div class="flex flex-col">
{#if typeof question.code === "string"}
<div class="flex justify-between items-center my-3 w-3/5">
<TextArea
bind:value={question.code}
rows="2"
placeholder="Write your code"
/>
<IconButton
value="write-code"
onClick={() => handleCode(question.id, false)}
>
<Delete24 class="carbon-icon" />
</IconButton>
</div>
{/if}
<div class="flex flex-col mt-2">
{#each question.options as option}
{#if QUESTION_TYPE.RADIO === question.type}
<RadioItem
@@ -86,7 +131,7 @@
<div slot="iconbutton">
<IconButton
value={option.id}
onClick={handleRemoveOption(question.id)}
onClick={handleRemoveOption(question.id, option.id)}
>
<Delete24 class="carbon-icon" />
</IconButton>
@@ -101,24 +146,18 @@
</div>
{#if QUESTION_TYPE.TEXTAREA !== question.type}
<div class="flex items-center">
<IconButton onClick={handleAddOption(question.id)}>
<div class="flex items-center mt-3">
<PrimaryButton
disablePadding={true}
className="p-2"
variant={VARIANTS.OUTLINED}
onClick={handleAddOption(question.id)}
>
<AddFilled24 class="carbon-icon" />
</IconButton>
<p class="italic">Add option</p>
<p class="ml-2">Add option</p>
</PrimaryButton>
</div>
{/if}
</QuestionContainer>
{/each}
</div>
<style>
.root {
max-width: 700px;
}
.head {
top: 63px;
z-index: 2;
}
</style>
@@ -1,9 +1,14 @@
<script>
import { questionnaire } from "./store/index";
// import RadioQuestion from "../../../../Question/RadioQuestion/index.svelte";
import RadioQuestion from "../../../../Question/RadioQuestion/index.svelte";
import CheckboxQuestion from "../../../../Question/CheckboxQuestion/index.svelte";
import PrimaryButton from "../../../../PrimaryButton/index.svelte";
import {
QUESTION_TYPE,
QUESTION_TYPES,
} from "../../../../Question/constants";
import Checkbox from "../../../../Form/Checkbox.svelte";
let answers = {};
let currentQuestionIndex = 0;
let currentQuestion = {};
@@ -34,7 +39,10 @@
currentQuestionIndex += 1;
}
$: currentQuestion = questions[currentQuestionIndex - 1];
$: {
currentQuestion = questions[currentQuestionIndex - 1];
console.log("currentQuestion", currentQuestion);
}
function onPrevious() {
currentQuestionIndex -= 1;
@@ -43,7 +51,7 @@
{#if currentQuestionIndex === 0}
<div>
<h2>{title}</h2>
<h2 class="my-1">{title}</h2>
<p>{description}</p>
<PrimaryButton
@@ -57,21 +65,24 @@
<pre>
<code>{JSON.stringify(answers)}</code>
</pre>
{:else}
<!-- <RadioQuestion
{:else if QUESTION_TYPE.RADIO === currentQuestion.type}
<RadioQuestion
title={currentQuestionIndex + ". " + currentQuestion.title}
name={currentQuestion.name}
name={`${currentQuestion.id}`}
options={currentQuestion.options}
defaultValue={answers[currentQuestion.name] || false}
code={currentQuestion.code}
defaultValue={answers[`${currentQuestion.id}`] || false}
{onSubmit}
{onPrevious}
disablePreviousButton={currentQuestionIndex === 1}
isLast={currentQuestionIndex === questions.length}
/> -->
/>
{:else if QUESTION_TYPE.CHECKBOX === currentQuestion.type}
<CheckboxQuestion
title={currentQuestionIndex + ". " + currentQuestion.title}
name={`${currentQuestion.id}`}
options={currentQuestion.options}
code={currentQuestion.code}
defaultValue={answers[currentQuestion.id] || []}
onSubmit={onCheckboxSubmit}
{onPrevious}
@@ -1,25 +1,48 @@
<script>
import PrimaryContainedButton from "../../../../PrimaryContainedButton/index.svelte";
import { handleAddQuestion } from "./store/index";
import PrimaryButton from "../../../../PrimaryButton/index.svelte";
import PageBody from "../../../../PageBody/index.svelte";
import EditMode from "./EditMode.svelte";
import ViewMode from "./ViewMode.svelte";
import MODES from "../../../../../utils/constants/mode.js";
import { VARIANTS } from "../../../../PrimaryButton/constants";
let mode = MODES.view;
let preview = false;
</script>
<PageBody>
<svelte:fragment slot="header">
<h3>Lesson 2 home exercises</h3>
<PrimaryContainedButton
label={mode === MODES.edit ? "Save" : "Edit"}
handleClick={() => (mode = mode === MODES.edit ? MODES.view : MODES.edit)}
/>
<div class="flex items-center">
<PrimaryButton
className="mr-2"
variant={VARIANTS.OUTLINED}
label={mode === MODES.edit ? "Save" : "Edit"}
onClick={() => (mode = mode === MODES.edit ? MODES.view : MODES.edit)}
/>
{#if mode === MODES.edit}
<PrimaryButton
variant={VARIANTS.OUTLINED}
label={preview ? "Edit" : "Preview"}
onClick={() => (preview = !preview)}
/>
{/if}
</div>
{#if mode === MODES.edit}
<div class="flex items-center">
<PrimaryButton
variant={VARIANTS.OUTLINED}
onClick={handleAddQuestion}
label="Add Question"
/>
</div>
{/if}
</svelte:fragment>
{#if mode === MODES.edit}
<EditMode />
{:else}
<ViewMode />
<ViewMode {preview} />
{/if}
</PageBody>
@@ -11,19 +11,23 @@ export const questionnaire = writable({
questions: [
{
id: "react-founder",
type: QUESTION_TYPE.CHECKBOX,
type: QUESTION_TYPE.RADIO,
title: "Who is the creator of React.js",
options: [
{
id: 1,
value: "Dan Abrahmov",
},
{
id: 2,
value: "Google",
},
{
id: 3,
value: "Facebook",
},
{
id: 4,
value: "Traversy Media",
},
],
@@ -32,17 +36,26 @@ export const questionnaire = writable({
id: "vue-founder",
type: QUESTION_TYPE.CHECKBOX,
title: "Who is the creator of Vue.js",
code: `const name = 'Josh Perez';\nconst element = <h1>Hello, {name}</h1>;\n\nReactDOM.render(\n
element,\,
document.getElementById('root')
)
`,
options: [
{
id: 1,
value: "Evan Vue",
},
{
id: 2,
value: "Mark Zukerberg",
},
{
id: 3,
value: "Prince Charles",
},
{
id: 4,
value: "Bill Gates",
},
],
@@ -53,34 +66,28 @@ export const questionnaire = writable({
title: "Who is the creator of Svelte.js",
options: [
{
id: 1,
value: "Hillary Svelte",
},
{
id: 2,
value: "Mircosoft",
},
{
id: 3,
value: "Elevate",
},
{
id: 4,
value: "Coding Train",
},
],
},
{
id: "angular-founder",
type: QUESTION_TYPE.CHECKBOX,
type: QUESTION_TYPE.TEXTAREA,
title: "Who is the creator of Angular.js",
options: [
{
value: "Laryy page",
},
{
value: "Google",
},
{
value: "Sales Force",
},
],
value: "",
},
],
});
@@ -95,6 +102,7 @@ export function handleAddQuestion() {
{
...QUESTION_TEMPLATE,
id: questions.length + 1,
value: "",
options: [
{
id: 1,
@@ -120,7 +128,7 @@ export function handleAddOption(questionId) {
options: [
...question.options,
{
id: question.options.length + 1,
id: question.options[question.options.length - 1].id + 1,
value: "",
},
],
@@ -143,6 +151,11 @@ export function handleRemoveOption(questionId, optionId) {
...q,
questions: questions.map((question) => {
if (question.id === questionId) {
console.log(
"filter",
optionId,
question.options.filter((option) => option.id !== optionId)
);
return {
...question,
options: [
@@ -170,3 +183,23 @@ export function handleRemoveQuestion(questionId) {
});
};
}
export function handleCode(questionId, shouldAdd = true) {
questionnaire.update((q) => {
const { questions } = q;
return {
...q,
questions: questions.map((question) => {
if (question.id === questionId) {
return {
...question,
code: shouldAdd ? "" : undefined,
};
}
return question;
}),
};
});
}
+54
View File
@@ -0,0 +1,54 @@
<script>
// import ChevronDown24 from "carbon-icons-svelte/lib/ChevronDown24";
export let options = [];
export let handleOptionClick;
let open = false;
function onClick(optionIndex) {
return () => {
open = !open;
console.log("onClick", optionIndex);
handleOptionClick(optionIndex);
};
}
function onBlur() {
// In case an option was clicked, let's wait for the on:click of that button before we close
setTimeout(() => (open = false), 100);
}
</script>
<div class="relative">
<button
class="flex flex-row items-center border rounded-lg border-grey py-2 px-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200"
on:click={() => (open = !open)}
on:blur={onBlur}
>
<slot />
<!-- <ChevronDown24 class="ml-2" /> -->
</button>
{#if open}
<div
class="dropdown w-48 bg-white border border-grey rounded-lg mt-2 py-2 absolute -top-0.5"
>
{#each options as option, index}
<button
class="block px-4 py-2 border-b w-full"
on:click={onClick(index)}
>
{option}
</button>
{/each}
</div>
{/if}
</div>
<style>
.dropdown {
box-shadow: 0px 5px 5px -3px rgb(0 0 0 / 20%),
0px 8px 10px 1px rgb(0 0 0 / 14%), 0px 3px 14px 2px rgb(0 0 0 / 12%);
}
</style>
+1 -1
View File
@@ -6,7 +6,7 @@
export let isEditable = false;
</script>
<label class="inline-flex items-center">
<label class="inline-flex items-center w-full">
<input
type="checkbox"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-offset-0 focus:ring-indigo-200 focus:ring-opacity-50"
+1 -1
View File
@@ -6,7 +6,7 @@
export let isEditable = false;
</script>
<label class="inline-flex items-center">
<label class="inline-flex items-center w-full">
<input
class="form-radio"
type="radio"
+35 -3
View File
@@ -2,18 +2,50 @@
export let label;
export let placeholder = "Write your answer...";
export let value;
export let rows = "3";
export let maxRows;
$: minHeight = `${1 + parseInt(rows, 10) * 1.2}em`;
$: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`;
</script>
<label class="block">
<label class="block w-full relative">
{#if label}
<span class="text-gray-700">{label}</span>
{/if}
<pre
aria-hidden="true"
style="min-height: {minHeight}; max-height: {maxHeight}">
{value + '\n'}
</pre>
<textarea
class="form-textarea mt-1 block w-full h-24"
rows="3"
{rows}
{placeholder}
bind:value
class="form-textarea block w-full bg-gray-100 border-t-0 border-l-0 border-r-0 border-b-1"
/>
<slot name="iconbutton" />
</label>
<style>
pre,
textarea {
font-family: inherit;
padding: 0.5em;
box-sizing: border-box;
border: 1px solid #eee;
line-height: 1.2;
overflow: hidden;
}
textarea {
position: absolute;
width: 100%;
height: 100%;
top: 0;
resize: none;
}
</style>
+2 -1
View File
@@ -3,9 +3,10 @@
export let placeholder;
export let value;
export let onKeyDown;
export let className = "";
</script>
<label class="block mb-3">
<label class="block {className}">
{#if label}<h4 class="m-0">{label}</h4>{/if}
<input
class="form-input p-2 mt-1 block w-full bg-gray-100 border-t-0 border-l-0 border-r-0 border-b-1"
@@ -1,11 +1,12 @@
<script>
// import
// border border-gray-300 rounded-md p-3 bg-blue-200 bg-opacity-25
</script>
<div class="m-3">
<div class="root mt-4 mx-auto max-w-3xl relative">
{#if $$slots.header}
<div
class="head flex bg-white items-center justify-between sticky right-0 w-full"
class="head flex bg-white items-center justify-between sticky right-0 w-full py-3"
>
<slot name="header" />
</div>
@@ -15,8 +16,12 @@
</div>
<style>
/* .root {
max-width: 700px;
} */
.head {
top: 63px;
z-index: 2;
top: 61px;
z-index: 1;
}
</style>
+1 -1
View File
@@ -6,5 +6,5 @@ export const VARIANTS = {
export const VARIANTS_CLASS = {
[VARIANTS.CONTAINED]: "bg-blue-700 hover:bg-blue-900 text-white font-bold",
[VARIANTS.OUTLINED]:
"border border-blue-700 hover:border-blue-700 hover:bg-blue-200 text-black",
"border border-gray-700 hover:border-black-300 hover:bg-gray-200 text-black",
};
+11 -2
View File
@@ -6,15 +6,24 @@
export let name = "";
export let type;
export let variant = VARIANTS.CONTAINED;
export let disablePadding = false;
export let isDisabled = false;
const variantClass = VARIANTS_CLASS[variant];
</script>
<button
class="h-auto {variantClass} py-1 px-3 rounded {className} transition delay-150 duration-300 ease-in-out"
class="{isDisabled &&
'bg-opacity-25 hover:bg-opacity-25 cursor-not-allowed'} flex items-center h-auto {variantClass} {!disablePadding &&
'py-1 px-5'} rounded {className} transition delay-150 duration-300 ease-in-out "
on:click={onClick}
{name}
{type}
disabled={isDisabled}
>
{label}
{#if label}
{label}
{:else}
<slot />
{/if}
</button>
@@ -1,8 +1,10 @@
<script>
import PrimaryButton from "../../PrimaryButton/index.svelte";
import CodeSnippet from "../../CodeSnippet/index.svelte";
import Checkbox from "../../Form/Checkbox.svelte";
export let title = "";
export let code;
export let name = "";
export let options = [];
export let onSubmit = () => {};
@@ -39,22 +41,31 @@
<form on:submit|preventDefault={handleFormSubmit}>
<h2>{title}</h2>
{#if code}
<CodeSnippet {code} />
{/if}
<div class="ml-4">
{#each options as option}
<div>
<button
class="text-left my-2 border border-gray-300 p-2 rounded-md cursor-pointer hover:bg-gray-200 w-full"
type="button"
>
<Checkbox
{name}
value={option.value}
checked={defaultValue.includes(option.value)}
label={option.label || option.value}
/>
</div>
</button>
{/each}
</div>
<div class="mt-3">
{#if disablePreviousButton === false}
<PrimaryButton onClick={handlePrevious} label="Previous" />
{/if}
<div class="mt-3 flex items-center justify-between w-full">
<PrimaryButton
onClick={handlePrevious}
label="Previous"
isDisabled={disablePreviousButton}
/>
<PrimaryButton type="submit" label={isLast ? "Finish" : "Next"} {name} />
</div>
</form>
@@ -1,8 +1,10 @@
<script>
import PrimaryButton from "../../PrimaryButton/index.svelte";
import RadioItem from "../../Form/RadioItem.svelte";
import CodeSnippet from "../../CodeSnippet/index.svelte";
export let title = "";
export let code;
export let name = "";
export let options = [];
export let onSubmit = () => {};
@@ -39,22 +41,31 @@
<form on:submit|preventDefault={handleFormSubmit}>
<h2>{title}</h2>
{#if code}
<CodeSnippet {code} />
{/if}
<div class="ml-4">
{#each options as option}
<div>
<button
class="text-left my-2 border border-gray-300 p-2 rounded-md cursor-pointer hover:bg-gray-200 w-full"
type="button"
>
<RadioItem
{name}
value={option.value}
checked={option.value === defaultValue}
label={option.label}
label={option.label || option.value}
/>
</div>
</button>
{/each}
</div>
<div class="mt-3">
{#if disablePreviousButton === false}
<PrimaryButton onClick={handlePrevious} label="Previous" />
{/if}
<div class="mt-3 flex items-center justify-between w-full">
<PrimaryButton
onClick={handlePrevious}
label="Previous"
isDisabled={disablePreviousButton}
/>
<PrimaryButton type="submit" label={isLast ? "Finish" : "Next"} {name} />
</div>
</form>
+1
View File
@@ -22,6 +22,7 @@ export const QUESTION_TYPES = [
export const QUESTION_TEMPLATE = {
id: 1,
title: "",
code: "",
type: QUESTION_TYPE.RADIO,
options: [
{
+18 -1
View File
@@ -1,13 +1,30 @@
<script>
// import { onMount } from "svelte";
import CloseButton from "../Buttons/Close/index.svelte";
export let isTitle = false;
export let onClose;
export let scrollToQuestion = false;
let ref;
// onMount(() => {
// if (scrollToQuestion) {
// ref.scrollIntoView({ block: "end", behavior: "smooth" });
// }
// });
$: {
if (ref && scrollToQuestion) {
ref.scrollIntoView({ block: "end", behavior: "smooth" });
}
}
</script>
<div
bind:this={ref}
class="{isTitle
? 'border'
: 'border-2'} border-gray border-r-2 rounded-md mb-6 relative root"
: 'border-2'} border-gray border-r-2 rounded-md mb-6 relative root hover:border-blue-700"
>
{#if isTitle}
<div class="title absolute bg-blue-700" />