Add an onboarding screen to get users started

Fix for #17.
This commit is contained in:
Lucas Verney 2018-07-16 17:26:10 +02:00
parent 959578cfe9
commit e9b4f41b1e
11 changed files with 230 additions and 47 deletions

View File

@ -3,7 +3,7 @@
<v-toolbar
app
>
<router-link :to="{ name: 'Map' }" class="noLinkDecoration">
<router-link :to="{ name: 'Onboarding' }" class="noLinkDecoration">
<v-toolbar-title v-text="title" class="ma-0"></v-toolbar-title>
</router-link>
<v-spacer></v-spacer>

View File

@ -8,17 +8,20 @@
<p>{{ $t('about.usageDescription') }}</p>
<h2 class="body-2">{{ $t('about.availableReportsTitle') }}</h2>
<ul class="ml-3">
<li><strong>{{ $t('reportLabels.accident') }}</strong>{{ $t('misc.spaceBeforeDoublePunctuations') }}: {{ $t('reportLabels.accidentDescription') }}</li>
<li><strong>{{ $t('reportLabels.gcum') }}</strong>{{ $t('misc.spaceBeforeDoublePunctuations') }}: {{ $t('reportLabels.gcumDescription') }}</li>
<li><strong>{{ $t('reportLabels.interrupt') }}</strong>{{ $t('misc.spaceBeforeDoublePunctuations') }}: {{ $t('reportLabels.interruptDescription') }}</li>
<li><strong>{{ $t('reportLabels.obstacle') }}</strong>{{ $t('misc.spaceBeforeDoublePunctuations') }}: {{ $t('reportLabels.obstacleDescription') }}</li>
<li><strong>{{ $t('reportLabels.pothole') }}</strong>{{ $t('misc.spaceBeforeDoublePunctuations') }}: {{ $t('reportLabels.potholeDescription') }}</li>
<li><strong>{{ $t('reportLabels.misc') }}</strong>{{ $t('misc.spaceBeforeDoublePunctuations') }}: {{ $t('reportLabels.miscDescription') }}</li>
</ul>
<ReportsDescription></ReportsDescription>
<p class="mt-3" v-html="$t('about.license')"></p>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import ReportsDescription from '@/components/ReportsDescription.vue';
export default {
components: {
ReportsDescription,
},
};
</script>

View File

@ -0,0 +1,34 @@
<template>
<table class="margin-auto">
<tr v-for="type in REPORT_TYPES_ORDER">
<td class="pa-2"><img class="reportIcon" :src="REPORT_TYPES[type].image"/></td>
<td class="pa-2 text-xs-left"><strong>{{ $t(REPORT_TYPES[type].label) }}</strong>{{ $t('misc.spaceBeforeDoublePunctuations') }}: {{ $t(REPORT_TYPES[type].description) }}</td>
</tr>
</table>
</template>
<script>
import { REPORT_TYPES, REPORT_TYPES_ORDER } from '@/constants';
export default {
data() {
return {
REPORT_TYPES,
REPORT_TYPES_ORDER,
};
},
};
</script>
<style scoped>
.margin-auto {
margin-left: auto;
margin-right: auto;
}
.reportIcon {
background: white;
width: 100px;
vertical-align: middle;
}
</style>

View File

