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>
<!-- 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">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'
@ -10,6 +11,16 @@
Content,
Header,
TextFrame
},
setup() {
const appStore = useAppStore()
return {
appStore
}
},
mounted() {
this.appStore.$reset()
}
}
</script>
@ -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&rsquo;s game.
I would nevertheless implore you to play <em>Wanderhome</em> if you haven&rsquo;t already.
</p>
</article>
</Content>
</TextFrame>

View File

@ -18,11 +18,12 @@
</Header>
<Content>
<article class="prose mx-auto">
<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">character quiz for Brendan Conway&rsquo;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>.
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>.
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.
@ -30,10 +31,12 @@
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>.
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"/>
<section>
<h2>
Technobabble
</h2>
@ -77,19 +80,22 @@
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>
<section>
<h2>
What Next?
</h3>
</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"><em>Microscope</em> by Ben Robbins</a>, published by Lame Mage Productions.
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>

View File

@ -12,27 +12,40 @@
}
</script>
<template>
<div>
<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 &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>
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>
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>
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>
</section>
</article>
</Content>
</TextFrame>
</div>
</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,13 +12,13 @@
}
</script>
<template>
<div>
<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.
@ -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>
</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&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>.
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 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>
</section>
</article>
</Content>
</TextFrame>
</div>
</template>

View File

@ -12,7 +12,6 @@
}
</script>
<template>
<div >
<TextFrame>
<Header>
Error: Not Found
@ -32,7 +31,6 @@
</article>
</Content>
</TextFrame>
</div>
</template>
<style scoped>
</style>

View File

@ -12,7 +12,6 @@
}
</script>
<template>
<div >
<TextFrame>
<Header>
Error: Connection Refused
@ -28,7 +27,6 @@
</article>
</Content>
</TextFrame>
</div>
</template>
<style scoped>
</style>

View File

@ -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')
})
}

View File

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

View File

@ -15,7 +15,6 @@
}
</script>
<template>
<div>
<TextFrame>
<Header>
Take the Quiz
@ -55,7 +54,6 @@
</article>
</Content>
</TextFrame>
</div>
</template>
<style scoped>
</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() {
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>

View File

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

View File

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

View File

@ -36,7 +36,7 @@
Playbook
</th>
<th>
Score
Score (&#x25;)
</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) }} &#x25;
{{ Math.round(100*score/this.appStore.results.max_score) }}
</td>
</tr>
</tbody>

View File

@ -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 (&#x25;)
</th>
<th>
Mean Score
Mean
</th>
<th>
Median Score
Median
</th>
<th>
Standard Deviation
<th class="lowercase underline" title="Standard Deviation">
&sigma;*
</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>

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>