Nearly finished app

This commit is contained in:
Vivek Santayana 2022-09-01 15:33:40 +01:00
parent 0965548a6c
commit 3788f08679
27 changed files with 703 additions and 337 deletions

View 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>

View File

@ -37,20 +37,20 @@
</router-link> </router-link>
<!-- Menu Items --> <!-- Menu Items -->
<div class="hidden md:flex space-x-3 justify-center items-center"> <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> </div>
<Menu v-slot="{ open }" as="div" class="relative inline-block text-left md:hidden"> <Menu v-slot="{ open }" as="div" class="relative inline-block text-left md:hidden">
<div> <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"> <span class="scale-150">
<mdiMenu/> <mdiMenu/>
</span> </span>
</MenuButton> </MenuButton>
</div> </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"> <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"> <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')"> <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"> <div class="inline-flex px-3 py-1 space-x-1">
<span class="scale-75"> <span class="scale-75">

View File

@ -18,7 +18,7 @@
<slot name="header"></slot> <slot name="header"></slot>
</h1> </h1>
<div class="flex flex-col text-md"> <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>
</div> </div>
</template> </template>

View File

@ -7,27 +7,25 @@ export default {
}, },
props: [ props: [
'prefix', 'prefix',
'svgPath', 'route'
'text',
'to'
] ]
} }
</script> </script>
<template> <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"> <div class="inline-flex px-2 py-1 items-center space-x-1">
<span class="scale-75"> <span class="scale-75">
<svg-icon type="mdi" :path="svgPath"></svg-icon> <svg-icon type="mdi" :path="route.meta.svgPath"></svg-icon>
</span> </span>
<span>{{ text }}</span> <span>{{ route.meta.title }}</span>
</div> </div>
</a> </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"> <div class="inline-flex px-2 py-1 items-center space-x-1">
<span class="scale-75"> <span class="scale-75">
<svg-icon type="mdi" :path="svgPath"></svg-icon> <svg-icon type="mdi" :path="route.meta.svgPath"></svg-icon>
</span> </span>
<span>{{ text }}</span> <span>{{ route.meta.title }}</span>
</div> </div>
</router-link> </router-link>
</template> </template>

View File