@ -15,6 +15,7 @@ import potholeIcon from '@/assets/pothole.svg';
export const REPORT_TYPES = {
accident: {
description: 'reportLabels.accidentDescription',
label: 'reportLabels.accident',
image: accidentIcon,
marker: L.icon({
@ -30,6 +31,7 @@ export const REPORT_TYPES = {
},
gcum: {
description: 'reportLabels.gcumDescription',
label: 'reportLabels.gcum',
image: gcumIcon,
marker: L.icon({
@ -45,6 +47,7 @@ export const REPORT_TYPES = {
},
interrupt: {
description: 'reportLabels.interruptDescription',
label: 'reportLabels.interrupt',
image: interruptIcon,
marker: L.icon({
@ -59,6 +62,7 @@ export const REPORT_TYPES = {
}),
},
misc: {
description: 'reportLabels.miscDescription',
label: 'reportLabels.misc',
image: miscIcon,
marker: L.icon({
@ -73,6 +77,7 @@ export const REPORT_TYPES = {
}),
},
obstacle: {
description: 'reportLabels.obstacleDescription',
label: 'reportLabels.obstacle',
image: obstacleIcon,
marker: L.icon({
@ -87,6 +92,7 @@ export const REPORT_TYPES = {
}),
},
pothole: {
description: 'reportLabels.potholeDescription',
label: 'reportLabels.pothole',
image: potholeIcon,
marker: L.icon({
@ -119,4 +125,4 @@ export const EARTH_RADIUS = 6378137;
export const DEFAULT_ZOOM = 17;
export const MIN_ZOOM = 10;
export const MAX_ZOOM = 18;
export const TILE_SERVER = process.env.TILE_SERVER || 'https://a.tile.thunderforest.com/cycle/{z}/{x}/{y}.png';
export const TILE_SERVER = process.env.TILE_SERVER || 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png';

View File

@ -2,8 +2,8 @@
export default {
about: {
availableReportsTitle: 'The available reports so far are:',
geolocationDescription: 'As of current version, your precise geolocation is handled within your device and never sent from it to any external service. The map background is downloaded on demand from <a href="http://thunderforest.com/">Thunderforest</a> and they have then access to an estimate of the displayed position.',
license: 'It is released under an <a href="https://opensource.org/licenses/MIT">MIT license</a> (<a href="https://framagit.org/phyks/cyclassist">source code</a>). Icons are based on creations from Wikimedia and Vecteezy. The map background is using tiles from <a href="https://www.opencyclemap.org/docs/">OpenCycleMap</a>, thanks to <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a> and <a href="http://leafletjs.com/">Leaflet</a>. Collected reports are available under <a href="https://opendatacommons.org/licenses/odbl/">ODbL license</a>.',
geolocationDescription: 'As of current version, your precise geolocation is handled within your device and never sent from it to any external service. The map background is downloaded on demand from <a href="https://carto.com/location-data-services/basemaps/">Carto.com</a> and they have then access to an estimate of the displayed position.',
license: 'It is released under an <a href="https://opensource.org/licenses/MIT">MIT license</a> (<a href="https://framagit.org/phyks/cyclassist">source code</a>). Icons are based on creations from Wikimedia and Vecteezy. The map background is using tiles from <a href="https://carto.com/location-data-services/basemaps/">Carto.com</a>, thanks to <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a> and <a href="http://leafletjs.com/">Leaflet</a>. Collected reports are available under <a href="https://opendatacommons.org/licenses/odbl/">ODbL license</a>.',
summary: 'This app lets you track and share issues with bike lanes.',
usage: 'How to use',
usageDescription: 'Use the button in the lower right corner to add a new report at your current location. To add a report elsewhere, do a click where you want the report to be shown. Press on a marker on the map to display more informations and report the problem as being still there or solved.',
@ -18,10 +18,16 @@ export default {
},
geolocation: {
fetching: 'Fetching current position…',
geolocation: 'Geolocation',
unavailable: 'Sorry, geolocation is not available in your browser.',
},
intro: {
start: "Let's go!",
checkingPermissions: 'Checking permissions',
next: 'Next',
ready: 'Ready to start',
reportTypes: 'Report types',
startReporting: 'Start reporting!',
welcome: 'Welcome',
},
menu: {
About: 'Help',
@ -42,9 +48,9 @@ export default {
},
reportLabels: {
accident: 'Accident',
accidentDescription: 'Any accident on the road (active for one hour).',
accidentDescription: 'Any accident on the road (automatically removed after one hour).',
gcum: 'GCUM',
gcumDescription: 'A car poorly parked on a bike lane. Such reports are automatically deleted after one hour, as they are by nature temporary (active for one hour).',
gcumDescription: 'A car poorly parked on a bike lane. Such reports are automatically deleted after one hour, as they are by nature temporary (automatically removed after one hour).',
interrupt: 'Interruption',
interruptDescription: 'An interruption of the bike lane (works, unexpected end of the bike lane, etc.).',
misc: 'Other',
@ -58,5 +64,6 @@ export default {
locale: 'Language',
preventSuspend: 'Prevent device from going to sleep',
save: 'Save',
skipOnboarding: 'Skip onboarding',
},
};

View File

@ -2,8 +2,8 @@
export default {
about: {
availableReportsTitle: "Les signalements disponibles pour l'instant sont :",
geolocationDescription: "Dans la version actuelle, votre position est traitée directement par votre appareil et n'est jamais envoyée à un service externe. Le fond de carte est téléchargé à la demande depuis <a href='http://thunderforest.com/'>Thunderforest</a> et ils ont donc accès à une estimation de la position affichée.",
license: "Le code source est sous <a href='https://opensource.org/licenses/MIT'>licence MIT license</a> (<a href='https://framagit.org/phyks/cyclassist'>code source</a>). Les icones sont basées sur des travaux de Wikimedia et Vecteezy. Les tuiles de fond de carte proviennent de chez <a href='https://www.opencyclemap.org/docs/'>OpenCycleMap</a>, grace aux <a href='https://www.openstreetmap.org/copyright'>contributeurs OpenStreetMap</a> et à <a href='http://leafletjs.com/'>Leaflet</a>. Les signalements sont disponibles sous <a href='https://opendatacommons.org/licenses/odbl/'>licence ODbL</a>.",
geolocationDescription: "Dans la version actuelle, votre position est traitée directement par votre appareil et n'est jamais envoyée à un service externe. Le fond de carte est téléchargé à la demande depuis <a href='https://carto.com/location-data-services/basemaps/'>Carto.com</a> et ils ont donc accès à une estimation de la position affichée.",
license: "Le code source est sous <a href='https://opensource.org/licenses/MIT'>licence MIT license</a> (<a href='https://framagit.org/phyks/cyclassist'>code source</a>). Les icones sont basées sur des travaux de Wikimedia et Vecteezy. Les tuiles de fond de carte proviennent de chez <a href='https://carto.com/location-data-services/basemaps/'>Carto.com</a>, grace aux <a href='https://www.openstreetmap.org/copyright'>contributeurs OpenStreetMap</a> et à <a href='http://leafletjs.com/'>Leaflet</a>. Les signalements sont disponibles sous <a href='https://opendatacommons.org/licenses/odbl/'>licence ODbL</a>.",
summary: 'Cette application vous permet de signaler et de partager des problèmes avec les itinéraires cyclables.',
usage: 'Utilisation',
usageDescription: "Utilisez le bouton en bas à droite pour ajouter un signalement à votre emplacement actuel. Pour ajouter un signalement ailleurs, faites un appui à l'emplacement souhaité sur la carte. Appuyer sur un marqueur sur la carte pour afficher plus d'informations et signaler que le problème est toujours présent ou non.",
@ -18,10 +18,16 @@ export default {
},
geolocation: {
fetching: 'En attente de votre position…',
geolocation: 'Géolocalisation',
unavailable: "Désolé, la géolocalisation n'est pas disponible dans votre navigateur.",
},
intro: {
start: "C'est parti !",
checkingPermissions: 'Vérification des permissions',
next: 'Suivant',
ready: 'Tout est prêt !',
reportTypes: 'Types de signalements',
startReporting: 'Commencer à signaler !',
welcome: 'Bienvenue !',
},
menu: {
About: 'Aide',
@ -42,9 +48,9 @@ export default {
},
reportLabels: {
accident: 'Accident',
accidentDescription: 'Un accident sur la route (actif pour une heure).',
accidentDescription: 'Un accident sur la route (automatiquement supprimé après une heure).',
gcum: 'GCUM',
gcumDescription: "Une voiture (mal) garée sur la piste cyclable. Ces signalements sont automatiquement supprimés au bout d'une heure car ils sont par essence temporaires (actif pour une heure).",
gcumDescription: 'Une voiture (mal) garée sur la piste cyclable (automatiquement supprimé après une heure).',
interrupt: 'Interruption',
interruptDescription: "Une interruption d'itinéraire cyclable (travaux, arrêt inattendu d'une piste cyclable, etc)",
misc: 'Autre',
@ -58,5 +64,6 @@ export default {
locale: 'Langue',
preventSuspend: "Empêcher l'appareil de passer en veille",
save: 'Sauver',
skipOnboarding: "Sauter l'introduction",
},
};

View File

@ -2,6 +2,7 @@ import Vue from 'vue';
import Router from 'vue-router';
import About from '@/components/About.vue';
import Map from '@/views/Map.vue';
import Onboarding from '@/views/Onboarding.vue';
import Settings from '@/views/Settings.vue';
Vue.use(Router);
@ -14,10 +15,15 @@ export default new Router({
component: About,
},
{
path: '/',
path: '/map',
name: 'Map',
component: Map,
},
{
path: '/',
name: 'Onboarding',
component: Onboarding,
},
{
path: '/settings',
name: 'Settings',

View File

@ -8,11 +8,16 @@ import * as types from './mutations-types';
// Load settings from storage
let locale = null;
let preventSuspend = null;
let skipOnboarding = null;
if (storageAvailable('localStorage')) {
preventSuspend = localStorage.getItem('preventSuspend');
if (preventSuspend) {
preventSuspend = JSON.parse(preventSuspend);
}
skipOnboarding = localStorage.getItem('skipOnboarding');
if (skipOnboarding) {
skipOnboarding = JSON.parse(skipOnboarding);
}
locale = localStorage.getItem('locale');
if (!messages[locale]) {
@ -42,6 +47,7 @@ export const initialState = {
settings: {
locale: locale || 'en',
preventSuspend: preventSuspend || true,
skipOnboarding: skipOnboarding || false,
},
};

View File

@ -1,13 +1,6 @@
<template>
<v-container fluid fill-height class="no-padding">
<v-layout v-if="isIntro" row wrap class="text-xs-center blue lighten-2">
<v-flex xs8 offset-xs2>
<p><img src="@/assets/logo.svg" alt="Logo"/></p></p>
<p>{{ $t('about.summary') }}</p>
<v-btn role="button" round color="green" dark @click="introButtonClick">{{ $t('intro.start') }}</v-btn>
</v-flex>
</v-layout>
<v-layout v-else row wrap fill-height>
<v-layout row wrap fill-height>
<ReportCard></ReportCard>
<v-flex xs12 fill-height v-if="latLng">
<Map :positionLatLng="latLng" :reportLatLng="reportLatLng" :polyline="positionHistory" :heading="heading" :accuracy="accuracy" :markers="reportsMarkers" :onPress="showReportDialog"></Map>
@ -58,12 +51,15 @@ export default {
ReportCard,
ReportDialog,
},
beforeDestroy() {
if (!this.isIntro) {
this.disableNoSleep();
this.disablePositionWatching();
window.removeEventListener('keydown', this.hideReportDialogOnEsc);
created() {
if (!this.$store.state.settings.skipOnboarding) {
this.$router.replace({ name: 'Onboarding' });
}
},
beforeDestroy() {
this.disableNoSleep();
this.disablePositionWatching();
window.removeEventListener('keydown', this.hideReportDialogOnEsc);
this.$store.dispatch('hideReportDetails');
},
computed: {
@ -88,7 +84,6 @@ export default {
dialog: false,
error: null,
heading: null,
isIntro: true,
latLng: null,
noSleep: null,
positionHistory: [],
@ -154,6 +149,11 @@ export default {
}
this.accuracy = position.coords.accuracy ? position.coords.accuracy : null;
},
disableNoSleep() {
if (this.noSleep) {
this.noSleep.disable();
}
},
setNoSleep() {
let preventSuspend = localStorage.getItem('preventSuspend');
if (preventSuspend) {
@ -167,11 +167,6 @@ export default {
this.noSleep.enable();
}
},
disableNoSleep() {
if (this.noSleep) {
this.noSleep.disable();
}
},
showReportDialog(latlng) {
if (latlng) {
this.reportLat = latlng.lat;
@ -193,13 +188,12 @@ export default {
this.dialog = false;
}
},
introButtonClick() {
this.setNoSleep();
this.isIntro = false;
this.initializePositionWatching();
this.$store.dispatch('fetchReports');
window.addEventListener('keydown', this.hideReportDialogOnEsc);
},
},
mounted() {
this.setNoSleep();
this.initializePositionWatching();
this.$store.dispatch('fetchReports');
window.addEventListener('keydown', this.hideReportDialogOnEsc);
},
};
</script>

113
src/views/Onboarding.vue Normal file
View File

@ -0,0 +1,113 @@
<template>
<v-container fluid fill-height class="no-padding">
<v-layout row wrap class="text-xs-center blue lighten-2 px-2" v-if="step == 0">
<v-flex xs12>
<h2 class="display-1 pa-3">{{ $t('intro.welcome') }}</h2>
<p>{{ $t('about.summary') }}</p>
<p><img src="@/assets/logo.svg" alt="Logo"/></p>
<v-btn role="button" round color="green" dark @click="step += 1">{{ $t('intro.next') }}</v-btn>
</v-flex>
</v-layout>
<v-layout row wrap class="text-xs-center blue lighten-2 px-2" v-if="step == 1">
<v-flex xs12>
<h2 class="headline pa-3">{{ $t('intro.reportTypes') }}</h2>
<p class="text-xs-center">En cliquant sur l'icône <v-icon>report_problem</v-icon>, la boîte de signalement s'ouvre. Vous pouvez signaler :</p>
<ReportsDescription></ReportsDescription>
<v-btn role="button" round color="green" dark @click="step += 1">{{ $t('intro.next') }}</v-btn>
</v-flex>
</v-layout>
<v-layout row wrap class="text-xs-center blue lighten-2" v-if="step == 2">
<v-flex xs8 offset-xs2>
<h2 class="headline pa-3">{{ $t('intro.checkingPermissions') }}</h2>
<v-layout row class="white mb-3">
<v-flex xs6 offset-xs3>
<v-switch class="switch" color="success" :label="$t('geolocation.geolocation')" v-model="hasGeolocationPermission" readonly @click="handleGeolocationPermission"></v-switch>
</v-flex>
</v-layout>
<v-btn :disabled="!hasGeolocationPermission" role="button" round color="green" dark @click="step += 1">{{ $t('intro.next') }}</v-btn>
</v-flex>
</v-layout>
<v-layout row wrap class="text-xs-center blue lighten-2" v-if="step == 3">
<v-flex xs8 offset-xs2>
<h2 class="headline pa-3">{{ $t('intro.ready') }}</h2>
<p><img src="@/assets/logo.svg" alt="Logo"/></p>
<v-btn role="button" round color="green" dark @click="goToMap">{{ $t('intro.startReporting') }}</v-btn>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import ReportsDescription from '@/components/ReportsDescription.vue';
export default {
components: {
ReportsDescription,
},
created() {
if (navigator.permissions) {
navigator.permissions.query({ name: 'geolocation' }).then((result) => {
this.hasGeolocationPermission = (result.state === 'granted');
});
}
},
data() {
let step = 0;
if (this.$store.state.settings.skipOnboarding) {
step = 3;
}
return {
hasGeolocationPermission: false,
step,
};
},
methods: {
handleGeolocationPermission() {
if (this.hasGeolocationPermission) {
// Permission already granted
return;
}
// Explicitly request the permission to the user
navigator.geolocation.getCurrentPosition(
() => {
this.hasGeolocationPermission = true;
},
(error) => {
if (error.code === 1) { // Permission denied
this.hasGeolocationPermission = false;
} else {
this.hasGeolocationPermission = true;
}
},
{
timeout: 0,
},
);
},
goToMap() {
if (!this.$store.state.settings.skipOnboarding) {
this.$store.dispatch('setSetting', { setting: 'skipOnboarding', value: true });
}
this.$router.push({ name: 'Map' });
},
},
};
</script>
<style scoped>
.no-padding {
padding: 0;
}
</style>
<style>
.switch .v-label {
color: rgba(0,0,0,.87);
}
</style>

View File

@ -16,6 +16,11 @@
v-model="preventSuspend"
></v-checkbox>
<v-checkbox
:label="$t('settings.skipOnboarding')"
v-model="skipOnboarding"
></v-checkbox>
<v-btn role="button" @click="submit">{{ $t('settings.save') }}</v-btn>
</form>
</v-flex>
@ -32,12 +37,14 @@ export default {
i18nItems: Object.keys(messages),
locale: this.$store.state.settings.locale,
preventSuspend: this.$store.state.settings.preventSuspend,
skipOnboarding: this.$store.state.settings.skipOnboarding,
};
},
methods: {
submit() {
this.$store.dispatch('setLocale', { locale: this.locale });
this.$store.dispatch('setSetting', { setting: 'preventSuspend', value: this.preventSuspend });
this.$store.dispatch('setSetting', { setting: 'skipOnboarding', value: this.skipOnboarding });
},
},
};