Skip to content

Commit

Permalink
feat: Add welcome screen (#222)
Browse files Browse the repository at this point in the history
* feat: Add pop-up sheet component

* feat: Add store to track open tutorials

* feat: Add flag support to app state

* refactor: Remove default tutorials

* feat: Set store_restored flag if store was restored

* feat: Add onboarding tutorial component

* feat: Add tutorial view

* feat: Use tutorial view on the timer page

* refactor: Increase padding in onboarding tutorial

* fix: Remove unused PWA installed tutorial reference

* feat: Persist tutorials store

* doc(tutorialView): Add explanation on enableComponent

* feat: Add more pop to onboarding buttons

* feat: Add translations to onboarding tutorial
  • Loading branch information
Hanziness committed May 21, 2022
1 parent 2c10064 commit cd92141
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 27 deletions.
24 changes: 24 additions & 0 deletions components/base/popupSheet.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<template>
<transition
enter-class="!translate-y-full"
enter-active-class="transition-all duration-300"
leave-to-class="!translate-y-full"
leave-active-class="transition-all duration-300"
appear
>
<div v-show="open" class="bottom-0 left-0 right-0 z-20 mx-auto transition-all">
<slot />
</div>
</transition>
</template>

<script>
export default {
props: {
open: {
type: Boolean,
default: false
}
}
}
</script>
65 changes: 65 additions & 0 deletions components/tutorial/_tutorialView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<template>
<div v-show="enableComponent" class="fixed top-0 left-0 z-40 w-screen h-screen transition">
<transition leave-to-class="opacity-0" enter-class="opacity-0" appear @after-leave="enableComponent = false">
<div v-show="currentTutorial !== null" class="fixed top-0 left-0 w-full h-full transition-all duration-500 bg-opacity-40 dark:bg-opacity-70 bg-slate-900" />
</transition>

<!-- Tutorial component -->
<transition
mode="out-in"
enter-class="!translate-y-full"
enter-active-class="transition duration-500 ease-out"
leave-to-class="!translate-y-full"
leave-active-class="transition duration-300"
appear
>
<component :is="tutorial[tutorialId]" v-for="tutorialId in [currentTutorial]" :key="tutorialId" :open="isTutorialOpen(tutorialId)" @close="closeTutorial(tutorialId)" />
</transition>
</div>
</template>

<script>
import { mapActions, mapState } from 'pinia'
import tutorialOnboarding from './tutorialOnboarding.vue'
import { useTutorials } from '~/stores/tutorials'
import { useMain, flags } from '@/stores/index'
export default {
data () {
return {
tutorial: {
onboarding: tutorialOnboarding
},
/** Controls whether the darkening backdrop is shown */
enableComponent: false
}
},
computed: {
...mapState(useTutorials, ['currentTutorial', 'isTutorialOpen']),
...mapState(useMain, ['isFlagActive'])
},
watch: {
currentTutorial (newValue) {
// if a tutorial is to be shown, enable the backdrop
if (newValue !== null) {
this.enableComponent = true
}
}
},
mounted () {
this.enableComponent = this.currentTutorial != null
if (!this.isFlagActive(flags.STORE_RESTORED)) {
this.openTutorial('onboarding')
}
},
methods: {
...mapActions(useTutorials, ['openTutorial', 'closeTutorial'])
}
}
</script>
66 changes: 66 additions & 0 deletions components/tutorial/tutorialOnboarding.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<template>
<popup-sheet class="fixed w-full max-w-2xl md:p-4" open>
<div class="flex flex-col px-6 pt-6 pb-6 transition bg-white shadow-lg md:pb-3 rounded-t-2xl md:rounded-2xl">
<transition enter-active-class="transition" enter-class="translate-x-4 opacity-0" leave-to-class="-translate-x-4 opacity-0" leave-active-class="transition" mode="out-in">
<!-- Welcome screen -->
<div v-if="page === 0" key="page-index" class="flex flex-col">
<div class="flex flex-col items-center gap-2 mb-2 text-center">
<img src="/favicon.svg" class="p-3 bg-work bg-opacity-20 rounded-xl" width="72">
<h2 class="mt-2 text-xl font-bold uppercase" v-text="$i18n.t('tutorials.onboarding.pages.0.title')" />
<p v-text="$i18n.t('tutorials.onboarding.pages.0.text')" />
</div>
</div>
<div v-if="page === 1" key="tutorial-1" class="flex flex-col items-center gap-2 text-center">
<ClockIcon size="72" class="p-3 bg-work bg-opacity-20 rounded-xl" />
<h2 class="mt-2 text-xl font-bold uppercase text-work" v-text="$i18n.t('tutorials.onboarding.pages.1.title')" />
<p v-text="$i18n.t('tutorials.onboarding.pages.1.text')" />
</div>
<div v-if="page === 2" key="tutorial-2" class="flex flex-col items-center gap-2 text-center">
<MugIcon size="72" class="p-3 bg-shortpause bg-opacity-20 rounded-xl" />
<h2 class="mt-2 text-xl font-bold uppercase text-shortpause" v-text="$i18n.t('tutorials.onboarding.pages.2.title')" />
<p v-text="$i18n.t('tutorials.onboarding.pages.2.text')" />
</div>
<div v-if="page === 3" key="tutorial-3" class="flex flex-col items-center gap-2 text-center">
<SettingsIcon size="72" class="p-3 bg-longpause bg-opacity-20 rounded-xl" />
<h2 class="mt-2 text-xl font-bold uppercase text-longpause" v-text="$i18n.t('tutorials.onboarding.pages.3.title')" />
<p v-text="$i18n.t('tutorials.onboarding.pages.3.text')" />
</div>
<div v-if="page === 4" key="tutorial-4" class="flex flex-col items-center gap-2 text-center">
<HeartIcon size="72" class="p-3 text-black bg-amber-400 rounded-xl" />
<h2 class="mt-2 text-xl font-bold uppercase text-amber-500" v-text="$i18n.t('tutorials.onboarding.pages.support.title')" />
<p v-text="$i18n.t('tutorials.onboarding.pages.support.text')" />
</div>
</transition>
<div class="flex-grow h-4" />
<div class="flex flex-col gap-2 mt-4 md:flex-row">
<button class="flex-grow w-full py-2 transition border-2 rounded-full text-work border-work hover:bg-work hover:text-white" @click="$emit('close')">
Close
</button>
<button v-if="page === 0" class="flex-grow w-full py-2 text-white transition border-2 rounded-full border-work bg-work hover:scale-105 hover:shadow-md hover:shadow-red-200" @click="page = 1">
Start tutorial
</button>
<button v-else-if="page < 4" class="flex-grow w-full py-2 text-white transition border-2 rounded-full border-work bg-work hover:shadow-md hover:shadow-red-200 hover:scale-105" @click="page += 1">
Next
</button>
<a v-else-if="page === 4" href="https://www.buymeacoffee.com/imreg?utm_source=anotherpomodoro&utm_medium=cta&utm_campaign=onboarding" target="_blank" class="flex-grow w-full py-2 text-center text-black transition border-2 rounded-full border-amber-400 bg-amber-400 hover:scale-105 hover:shadow-md hover:shadow-amber-200">
Support the project
</a>
</div>
</div>
</popup-sheet>
</template>

<script>
import { ClockIcon, MugIcon, SettingsIcon, HeartIcon } from 'vue-tabler-icons'
import PopupSheet from '@/components/base/popupSheet.vue'
export default {
components: { PopupSheet, ClockIcon, MugIcon, SettingsIcon, HeartIcon },
data () {
return {
page: 0
}
}
}
</script>
32 changes: 32 additions & 0 deletions i18n/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -442,5 +442,37 @@ export default {
workaholic: 'For long work sessions.'
},
description: '{brief} \n {worklength} minutes of work with {splength} minutes short and {lplength} minutes long breaks after every {lpfreq} work sessions.'
},
tutorials: {
onboarding: {
pages: {
0: {
title: 'Welcome to AnotherPomodoro!',
text: 'Take a quick look around on how to use the app effectively.'
},
1: {
title: 'Follow the clock',
text: 'Work or take a break until the timer runs out. Then proceed with your next timer.'
},
2: {
title: 'Rest regularly',
text: 'Every few breaks you get more time to rest. Make good use of it.'
},
3: {
title: 'Stay flexible',
text: 'Set your own timers, use the task list and notifications to your advantage. Check out the settings menu for more opportunities!'
},
support: {
title: 'Support the project',
text: 'If this project helped you, consider inviting the author to a coffee. You can find the support button in the settings menu, too.'
}
},
buttons: {
close: 'Close',
start: 'Start tutorial',
next: 'Next',
support: 'Support the project'
}
}
}
}
23 changes: 13 additions & 10 deletions pages/timer.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<section class="dark:text-gray-50 h-full overflow-hidden transition-colors duration-300 ease-in" :class="[{'dark' : settingsStore.visuals.darkMode }]">
<section class="h-full overflow-hidden transition-colors duration-300 ease-in dark:text-gray-50" :class="[{'dark' : settingsStore.visuals.darkMode }]">
<!-- Dark mode background override -->
<div class="dark:bg-gray-900 absolute w-full h-full" />
<div class="absolute w-full h-full dark:bg-gray-900" />

