finish the wizard UI

This commit is contained in:
2024-04-10 23:20:48 -06:00
parent bc8454e597
commit 0cb7d34bcf
4 changed files with 204 additions and 120 deletions

View File

@ -6,9 +6,10 @@
/*padding: 2rem;*/ /*padding: 2rem;*/
/*font-weight: normal;*/ /*font-weight: normal;*/
height: 100vh; height: 100vh;
display: flex; width: 100vw;
flex-direction: column; /*display: flex;*/
justify-content: center; /*flex-direction: column;*/
/*justify-content: center;*/
padding: 1rem; padding: 1rem;
} }

View File

@ -16,6 +16,7 @@ const lastName = ref('');
const email = ref(''); const email = ref('');
const newsletterConsent = ref(true); const newsletterConsent = ref(true);
const isOpen = ref(true); const isOpen = ref(true);
const formSubmitted = ref(false);
function submitForm() { function submitForm() {
console.log('Form submitted'); console.log('Form submitted');
console.log('First name:', firstName.value); console.log('First name:', firstName.value);
@ -32,6 +33,7 @@ function submitForm() {
}).catch((e) => { }).catch((e) => {
console.error('Error submitting email:', e); console.error('Error submitting email:', e);
}).finally(() => { }).finally(() => {
formSubmitted.value = true;
closeModal(); closeModal();
}); });
} }
@ -57,6 +59,9 @@ async function submitEmail() {
}); });
} }
function closeModal() { function closeModal() {
if (!formSubmitted.value) {
return;
}
isOpen.value = false isOpen.value = false
} }
function openModal() { function openModal() {
@ -66,13 +71,13 @@ function openModal() {
<template> <template>
<div class="inset-0 flex items-center justify-center"> <div class="inset-0 flex items-center justify-center">
<button <!-- <button-->
type="button" <!-- type="button"-->
@click="openModal" <!-- @click="openModal"-->
class="rounded-md bg-black/20 px-4 py-2 text-sm font-medium text-white hover:bg-black/30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/75" <!-- class="rounded-md bg-black/20 px-4 py-2 text-sm font-medium text-white hover:bg-black/30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/75"-->
> <!-- >-->
Open dialog <!-- Open dialog-->
</button> <!-- </button>-->
</div> </div>
<TransitionRoot appear :show="isOpen" as="template"> <TransitionRoot appear :show="isOpen" as="template">
<Dialog as="div" @close="closeModal" class="relative z-10"> <Dialog as="div" @close="closeModal" class="relative z-10">

View File

@ -5,6 +5,10 @@ import {
ListboxButton, ListboxButton,
ListboxOptions, ListboxOptions,
ListboxOption, ListboxOption,
RadioGroup,
RadioGroupLabel,
RadioGroupOption,
TransitionRoot
} from "@headlessui/vue"; } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/vue/20/solid'; import { CheckIcon, ChevronUpDownIcon } from '@heroicons/vue/20/solid';
@ -25,11 +29,11 @@ const questions = ref<Question[]>([
clarification: 'Some authentication tools are better suited for specific types of applications.', clarification: 'Some authentication tools are better suited for specific types of applications.',
answerType: 'checkbox', answerType: 'checkbox',
answer: [], answer: [],
options: ['Web', 'Mobile', 'Desktop', 'SaaS', 'Other'] options: ['Web', 'Mobile', 'Desktop', 'SaaS', 'Embedded', 'Other']
}, },
{ {
id: 2, id: 2,
question: 'How many users do you expect to have in the short term and long term?', question: 'How many users do you plan to support?',
clarification: 'This will help determine the scalability requirements of the authentication tool.', clarification: 'This will help determine the scalability requirements of the authentication tool.',
answerType: 'select', answerType: 'select',
answer: '', answer: '',
@ -71,9 +75,9 @@ const questions = ref<Question[]>([
id: 7, id: 7,
question: 'Do you need to support third-party logins via OIDC or SAML?', question: 'Do you need to support third-party logins via OIDC or SAML?',
clarification: 'Supporting third-party logins can simplify the sign-in process for users who already have accounts with these providers.', clarification: 'Supporting third-party logins can simplify the sign-in process for users who already have accounts with these providers.',
answerType: 'select', answerType: 'radio',
answer: '', answer: '',
options: ['OIDC', 'SAML', 'Neither'] options: ['OIDC', 'SAML', 'Both', 'Neither']
}, },
{ {
id: 8, id: 8,
@ -87,9 +91,9 @@ const questions = ref<Question[]>([
id: 9, id: 9,
question: 'What level of customization do you require for the MFA features?', question: 'What level of customization do you require for the MFA features?',
clarification: 'Some authentication tools offer more customization options for MFA, such as custom branding, custom workflows, or custom policies.', clarification: 'Some authentication tools offer more customization options for MFA, such as custom branding, custom workflows, or custom policies.',
answerType: 'select', answerType: 'radio',
answer: '', answer: '',
options: ['Basic', 'Intermediate', 'Advanced'] options: ['Basic', 'Intermediate', 'Advanced', 'No Customization']
}, },
{ {
id: 10, id: 10,
@ -125,14 +129,6 @@ const questions = ref<Question[]>([
}, },
{ {
id: 14, id: 14,
question: 'How often do you anticipate needing to upgrade or update the authentication tool?',
clarification: 'Regular updates can help keep your authentication tool secure and up-to-date with the latest features and standards.',
answerType: 'select',
answer: '',
options: ['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Annually', 'Never']
},
{
id: 15,
question: 'Do you have any preferences for user interface and experience?', question: 'Do you have any preferences for user interface and experience?',
clarification: 'The user interface and experience of an authentication tool can impact user adoption and satisfaction.', clarification: 'The user interface and experience of an authentication tool can impact user adoption and satisfaction.',
answerType: 'radio', answerType: 'radio',
@ -156,63 +152,134 @@ function nextQuestion() {
currentQuestionIndex.value++; currentQuestionIndex.value++;
} }
} }
const onLastQuestion = computed(() => {
return currentQuestionIndex.value >= questions.value.length - 1;
});
// should return true if we are on the last button and the answer is not filled out // should return true if we are on the last button and the answer is not filled out
const isNextButtonDisabled = computed(() => { const isNextButtonDisabled = computed(() => {
const onLastQuestion = currentQuestionIndex.value >= questions.value.length - 1;
if (questions.value[currentQuestionIndex.value].answerType === 'checkbox') { if (questions.value[currentQuestionIndex.value].answerType === 'checkbox') {
return !(questions.value[currentQuestionIndex.value].answer as string[]).length || onLastQuestion; return !(questions.value[currentQuestionIndex.value].answer as string[]).length;
} }
if (questions.value[currentQuestionIndex.value].answerType === 'select') { if (questions.value[currentQuestionIndex.value].answerType === 'select') {
return (questions.value[currentQuestionIndex.value].answer as string).trim().length <= 0 || onLastQuestion; return (questions.value[currentQuestionIndex.value].answer as string).trim().length <= 0;
} }
if (questions.value[currentQuestionIndex.value].answerType === 'radio') { if (questions.value[currentQuestionIndex.value].answerType === 'radio') {
return (questions.value[currentQuestionIndex.value].answer as string).trim().length <= 0 || onLastQuestion; return (questions.value[currentQuestionIndex.value].answer as string).trim().length <= 0;
} }
return onLastQuestion; return onLastQuestion.value;
}); });
function done() {
alert('done');
}
</script> </script>
<template> <template>
<!-- <h1 class="">Authentication Tool Wizard</h1>--> <div class="relative w-full flex flex-col justify-center items-center" v-for="(question, index) in questions" :key="index">
<div class="flex flex-col"> <TransitionRoot
<h2 class="text-2xl">{{ questions[currentQuestionIndex].question }}</h2> appear
<p class="text-gray-500" v-if="questions[currentQuestionIndex].clarification">{{ questions[currentQuestionIndex].clarification }}</p> :show="currentQuestionIndex === index"
<div class="mt-10 flex justify-center z-10"> as="template"
<div v-if="questions[currentQuestionIndex].answerType === 'radio'"> enter="transition-opacity ease-linear duration-300"
<div v-for="(option, index) in questions[currentQuestionIndex].options" :key="index"> enter-from="opacity-0"
<input type="radio" :id="option" :value="option" v-model="questions[currentQuestionIndex].answer" /> enter-to="opacity-100"
<label :for="option">{{ option }}</label> leave="transition-opacity ease-linear duration-300"
</div> leave-from="opacity-100"
</div> leave-to="opacity-0"
<div v-else-if="questions[currentQuestionIndex].answerType === 'checkbox'"> class="absolute"
<Listbox v-model="questions[currentQuestionIndex].answer" multiple> >
<ListboxButton <div class="flex flex-col">
class="relative cursor-default rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm" <h2 class="text-2xl">{{ question.question }}</h2>
> <p class="text-gray-500" v-if="question.clarification">{{ question.clarification }}</p>
<span>{{ !!(questions[currentQuestionIndex].answer as string[]).length ? (questions[currentQuestionIndex].answer as string[]).join(', ') : 'Select options'}}</span> <div class="mt-10 flex justify-center z-10">
<span <div class="mx-auto w-full max-w-md" v-if="question.answerType === 'radio'">
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
> <RadioGroup v-model="question.answer">
<RadioGroupLabel class="sr-only">Options</RadioGroupLabel>
<div class="space-y-2">
<RadioGroupOption
v-for="(option, index) in question.options"
as="template"
v-slot="{ active, checked }"
:key="index"
:value="option"
>
<div
:class="[
active
? 'ring-2 ring-white/60 ring-offset-2 ring-offset-amber-300'
: '',
checked ? 'bg-amber-900/75 text-white ' : 'bg-white ',
]"
class="relative flex cursor-pointer rounded-lg px-5 py-4 shadow-md focus:outline-none"
>
<div class="flex w-full items-center justify-between">
<div class="flex items-center">
<div class="text-sm">
<RadioGroupLabel
as="p"
:class="checked ? 'text-white' : 'text-gray-900'"
class="font-medium"
>
{{ option }}
</RadioGroupLabel>
</div>
</div>
<div v-show="checked" class="shrink-0 text-white">
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none">
<circle
cx="12"
cy="12"
r="12"
fill="#fff"
fill-opacity="0.2"
/>
<path
d="M7 13l3 3 7-7"
stroke="#fff"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
</div>
</div>
</RadioGroupOption>
</div>
</RadioGroup>
</div>
<div class="mx-auto" v-else-if="question.answerType === 'checkbox'">
<Listbox v-model="question.answer" multiple>
<ListboxButton
class="flex justify-between items-center cursor-default rounded-lg bg-white py-2 pl-3 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm"
style="width: 250px"
>
<span>{{ !!(question.answer as string[]).length ? (question.answer as string[]).join(', ') : 'Select options'}}</span>
<span
class="pointer-events-none flex items-center pr-2"
>
<ChevronUpDownIcon <ChevronUpDownIcon
class="h-5 w-5 text-gray-400" class="h-5 w-5 text-gray-400"
aria-hidden="true" aria-hidden="true"
/> />
</span> </span>
</ListboxButton> </ListboxButton>
<ListboxOptions <ListboxOptions
class="absolute mt-1 max-h-60 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm" class="absolute mt-1 max-h-60 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm"
> >
<ListboxOption <ListboxOption
v-slot="{ active, selected }" v-slot="{ active, selected }"
v-for="(option, index) in questions[currentQuestionIndex].options" v-for="(option, index) in question.options"
:key="index" :key="index"
:value="option" :value="option"
as="template" as="template"
> >
<li <li
:class="[ :class="[
active ? 'bg-amber-100 text-amber-900' : 'text-gray-900', active ? 'bg-amber-100 text-amber-900' : 'text-gray-900',
'relative cursor-default select-none py-2 pl-10 pr-4', 'relative cursor-default select-none py-2 pl-10 pr-4',
]"> ]">
@ -222,44 +289,44 @@ const isNextButtonDisabled = computed(() => {
'block truncate', 'block truncate',
]" ]"
>{{ option }}</span> >{{ option }}</span>
<span <span
v-if="selected" v-if="selected"
class="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600" class="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600"
> >
<CheckIcon class="h-5 w-5" aria-hidden="true" /> <CheckIcon class="h-5 w-5" aria-hidden="true" />
</span> </span>
</li> </li>
</ListboxOption> </ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
</div> </div>
<div v-else-if="questions[currentQuestionIndex].answerType === 'select'"> <div v-else-if="question.answerType === 'select'">
<Listbox v-model="questions[currentQuestionIndex].answer"> <Listbox v-model="question.answer">
<ListboxButton <ListboxButton
class="relative cursor-default rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm" class="relative cursor-default rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm"
> >
<span>{{!!questions[currentQuestionIndex].answer ? questions[currentQuestionIndex].answer : 'Select an option'}}</span> <span>{{!!question.answer ? question.answer : 'Select an option'}}</span>
<span <span
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2" class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
> >
<ChevronUpDownIcon <ChevronUpDownIcon
class="h-5 w-5 text-gray-400" class="h-5 w-5 text-gray-400"
aria-hidden="true" aria-hidden="true"
/> />
</span> </span>
</ListboxButton> </ListboxButton>
<ListboxOptions <ListboxOptions
class="absolute mt-1 max-h-60 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm" class="absolute mt-1 max-h-60 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm"
> >
<ListboxOption <ListboxOption
v-slot="{ active, selected }" v-slot="{ active, selected }"
v-for="(option, index) in questions[currentQuestionIndex].options" v-for="(option, index) in question.options"
:key="index" :key="index"
:value="option" :value="option"
as="template" as="template"
> >
<li <li
:class="[ :class="[
active ? 'bg-amber-100 text-amber-900' : 'text-gray-900', active ? 'bg-amber-100 text-amber-900' : 'text-gray-900',
'relative cursor-default select-none py-2 pl-10 pr-4', 'relative cursor-default select-none py-2 pl-10 pr-4',
]"> ]">
@ -269,33 +336,43 @@ const isNextButtonDisabled = computed(() => {
'block truncate', 'block truncate',
]" ]"
>{{ option }}</span> >{{ option }}</span>
<span <span
v-if="selected" v-if="selected"
class="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600" class="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600"
> >
<CheckIcon class="h-5 w-5" aria-hidden="true" /> <CheckIcon class="h-5 w-5" aria-hidden="true" />
</span> </span>
</li> </li>
</ListboxOption> </ListboxOption>
</ListboxOptions> </ListboxOptions>
</Listbox> </Listbox>
</div> </div>
</div> </div>
<div class="mt-20 button-container flex justify-center"> <div class="mt-20 button-container flex justify-center">
<button <button
type="button" type="button"
class="nav-button rounded-md border border-transparent bg-amber-200 px-4 py-2 text-sm font-medium text-orange-950 hover:bg-amber-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2" class="nav-button rounded-md border border-transparent bg-amber-200 px-4 py-2 text-sm font-medium text-orange-950 hover:bg-amber-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
@click="previousQuestion" @click="previousQuestion"
:disabled="currentQuestionIndex === 0"> :disabled="currentQuestionIndex === 0">
<span>Previous</span> <span>Previous</span>
</button> </button>
<button <button
class="nav-button ml-2 rounded-md border border-transparent bg-amber-500 px-4 py-2 text-sm font-medium text-orange-950 hover:bg-amber-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2" class="nav-button ml-2 rounded-md border border-transparent bg-amber-500 px-4 py-2 text-sm font-medium text-orange-950 hover:bg-amber-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
@click="nextQuestion" v-show="currentQuestionIndex < questions.length - 1"
:disabled="isNextButtonDisabled"> @click="nextQuestion"
<span>Next</span> :disabled="isNextButtonDisabled || onLastQuestion">
</button> <span>Next</span>
</div> </button>
<button
class="nav-button ml-2 rounded-md border border-transparent bg-amber-500 px-4 py-2 text-sm font-medium text-orange-950 hover:bg-amber-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
v-show="currentQuestionIndex >= questions.length - 1"
@click="done"
:disabled="isNextButtonDisabled && onLastQuestion">
<span>See Results</span>
</button>
</div>
</div>
</TransitionRoot>
</div> </div>
</template> </template>

View File

@ -6,11 +6,12 @@ import WizardQuestions from "@/components/wizard-questions.vue";
<template> <template>
<div class="h-full w-full flex flex-col justify-center items-center"> <div class="h-full w-full flex flex-col justify-center items-center">
<enter-details-modal /> <!-- <div class="h-full w-full flex flex-col justify-center items-center">-->
<enter-details-modal class="absolute" />
<!-- <iframe src="https://mw.sa.vin/index.php/lists/zq107l51gw5fc/subscribe?output=embed&width=400&height=400" width="400" height="400" frameborder="0" scrolling="yes"></iframe>--> <!-- <iframe src="https://mw.sa.vin/index.php/lists/zq107l51gw5fc/subscribe?output=embed&width=400&height=400" width="400" height="400" frameborder="0" scrolling="yes"></iframe>-->
<wizard-questions class="" /> <wizard-questions />
</div> </div>
</template> </template>