@ -1,7 +1,8 @@
import { createRouter, createWebHistory } from 'vue-router' 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 About from '@/views/about/Index.vue'
import AboutPage from '@/views/about/About.vue' import AboutPage from '@/views/about/About.vue'
import Licenses from '@/views/about/Licenses.vue'
import Acknowledgements from '@/views/about/Acknowledgements.vue' import Acknowledgements from '@/views/about/Acknowledgements.vue'
import Wanderhome from '@/views/about/Wanderhome.vue' import Wanderhome from '@/views/about/Wanderhome.vue'
import Home from '@/views/Home.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 Results from '@/views/results/Index.vue'
import ResultsPage from '@/views/results/Results.vue' import ResultsPage from '@/views/results/Results.vue'
import Scores from '@/views/results/Scores.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 Statistics from '@/views/results/Statistics.vue'
import CompareResults from '@/views/results/CompareResults.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 = [ const routes = [
{ {
@ -23,7 +26,7 @@ const routes = [
name: 'Home', name: 'Home',
component: Home, component: Home,
meta: { meta: {
index: 0, indexBase: 0,
svgPath: mdiHome, svgPath: mdiHome,
title: 'Home' title: 'Home'
} }
@ -33,7 +36,7 @@ const routes = [
name: 'Quiz', name: 'Quiz',
component: Quiz, component: Quiz,
meta: { meta: {
index: 1, indexBase: 1,
svgPath: mdiAccountQuestion, svgPath: mdiAccountQuestion,
title: 'Take the Quiz' title: 'Take the Quiz'
}, },
@ -43,8 +46,8 @@ const routes = [
name: 'Quiz', name: 'Quiz',
component: QuizPage, component: QuizPage,
meta: { meta: {
index: 0, indexChild: 0,
parentIndex: 1, indexBase: 1,
title: 'Take the Quiz' title: 'Take the Quiz'
} }
}, },
@ -56,8 +59,8 @@ const routes = [
id: parseInt(route.params.id) id: parseInt(route.params.id)
}), }),
meta: { meta: {
index: 1, indexChild: 1,
parentIndex: 1, indexBase: 1,
title: 'Take the Quiz' title: 'Take the Quiz'
} }
} }
@ -68,7 +71,7 @@ const routes = [
name: 'About', name: 'About',
component: About, component: About,
meta: { meta: {
index: 2, indexBase: 2,
svgPath: mdiInformation, svgPath: mdiInformation,
title: 'About' title: 'About'
}, },
@ -78,19 +81,30 @@ const routes = [
name: 'About', name: 'About',
component: AboutPage, component: AboutPage,
meta: { meta: {
index: 0, indexChild: 0,
parentIndex: 2, indexBase: 2,
svgPath: mdiInformation, svgPath: mdiInformation,
title: 'Background' title: 'Background'
} }
}, },
{
path: 'licenses',
name: 'Licenses',
component: Licenses,
meta: {
indexChild: 1,
indexBase: 2,
svgPath: mdiLicense,
title: 'Licenses'
}
},
{ {
path: 'wanderhome', path: 'wanderhome',
name: 'Wanderhome', name: 'Wanderhome',
component: Wanderhome, component: Wanderhome,
meta: { meta: {
index: 1, indexChild: 2,
parentIndex: 2, indexBase: 2,
svgPath: mdiHandCoin, svgPath: mdiHandCoin,
title: 'About Wanderhome' title: 'About Wanderhome'
} }
@ -100,8 +114,8 @@ const routes = [
name: 'Acknowledgements', name: 'Acknowledgements',
component: Acknowledgements, component: Acknowledgements,
meta: { meta: {
index: 2, indexChild: 3,
parentIndex: 2, indexBase: 2,
svgPath: mdiAccountGroupOutline, svgPath: mdiAccountGroupOutline,
title: 'Acknowledgements' title: 'Acknowledgements'
} }
@ -110,8 +124,8 @@ const routes = [
path: 'https://git.vsnt.uk/viveksantayana/wanderhome-quiz', path: 'https://git.vsnt.uk/viveksantayana/wanderhome-quiz',
name: 'SourceCode', name: 'SourceCode',
meta: { meta: {
index: 3, indexChild: 4,
parentIndex: 2, indexBase: 2,
svgPath: mdiCodeTags, svgPath: mdiCodeTags,
title: 'View Source Code' title: 'View Source Code'
} }
@ -123,7 +137,7 @@ const routes = [
name: 'Results', name: 'Results',
component: Results, component: Results,
meta: { meta: {
index: -1, indexBase: -1,
title: 'Results', title: 'Results',
svgPath: mdiAccountBoxMultiple svgPath: mdiAccountBoxMultiple
}, },
@ -133,10 +147,10 @@ const routes = [
name: 'Result', name: 'Result',
component: ResultsPage, component: ResultsPage,
meta: { meta: {
index: 0, indexChild: 0,
title: 'Your Results', title: 'Your Results',
svgPath: mdiAccountBoxMultiple, svgPath: mdiAccountBoxMultiple,
parentIndex: -1 indexBase: -1
} }
}, },
{ {
@ -144,10 +158,10 @@ const routes = [
name: 'Scores', name: 'Scores',
component: Scores, component: Scores,
meta: { meta: {
index: 1, indexChild: 1,
title: 'Your Scores', title: 'Your Scores',
svgPath: mdiCounter, svgPath: mdiCounter,
parentIndex: -1 indexBase: -1
} }
}, },
{ {
@ -155,10 +169,10 @@ const routes = [
name: 'CompareResults', name: 'CompareResults',
component: CompareResults, component: CompareResults,
meta: { meta: {
index: 2, indexChild: 2,
title: 'Compare Results', title: 'Compare Results',
svgPath: mdiCompare, svgPath: mdiCompare,
parentIndex: -1 indexBase: -1
} }
}, },
{ {
@ -166,22 +180,51 @@ const routes = [
name: 'Statistics', name: 'Statistics',
component: Statistics, component: Statistics,
meta: { meta: {
index: 3, indexChild: 3,
title: 'Statistics', title: 'Statistics',
svgPath: mdiChartBox, svgPath: mdiChartBox,
parentIndex: -1 indexBase: -1
} }
}, },
{ {
path: 'answers', path: 'answers',
name: 'Answers', name: 'Answers',
component: CompareAnswers, component: CompareAnswersIndex,
meta: { meta: {
index: 4, indexChild: 4,
title: 'Compare Answers', title: 'Compare Answers',
svgPath: mdiEarth, svgPath: mdiForum,
parentIndex: -1 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', name: 'NotFound',
component: NotFound, component: NotFound,
meta: { meta: {
index: -1, indexBase: -2,
title: 'Error: Not Found', title: 'Error: Not Found',
svgPath: mdiWeb svgPath: mdiWeb
} }
}, },
{ {
path: "/:catchAll(.*)", path: "/:catchAll(.*)",
name: 'NotFound', name: 'CatchAll',
redirect: '/err_notfound', redirect: '/err_notfound',
meta: { meta: {
index: -1, indexBase: -3,
title: 'Error: Not Found', title: 'Error: Not Found',
svgPath: mdiWeb svgPath: mdiWeb
} }
@ -210,7 +253,7 @@ const routes = [
name: 'Refused', name: 'Refused',
component: Refused, component: Refused,
meta: { meta: {
index: -1, indexBase: -4,
title: 'Error: Connection Refused', title: 'Error: Connection Refused',
svgPath: mdiWeb svgPath: mdiWeb
} }
@ -222,31 +265,36 @@ const Router = createRouter({
routes routes
}) })
Router.beforeEach((to, from, next) => { Router.beforeEach((to, from) => {
if (to.meta.title != 'Home') { if (to.meta.title != 'Home') {
document.title = `Wanderhome Quiz | V.S. - ${to.meta.title}` document.title = `Wanderhome Quiz | V.S. - ${to.meta.title}`
} else { } else {
document.title = `Wanderhome Quiz | V.S.` 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) => { Router.afterEach( (to, from) => {
const enterActiveClass = 'transition-all duration-300 origin-top ease-in' const enterActiveClass = 'transition-all duration-300 origin-top ease-in'
const leaveActiveClass = 'transition-all duration-300 origin-top ease-out' const leaveActiveClass = 'transition-all duration-300 origin-top ease-out'
const baseFromClass = 'transform opacity-0 md:scale-75' const baseFromClass = 'transform opacity-0 md:scale-75'
if ( to.fullPath.split('/')[1] !== from.fullPath.split('/')[1] || !from.matched.length ) { if ( from.meta.indexBase !== to.meta.indexBase ) {
const fromIndex = from.meta.parentIndex ? from.meta.parentIndex : from.meta.index const fromIndex = from.meta.indexBase
const toIndex = to.meta.parentIndex ? to.meta.parentIndex : to.meta.index const toIndex = to.meta.indexBase
const slideInDirection = toIndex < fromIndex ? 'md:-translate-x-1/3' : 'md:translate-x-1/3' 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' const slideOutDirection = toIndex < fromIndex ? 'md:translate-x-1/3' : 'md:-translate-x-1/3'
to.meta.mainEnterActiveClass = enterActiveClass to.meta.mainEnterActiveClass = enterActiveClass
to.meta.mainLeaveActiveClass = leaveActiveClass to.meta.mainLeaveActiveClass = leaveActiveClass
to.meta.mainEnterFromClass = baseFromClass + ' ' + slideInDirection to.meta.mainEnterFromClass = baseFromClass + ' ' + slideInDirection
to.meta.mainLeaveToClass = baseFromClass + ' ' + slideOutDirection to.meta.mainLeaveToClass = baseFromClass + ' ' + slideOutDirection
} else if ( to.fullPath.split('/')[1] == from.fullPath.split('/')[1] && from.matched.length ) { } else if ( from.meta.indexChild !== to.meta.indexChild ) {
const fromIndex = from.meta.index const fromIndex = from.meta.indexChild
const toIndex = to.meta.index const toIndex = to.meta.indexChild
const slideInDirection = toIndex < fromIndex ? 'md:-translate-y-1/3' : 'md:translate-y-1/3' 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' const slideOutDirection = toIndex < fromIndex ? 'md:translate-y-1/3' : 'md:-translate-y-1/3'
to.meta.panelEnterActiveClass = enterActiveClass to.meta.panelEnterActiveClass = enterActiveClass

View File

@ -4,6 +4,7 @@ export const useAnswersStore = defineStore({
id: 'answers', id: 'answers',
state: () => ({ state: () => ({
answers: [], answers: [],
quizCurrent: null
}), }),
actions: { actions: {
isAnswered(index) { isAnswered(index) {

View File

@ -3,18 +3,14 @@ import { defineStore } from 'pinia'
export const useAppStore = defineStore({ export const useAppStore = defineStore({
id: 'app', id: 'app',
state: () => ({ state: () => ({
hasData: false,
answers: [], answers: [],
playbooks: {}, playbooks: {},
results: {}, results: {},
scores: {}, scores: {},
count: null count: null,
compareCurrent: null
}), }),
actions: { actions: {
toggleData() {
this.hasData = !this.hasData
console.log('Toggled Has Data. New value', this.hasData)
},
store(key, data) { store(key, data) {
this[key] = JSON.parse(JSON.stringify(data)) this[key] = JSON.parse(JSON.stringify(data))
} }

View File

@ -3,8 +3,7 @@ import { defineStore } from 'pinia'
export const useQuestionStore = defineStore({ export const useQuestionStore = defineStore({
id: 'questions', id: 'questions',
state: () => ({ state: () => ({
questions: [], questions: []
currentQuestion: 0
}), }),
actions: { actions: {
storeQuestions(questionArray) { storeQuestions(questionArray) {

View File

@ -1,4 +1,5 @@
<script> <script>
import { useAppStore } from '@/stores/app.js'
import Content from '@/components/Content.vue' import Content from '@/components/Content.vue'
import Header from '@/components/Header.vue' import Header from '@/components/Header.vue'
import TextFrame from '@/components/TextFrame.vue' import TextFrame from '@/components/TextFrame.vue'
@ -6,11 +7,21 @@
export default { export default {
name: 'Home', name: 'Home',
components: { components: {
AccountQuestion, AccountQuestion,
Content, Content,
Header, Header,
TextFrame TextFrame
} },
setup() {
const appStore = useAppStore()
return {
appStore
}
},
mounted() {
this.appStore.$reset()
}
} }
</script> </script>
<template> <template>
@ -41,11 +52,6 @@
</div> </div>
</router-link> </router-link>
</div> </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&rsquo;s game.
I would nevertheless implore you to play <em>Wanderhome</em> if you haven&rsquo;t already.
</p>
</article> </article>
</Content> </Content>
</TextFrame> </TextFrame>

View File

@ -18,78 +18,84 @@
</Header> </Header>
<Content> <Content>
<article class="prose mx-auto"> <article class="prose mx-auto">
<h2>What Exactly Is This?</h2> <section>
<p> <h2>What Exactly Is This?</h2>
This web app is a personality quiz to see what <em class="uncial-antiqua">Wanderhome</em> playbook you are. <p>
I have previously made a smilar <a href="https://masks.vsnt.uk/" target="_blank" rel="noopener noreferrer">character quiz for Brendan Conway&rsquo;s <em>Masks: a New Generation</em></a>. This web app is a personality quiz to see what <em class="uncial-antiqua">Wanderhome</em> playbook you are.
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>. 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&rsquo;s <em>Masks: a New Generation</em></a>.
</p> 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. <p>
But quizzes like this are also really enjoyable to do, and not to mention compare our results with others. This was a fun programming exercise I set myself.
And I would love to make this quiz if it gives me a reason to talk to people I know about my favourite TTRPGs. But quizzes like this are also really enjoyable to do, and not to mention compare our results with others.
</p> 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>. <p>
I wholeheartedly recommend playing it! 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>.
</p> I wholeheartedly recommend playing it!
</p>
</section>
<hr class="border-gray-400 w-2/3 mx-auto"/> <hr class="border-gray-400 w-2/3 mx-auto"/>
<h2> <section>
Technobabble <h2>
</h2> Technobabble
<p> </h2>
It has been a long time since I made the <em>Masks</em> quiz, and this quiz has been proportionately over-engineered! <p>
</p> It has been a long time since I made the <em>Masks</em> quiz, and this quiz has been proportionately over-engineered!
<h3> </p>
Dividing Client/Server Side <h3>
</h3> Dividing Client/Server Side
<p> </h3>
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. <p>
The app runs on Python and Flask, and its pages are rendered primarily through Jinja. 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.
This means that the Python server handles the quiz and also serves the web site, which is not the most robust. The app runs on Python and Flask, and its pages are rendered primarily through Jinja.
</p> 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. <p>
The server is a proper system-agnostic API. 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.
</p> The server is a proper system-agnostic API.
<h3> </p>
Storing Results <h3>
</h3> Storing Results
<p> </h3>
One of the things I have added to the quiz is that it will store the results people get. <p>
It will not collect anybody&rsquo;s name or any identifying information. One of the things I have added to the quiz is that it will store the results people get.
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 not collect anybody&rsquo;s name or any identifying information.
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. 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.
</p> 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>
</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.
<h3> </p>
Different Frameworks <h3>
</h3> Different Frameworks
<p> </h3>
I made the regrettable decision of teaching myself a new framework for this project. <p>
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. I made the regrettable decision of teaching myself a new framework for this project.
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. 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.
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. 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.
</p> 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. <p>
The result here is one that has much more polish. 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.
</p> The result here is one that has much more polish.
</p>
</section>
<hr class="border-gray-400 w-2/3 mx-auto"/> <hr class="border-gray-400 w-2/3 mx-auto"/>
<h3> <section>
What Next? <h2>
</h3> What Next?
<p> </h2>
I am running out of geeky TTRPG-related programming projects. <p>
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. I am running out of geeky TTRPG-related programming projects.
There is an app that exists, but every time I tried using it I struggled to get it to work properly. 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.
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. There is an app that exists, but every time I tried using it I struggled to get it to work properly.
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. 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.
That might be the next project I tinker with. 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.
It will apply all the things I currently know, and also add websockets and state synchronisation to the mix. That might be the next project I tinker with.
</p> It will apply all the things I currently know, and also add websockets and state synchronisation to the mix.
</p>
</section>
</article> </article>
</Content> </Content>
</TextFrame> </TextFrame>

View File

@ -12,27 +12,40 @@
} }
</script> </script>
<template> <template>
<div> <TextFrame>
<TextFrame> <Header>
<Header> Acknowledgements
Acknowledgements </Header>
</Header> <Content>
<Content> <article class="prose mx-auto">
<article class="prose mx-auto"> <section>
<p class="text-leader mx-12">
This one is for all my friends and &lsquo;incidental companions&rsquo; 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> <p>
First and foremost, I am delighted that Jay Dragon and Possum Creek Games created such a magnificent work of art. 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&rsquo;s enthusiasm about this project.
</p> </p>
<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&rsquo; 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&rsquo;t for his relentless shilling for it.
</p> </p>
<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>
<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> </p>
</article> </section>
</Content> </article>
</TextFrame> </Content>
</div> </TextFrame>
</template> </template>

View 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&aelig;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>

View File

@ -12,19 +12,19 @@
} }
</script> </script>
<template> <template>
<div> <TextFrame>
<TextFrame> <Header>
<Header> About <em>Wanderhome</em>
About <em>Wanderhome</em> </Header>
</Header> <Content>
<Content> <article class="prose mx-auto">
<article class="prose mx-auto"> <section>
<p> <p>
<em class="uncial-antiqua">Wanderhome</em> is a pastoral fantasy table-top role-playing game by Jay Dragon, published by Possum Creek Games. <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. 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. 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. It has a very distinct character, and certainly one of the most compelling and delightful games I have played recently.
</p> </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. 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&aelig;th is the spectre of a terrible war that has recently ended. Looming in the setting of H&aelig;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. 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. And it is hopeful.
</p> </p>
<hr class="border-gray-400 w-2/3 mx-auto"/> </section>
<hr class="border-gray-400 w-2/3 mx-auto"/>
<section>
<h2> <h2>
Where to Find a Copy Where to Find a Copy
</h2> </h2>
<p> <p>
You can buy the game from <a href="https://possumcreekgames.com/en-gb/products/wanderhome" target="_blank" rel="noopener noreferrer">Possum Creek Games&rsquo; web site</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&rsquo; 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>. 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>
<p> <p>
If you are one of my friends and would like to play <em class="uncial-antiqua">Wanderhome</em>, let me know! If you are one of my friends and would like to play <em class="uncial-antiqua">Wanderhome</em>, let me know!
</p> </p>
</article> </section>
</Content> </article>
</TextFrame> </Content>
</div> </TextFrame>
</template> </template>

View File

@ -12,27 +12,25 @@
} }
</script> </script>
<template> <template>
<div > <TextFrame>
<TextFrame> <Header>
<Header> Error: Not Found
Error: Not Found </Header>
</Header> <Content>
<Content> <article class="prose mx-auto">
<article class="prose mx-auto"> <p class="text-leader">
<p class="text-leader"> Uh-oh.
Uh-oh. </p>
</p> <p>
<p> Sometimes we can get lost along the path.
Sometimes we can get lost along the path. </p>
</p> <p>
<p> But it&rsquo;s okay.
But it&rsquo;s okay. Just retrace your steps and you will find your way back again.
Just retrace your steps and you will find your way back again. </p>
</p> </article>
</article> </Content>
</Content> </TextFrame>
</TextFrame>
</div>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@ -12,23 +12,21 @@
} }
</script> </script>
<template> <template>
<div > <TextFrame>
<TextFrame> <Header>
<Header> Error: Connection Refused
Error: Connection Refused </Header>
</Header> <Content>
<Content> <article class="prose mx-auto">
<article class="prose mx-auto"> <p class="text-leader">
<p class="text-leader"> The client is unable to connect to the server.
The client is unable to connect to the server. That&rsquo;s okay.
That&rsquo;s okay. Sometimes, we run into obstacles along the way.
Sometimes, we run into obstacles along the way. And we just need to try again after a while.
And we just need to try again after a while. </p>
</p> </article>
</article> </Content>
</Content> </TextFrame>
</TextFrame>
</div>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@ -19,28 +19,27 @@
}, },
name: 'QuestionIndex', name: 'QuestionIndex',
created() { created() {
if (!this.appStore.hasData) { if (this.questionStore.questions.length == 0) {
this.getQuestions() this.getQuestions()
} }
}, },
data() { mounted() {
return { if (this.answersStore.quizCurrent !== null) {
error: null, this.$router.push(`/quiz/question/${this.answersStore.quizCurrent}`)
loading: false, } else {
questions: null this.answersStore.$reset()
this.appStore.$reset()
} }
}, },
methods: { methods: {
getQuestions() { getQuestions() {
this.error = this.questions = null
this.loading = true
axios.get(`${Config.SERVER}api/questions/`) axios.get(`${Config.SERVER}api/questions/`)
.then((response) => { .then((response) => {
this.error = false console.log('Fetched questions from server.')
this.questionStore.storeQuestions(response.data) this.questionStore.storeQuestions(response.data)
this.appStore.toggleData()
}) })
.catch( error => { .catch( error => {
console.log(error)
this.$router.push('/err_refused') this.$router.push('/err_refused')
}) })
} }

View File

@ -35,52 +35,28 @@
this.answersStore.makeArray(this.id, this.questionStore.questions[this.id].select) this.answersStore.makeArray(this.id, this.questionStore.questions[this.id].select)
}, },
mounted() { mounted() {
this.answersStore.quizCurrent = this.id
this.$watch( this.$watch(
() => this.$route.params, (toParams, previousParams) => { () => this.$route.params, (toParams, previousParams) => {
if (this.$route.name == 'Question') { if (this.$route.name == 'Question') {
if (toParams.id >= this.questionStore.questions.length || this.id < 0) { if (toParams.id >= this.questionStore.questions.length || this.id < 0) {
this.$router.push('/err_notfound') this.$router.push('/err_notfound')
} else { } else {
if ( toParams.id == 0 ) { this.answersStore.quizCurrent = toParams.id
this.enableBack = false
}
if ( toParams.id == this.questionStore.questions.length - 1 ) {
console.log('Condition met')
this.enableNext = false
} else {
this.enableNext = true
}
} }
} }
} }
) )
this.$watch( this.$watch(
() => this.appStore.hasData, (newValue) => { () => this.questionStore.questions, (newValue) => {
if (this.id >= this.questionStore.questions.length || this.id < 0) { if (this.id >= this.questionStore.questions.length || this.id < 0) {
this.$router.push('/err_notfound') this.$router.push('/err_notfound')
} else { } else {
this.answersStore.makeArray(this.id, this.questionStore.questions[this.id].select) 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: { methods: {
submitAnswers() { submitAnswers() {
const data = JSON.stringify(this.answersStore.answers) const data = JSON.stringify(this.answersStore.answers)
@ -93,10 +69,13 @@
} }
} }
).then( (response) => { ).then( (response) => {
console.log('Submitted results.')
this.appStore.store('results', response.data) this.appStore.store('results', response.data)
console.log('Results fetched from the server.') console.log('Results fetched from the server.')
this.answersStore.quizCurrent = null
this.$router.push('/results') this.$router.push('/results')
}).catch( (error) => { }).catch( (error) => {
console.log(error)
this.$router.push('/err_refused') this.$router.push('/err_refused')
}) })
} }
@ -106,8 +85,7 @@
type: Number, type: Number,
required: true required: true
} }
}, }
} }
</script> </script>
@ -119,7 +97,8 @@
<Content> <Content>
<div v-if="questionStore.questions[this.id]"> <div v-if="questionStore.questions[this.id]">
<article class="prose"> <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> </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 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"> <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> </template>
<style scoped> <style scoped>
.checkbox { .checkbox {
position: absolute; position: absolute;
opacity: 0; opacity: 0;

View File

@ -15,47 +15,45 @@
} }
</script> </script>
<template> <template>
<div> <TextFrame>
<TextFrame> <Header>
<Header> Take the Quiz
Take the Quiz </Header>
</Header> <Content>
<Content> <article class="prose mx-auto">
<article class="prose mx-auto"> <h2 class="uncial-antiqua" key="welcome-0">
<h2 class="uncial-antiqua" key="welcome-0"> Gather your Things
Gather your Things </h2>
</h2> <p class="text-leader" key="welcome-1">
<p class="text-leader" key="welcome-1"> We are about to embark upon a journey.
We are about to embark upon a journey. </p>
</p> <p class="text-leader" key="welcome-2">
<p class="text-leader" key="welcome-2"> In a minute, I will ask you a series of multiple choice questions.
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.
For most of them, you will need to select the one answer. Some questions will need multiple answers.
Some questions will need multiple answers. Don&rsquo;t over-think them!
Don&rsquo;t over-think them! </p>
</p> <p class="text-leader" key="welcome-3">
<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.
<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.
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.
This quiz will help you figure out what kinds of stories you will bring to the table. </p>
</p> <p class="text-leader" key="welcome-4">
<p class="text-leader" key="welcome-4"> Will you join me?
Will you join me? </p>
</p> <div class="w-fit mx-auto" key="welcome-5">
<div class="w-fit mx-auto" key="welcome-5"> <router-link active-class="active-link" class="navlink" to="/quiz/question/0">
<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">
<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">
<span class="scale-75"> <RoadVariant/>
<RoadVariant/> </span>
</span> <span class="uncial-antiqua">Let&rsquo;s Go</span>
<span class="uncial-antiqua">Let&rsquo;s Go</span> </div>
</div> </router-link>
</router-link> </div>
</div> </article>
</article> </Content>
</Content> </TextFrame>
</TextFrame>
</div>
</template> </template>
<style scoped> <style scoped>
</style> </style>

View File

@ -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>

View File

@ -29,18 +29,20 @@
getResults() { getResults() {
axios.get(`${Config.SERVER}api/playbooks/`) axios.get(`${Config.SERVER}api/playbooks/`)
.then((response) => { .then((response) => {
this.error = false console.log('Fetched playbook stats from the server.')
this.appStore.store('playbooks', response.data) this.appStore.store('playbooks', response.data)
}) })
.catch( error => { .catch( error => {
console.log(error)
this.$router.push('/err_refused') this.$router.push('/err_refused')
}) })
axios.get(`${Config.SERVER}api/count/`) axios.get(`${Config.SERVER}api/count/`)
.then((response) => { .then((response) => {
this.error = false console.log('Fetched user count from the server.')
this.appStore.store('count', response.data) this.appStore.store('count', response.data)
}) })
.catch( error => { .catch( error => {
console.log(error)
this.$router.push('/err_refused') this.$router.push('/err_refused')
}) })
} }
@ -92,7 +94,7 @@
</td> </td>
</tr> </tr>
<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> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -1,9 +1,25 @@
<script> <script>
import Sidebar from '@/components/sidebar/Index.vue' import Sidebar from '@/components/sidebar/Index.vue'
import { useAppStore } from '@/stores/app.js'
export default { export default {
setup() {
const appStore = useAppStore()
return {
appStore
}
},
name: 'ResultsIndex', name: 'ResultsIndex',
components: { components: {
Sidebar 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> </script>

View File

@ -53,13 +53,10 @@
</span> </span>
<span class="text-sm">Pages {{ Object.values(playbook)[0].pages }}</span> <span class="text-sm">Pages {{ Object.values(playbook)[0].pages }}</span>
</h2> </h2>
<p class="text-leader" v-html="Object.values(playbook)[0].flavour"> <p class="text-leader" v-html="Object.values(playbook)[0].flavour"/>
</p> <p v-html="Object.values(playbook)[0].blurb"/>
<p> <p>
{{ Object.values(playbook)[0].blurb }} You are alive. Your care is <strong v-html="Object.values(playbook)[0].care"></strong>.
</p>
<p>
You are alive. Your care is <strong>{{ Object.values(playbook)[0].care }}</strong>.
</p> </p>
<p> <p>
Your animal form is {{ renderAnimals(Object.values(playbook)[0].animals) }} Your animal form is {{ renderAnimals(Object.values(playbook)[0].animals) }}

View File

@ -36,7 +36,7 @@
Playbook Playbook
</th> </th>
<th> <th>
Score Score (&#x25;)
</th> </th>
</tr> </tr>
</thead> </thead>
@ -46,7 +46,7 @@
The {{ playbook }} The {{ playbook }}
</td> </td>
<td scope="col" class="py-1"> <td scope="col" class="py-1">
{{ Math.round(100*score/this.appStore.results.max_score) }} &#x25; {{ Math.round(100*score/this.appStore.results.max_score) }}
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -29,18 +29,20 @@
getScores() { getScores() {
axios.get(`${Config.SERVER}api/scores/`) axios.get(`${Config.SERVER}api/scores/`)
.then((response) => { .then((response) => {
this.error = false console.log('Fetched score stats from the server.')
this.appStore.store('scores', response.data) this.appStore.store('scores', response.data)
}) })
.catch( error => { .catch( error => {
console.log(error)
this.$router.push('/err_refused') this.$router.push('/err_refused')
}) })
axios.get(`${Config.SERVER}api/count/`) axios.get(`${Config.SERVER}api/count/`)
.then((response) => { .then((response) => {
this.error = false console.log('Fetched user count from the server.')
this.appStore.store('count', response.data) this.appStore.store('count', response.data)
}) })
.catch( error => { .catch( error => {
console.log(error)
this.$router.push('/err_refused') 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. 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. Or, indeed, it can provide insight into how the questions we ask of each other bias the way we perceive the world.
</p> </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"> <thead class="text-xs text-gray-700 uppercase bg-lime-100 dark:bg-gray-700 dark:text-gray-400">
<tr> <tr>
<th> <th>
@ -67,16 +69,16 @@
Playbook Playbook
</th> </th>
<th> <th>
Your Score Your Score (&#x25;)
</th> </th>
<th> <th>
Mean Score Mean
</th> </th>
<th> <th>
Median Score Median
</th> </th>
<th> <th class="lowercase underline" title="Standard Deviation">
Standard Deviation &sigma;*
</th> </th>
</tr> </tr>
</thead> </thead>
@ -105,7 +107,7 @@
</td> </td>
</tr> </tr>
<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> </tr>
</tbody> </tbody>
</table> </table>

View 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) }} &#37;</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>

View File

@ -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&rsquo;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>

View 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>