<!-- Settings panel -->
<div>
Expand All @@ -20,24 +20,24 @@
>
<div class="z-10 flex flex-row w-full">
<div
class="md:w-auto flex flex-col overflow-hidden transition-all duration-300 bg-gray-800 shadow-lg"
class="flex flex-col overflow-hidden transition-all duration-300 bg-gray-800 shadow-lg md:w-auto"
:class="[settingsStore.schedule.visibility.enabled ? 'mt-0 md:mt-3 md:rounded-lg w-full max-w-full mx-auto self-center' : 'ml-auto p-2 rounded-l-lg mt-3']"
>
<div class="flex flex-row gap-3" :class="[settingsStore.schedule.visibility.enabled ? 'px-3' : '']">
<ScheduleDisplay v-show="settingsStore.schedule.visibility.enabled" class="px-0" />
<!-- Settings button -->
<div class="flex-column flex items-center">
<div class="flex items-center flex-column">
<button
:aria-label="$i18n.t('settings.heading')"
class="hover:bg-slate-200 hover:bg-opacity-30 active:bg-opacity-50 p-3 text-gray-200 transition rounded-full"
class="p-3 text-gray-200 transition rounded-full hover:bg-slate-200 hover:bg-opacity-30 active:bg-opacity-50"
:class="{ 'pointer-events-none': preview }"
@click="showSettings = true"
>
<CogIcon :aria-label="$i18n.t('settings.heading')" />
</button>
</div>
</div>
<div v-if="settingsStore.schedule.visibility.enabled && settingsStore.schedule.visibility.showSectionType" class="text-gray-50 py-2 text-center bg-gray-700 select-none">
<div v-if="settingsStore.schedule.visibility.enabled && settingsStore.schedule.visibility.showSectionType" class="py-2 text-center bg-gray-700 select-none text-gray-50">
{{ $i18n.t('section.' + scheduleStore.getCurrentItem.type).toLowerCase() }}
</div>
</div>
Expand All @@ -61,24 +61,25 @@
:time-original="timeOriginal"
:timer-state="timerState"
:timer-widget="settingsStore.currentTimer"
class="place-items-center absolute grid"
class="absolute grid place-items-center"
@tick="timeString = $event"
/>
</div>

