Skip to content

Commit

Permalink
[ui] Add product tour for first time users
Browse files Browse the repository at this point in the history
Redirects to the '/product-tour' route the first
time the UI is loaded on a device. The tour uses
vue-shepherd to display a series of tooltips that describe
some of the website's features and can be skipped any time.
When the tour is finished or skipped it is saved as
viewed on a cookie to avoid loading it again.

Signed-off-by: Eva Millán <[email protected]>
  • Loading branch information
evamillan authored and sduenas committed May 29, 2023
1 parent 7dfc53f commit f29e840
Show file tree
Hide file tree
Showing 16 changed files with 686 additions and 5 deletions.
9 changes: 9 additions & 0 deletions releases/unreleased/new-user-onboarding.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: New user onboarding
category: added
author: Eva Millan <[email protected]>
issue: null
notes: >
Loading the user interface for the fist time
takes users to an optional tour explaining
SortingHat's most relevant features.
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"vue": "^2.7.14",
"vue-apollo": "^3.1.0",
"vue-router": "^3.4.9",
"vue-shepherd": "0.3.0",
"vuetify": "^2.6.10",
"vuex": "^3.1.3",
"yargs": "^17.0.1"
Expand Down
Binary file added ui/public/images/affiliate.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui/public/images/lock.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui/public/images/merge.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui/public/images/workspace.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion ui/src/components/IndividualsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
:items-per-page="itemsPerPage"
:loading="loading"
>
<template v-slot:item="{ item, expand, isExpanded }">
<template v-slot:item="{ item, expand, isExpanded, index }">
<individual-entry
draggable
:name="item.name"
Expand Down Expand Up @@ -144,6 +144,7 @@
@lock="handleLock(item.uuid, $event)"
@enroll="confirmEnroll(item, $event)"
@openMatchesModal="openMatchesModal(item.uuid)"
:ref="`indv_entry_${index}`"
/>
</template>
<template v-if="isExpandable" v-slot:expanded-item="{ item }">
Expand Down
3 changes: 2 additions & 1 deletion ui/src/components/OrganizationsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
:page.sync="page"
:loading="loading"
>
<template v-slot:item="{ item, expand, isExpanded }">
<template v-slot:item="{ item, expand, isExpanded, index }">
<organization-entry
:name="item.name"
:enrollments="getEnrolledIndividuals(item.enrollments)"
Expand All @@ -79,6 +79,7 @@
@delete="confirmDelete(item.name)"
@getEnrollments="$emit('getEnrollments', { enrollment: item.name })"
@addTeam="createTeam(item.name, $event)"
:ref="`org_entry_${index}`"
/>
</template>
<template v-slot:expanded-item="{ item }">
Expand Down
2 changes: 2 additions & 0 deletions ui/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Cookies from "js-cookie";
import { ApolloLink } from "apollo-link";
import Logger from "./plugins/logger";
import GetErrorMessage from "./plugins/errors";
import VueShepherd from "vue-shepherd";

const API_URL = process.env.VUE_APP_API_URL || `${process.env.BASE_URL}api/`;

Expand Down Expand Up @@ -54,6 +55,7 @@ fetch(API_URL, { credentials: "include" }).then(() => {
Vue.use(VueRouter);
Vue.use(Logger);
Vue.use(GetErrorMessage);
Vue.use(VueShepherd);

const apolloProvider = new VueApollo({
defaultClient: apolloClient,
Expand Down
16 changes: 15 additions & 1 deletion ui/src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const routes = [
path: "/",
name: "Dashboard",
component: () => import("../views/Dashboard"),
meta: { requiresAuth: true, title: "Sorting Hat" },
meta: { requiresAuth: true, showTour: true, title: "Sorting Hat" },
},
{
path: "/individual/:mk",
Expand Down Expand Up @@ -50,6 +50,12 @@ const routes = [
component: () => import("../views/Organization"),
meta: { requiresAuth: true, title: "Sorting Hat" },
},
{
path: "/product-tour",
name: "ProductTour",
component: () => import("../views/ProductTour"),
meta: { requiresAuth: true, title: "Sorting Hat" },
},
];

const router = new Router({
Expand All @@ -60,11 +66,19 @@ const router = new Router({

router.beforeEach((to, from, next) => {
const isAuthenticated = store.getters.isAuthenticated;
const showTour =
to.matched.some((record) => record.meta.showTour) &&
store.getters.shouldShowTour;

if (to.matched.some((record) => record.meta.requiresAuth)) {
if (!isAuthenticated) {
next({
path: "/login",
});
} else if (showTour) {
next({
path: "/product-tour",
});
} else {
next();
}
Expand Down
12 changes: 12 additions & 0 deletions ui/src/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default new Vuex.Store({
token: Cookies.get("sh_authtoken"),
user: Cookies.get("sh_user"),
workspace: JSON.parse(localStorage.getItem("sh_workspace")),
tourViewed: Cookies.get("sh_tour:viewed"),
},
mutations: {
setToken(state, token) {
Expand All @@ -21,11 +22,15 @@ export default new Vuex.Store({
setWorkspace(state, workspaceData) {
state.workspace = workspaceData;
},
setTourViewed(state, value) {
state.tourViewed = value;
},
},
getters: {
isAuthenticated: (state) => !!state.token,
user: (state) => state.user,
workspace: (state) => state.workspace,
shouldShowTour: (state) => !state.tourViewed,
},
actions: {
async login({ commit }, authDetails) {
Expand Down Expand Up @@ -53,6 +58,13 @@ export default new Vuex.Store({
localStorage.setItem("sh_workspace", JSON.stringify([]));
commit("setWorkspace", []);
},
setTourViewed({ commit }, value) {
Cookies.set("sh_tour:viewed", value, {
expires: 400,
sameSite: "strict",
});
commit("setTourViewed", value);
},
},
modules: {},
});
72 changes: 72 additions & 0 deletions ui/src/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,75 @@ button.v-pagination__item {
.v-snack__content::first-letter {
text-transform: capitalize;
}

.shepherd-modal-overlay-container.shepherd-modal-is-visible {
opacity: 0.46 !important;
}

.shepherd-theme-custom.shepherd-element {
box-shadow: 0px 11px 15px -7px rgba(0, 0, 0, 0.2),
0px 24px 38px 3px rgba(0, 0, 0, 0.14),
0px 9px 46px 8px rgba(0, 0, 0, 0.12);
font-family: "Roboto", sans-serif;
font-size: 0.875rem;
color: rgba(0, 0, 0, 0.87);
border-radius: 4px;
max-width: 430px;

.shepherd-arrow::before {
border: thin solid $border-color;
z-index: -1;
}

.shepherd-title {
font-size: 1rem;
font-weight: 500;
}

.shepherd-footer {
justify-content: space-between;
align-items: center;
padding: 14px;
background-color: #FFFFFF;

.shepherd-button {
font-family: 'Roboto', sans-serif;
font-size: 0.875rem;
font-weight: 500;
background: #003756;
color: #FFFFFF;
}

.shepherd-button-secondary {
background: transparent;
color: $selected-color;

&:not(:disabled):hover {
background: transparent;
color: $selected-color;
text-decoration: underline;
}
}

span {
flex-grow: 1;
font-size: 0.875rem;
color: rgba(0, 0, 0, 0.6);
}
}

.shepherd-text {
background-color: #FFFFFF;
font-family: 'Roboto', sans-serif;
padding: 14px;

p + p {
margin-top: 16px;
}
}

.shepherd-content .shepherd-header {
border-bottom: thin solid $border-color;
background: #FFFFFF;
}
}
Loading

0 comments on commit f29e840

Please sign in to comment.