Nearly finished app
This commit is contained in:
parent
0965548a6c
commit
3788f08679
23
client/src/components/icons/FeatureSearch.vue
Normal file
23
client/src/components/icons/FeatureSearch.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<svg-icon type="mdi" :path="path"></svg-icon>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import SvgIcon from '@jamescoyle/vue-icon'
|
||||
import { mdiFeatureSearch } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
name: "MdiFeatureSearch",
|
||||
|
||||
components: {
|
||||
SvgIcon
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
path: mdiFeatureSearch,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -37,20 +37,20 @@
|
||||
</router-link>
|
||||
<!-- Menu Items -->
|
||||
<div class="hidden md:flex space-x-3 justify-center items-center">
|
||||
<Navlink :svgPath="route.meta.svgPath" :text="route.meta.title" :to="route.path" v-for="route in $router.options.routes.filter( value => value.meta.index >= 0 )" :key="route.path" />
|
||||
<Navlink :svgPath="route.meta.svgPath" :text="route.meta.title" :to="route.path" v-for="route in $router.options.routes.filter( value => value.meta.indexBase >= 0 )" :key="route.path" />
|
||||
</div>
|
||||
<Menu v-slot="{ open }" as="div" class="relative inline-block text-left md:hidden">
|
||||
<div>
|
||||
<MenuButton class="inline-flex justify-center w-full rounded-md shadow-sm px-4 py-2 bg-lime-800 text-white hover:bg-lime-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500">
|
||||
<MenuButton class="inline-flex justify-center w-full rounded-md drop-shadow-sm px-4 py-2 bg-lime-800 text-white hover:bg-lime-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500">
|
||||
<span class="scale-150">
|
||||
<mdiMenu/>
|
||||
</span>
|
||||
</MenuButton>
|
||||
</div>
|
||||
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95" enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75" leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
|
||||
<MenuItems class="origin-top-right absolute right-0 mt-2 p-1 space-y-2 w-56 rounded-md shadow-lg bg-lime-700 ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<MenuItems class="origin-top-right absolute right-0 mt-2 p-1 space-y-2 w-56 rounded-md drop-shadow-lg bg-lime-700 ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div class="py-1">
|
||||
<MenuItem v-slot="{ active }" v-for="route in $router.options.routes.filter( value => value.meta.index >= 0 )" :key="route.path" >
|
||||
<MenuItem v-slot="{ active }" v-for="route in $router.options.routes.filter( value => value.meta.indexBase >= 0 )" :key="route.path" >
|
||||
<a :href="route.path" target="_blank" rel="noopener noreferrer" :class="[active ? 'text-orange-300' : 'text-white', 'flex px-4 py-2 active:text-orange-500 rounded-lg h-fit align-middle transition-all ease-in-out duration-500']" v-if="route.path.startsWith('http')">
|
||||
<div class="inline-flex px-3 py-1 space-x-1">
|
||||
<span class="scale-75">
|
||||
|
@ -18,7 +18,7 @@
|
||||
<slot name="header"></slot>
|
||||
</h1>
|
||||
<div class="flex flex-col text-md">
|
||||
<Navlink :svgPath="route.meta.svgPath" :text="route.meta.title" :prefix="prefix" :to="route.path" v-for="route in routes" :key="route.path" />
|
||||
<Navlink :route="route" :prefix="prefix" v-for="route in routes" :key="route.path" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -7,27 +7,25 @@ export default {
|
||||
},
|
||||
props: [
|
||||
'prefix',
|
||||
'svgPath',
|
||||
'text',
|
||||
'to'
|
||||
'route'
|
||||
]
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<a :href="to" class="navlink" target="_blank" rel="noopener noreferrer" v-if="to.startsWith('http')">
|
||||
<a :href="route.path" class="navlink" target="_blank" rel="noopener noreferrer" v-if="route.path.startsWith('http')">
|
||||
<div class="inline-flex px-2 py-1 items-center space-x-1">
|
||||
<span class="scale-75">
|
||||
<svg-icon type="mdi" :path="svgPath"></svg-icon>
|
||||
<svg-icon type="mdi" :path="route.meta.svgPath"></svg-icon>
|
||||
</span>
|
||||
<span>{{ text }}</span>
|
||||
<span>{{ route.meta.title }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<router-link exact-active-class="active-link" class="navlink" :to="prefix + '/' + to" v-else>
|
||||
<router-link :exact-active-class="route.hasOwnProperty('children') ? `` : `active-link`" :active-class="route.hasOwnProperty('children') ? 'active-link' : ''" class="navlink" :to="prefix + '/' + route.path" v-else>
|
||||
<div class="inline-flex px-2 py-1 items-center space-x-1">
|
||||
<span class="scale-75">
|
||||
<svg-icon type="mdi" :path="svgPath"></svg-icon>
|
||||
<svg-icon type="mdi" :path="route.meta.svgPath"></svg-icon>
|
||||
</span>
|
||||
<span>{{ text }}</span>
|
||||
<span>{{ route.meta.title }}</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { mdiAccountBoxMultiple, mdiAccountGroupOutline, mdiAccountQuestion, mdiChartBox, mdiCodeTags, mdiCompare, mdiCounter, mdiEarth, mdiHandCoin, mdiHome, mdiInformation, mdiWeb } from '@mdi/js'
|
||||
import { mdiAccountBoxMultiple, mdiAccountGroupOutline, mdiAccountQuestion, mdiChartBox, mdiCodeTags, mdiCompare, mdiCounter, mdiForum, mdiHandCoin, mdiHome, mdiInformation, mdiLicense, mdiWeb } from '@mdi/js'
|
||||
import About from '@/views/about/Index.vue'
|
||||
import AboutPage from '@/views/about/About.vue'
|
||||
import Licenses from '@/views/about/Licenses.vue'
|
||||
import Acknowledgements from '@/views/about/Acknowledgements.vue'
|
||||
import Wanderhome from '@/views/about/Wanderhome.vue'
|
||||
import Home from '@/views/Home.vue'
|
||||
@ -13,9 +14,11 @@ import Refused from '@/views/errors/Refused.vue'
|
||||
import Results from '@/views/results/Index.vue'
|
||||
import ResultsPage from '@/views/results/Results.vue'
|
||||
import Scores from '@/views/results/Scores.vue'
|
||||
import CompareAnswers from '@/views/results/CompareAnswers.vue'
|
||||
import CompareAnswersIndex from '@/views/results/compare-answers/Index.vue'
|
||||
import Statistics from '@/views/results/Statistics.vue'
|
||||
import CompareResults from '@/views/results/CompareResults.vue'
|
||||
import CompareQuestion from '@/views/results/compare-answers/CompareQuestion.vue'
|
||||
import CompareQuestions from '@/views/results/compare-answers/CompareQuestions.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@ -23,7 +26,7 @@ const routes = [
|
||||
name: 'Home',
|
||||
component: Home,
|
||||
meta: {
|
||||
index: 0,
|
||||
indexBase: 0,
|
||||
svgPath: mdiHome,
|
||||
title: 'Home'
|
||||
}
|
||||
@ -33,7 +36,7 @@ const routes = [
|
||||
name: 'Quiz',
|
||||
component: Quiz,
|
||||
meta: {
|
||||
index: 1,
|
||||
indexBase: 1,
|
||||
svgPath: mdiAccountQuestion,
|
||||
title: 'Take the Quiz'
|
||||
},
|
||||
@ -43,8 +46,8 @@ const routes = [
|
||||
name: 'Quiz',
|
||||
component: QuizPage,
|
||||
meta: {
|
||||
index: 0,
|
||||
parentIndex: 1,
|
||||
indexChild: 0,
|
||||
indexBase: 1,
|
||||
title: 'Take the Quiz'
|
||||
}
|
||||
},
|
||||
@ -56,8 +59,8 @@ const routes = [
|
||||
id: parseInt(route.params.id)
|
||||
}),
|
||||
meta: {
|
||||
index: 1,
|
||||
parentIndex: 1,
|
||||
indexChild: 1,
|
||||
indexBase: 1,
|
||||
title: 'Take the Quiz'
|
||||
}
|
||||
}
|
||||
@ -68,7 +71,7 @@ const routes = [
|
||||
name: 'About',
|
||||
component: About,
|
||||
meta: {
|
||||
index: 2,
|
||||
indexBase: 2,
|
||||
svgPath: mdiInformation,
|
||||
title: 'About'
|
||||
},
|
||||
@ -78,19 +81,30 @@ const routes = [
|
||||
name: 'About',
|
||||
component: AboutPage,
|
||||
meta: {
|
||||
index: 0,
|
||||
parentIndex: 2,
|
||||
indexChild: 0,
|
||||
indexBase: 2,
|
||||
svgPath: mdiInformation,
|
||||
title: 'Background'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'licenses',
|
||||
name: 'Licenses',
|
||||
component: Licenses,
|
||||
meta: {
|
||||
indexChild: 1,
|
||||
indexBase: 2,
|
||||
svgPath: mdiLicense,
|
||||
title: 'Licenses'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wanderhome',
|
||||
name: 'Wanderhome',
|
||||
component: Wanderhome,
|
||||
meta: {
|
||||
index: 1,
|
||||
parentIndex: 2,
|
||||
indexChild: 2,
|
||||
indexBase: 2,
|
||||
svgPath: mdiHandCoin,
|
||||
title: 'About Wanderhome'
|
||||
}
|
||||
@ -100,8 +114,8 @@ const routes = [
|
||||
name: 'Acknowledgements',
|
||||
component: Acknowledgements,
|
||||
meta: {
|
||||
index: 2,
|
||||
parentIndex: 2,
|
||||
indexChild: 3,
|
||||
indexBase: 2,
|
||||
svgPath: mdiAccountGroupOutline,
|
||||
title: 'Acknowledgements'
|
||||
}
|
||||
@ -110,8 +124,8 @@ const routes = [
|
||||
path: 'https://git.vsnt.uk/viveksantayana/wanderhome-quiz',
|
||||
name: 'SourceCode',
|
||||
meta: {
|
||||
index: 3,
|
||||
parentIndex: 2,
|
||||
indexChild: 4,
|
||||
indexBase: 2,
|
||||
svgPath: mdiCodeTags,
|
||||
title: 'View Source Code'
|
||||
}
|
||||
@ -123,7 +137,7 @@ const routes = [
|
||||
name: 'Results',
|
||||
component: Results,
|
||||
meta: {
|
||||
index: -1,
|
||||
indexBase: -1,
|
||||
title: 'Results',
|
||||
svgPath: mdiAccountBoxMultiple
|
||||
},
|
||||
@ -133,10 +147,10 @@ const routes = [
|
||||
name: 'Result',
|
||||
component: ResultsPage,
|
||||
meta: {
|
||||
index: 0,
|
||||
indexChild: 0,
|
||||
title: 'Your Results',
|
||||
svgPath: mdiAccountBoxMultiple,
|
||||
parentIndex: -1
|
||||
indexBase: -1
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -144,10 +158,10 @@ const routes = [
|
||||
name: 'Scores',
|
||||
component: Scores,
|
||||
meta: {
|
||||
index: 1,
|
||||
indexChild: 1,
|
||||
title: 'Your Scores',
|
||||
svgPath: mdiCounter,
|
||||
parentIndex: -1
|
||||
indexBase: -1
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -155,10 +169,10 @@ const routes = [
|
||||
name: 'CompareResults',
|
||||
component: CompareResults,
|
||||
meta: {
|
||||
index: 2,
|
||||
indexChild: 2,
|
||||
title: 'Compare Results',
|
||||
svgPath: mdiCompare,
|
||||
parentIndex: -1
|
||||
indexBase: -1
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -166,22 +180,51 @@ const routes = [
|
||||
name: 'Statistics',
|
||||
component: Statistics,
|
||||
meta: {
|
||||
index: 3,
|
||||
indexChild: 3,
|
||||
title: 'Statistics',
|
||||
svgPath: mdiChartBox,
|
||||
parentIndex: -1
|
||||
indexBase: -1
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'answers',
|
||||
name: 'Answers',
|
||||
component: CompareAnswers,
|
||||
component: CompareAnswersIndex,
|
||||
meta: {
|
||||
index: 4,
|
||||
indexChild: 4,
|
||||
title: 'Compare Answers',
|
||||
svgPath: mdiEarth,
|
||||
parentIndex: -1
|
||||
}
|
||||
svgPath: mdiForum,
|
||||
indexBase: -1
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'RootAnswer',
|
||||
component: CompareQuestions,
|
||||
meta: {
|
||||
indexGrandchild: 0,
|
||||
title: 'Compare Answers',
|
||||
svgPath: mdiForum,
|
||||
indexBase: -1,
|
||||
indexChild: 4
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
name: 'CompareQuestion',
|
||||
component: CompareQuestion,
|
||||
props: route => ({
|
||||
id: parseInt(route.params.id)
|
||||
}),
|
||||
meta: {
|
||||
indexGrandchild: 1,
|
||||
title: 'Compare Answers',
|
||||
svgPath: mdiForum,
|
||||
indexBase: -1,
|
||||
indexChild: 4
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -190,17 +233,17 @@ const routes = [
|
||||
name: 'NotFound',
|
||||
component: NotFound,
|
||||
meta: {
|
||||
index: -1,
|
||||
indexBase: -2,
|
||||
title: 'Error: Not Found',
|
||||
svgPath: mdiWeb
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/:catchAll(.*)",
|
||||
name: 'NotFound',
|
||||
name: 'CatchAll',
|
||||
redirect: '/err_notfound',
|
||||
meta: {
|
||||
index: -1,
|
||||
indexBase: -3,
|
||||
title: 'Error: Not Found',
|
||||
svgPath: mdiWeb
|
||||
}
|
||||
@ -210,7 +253,7 @@ const routes = [
|
||||
name: 'Refused',
|
||||
component: Refused,
|
||||
meta: {
|
||||
index: -1,
|
||||
indexBase: -4,
|
||||
title: 'Error: Connection Refused',
|
||||
svgPath: mdiWeb
|
||||
}
|
||||
@ -222,31 +265,36 @@ const Router = createRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
Router.beforeEach((to, from, next) => {
|
||||
Router.beforeEach((to, from) => {
|
||||
if (to.meta.title != 'Home') {
|
||||
document.title = `Wanderhome Quiz | V.S. - ${to.meta.title}`
|
||||
} else {
|
||||
document.title = `Wanderhome Quiz | V.S.`
|
||||
}
|
||||
next()
|
||||
if (from.meta.indexBase == -1 && to.meta.indexBase != -1) {
|
||||
if (!confirm('If you navigate away from the Results section, then you will lose your results and need to start the quiz again. Are you sure?')) {
|
||||
return false
|
||||
}
|
||||
to.meta.resetApp = true
|
||||
}
|
||||
})
|
||||
|
||||
Router.afterEach( (to, from) => {
|
||||
const enterActiveClass = 'transition-all duration-300 origin-top ease-in'
|
||||
const leaveActiveClass = 'transition-all duration-300 origin-top ease-out'
|
||||
const baseFromClass = 'transform opacity-0 md:scale-75'
|
||||
if ( to.fullPath.split('/')[1] !== from.fullPath.split('/')[1] || !from.matched.length ) {
|
||||
const fromIndex = from.meta.parentIndex ? from.meta.parentIndex : from.meta.index
|
||||
const toIndex = to.meta.parentIndex ? to.meta.parentIndex : to.meta.index
|
||||
if ( from.meta.indexBase !== to.meta.indexBase ) {
|
||||
const fromIndex = from.meta.indexBase
|
||||
const toIndex = to.meta.indexBase
|
||||
const slideInDirection = toIndex < fromIndex ? 'md:-translate-x-1/3' : 'md:translate-x-1/3'
|
||||
const slideOutDirection = toIndex < fromIndex ? 'md:translate-x-1/3' : 'md:-translate-x-1/3'
|
||||
to.meta.mainEnterActiveClass = enterActiveClass
|
||||
to.meta.mainLeaveActiveClass = leaveActiveClass
|
||||
to.meta.mainEnterFromClass = baseFromClass + ' ' + slideInDirection
|
||||
to.meta.mainLeaveToClass = baseFromClass + ' ' + slideOutDirection
|
||||
} else if ( to.fullPath.split('/')[1] == from.fullPath.split('/')[1] && from.matched.length ) {
|
||||
const fromIndex = from.meta.index
|
||||
const toIndex = to.meta.index
|
||||
} else if ( from.meta.indexChild !== to.meta.indexChild ) {
|
||||
const fromIndex = from.meta.indexChild
|
||||
const toIndex = to.meta.indexChild
|
||||
const slideInDirection = toIndex < fromIndex ? 'md:-translate-y-1/3' : 'md:translate-y-1/3'
|
||||
const slideOutDirection = toIndex < fromIndex ? 'md:translate-y-1/3' : 'md:-translate-y-1/3'
|
||||
to.meta.panelEnterActiveClass = enterActiveClass
|
||||
|
@ -4,6 +4,7 @@ export const useAnswersStore = defineStore({
|
||||
id: 'answers',
|
||||
state: () => ({
|
||||
answers: [],
|
||||
quizCurrent: null
|
||||
}),
|
||||
actions: {
|
||||
isAnswered(index) {
|
||||
|
@ -3,18 +3,14 @@ import { defineStore } from 'pinia'
|
||||
export const useAppStore = defineStore({
|
||||
id: 'app',
|
||||
state: () => ({
|
||||
hasData: false,
|
||||
answers: [],
|
||||
playbooks: {},
|
||||
results: {},
|
||||
scores: {},
|
||||
count: null
|
||||
count: null,
|
||||
compareCurrent: null
|
||||
}),
|
||||
actions: {
|
||||
toggleData() {
|
||||
this.hasData = !this.hasData
|
||||
console.log('Toggled Has Data. New value', this.hasData)
|
||||
},
|
||||
store(key, data) {
|
||||
this[key] = JSON.parse(JSON.stringify(data))
|
||||
}
|
||||
|
@ -3,8 +3,7 @@ import { defineStore } from 'pinia'
|
||||
export const useQuestionStore = defineStore({
|
||||
id: 'questions',
|
||||
state: () => ({
|
||||
questions: [],
|
||||
currentQuestion: 0
|
||||
questions: []
|
||||
}),
|
||||
actions: {
|
||||
storeQuestions(questionArray) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { useAppStore } from '@/stores/app.js'
|
||||
import Content from '@/components/Content.vue'
|
||||
import Header from '@/components/Header.vue'
|
||||
import TextFrame from '@/components/TextFrame.vue'
|
||||
@ -6,11 +7,21 @@
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
AccountQuestion,
|
||||
Content,
|
||||
Header,
|
||||
TextFrame
|
||||
}
|
||||
AccountQuestion,
|
||||
Content,
|
||||
Header,
|
||||
TextFrame
|
||||
},
|
||||
setup() {
|
||||
const appStore = useAppStore()
|
||||
|
||||
return {
|
||||
appStore
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.appStore.$reset()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
@ -41,11 +52,6 @@
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
<p class="text-sm">
|
||||
This quiz is not affiliated with, endorsed by, or in any way connected to Possum Creek Games.
|
||||
It is a fan project and a programming exercise by someone who loves Jay’s game.
|
||||
I would nevertheless implore you to play <em>Wanderhome</em> if you haven’t already.
|
||||
</p>
|
||||
</article>
|
||||
</Content>
|
||||
</TextFrame>
|
||||
|
@ -18,78 +18,84 @@
|
||||
</Header>
|
||||
<Content>
|
||||
<article class="prose mx-auto">
|
||||
<h2>What Exactly Is This?</h2>
|
||||
<p>
|
||||
This web app is a personality quiz to see what <em class="uncial-antiqua">Wanderhome</em> playbook you are.
|
||||
I have previously made a smilar <a href="https://masks.vsnt.uk/" target="_blank" rel="noopener noreferrer">character quiz for Brendan Conway’s <em>Masks: a New Generation</em></a>.
|
||||
I have also made a digital version of the <a href="https://reftest.vsnt.uk/" target="_blank" rel="noopener noreferrer">Scottish Korfball refereeing exam</a>.
|
||||
</p>
|
||||
<p>
|
||||
This was a fun programming exercise I set myself.
|
||||
But quizzes like this are also really enjoyable to do, and not to mention compare our results with others.
|
||||
And I would love to make this quiz if it gives me a reason to talk to people I know about my favourite TTRPGs.
|
||||
</p>
|
||||
<p>
|
||||
If you want to read more about <em class="uncial-antiqua">Wanderhome</em>, you can do so in the <router-link to="/about/wanderhome">relevant page in this app</router-link>.
|
||||
I wholeheartedly recommend playing it!
|
||||
</p>
|
||||
<section>
|
||||
<h2>What Exactly Is This?</h2>
|
||||
<p>
|
||||
This web app is a personality quiz to see what <em class="uncial-antiqua">Wanderhome</em> playbook you are.
|
||||
I have previously made a smilar <a href="https://masks.vsnt.uk/" target="_blank" rel="noopener noreferrer" class="hover:bg-orange-200">character quiz for Brendan Conway’s <em>Masks: a New Generation</em></a>.
|
||||
I have also made a digital version of the <a href="https://reftest.vsnt.uk/" target="_blank" rel="noopener noreferrer" class="hover:bg-orange-200">Scottish Korfball refereeing exam</a>.
|
||||
</p>
|
||||
<p>
|
||||
This was a fun programming exercise I set myself.
|
||||
But quizzes like this are also really enjoyable to do, and not to mention compare our results with others.
|
||||
And I would love to make this quiz if it gives me a reason to talk to people I know about my favourite TTRPGs.
|
||||
</p>
|
||||
<p>
|
||||
If you want to read more about <em class="uncial-antiqua">Wanderhome</em>, you can do so in the <router-link to="/about/wanderhome" class="hover:bg-orange-200">relevant section</router-link>.
|
||||
I wholeheartedly recommend playing it!
|
||||
</p>
|
||||
</section>
|
||||
<hr class="border-gray-400 w-2/3 mx-auto"/>
|
||||
<h2>
|
||||
Technobabble
|
||||
</h2>
|
||||
<p>
|
||||
It has been a long time since I made the <em>Masks</em> quiz, and this quiz has been proportionately over-engineered!
|
||||
</p>
|
||||
<h3>
|
||||
Dividing Client/Server Side
|
||||
</h3>
|
||||
<p>
|
||||
In my last upgrade to the <em>Masks</em> quiz, I made the quiz run on a server which renders the questions and evaluates the restuls.
|
||||
The app runs on Python and Flask, and its pages are rendered primarily through Jinja.
|
||||
This means that the Python server handles the quiz and also serves the web site, which is not the most robust.
|
||||
</p>
|
||||
<p>
|
||||
The <em class="uncial-antiqua">Wanderhome</em> quiz in contrast separates the server (running on Python) that renders the quiz and evaluates the results, and the client (running on JavaScript and HTML) that provides the interface.
|
||||
The server is a proper system-agnostic API.
|
||||
</p>
|
||||
<h3>
|
||||
Storing Results
|
||||
</h3>
|
||||
<p>
|
||||
One of the things I have added to the quiz is that it will store the results people get.
|
||||
It will not collect anybody’s name or any identifying information.
|
||||
But it will record how people answered, and what results people got, so users can compare how they did with other users anonymously, and so I can evaluate whether or not the questions are biased or skewing results.
|
||||
It will store all of this in a database: originally an SQLite database, but I can configure the app to use a MySQL database if I wanted.
|
||||
</p>
|
||||
<p>
|
||||
One of the biggest challenges of these personality quiz projects, funnily enough, is the mathematical quandary that writing the questions entails: ensuring that all outcomes are equally possible in the quiz overall, but also that there are sufficiently varied permutations of questions and answers that score for different outcomes to avoid some playbooks collapsing into each other.
|
||||
</p>
|
||||
<h3>
|
||||
Different Frameworks
|
||||
</h3>
|
||||
<p>
|
||||
I made the regrettable decision of teaching myself a new framework for this project.
|
||||
I built the UI using Vue with the Tailwind CSS framework, and it was an experience I found really frustrating because of how I threw myself in the deep end.
|
||||
It is unbelievably fiddly, with even some of the most intuitive features being an utter pain to set up, needing detective work of finding compatible libraries and plug-ins.
|
||||
What was even worse is that the JQuery library that I normally use for API calls is not compatible with Vue, so I will need to learn how to use Axios.
|
||||
</p>
|
||||
<p>
|
||||
On the whole, despite how much of a hassle it was, I preferred the outcome of using Vue and Tailwind because the result looks a lot less samey to my Flask/Bootstrap projects.
|
||||
The result here is one that has much more polish.
|
||||
</p>
|
||||
<section>
|
||||
<h2>
|
||||
Technobabble
|
||||
</h2>
|
||||
<p>
|
||||
It has been a long time since I made the <em>Masks</em> quiz, and this quiz has been proportionately over-engineered!
|
||||
</p>
|
||||
<h3>
|
||||
Dividing Client/Server Side
|
||||
</h3>
|
||||
<p>
|
||||
In my last upgrade to the <em>Masks</em> quiz, I made the quiz run on a server which renders the questions and evaluates the restuls.
|
||||
The app runs on Python and Flask, and its pages are rendered primarily through Jinja.
|
||||
This means that the Python server handles the quiz and also serves the web site, which is not the most robust.
|
||||
</p>
|
||||
<p>
|
||||
The <em class="uncial-antiqua">Wanderhome</em> quiz in contrast separates the server (running on Python) that renders the quiz and evaluates the results, and the client (running on JavaScript and HTML) that provides the interface.
|
||||
The server is a proper system-agnostic API.
|
||||
</p>
|
||||
<h3>
|
||||
Storing Results
|
||||
</h3>
|
||||
<p>
|
||||
One of the things I have added to the quiz is that it will store the results people get.
|
||||
It will not collect anybody’s name or any identifying information.
|
||||
But it will record how people answered, and what results people got, so users can compare how they did with other users anonymously, and so I can evaluate whether or not the questions are biased or skewing results.
|
||||
It will store all of this in a database: originally an SQLite database, but I can configure the app to use a MySQL database if I wanted.
|
||||
</p>
|
||||
<p>
|
||||
One of the biggest challenges of these personality quiz projects, funnily enough, is the mathematical quandary that writing the questions entails: ensuring that all outcomes are equally possible in the quiz overall, but also that there are sufficiently varied permutations of questions and answers that score for different outcomes to avoid some playbooks collapsing into each other.
|
||||
</p>
|
||||
<h3>
|
||||
Different Frameworks
|
||||
</h3>
|
||||
<p>
|
||||
I made the regrettable decision of teaching myself a new framework for this project.
|
||||
I built the UI using Vue with the Tailwind CSS framework, and it was an experience I found really frustrating because of how I threw myself in the deep end.
|
||||
It is unbelievably fiddly, with even some of the most intuitive features being an utter pain to set up, needing detective work of finding compatible libraries and plug-ins.
|
||||
What was even worse is that the JQuery library that I normally use for API calls is not compatible with Vue, so I will need to learn how to use Axios.
|
||||
</p>
|
||||
<p>
|
||||
On the whole, despite how much of a hassle it was, I preferred the outcome of using Vue and Tailwind because the result looks a lot less samey to my Flask/Bootstrap projects.
|
||||
The result here is one that has much more polish.
|
||||
</p>
|
||||
</section>
|
||||
<hr class="border-gray-400 w-2/3 mx-auto"/>
|
||||
<h3>
|
||||
What Next?
|
||||
</h3>
|
||||
<p>
|
||||
I am running out of geeky TTRPG-related programming projects.
|
||||
I was thinking of making a web app to play a game of <a href="https://www.lamemage.com/microscope/" target="_blank" rel="noopener noreferrer"><em>Microscope</em> by Ben Robbins</a>, published by Lame Mage Productions.
|
||||
There is an app that exists, but every time I tried using it I struggled to get it to work properly.
|
||||
The developer has not responded to my issues on GitHub, so I am tempted to reverse engineer it and make one from the ground up.
|
||||
The original app was written in programming languages that I have no clue about, so I would rather just build it from sratch in Python and, unfortunately, Vue.
|
||||
That might be the next project I tinker with.
|
||||
It will apply all the things I currently know, and also add websockets and state synchronisation to the mix.
|
||||
</p>
|
||||
<section>
|
||||
<h2>
|
||||
What Next?
|
||||
</h2>
|
||||
<p>
|
||||
I am running out of geeky TTRPG-related programming projects.
|
||||
I was thinking of making a web app to play a game of <a href="https://www.lamemage.com/microscope/" target="_blank" rel="noopener noreferrer" class="hover:bg-orange-200"><em>Microscope</em> by Ben Robbins</a>, published by Lame Mage Productions.
|
||||
There is an app that exists, but every time I tried using it I struggled to get it to work properly.
|
||||
The developer has not responded to my issues on GitHub, so I am tempted to reverse engineer it and make one from the ground up.
|
||||
The original app was written in programming languages that I have no clue about, so I would rather just build it from sratch in Python and, unfortunately, Vue.
|
||||
That might be the next project I tinker with.
|
||||
It will apply all the things I currently know, and also add websockets and state synchronisation to the mix.
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
</Content>
|
||||
</TextFrame>
|
||||
|
@ -12,27 +12,40 @@
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<TextFrame>
|
||||
<Header>
|
||||
Acknowledgements
|
||||
</Header>
|
||||
<Content>
|
||||
<article class="prose mx-auto">
|
||||
<TextFrame>
|
||||
<Header>
|
||||
Acknowledgements
|
||||
</Header>
|
||||
<Content>
|
||||
<article class="prose mx-auto">
|
||||
<section>
|
||||
<p class="text-leader mx-12">
|
||||
This one is for all my friends and ‘incidental companions’ alike whose paths criss-crossed with mine;
|
||||
</p>
|
||||
<p class="text-leader mx-12">
|
||||
And all the wonderful people from my home town whom I ironically met only after leaving;
|
||||
</p>
|
||||
<p class="text-leader mx-12">
|
||||
Especially you, 8Bit.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<p>
|
||||
First and foremost, I am delighted that Jay Dragon and Possum Creek Games created such a magnificent work of art.
|
||||
I am also grateful for Jay’s enthusiasm about this project.
|
||||
</p>
|
||||
<p>
|
||||
I would also like to thank 8bit for his inspiring enthusiasm: I would have arrived at this game a whole lot later if it weren’ for his shilling for it.
|
||||
I would also like to thank 8bit for his inspiring enthusiasm: I would have arrived at this game a whole lot later if it weren’t for his relentless shilling for it.
|
||||
</p>
|
||||
<p>
|
||||
And I am deeply grateful to my friends along the way: Claire Bath for being the first playtester of the quiz.
|
||||
And I am deeply grateful to my friends along the way: Claire Bath for being the first playtester of the quiz, and for impeccable design advice.
|
||||
There will be no doubt others to go to this list for helping me test this app.
|
||||
</p>
|
||||
<p>
|
||||
This section is still a work in progress because the programme is still not yet finished.
|
||||
And not to mention, the innumerable developers with their tutorials and guides that helped me learn a new programming framework to make this.
|
||||
</p>
|
||||
</article>
|
||||
</Content>
|
||||
</TextFrame>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</Content>
|
||||
</TextFrame>
|
||||
</template>
|
67
client/src/views/about/Licenses.vue
Normal file
67
client/src/views/about/Licenses.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<script>
|
||||
import Content from '@/components/Content.vue'
|
||||
import Header from '@/components/Header.vue'
|
||||
import TextFrame from '@/components/TextFrame.vue'
|
||||
export default {
|
||||
name: 'Licenses',
|
||||
components: {
|
||||
Content,
|
||||
Header,
|
||||
TextFrame
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<TextFrame>
|
||||
<Header>
|
||||
Licenses
|
||||
</Header>
|
||||
<Content>
|
||||
<article class="prose mx-auto">
|
||||
<section>
|
||||
<a href="https://possumcreekgames.com/en-gb/pages/wanderhome-3rd-party-license" target="_blank" rel="noopener noreferrer">
|
||||
<img src="@/assets/img/wh_ic_banner.png" alt="Wanderhome Independent Content banner" class="drop-shadow-lg scale-75 my-0 inline-block">
|
||||
</a>
|
||||
<h2>
|
||||
<em class="uncial-antiqua">Wanderhome</em> Text and Artwork
|
||||
</h2>
|
||||
<p>
|
||||
<em class="uncial-antiqua">Wanderhome</em> is copyright of Possum Creek Games Inc.
|
||||
</p>
|
||||
<p>
|
||||
The <em><span class="uncial-antiqua">Wanderhome</span> Quiz | V.S.</em> is an independent production by Vivek Santayana and is not affiliated with Possum Creek Games Inc.
|
||||
It is published under the <a href="https://possumcreekgames.com/en-gb/pages/wanderhome-3rd-party-license" target="_blank" class="hover:bg-orange-200" rel="noopener noreferrer">Wanderhome Third Party License</a>.
|
||||
</p>
|
||||
<p>
|
||||
The excerpts from <em class="uncial-antiqua">Wanderhome</em>, including the names of the playbook, flavour text, playbook descriptions, natures, animal forms, and character action options, as well as the setting of Hæth, are copyright of Possum Creek Games Inc.
|
||||
</p>
|
||||
<p>
|
||||
Artwork from <em class="uncial-antiqua">Wanderhome</em>, including game art and illustrations of playbooks, are also copyright Possum Creek Games Inc.
|
||||
</p>
|
||||
</section>
|
||||
<hr class="border-gray-400 w-2/3 mx-auto"/>
|
||||
<section>
|
||||
<h2>
|
||||
Question Text
|
||||
</h2>
|
||||
<p>
|
||||
The questions used in this quiz were written by Vivek Santayana, and are licensed under a <a rel="license noopener noreferrer" target="_blank" class="hover:bg-orange-200" href="http://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>.
|
||||
</p>
|
||||
<div class="flex">
|
||||
<a class="mx-auto w-fit" target="_blank" rel="license noopener noreferrer" href="http://creativecommons.org/licenses/by-sa/4.0/"><img src="https://i.creativecommons.org/l/by-sa/4.0/88x31.png" alt="Creative Commons Licence BY SA" class="m-0 inline-block"></a>
|
||||
</div>
|
||||
</section>
|
||||
<hr class="border-gray-400 w-2/3 mx-auto"/>
|
||||
<section>
|
||||
<h2>
|
||||
Source Code
|
||||
</h2>
|
||||
<p>
|
||||
This quiz is an opensource project.
|
||||
The source code for this project, including <a rel="noopener noreferrer" target="_blank" class="hover:bg-orange-200" href="https://git.vsnt.uk/viveksantayana/wanderhome-quiz">the quiz server and API</a> and the <a rel="noopener noreferrer" target="_blank" class="hover:bg-orange-200" href="https://git.vsnt.uk/viveksantayana/wanderhome-quiz-client">the client app</a>, are available under an MIT License at the respective GIT repositories.
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
</Content>
|
||||
</TextFrame>
|
||||
</template>
|
@ -12,19 +12,19 @@
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<TextFrame>
|
||||
<Header>
|
||||
About <em>Wanderhome</em>
|
||||
</Header>
|
||||
<Content>
|
||||
<article class="prose mx-auto">
|
||||
<TextFrame>
|
||||
<Header>
|
||||
About <em>Wanderhome</em>
|
||||
</Header>
|
||||
<Content>
|
||||
<article class="prose mx-auto">
|
||||
<section>
|
||||
<p>
|
||||
<em class="uncial-antiqua">Wanderhome</em> is a pastoral fantasy table-top role-playing game by Jay Dragon, published by Possum Creek Games.
|
||||
Players play anthropomorphic animals who are travelling through a vast landscape, dotted with different features and locales.
|
||||
The game is very focussed on the turning of seasons, how different places live, and how journeys change us.
|
||||
It has a very distinct character, and certainly one of the most compelling and delightful games I have played recently.
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
Despite how whimsical the premise may sound at first, the game is very much intent on exploring incredibly complex dilemmas and conflicts.
|
||||
Looming in the setting of Hæth is the spectre of a terrible war that has recently ended.
|
||||
@ -32,19 +32,21 @@
|
||||
In the midst of this, the game does not have any combat: it is explicitly about healing and building a fragile peace.
|
||||
And it is hopeful.
|
||||
</p>
|
||||
<hr class="border-gray-400 w-2/3 mx-auto"/>
|
||||
</section>
|
||||
<hr class="border-gray-400 w-2/3 mx-auto"/>
|
||||
<section>
|
||||
<h2>
|
||||
Where to Find a Copy
|
||||
</h2>
|
||||
<p>
|
||||
You can buy the game from <a href="https://possumcreekgames.com/en-gb/products/wanderhome" target="_blank" rel="noopener noreferrer">Possum Creek Games’ web site</a>.
|
||||
Physical books have sold out at the time of writing, but you can always <a href="https://possumcreekgames.com/en-gb/products/wanderhome-pdf" target="_blank" rel="noopener noreferrer">buy a pdf</a>.
|
||||
You can buy the game from <a href="https://possumcreekgames.com/en-gb/products/wanderhome" target="_blank" rel="noopener noreferrer" class="hover:bg-orange-200">Possum Creek Games’ web site</a>.
|
||||
Physical books have been re-stocked in a new print run at the time of writing, and you can also <a href="https://possumcreekgames.com/en-gb/products/wanderhome-pdf" target="_blank" rel="noopener noreferrer" class="hover:bg-orange-200">buy a pdf</a>.
|
||||
</p>
|
||||
<p>
|
||||
If you are one of my friends and would like to play <em class="uncial-antiqua">Wanderhome</em>, let me know!
|
||||
</p>
|
||||
</article>
|
||||
</Content>
|
||||
</TextFrame>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</Content>
|
||||
</TextFrame>
|
||||
</template>
|
@ -12,27 +12,25 @@
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div >
|
||||
<TextFrame>
|
||||
<Header>
|
||||
Error: Not Found
|
||||
</Header>
|
||||
<Content>
|
||||
<article class="prose mx-auto">
|
||||
<p class="text-leader">
|
||||
Uh-oh.
|
||||
</p>
|
||||
<p>
|
||||
Sometimes we can get lost along the path.
|
||||
</p>
|
||||
<p>
|
||||
But it’s okay.
|
||||
Just retrace your steps and you will find your way back again.
|
||||
</p>
|
||||
</article>
|
||||
</Content>
|
||||
</TextFrame>
|
||||
</div>
|
||||
<TextFrame>
|
||||
<Header>
|
||||
Error: Not Found
|
||||
</Header>
|
||||
<Content>
|
||||
<article class="prose mx-auto">
|
||||
<p class="text-leader">
|
||||
Uh-oh.
|
||||
</p>
|
||||
<p>
|
||||
Sometimes we can get lost along the path.
|
||||
</p>
|
||||
<p>
|
||||
But it’s okay.
|
||||
Just retrace your steps and you will find your way back again.
|
||||
</p>
|
||||
</article>
|
||||
</Content>
|
||||
</TextFrame>
|
||||
</template>
|
||||
<style scoped>
|
||||
</style>
|
@ -12,23 +12,21 @@
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div >
|
||||
<TextFrame>
|
||||
<Header>
|
||||
Error: Connection Refused
|
||||
</Header>
|
||||
<Content>
|
||||
<article class="prose mx-auto">
|
||||
<p class="text-leader">
|
||||
The client is unable to connect to the server.
|
||||
That’s okay.
|
||||
Sometimes, we run into obstacles along the way.
|
||||
And we just need to try again after a while.
|
||||
</p>
|
||||
</article>
|
||||
</Content>
|
||||
</TextFrame>
|
||||
</div>
|
||||
<TextFrame>
|
||||
<Header>
|
||||
Error: Connection Refused
|
||||
</Header>
|
||||
<Content>
|
||||
<article class="prose mx-auto">
|
||||
<p class="text-leader">
|
||||
The client is unable to connect to the server.
|
||||
That’s okay.
|
||||
Sometimes, we run into obstacles along the way.
|
||||
And we just need to try again after a while.
|
||||
</p>
|
||||
</article>
|
||||
</Content>
|
||||
</TextFrame>
|
||||
</template>
|
||||
<style scoped>
|
||||
</style>
|
@ -19,28 +19,27 @@
|
||||
},
|
||||
name: 'QuestionIndex',
|
||||
created() {
|
||||
if (!this.appStore.hasData) {
|
||||
if (this.questionStore.questions.length == 0) {
|
||||
this.getQuestions()
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
loading: false,
|
||||
questions: null
|
||||
mounted() {
|
||||
if (this.answersStore.quizCurrent !== null) {
|
||||
this.$router.push(`/quiz/question/${this.answersStore.quizCurrent}`)
|
||||
} else {
|
||||
this.answersStore.$reset()
|
||||
this.appStore.$reset()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getQuestions() {
|
||||
this.error = this.questions = null
|
||||
this.loading = true
|
||||
axios.get(`${Config.SERVER}api/questions/`)
|
||||
.then((response) => {
|
||||
this.error = false
|
||||
console.log('Fetched questions from server.')
|
||||
this.questionStore.storeQuestions(response.data)
|
||||
this.appStore.toggleData()
|
||||
})
|
||||
.catch( error => {
|
||||
console.log(error)
|
||||
this.$router.push('/err_refused')
|
||||
})
|
||||
}
|
||||
|
@ -35,52 +35,28 @@
|
||||
this.answersStore.makeArray(this.id, this.questionStore.questions[this.id].select)
|
||||
},
|
||||
mounted() {
|
||||
this.answersStore.quizCurrent = this.id
|
||||
this.$watch(
|
||||
() => this.$route.params, (toParams, previousParams) => {
|
||||
if (this.$route.name == 'Question') {
|
||||
if (toParams.id >= this.questionStore.questions.length || this.id < 0) {
|
||||
this.$router.push('/err_notfound')
|
||||
} else {
|
||||
if ( toParams.id == 0 ) {
|
||||
this.enableBack = false
|
||||
}
|
||||
if ( toParams.id == this.questionStore.questions.length - 1 ) {
|
||||
console.log('Condition met')
|
||||
this.enableNext = false
|
||||
} else {
|
||||
this.enableNext = true
|
||||
}
|
||||
this.answersStore.quizCurrent = toParams.id
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
this.$watch(
|
||||
() => this.appStore.hasData, (newValue) => {
|
||||
() => this.questionStore.questions, (newValue) => {
|
||||
if (this.id >= this.questionStore.questions.length || this.id < 0) {
|
||||
this.$router.push('/err_notfound')
|
||||
} else {
|
||||
this.answersStore.makeArray(this.id, this.questionStore.questions[this.id].select)
|
||||
if ( this.id == this.questionStore.questions.length -1 ) {
|
||||
this.enableNext = false
|
||||
} else {
|
||||
this.enableNext = true
|
||||
}
|
||||
if ( this.id == 0 ) {
|
||||
this.enableBack = false
|
||||
} else {
|
||||
this.enableBack = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
question: null,
|
||||
select: null,
|
||||
answers: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submitAnswers() {
|
||||
const data = JSON.stringify(this.answersStore.answers)
|
||||
@ -93,10 +69,13 @@
|
||||
}
|
||||
}
|
||||
).then( (response) => {
|
||||
console.log('Submitted results.')
|
||||
this.appStore.store('results', response.data)
|
||||
console.log('Results fetched from the server.')
|
||||
this.answersStore.quizCurrent = null
|
||||
this.$router.push('/results')
|
||||
}).catch( (error) => {
|
||||
console.log(error)
|
||||
this.$router.push('/err_refused')
|
||||
})
|
||||
}
|
||||
@ -106,8 +85,7 @@
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -119,7 +97,8 @@
|
||||
<Content>
|
||||
<div v-if="questionStore.questions[this.id]">
|
||||
<article class="prose">
|
||||
<div v-html="questionStore.questions[this.id].question"></div>
|
||||
<h3>Question {{ this.id + 1}}</h3>
|
||||
<div v-html="questionStore.questions[this.id].question"/>
|
||||
</article>
|
||||
<div class="grid grid-cols-1 content-evenly items-center gap-2 justify-center my-12 mx-auto md:grid-cols-2 lg:grid-cols-3" :aria-multiselectable="questionStore.questions[this.id].select > 1 ? true: false">
|
||||
<div v-for="(answer, index) in questionStore.questions[this.id].answers" :key="`q${this.id}-o${index}`" class="inline-flex">
|
||||
@ -188,7 +167,6 @@
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.checkbox {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
|
@ -15,47 +15,45 @@
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<TextFrame>
|
||||
<Header>
|
||||
Take the Quiz
|
||||
</Header>
|
||||
<Content>
|
||||
<article class="prose mx-auto">
|
||||
<h2 class="uncial-antiqua" key="welcome-0">
|
||||
Gather your Things
|
||||
</h2>
|
||||
<p class="text-leader" key="welcome-1">
|
||||
We are about to embark upon a journey.
|
||||
</p>
|
||||
<p class="text-leader" key="welcome-2">
|
||||
In a minute, I will ask you a series of multiple choice questions.
|
||||
For most of them, you will need to select the one answer.
|
||||
Some questions will need multiple answers.
|
||||
Don’t over-think them!
|
||||
</p>
|
||||
<p class="text-leader" key="welcome-3">
|
||||
<em class="uncial-antiqua">Wanderhome</em> is about going wherever the road takes you, and reflecting on how the journey changes us.
|
||||
The road is full of myriad travellers, each with their rich lives and stories.
|
||||
This quiz will help you figure out what kinds of stories you will bring to the table.
|
||||
</p>
|
||||
<p class="text-leader" key="welcome-4">
|
||||
Will you join me?
|
||||
</p>
|
||||
<div class="w-fit mx-auto" key="welcome-5">
|
||||
<router-link active-class="active-link" class="navlink" to="/quiz/question/0">
|
||||
<div class="inline-flex px-3 py-1 space-x-2 rounded-md bg-lime-800 text-white transition-all duration-300 ease-in-out hover:bg-lime-600">
|
||||
<span class="scale-75">
|
||||
<RoadVariant/>
|
||||
</span>
|
||||
<span class="uncial-antiqua">Let’s Go</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</article>
|
||||
</Content>
|
||||
</TextFrame>
|
||||
</div>
|
||||
<TextFrame>
|
||||
<Header>
|
||||
Take the Quiz
|
||||
</Header>
|
||||
<Content>
|
||||
<article class="prose mx-auto">
|
||||
<h2 class="uncial-antiqua" key="welcome-0">
|
||||
Gather your Things
|
||||
</h2>
|
||||
<p class="text-leader" key="welcome-1">
|
||||
We are about to embark upon a journey.
|
||||
</p>
|
||||
<p class="text-leader" key="welcome-2">
|
||||
In a minute, I will ask you a series of multiple choice questions.
|
||||
For most of them, you will need to select the one answer.
|
||||
Some questions will need multiple answers.
|
||||
Don’t over-think them!
|
||||
</p>
|
||||
<p class="text-leader" key="welcome-3">
|
||||
<em class="uncial-antiqua">Wanderhome</em> is about going wherever the road takes you, and reflecting on how the journey changes us.
|
||||
The road is full of myriad travellers, each with their rich lives and stories.
|
||||
This quiz will help you figure out what kinds of stories you will bring to the table.
|
||||
</p>
|
||||
<p class="text-leader" key="welcome-4">
|
||||
Will you join me?
|
||||
</p>
|
||||
<div class="w-fit mx-auto" key="welcome-5">
|
||||
<router-link active-class="active-link" class="navlink" to="/quiz/question/0">
|
||||
<div class="inline-flex px-3 py-1 space-x-2 rounded-md bg-lime-800 text-white transition-all duration-300 ease-in-out hover:bg-lime-600">
|
||||
<span class="scale-75">
|
||||
<RoadVariant/>
|
||||
</span>
|
||||
<span class="uncial-antiqua">Let’s Go</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</article>
|
||||
</Content>
|
||||
</TextFrame>
|
||||
</template>
|
||||
<style scoped>
|
||||
</style>
|
@ -1,23 +0,0 @@
|
||||
<script>
|
||||
import Content from '@/components/Content.vue'
|
||||
import Header from '@/components/Header.vue'
|
||||
import TextFrame from '@/components/TextFrame.vue'
|
||||
export default {
|
||||
name: 'Answers',
|
||||
components: {
|
||||
Content,
|
||||
Header,
|
||||
TextFrame
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<TextFrame>
|
||||
<Header>
|
||||
Compare Answers
|
||||
</Header>
|
||||
<Content>
|
||||
|
||||
</Content>
|
||||
</TextFrame>
|
||||
</template>
|
@ -29,18 +29,20 @@
|
||||
getResults() {
|
||||
axios.get(`${Config.SERVER}api/playbooks/`)
|
||||
.then((response) => {
|
||||
this.error = false
|
||||
console.log('Fetched playbook stats from the server.')
|
||||
this.appStore.store('playbooks', response.data)
|
||||
})
|
||||
.catch( error => {
|
||||
console.log(error)
|
||||
this.$router.push('/err_refused')
|
||||
})
|
||||
axios.get(`${Config.SERVER}api/count/`)
|
||||
.then((response) => {
|
||||
this.error = false
|
||||
console.log('Fetched user count from the server.')
|
||||
this.appStore.store('count', response.data)
|
||||
})
|
||||
.catch( error => {
|
||||
console.log(error)
|
||||
this.$router.push('/err_refused')
|
||||
})
|
||||
}
|
||||
@ -92,7 +94,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-lg">Out of a total {{ this.appStore.count }} users</td>
|
||||
<td colspan="4" class="text-center text-lg text-gray-600">Out of {{ this.appStore.count }} users</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -1,9 +1,25 @@
|
||||
<script>
|
||||
import Sidebar from '@/components/sidebar/Index.vue'
|
||||
import { useAppStore } from '@/stores/app.js'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const appStore = useAppStore()
|
||||
|
||||
return {
|
||||
appStore
|
||||
}
|
||||
},
|
||||
name: 'ResultsIndex',
|
||||
components: {
|
||||
Sidebar
|
||||
},
|
||||
created() {
|
||||
if (this.appStore.results.playbooks.length > 1) {
|
||||
this.$router.options.routes[3].children[0].meta.title = 'Your Results'
|
||||
} else {
|
||||
this.$router.options.routes[3].children[0].meta.title = 'Your Result'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -53,13 +53,10 @@
|
||||
</span>
|
||||
<span class="text-sm">Pages {{ Object.values(playbook)[0].pages }}</span>
|
||||
</h2>
|
||||
<p class="text-leader" v-html="Object.values(playbook)[0].flavour">
|
||||
</p>
|
||||
<p class="text-leader" v-html="Object.values(playbook)[0].flavour"/>
|
||||
<p v-html="Object.values(playbook)[0].blurb"/>
|
||||
<p>
|
||||
{{ Object.values(playbook)[0].blurb }}
|
||||
</p>
|
||||
<p>
|
||||
You are alive. Your care is <strong>{{ Object.values(playbook)[0].care }}</strong>.
|
||||
You are alive. Your care is <strong v-html="Object.values(playbook)[0].care"></strong>.
|
||||
</p>
|
||||
<p>
|
||||
Your animal form is {{ renderAnimals(Object.values(playbook)[0].animals) }}
|
||||
|
@ -36,7 +36,7 @@
|
||||
Playbook
|
||||
</th>
|
||||
<th>
|
||||
Score
|
||||
Score (%)
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -46,7 +46,7 @@
|
||||
The {{ playbook }}
|
||||
</td>
|
||||
<td scope="col" class="py-1">
|
||||
{{ Math.round(100*score/this.appStore.results.max_score) }} %
|
||||
{{ Math.round(100*score/this.appStore.results.max_score) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -29,18 +29,20 @@
|
||||
getScores() {
|
||||
axios.get(`${Config.SERVER}api/scores/`)
|
||||
.then((response) => {
|
||||
this.error = false
|
||||
console.log('Fetched score stats from the server.')
|
||||
this.appStore.store('scores', response.data)
|
||||
})
|
||||
.catch( error => {
|
||||
console.log(error)
|
||||
this.$router.push('/err_refused')
|
||||
})
|
||||
axios.get(`${Config.SERVER}api/count/`)
|
||||
.then((response) => {
|
||||
this.error = false
|
||||
console.log('Fetched user count from the server.')
|
||||
this.appStore.store('count', response.data)
|
||||
})
|
||||
.catch( error => {
|
||||
console.log(error)
|
||||
this.$router.push('/err_refused')
|
||||
})
|
||||
}
|
||||
@ -57,7 +59,7 @@
|
||||
Sometimes, it can help to have some additional insight into the likelihood of meeting different people.
|
||||
Or, indeed, it can provide insight into how the questions we ask of each other bias the way we perceive the world.
|
||||
</p>
|
||||
<table class="table-auto w-full max-w-lg mx-auto text-left">
|
||||
<table class="table-auto w-full mx-auto max-w-lg text-left">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-lime-100 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
<th>
|
||||
@ -67,16 +69,16 @@
|
||||
Playbook
|
||||
</th>
|
||||
<th>
|
||||
Your Score
|
||||
Your Score (%)
|
||||
</th>
|
||||
<th>
|
||||
Mean Score
|
||||
Mean
|
||||
</th>
|
||||
<th>
|
||||
Median Score
|
||||
Median
|
||||
</th>
|
||||
<th>
|
||||
Standard Deviation
|
||||
<th class="lowercase underline" title="Standard Deviation">
|
||||
σ*
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -105,7 +107,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-lg">Out of a total {{ this.appStore.count }} users</td>
|
||||
<td colspan="6" class="text-center text-lg text-gray-600">From {{ this.appStore.count }} users</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
125
client/src/views/results/compare-answers/CompareQuestion.vue
Normal file
125
client/src/views/results/compare-answers/CompareQuestion.vue
Normal file
@ -0,0 +1,125 @@
|
||||
<script>
|
||||
import { useQuestionStore } from '@/stores/questions.js'
|
||||
import { useAnswersStore } from '@/stores/answers.js'
|
||||
import { useAppStore } from '@/stores/app.js'
|
||||
import Content from '@/components/Content.vue'
|
||||
import Header from '@/components/Header.vue'
|
||||
import TextFrame from '@/components/TextFrame.vue'
|
||||
import ArrowLeftBold from '@/components/icons/ArrowLeftBold.vue'
|
||||
import ArrowRightBold from '@/components/icons/ArrowRightBold.vue'
|
||||
|
||||
export default {
|
||||
name: 'CompareQuestion',
|
||||
components: {
|
||||
ArrowLeftBold,
|
||||
ArrowRightBold,
|
||||
Content,
|
||||
Header,
|
||||
TextFrame
|
||||
},
|
||||
setup() {
|
||||
const questionStore = useQuestionStore()
|
||||
const answersStore = useAnswersStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
return {
|
||||
questionStore,
|
||||
answersStore,
|
||||
appStore
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
optionSelected(id, index) {
|
||||
if (Array.isArray(this.answersStore.answers[id])) {
|
||||
return this.answersStore.answers[id].includes(index)
|
||||
}
|
||||
return this.answersStore.answers[id] == index
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.appStore.compareCurrent = this.id
|
||||
this.$watch(
|
||||
() => this.$route.params, (toParams, previousParams) => {
|
||||
if (this.$route.name == 'CompareQuestion') {
|
||||
if (toParams.id >= this.questionStore.questions.length || this.id < 0) {
|
||||
this.$router.push('/err_notfound')
|
||||
} else {
|
||||
this.appStore.compareCurrent = toParams.id
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<TextFrame>
|
||||
<Header>
|
||||
Compare Answers
|
||||
</Header>
|
||||
<Content>
|
||||
<div v-if="questionStore.questions[this.id] && answersStore.answers[this.id] != null && appStore.answers[this.id]">
|
||||
<article class="prose">
|
||||
<h3>Question {{ this.id + 1}}</h3>
|
||||
<div v-html="questionStore.questions[this.id].question"></div>
|
||||
</article>
|
||||
<div class="grid grid-cols-1 content-evenly items-center gap-2 justify-between mt-12 mx-auto lg:grid-cols-2">
|
||||
<div v-for="(answer, index) in questionStore.questions[this.id].answers" :key="`q${this.id}-o${index}`" class="inline-flex justify-between rounded-md bg-lime-200" :style="`background: linear-gradient(to right, rgb(217 249 157) ${100*this.appStore.answers[this.id][index] / this.appStore.count}%, transparent ${100*this.appStore.answers[this.id][index] / this.appStore.count+3}%)`">
|
||||
<div v-html="answer" class="flex uncial-antiqua h-full p-2 select-none transition-all duration-100 ease-in-out" :class=" optionSelected(this.id, index) ? 'option-selected' : '' " :aria-label="answer" :title="answer"></div>
|
||||
<div class="flex p-2 w-fit items-center" style="white-space:nowrap;">{{ Math.round(100*this.appStore.answers[this.id][index] / this.appStore.count) }} %</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-6 mb-12 text-lg text-gray-600">
|
||||
From of {{ this.appStore.count }} users.
|
||||
</div>
|
||||
<div class="w-full flex mx-auto items-center justify-between max-w-sm">
|
||||
<router-link :to="`/results/answers/${this.id-1}`" v-if="this.id > 0">
|
||||
<div class="inline-flex h-full px-3 py-1 space-x-1 rounded-md bg-lime-800 text-white items-center transition-all duration-300 ease-in-out hover:bg-lime-600" title="Back">
|
||||
<span class="scale-100">
|
||||
<ArrowLeftBold/>
|
||||
</span>
|
||||
<span class="uncial-antiqua">Back</span>
|
||||
</div>
|
||||
</router-link>
|
||||
<div class="inline-flex h-full px-3 py-1 space-x-1 rounded-md bg-lime-800 opacity-50 text-white items-center hover:cursor-not-allowed" v-else>
|
||||
<span class="scale-100">
|
||||
<ArrowLeftBold/>
|
||||
</span>
|
||||
<span class="uncial-antiqua">Back</span>
|
||||
</div>
|
||||
<router-link :to="`/results/answers/${this.id+1}`" v-if="this.id < this.questionStore.questions.length - 1">
|
||||
<div class="inline-flex h-full px-3 py-1 space-x-1 rounded-md bg-lime-800 text-white items-center transition-all duration-300 ease-in-out hover:bg-lime-600" title="Next">
|
||||
<span class="uncial-antiqua">Next</span>
|
||||
<span class="scale-100">
|
||||
<ArrowRightBold/>
|
||||
</span>
|
||||
</div>
|
||||
</router-link>
|
||||
<div class="inline-flex h-full px-3 py-1 space-x-1 rounded-md bg-lime-800 opacity-50 text-white items-center hover:cursor-not-allowed" v-else>
|
||||
<span class="uncial-antiqua">Next</span>
|
||||
<span class="scale-100">
|
||||
<ArrowRightBold/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
||||
</div>
|
||||
</Content>
|
||||
</TextFrame>
|
||||
</template>
|
||||
<style scoped>
|
||||
.option-selected {
|
||||
@apply border-lime-600;
|
||||
@apply border-solid;
|
||||
@apply border-l-2;
|
||||
@apply text-orange-600;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,54 @@
|
||||
<script>
|
||||
import { useAppStore } from '@/stores/app.js'
|
||||
import Content from '@/components/Content.vue'
|
||||
import Header from '@/components/Header.vue'
|
||||
import TextFrame from '@/components/TextFrame.vue'
|
||||
import FeatureSearch from '@/components/icons/FeatureSearch.vue'
|
||||
|
||||
export default {
|
||||
name: 'CompareQuestions',
|
||||
components: {
|
||||
Content,
|
||||
FeatureSearch,
|
||||
Header,
|
||||
TextFrame
|
||||
},
|
||||
setup() {
|
||||
const appStore = useAppStore()
|
||||
|
||||
return {
|
||||
appStore
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.appStore.compareCurrent !== null) {
|
||||
this.$router.push(`/results/answers/${this.appStore.compareCurrent}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<TextFrame>
|
||||
<Header>
|
||||
Compare Answers
|
||||
</Header>
|
||||
<Content>
|
||||
<article class="prose mx-auto">
|
||||
<p class="text-leader">
|
||||
There are many turning points and forks in the road we will encounter along the way.
|
||||
There is a little illustration of how your choices compared to other people’s.
|
||||
</p>
|
||||
<div class="w-fit mx-auto">
|
||||
<router-link active-class="active-link" class="navlink" to="/results/answers/0">
|
||||
<div class="inline-flex px-3 py-1 space-x-2 rounded-md bg-lime-800 text-white transition-all duration-300 ease-in-out hover:bg-lime-600">
|
||||
<span class="scale-75">
|
||||
<FeatureSearch/>
|
||||
</span>
|
||||
<span class="uncial-antiqua">Take a Look</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</article>
|
||||
</Content>
|
||||
</TextFrame>
|
||||
</template>
|
63
client/src/views/results/compare-answers/Index.vue
Normal file
63
client/src/views/results/compare-answers/Index.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import Config from '@/config.js'
|
||||
import { useQuestionStore } from '@/stores/questions.js'
|
||||
import { useAnswersStore } from '@/stores/answers.js'
|
||||
import { useAppStore } from '@/stores/app.js'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const questionStore = useQuestionStore()
|
||||
const answersStore = useAnswersStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
return {
|
||||
questionStore,
|
||||
answersStore,
|
||||
appStore
|
||||
}
|
||||
},
|
||||
name: 'CompareAnswersIndex',
|
||||
mounted() {
|
||||
this.getAnswers()
|
||||
},
|
||||
methods: {
|
||||
getAnswers() {
|
||||
this.error = this.questions = null
|
||||
this.loading = true
|
||||
axios.get(`${Config.SERVER}api/answers/`)
|
||||
.then((response) => {
|
||||
console.log('Fetched answer stats from the server.')
|
||||
this.appStore.store('answers', response.data)
|
||||
})
|
||||
.catch( error => {
|
||||
console.log(error)
|
||||
this.$router.push('/err_refused')
|
||||
})
|
||||
axios.get(`${Config.SERVER}api/count/`)
|
||||
.then((response) => {
|
||||
console.log('Fetched user count from the server.')
|
||||
this.appStore.store('count', response.data)
|
||||
})
|
||||
.catch( error => {
|
||||
console.log(error)
|
||||
this.$router.push('/err_refused')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<router-view v-slot="{ Component, route }" class="w-full" ref="panel" appear>
|
||||
<Transition mode="out-in"
|
||||
enter-active-class="transition-all duration-300 origin-center ease-in"
|
||||
enter-from-class="transform opacity-0 md:scale-95"
|
||||
leave-active-class="transition-all duration-300 origin-center ease-out"
|
||||
leave-to-class="transform opacity-0 md:scale-95"
|
||||
>
|
||||
<component :is="Component" :key="route.path"/>
|
||||
</Transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
Loading…
Reference in New Issue
Block a user