<div class="relative flex flex-row items-center justify-center w-full gap-2 mb-4">
<TimerControls :class="[{ 'pointer-events-none': preview }]" :can-use-keyboard="!preview && !showSettings" />

<button v-show="settingsStore.tasks.enabled" class="right-4 dark:bg-gray-700 sm:absolute p-4 transition-all bg-gray-200 rounded-full shadow-md" :class="{'scale-0': showTodoManager}" @click="showTodoManager = true">
<button v-show="settingsStore.tasks.enabled" class="p-4 transition-all bg-gray-200 rounded-full shadow-md right-4 dark:bg-gray-700 sm:absolute" :class="{'scale-0': showTodoManager}" @click="showTodoManager = true">
<ListCheckIcon />
</button>
</div>
<transition enter-class="translate-y-full" enter-active-class="duration-300 ease-out" leave-to-class="translate-y-full" leave-active-class="duration-150 ease-in">
<TodoList v-show="settingsStore.tasks.enabled && showTodoManager" class="rounded-t-xl xl:right-4 xl:pb-8 fixed bottom-0 z-10 w-full max-w-lg transition-all" :editing="[0].includes(scheduleStore.timerState)" @hide="showTodoManager = false" />
<TodoList v-show="settingsStore.tasks.enabled && showTodoManager" class="fixed bottom-0 z-10 w-full max-w-lg transition-all rounded-t-xl xl:right-4 xl:pb-8" :editing="[0].includes(scheduleStore.timerState)" @hide="showTodoManager = false" />
</transition>
</div>
</Ticker>
</NotificationController>
<TutorialView />
</section>
</template>

Expand All @@ -88,6 +89,7 @@ import { SettingsIcon, ListCheckIcon } from 'vue-tabler-icons'
import { useSchedule } from '~/stores/schedule'
import { useSettings } from '~/stores/settings'
import { useEvents } from '@/stores/events'
import TutorialView from '@/components/tutorial/_tutorialView.vue'
// Static imports:
Expand All @@ -104,7 +106,8 @@ export default {
UiOverlay: () => import(/* webpackChunkName: "uibase", webpackPrefetch: true */ '@/components/base/overlay.vue'),
TodoList: () => import(/* webpackChunkName: "todo" */ '@/components/todoList/main.vue'),
CogIcon: SettingsIcon,
ListCheckIcon
ListCheckIcon,
TutorialView
},
layout: 'timer',
Expand Down
9 changes: 8 additions & 1 deletion plugins/store-persist.client.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const persistStores = ['settings', 'tasklist']
import { useMain, flags } from '@/stores/index'

const persistStores = ['settings', 'tasklist', 'tutorials']
const storeResetKey = '--reset-store'

/** Get the persistence key of the store by its ID */
Expand All @@ -10,13 +12,18 @@ function restoreStore (store) {

if (stateToRestore !== null) {
store.$patch(stateToRestore)

console.log(`Restoring ${store.$id}`)
const mainStore = useMain()
mainStore.registerFlag(flags.STORE_RESTORED)
}
}

const PiniaNuxtPersistencePlugin = ({ app, $pinia }) => {
const PiniaPersistPlugin = ({ store }) => {
if (persistStores.includes(store.$id)) {
const restore = localStorage.getItem(storeResetKey) == null

// Restore the store first
if (restore) {
restoreStore(store)
Expand Down
35 changes: 19 additions & 16 deletions stores/index.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import { defineStore } from 'pinia'

export const flags = {
STORE_RESTORED: 'store_restored'
}

export const useMain = defineStore('main', {
state: () => ({
version: process.env.PACKAGE_VERSION
})
})
version: process.env.PACKAGE_VERSION,
flags: []
}),

// export const {
// state () {
// return {
// version: process.env.PACKAGE_VERSION
// }
// },
// actions: {
// nuxtServerInit ({ commit }) {
// // initialize with 10 entries, no need for phantom entries then
// commit('schedule/initSchedule', 10)
// }
// }
// }
getters: {
isFlagActive: state => flag => state.flags.includes(flag)
},

actions: {
registerFlag (flag) {
if (!this.flags.includes(flag)) {
this.flags.push(flag)
}
}
}
})
30 changes: 30 additions & 0 deletions stores/tutorials.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineStore } from 'pinia'

export const useTutorials = defineStore('tutorials', {
state: () => ({
openTutorials: []
}),

getters: {
isTutorialOpen: (state) => {
return tutorialId => state.openTutorials.indexOf(tutorialId) === 0
},

currentTutorial: state => state.openTutorials[0] ?? null
},

actions: {
openTutorial (tutorialId) {
if (!this.openTutorials.includes(tutorialId)) {
this.openTutorials.push(tutorialId)
}
},

closeTutorial (tutorialId) {
const tutorialIndex = this.openTutorials.indexOf(tutorialId)
if (tutorialIndex >= 0) {
this.openTutorials.splice(tutorialIndex, 1)
}
}
}
})

0 comments on commit cd92141

Please sign in to comment.