Compare commits

...

22 Commits

Author SHA1 Message Date
5876e0c9df Merge branch 'release/0.1.0' 2021-03-26 18:00:50 +01:00
ace5c958f1 version bump to 0.1.0 2021-03-26 18:00:31 +01:00
aa287cf452 add required confirmation of data on registration 2021-03-26 17:47:50 +01:00
aca2dff129 drop profile data editing
close #20
2021-03-26 16:48:34 +01:00
d41824b735 Merge pull request 'feature/15_company_registration' (#16) from feature/15_company_registration into dev
Reviewed-on: #16
close #15
2021-03-25 19:01:59 +00:00
7904151a52 enable org registration
ref #15
2021-03-25 19:51:44 +01:00
a39cf75c7f move profile to path param
ref #15
2021-03-25 19:49:35 +01:00
79d8545ed6 working path params for org registration
ref #15
2021-03-25 19:36:31 +01:00
18d1ddacf7 basic /register/:token route
ref #15
2021-03-25 19:29:37 +01:00
36baf174a5 dependency bump 2021-03-24 17:38:51 +01:00
857b5c3d89 Merge pull request 'i18n support for TOS checkbox' (#14) from feature/4-component-interpolation into dev
Reviewed-on: #14
close #4
2021-03-09 16:56:34 +00:00
9da54d15bf Merge branch 'dev' into feature/4-component-interpolation 2021-03-09 16:53:45 +00:00
9e5252181e Merge pull request 'feature/11-user-scans' (#13) from feature/11-user-scans into dev
Reviewed-on: #13
close #11
2021-03-09 16:51:53 +00:00
45c0bdd5cf Merge branch 'dev' into feature/11-user-scans
# Conflicts:
#	src/components/Profile.vue
2021-03-09 17:45:19 +01:00
d405557aaf i18n support for TOS checkbox
ref #4
2021-03-08 22:18:19 +01:00
ff1f43e235 format lap times + only display valid scans
ref #11
2021-03-07 14:22:03 +01:00
c8d462100a use api response object properly
ref #11
2021-03-07 14:08:06 +01:00
69db350461 🌎 added missing translations
ref #11
2021-03-07 14:06:11 +01:00
cb3dc13c8a center empty state image
ref #11
2021-03-07 14:05:28 +01:00
4fb9aee255 re-order language files 😬
ref #11
2021-03-07 14:04:56 +01:00
49c3507a71 added empty state for scans
ref #11
2021-03-07 14:04:40 +01:00
14369480ea basic lap/ scans ui
ref #11
2021-03-07 14:04:29 +01:00
10 changed files with 123 additions and 55 deletions

5
CHANGELOG.md Normal file
View File

@@ -0,0 +1,5 @@
### Changelog
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### 0.0.1

View File

@@ -1,6 +1,6 @@
{ {
"name": "@odit/lfk-selfservice", "name": "@odit/lfk-selfservice",
"version": "0.0.1", "version": "0.1.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
@@ -20,13 +20,13 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^1.1.5", "@vitejs/plugin-vue": "^1.1.5",
"@vue/compiler-sfc": "^3.0.6", "@vue/compiler-sfc": "^3.0.7",
"autoprefixer": "^10.2.4", "autoprefixer": "^10.2.5",
"postcss": "^8.2.6", "postcss": "^8.2.8",
"release-it": "^14.4.1", "release-it": "^14.5.0",
"tailwindcss": "^2.0.3", "tailwindcss": "^2.0.4",
"vite": "^2.0.3", "vite": "^2.1.2",
"vite-plugin-windicss": "^0.5.4" "vite-plugin-windicss": "^0.9.11"
}, },
"release-it": { "release-it": {
"git": { "git": {

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@@ -97,11 +97,8 @@ import Toastify from "toastify-js";
let mail = ref(""); let mail = ref("");
let loading = ref(false); let loading = ref(false);
function login() { function login() {
console.log("ihi");
console.log(mail.value);
loading.value = true; loading.value = true;
axios.get("").then((res) => { axios.get("").then((res) => {
console.log(res.data);
loading.value = false; loading.value = false;
Toastify({ Toastify({
text: "This is a toast", text: "This is a toast",

View File

@@ -97,11 +97,8 @@ import Toastify from "toastify-js";
let mail = ref(""); let mail = ref("");
let loading = ref(false); let loading = ref(false);
function login() { function login() {
console.log("ihi");
console.log(mail.value);
loading.value = true; loading.value = true;
axios.get("").then((res) => { axios.get("").then((res) => {
console.log(res.data);
loading.value = false; loading.value = false;
Toastify({ Toastify({
text: "This is a toast", text: "This is a toast",

View File

@@ -11,7 +11,7 @@
></p> ></p>
<p class="text-md whitespace-nowrap">{{ state.group }}</p> <p class="text-md whitespace-nowrap">{{ state.group }}</p>
</div> </div>
<div class="inline-flex md:ml-auto md:mr-0 mx-auto items-center space-x-4"> <div class="inline-flex md:ml-auto md:mr-0 mx-auto items-center">
<button <button
type="button" type="button"
class="focus:border-black focus:ring-2 focus:ring-black text-white text-sm py-2.5 px-5 rounded-md bg-blue-500 hover:bg-blue-600 hover:shadow-lg" class="focus:border-black focus:ring-2 focus:ring-black text-white text-sm py-2.5 px-5 rounded-md bg-blue-500 hover:bg-blue-600 hover:shadow-lg"
@@ -35,10 +35,6 @@
</svg> </svg>
{{ $t('download_certificate') }} {{ $t('download_certificate') }}
</button> </button>
<button
type="button"
class="inline-flex focus:border-black focus:ring-2 focus:ring-black text-white text-sm py-2.5 px-5 rounded-md bg-green-600 hover:bg-green-800 hover:shadow-lg opacity-50"
>{{ $t('save_changes') }}</button>
</div> </div>
</div> </div>
</section> </section>
@@ -79,35 +75,35 @@
<div class="form-element"> <div class="form-element">
<div class="text-lg">{{ $t('vorname') }}</div> <div class="text-lg">{{ $t('vorname') }}</div>
<p <p
class="w-full dark:bg-gray-800 rounded text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out" class="h-10 w-full dark:bg-gray-800 rounded text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
v-text="state.firstname" v-text="state.firstname"
/> />
</div> </div>
<div class="form-element"> <div class="form-element">
<div class="text-lg">{{ $t('mittelname') }}</div> <div class="text-lg">{{ $t('mittelname') }}</div>
<p <p
class="w-full dark:bg-gray-800 rounded text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out" class="h-10 w-full dark:bg-gray-800 rounded text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
v-text="state.middlename" v-text="state.middlename"
/> />
</div> </div>
<div class="form-element"> <div class="form-element">
<div class="text-lg">{{ $t('nachname') }}</div> <div class="text-lg">{{ $t('nachname') }}</div>
<p <p
class="w-full dark:bg-gray-800 rounded text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out" class="h-10 w-full dark:bg-gray-800 rounded text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
v-text="state.lastname" v-text="state.lastname"
/> />
</div> </div>
<div class="form-element"> <div class="form-element">
<div class="text-lg">{{ $t('e_mail_adress') }}</div> <div class="text-lg">{{ $t('e_mail_adress') }}</div>
<p <p
class="w-full dark:bg-gray-800 rounded text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out" class="h-10 w-full dark:bg-gray-800 rounded text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
v-text="state.email" v-text="state.email"
/> />
</div> </div>
<div class="form-element"> <div class="form-element">
<div class="text-lg">{{ $t('phone_number') }}</div> <div class="text-lg">{{ $t('phone_number') }}</div>
<p <p
class="w-full dark:bg-gray-800 rounded text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out" class="h-10 w-full dark:bg-gray-800 rounded text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
v-text="state.phone" v-text="state.phone"
/> />
</div> </div>
@@ -121,7 +117,10 @@
<section class="text-gray-400 dark:bg-gray-900 body-font"> <section class="text-gray-400 dark:bg-gray-900 body-font">
<div class="container mx-auto"> <div class="container mx-auto">
<div class="lg:w-2/3 w-full mx-auto overflow-auto"> <div class="lg:w-2/3 w-full mx-auto overflow-auto">
<table class="table-auto w-full text-left whitespace-no-wrap"> <table
v-if="state.scans.length > 0"
class="table-auto w-full text-left whitespace-no-wrap"
>
<thead <thead
class="text-black bg-gray-300 dark:text-white text-sm dark:bg-gray-800" class="text-black bg-gray-300 dark:text-white text-sm dark:bg-gray-800"
> >
@@ -135,24 +134,22 @@
</tr> </tr>
</thead> </thead>
<tbody class="text-gray-900 dark:text-gray-50"> <tbody class="text-gray-900 dark:text-gray-50">
<tr class="border-t-2 border-gray-800"> <tr v-for="s in state.scans" :key="s.id">
<td class="px-4 py-3">400m</td> <td class="px-4 py-3">
<td class="px-4 py-3">0min 57s</td> <span v-text="s.distance"></span>m
</tr> </td>
<tr class="border-t-2 border-gray-800"> <td class="px-4 py-3" v-text="s.lapTime"></td>
<td class="px-4 py-3">400m</td>
<td class="px-4 py-3">1min 15s</td>
</tr>
<tr class="border-t-2 border-gray-800">
<td class="px-4 py-3">1km</td>
<td class="px-4 py-3">2min 50s</td>
</tr>
<tr class="border-t-2 border-gray-800">
<td class="px-4 py-3">1km</td>
<td class="px-4 py-3">3min 00s</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div v-else class="text-center font-bold text-black dark:text-white text-2xl">
<img
src="../assets/empty_laps.svg"
style="height:25rem; margin:0 auto;"
:alt="[[$t('no_laps_scans_were_recorded_yet')]]"
/>
{{ $t('no_laps_scans_were_recorded_yet') }}
</div>
</div> </div>
</div> </div>
</section> </section>
@@ -181,12 +178,16 @@ const state = reactive({
firstname: "", firstname: "",
middlename: "", middlename: "",
lastname: "", lastname: "",
scans: [],
group: "", group: "",
activetab: "profile", activetab: "profile",
}) })
const toast = useToast(); const toast = useToast();
const token = location.hash.substr(1).split('&')[0].split('=')[1]; const props = defineProps({
axios.get(`${config.baseurl}api/runners/me/${token}`) token: String
})
const accesstoken = atob(props.token);
axios.get(`${config.baseurl}api/runners/me/${accesstoken}`)
.then(({ data }) => { .then(({ data }) => {
state.phone = data.phone; state.phone = data.phone;
state.email = data.email; state.email = data.email;
@@ -197,4 +198,15 @@ axios.get(`${config.baseurl}api/runners/me/${token}`)
}).catch((error) => { }).catch((error) => {
toast.error("An error occured while loading your profile data"); toast.error("An error occured while loading your profile data");
}) })
axios.get(`${config.baseurl}api/runners/me/${accesstoken}/scans`)
.then(({ data }) => {
data.map(function(s) {
s.lapTime = Math.floor(s.lapTime / 60) + 'min ' + (Math.floor(s.lapTime % 60) + "").padStart(2, "0") + "s"
return s;
})
data.filter(s => s.valid === true);
state.scans = data;
}).catch((error) => {
toast.error("An error occured while loading your profile data");
})
</script> </script>

View File

@@ -6,6 +6,11 @@
class="sm:text-3xl text-2xl font-medium title-font mb-4 text-center" class="sm:text-3xl text-2xl font-medium title-font mb-4 text-center"
>Lauf für Kaya! - {{ $t('registrieren') }}</h1> >Lauf für Kaya! - {{ $t('registrieren') }}</h1>
<p class="mx-auto leading-relaxed text-base text-center">{{ $t('register.register_now') }}</p> <p class="mx-auto leading-relaxed text-base text-center">{{ $t('register.register_now') }}</p>
<p
v-if="state.org_name !== ''"
class="mx-auto leading-relaxed text-base text-center"
>Organization: {{ state.org_name }}</p>
<p v-else class="mx-auto leading-relaxed text-base text-center">Bürgerlauf</p>
<div class="mt-4"> <div class="mt-4">
<label for="first_name" class="block font-medium"> <label for="first_name" class="block font-medium">
{{ $t('vorname') }} {{ $t('vorname') }}
@@ -177,8 +182,31 @@
</div> </div>
<div class="ml-3 text-sm"> <div class="ml-3 text-sm">
<label for="agb_accepted" class="font-medium text-gray-400 select-none"> <label for="agb_accepted" class="font-medium text-gray-400 select-none">
Ich habe die {{ $t('i_accept', { tos: $t('tos') }) }}
<a target="_blank" rel="noreferrer,noopener" href class="underline">AGBs</a> gelesen und akzeptiert. <a
target="_blank"
rel="noreferrer,noopener"
href
class="underline"
>{{ $t('tos') }}</a>
{{ $t('i_accept_end') }}
<span class="font-bold">*</span>
</label>
</div>
</div>
<div class="flex items-start mt-6">
<div class="flex items-center h-5">
<input
v-model="data_confirmed"
id="data_confirmed"
name="data_confirmed"
type="checkbox"
class="h-4 w-4 text-indigo-600 border-gray-300 rounded"
/>
</div>
<div class="ml-3 text-sm">
<label for="data_confirmed" class="font-medium text-gray-400 select-none">
{{ $t('confirm_personal_data') }}
<span class="font-bold">*</span> <span class="font-bold">*</span>
</label> </label>
</div> </div>
@@ -213,19 +241,35 @@
</template> </template>
<script setup> <script setup>
import { computed, ref, reactive, watch } from "vue"; import { computed, ref, reactive, defineProps } from "vue";
import axios from "redaxios"; import axios from "redaxios";
import isEmail from 'validator/es/lib/isEmail'; import isEmail from 'validator/es/lib/isEmail';
import isMobilePhone from 'validator/es/lib/isMobilePhone'; import isMobilePhone from 'validator/es/lib/isMobilePhone';
import isPostalCode from 'validator/es/lib/isPostalCode'; import isPostalCode from 'validator/es/lib/isPostalCode';
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
const props = defineProps({
token: String
})
if (props.token) {
props.token = atob(props.token);
axios.get(`${config.baseurl}api/organizations/selfservice/${props.token}`)
.then(({ data }) => {
state.org_name = data.name;
})
.catch((error) => {
console.log(error);
});
}
let userdetails = ref({ firstname: "", lastname: "", middlename: "", mail: "", phone: "", address: { street: "", address2: "", city: "", zipcode: "" } }); let userdetails = ref({ firstname: "", lastname: "", middlename: "", mail: "", phone: "", address: { street: "", address2: "", city: "", zipcode: "" } });
let provide_address = ref(false); let provide_address = ref(false);
let agb_accepted = ref(false); let agb_accepted = ref(false);
let data_confirmed = ref(false);
// //
const state = reactive({ const state = reactive({
submit_enabled: computed(() => agb_accepted.value === true && (isMobilePhone(userdetails.value.phone) || !userdetails.value.phone.trim()) && isEmail(userdetails.value.mail) org_name: "",
submit_enabled: computed(() => agb_accepted.value === true && data_confirmed.value === true && (isMobilePhone(userdetails.value.phone) || !userdetails.value.phone.trim()) && isEmail(userdetails.value.mail)
&& userdetails.value.firstname && userdetails.value.firstname
&& userdetails.value.lastname && (provide_address.value === false || provide_address.value === true && (userdetails.value.address.street.trim() && userdetails.value.address.city.trim() && isPostalCode(userdetails.value.address.zipcode, "DE")))) && userdetails.value.lastname && (provide_address.value === false || provide_address.value === true && (userdetails.value.address.street.trim() && userdetails.value.address.city.trim() && isPostalCode(userdetails.value.address.zipcode, "DE"))))
}) })
@@ -254,13 +298,14 @@ function login() {
} }
} }
toast("registration in progress..."); toast("registration in progress...");
axios.post(`${config.baseurl}api/runners/register`, postdata) let url = `${config.baseurl}api/runners/register`;
.then((response) => { if (props.token) {
response = response.data; url = `${config.baseurl}api/runners/register/${props.token}`
const token = response.token; }
const userid = JSON.parse(atob(token.split(".")[1])).id; axios.post(url, postdata)
console.log({ token }); .then(({ data }) => {
console.log({ userid }); const token = btoa(data.token);
location.replace("../profile/" + token)
// //
toast.success("You have been registered!"); toast.success("You have been registered!");
}) })

View File

@@ -2,10 +2,13 @@
"already_have_an_account": "Sie haben bereits einen Account?", "already_have_an_account": "Sie haben bereits einen Account?",
"apartment_suite_etc": "Addresszeile 2", "apartment_suite_etc": "Addresszeile 2",
"configuration_error": "Konfigurationsfehler", "configuration_error": "Konfigurationsfehler",
"confirm_personal_data": "Hiermit bestätige ich die Vollständigkeit und Richtigkeit der oben genannten Angaben",
"distance": "Distanz", "distance": "Distanz",
"download_certificate": "Urkunde herunterladen", "download_certificate": "Urkunde herunterladen",
"e_mail_adress": "E-Mail Adresse", "e_mail_adress": "E-Mail Adresse",
"go_to_login": "Zum Login", "go_to_login": "Zum Login",
"i_accept": "Ich habe die ",
"i_accept_end": "gelesen und akzeptiert.",
"if_you_are_the_system_administrator_please_refer_to_the_official_product_documentation_readme_for_configuration_guidance": "Wenn Sie der Systemadministrator sind, finden Sie Konfigurationsanweisungen in der offiziellen Produktdokumentation / README.", "if_you_are_the_system_administrator_please_refer_to_the_official_product_documentation_readme_for_configuration_guidance": "Wenn Sie der Systemadministrator sind, finden Sie Konfigurationsanweisungen in der offiziellen Produktdokumentation / README.",
"imprint": "Impressum", "imprint": "Impressum",
"lap_time": "Rundenzeit", "lap_time": "Rundenzeit",
@@ -13,6 +16,7 @@
"main_page_text": "Hier können Sie sich für den Lauf Für Kaya! registrieren oder ihr Läuferprofil verwalten.", "main_page_text": "Hier können Sie sich für den Lauf Für Kaya! registrieren oder ihr Läuferprofil verwalten.",
"mittelname": "Mittelname", "mittelname": "Mittelname",
"nachname": "Nachname", "nachname": "Nachname",
"no_laps_scans_were_recorded_yet": "Es wurden noch keine Runden / Scans aufgezeichnet ...",
"ort": "Ort", "ort": "Ort",
"phone_number": "Telefonnummer", "phone_number": "Telefonnummer",
"please_provide_a_valid_zipcode": "Bitte geben Sie eine gültige Postleitzahl an...", "please_provide_a_valid_zipcode": "Bitte geben Sie eine gültige Postleitzahl an...",
@@ -31,6 +35,7 @@
"strasse": "Straße", "strasse": "Straße",
"the_system_is_not_properly_configured_please_contact_the_system_administrator_for_help": "Das System ist nicht richtig konfiguriert. Bitte wenden Sie sich an den Systemadministrator, um Hilfe zu erhalten.", "the_system_is_not_properly_configured_please_contact_the_system_administrator_for_help": "Das System ist nicht richtig konfiguriert. Bitte wenden Sie sich an den Systemadministrator, um Hilfe zu erhalten.",
"this_is_not_a_valid_international_phone_number": "Dies ist keine gültige internationale Telefonnummer", "this_is_not_a_valid_international_phone_number": "Dies ist keine gültige internationale Telefonnummer",
"tos": "AGBs",
"view_my_data": "Meine Läuferdaten einsehen", "view_my_data": "Meine Läuferdaten einsehen",
"vorname": "Vorname" "vorname": "Vorname"
} }

View File

@@ -2,10 +2,13 @@
"already_have_an_account": "Already have an account?", "already_have_an_account": "Already have an account?",
"apartment_suite_etc": "Apartment, suite, etc.", "apartment_suite_etc": "Apartment, suite, etc.",
"configuration_error": "Configuration error", "configuration_error": "Configuration error",
"confirm_personal_data": "I hereby confirm that the above information is complete and correct",
"distance": "Distance", "distance": "Distance",
"download_certificate": "Download certificate", "download_certificate": "Download certificate",
"e_mail_adress": "mail address", "e_mail_adress": "mail address",
"go_to_login": "Go To Login", "go_to_login": "Go To Login",
"i_accept": "I have read and accepted the ",
"i_accept_end": "",
"if_you_are_the_system_administrator_please_refer_to_the_official_product_documentation_readme_for_configuration_guidance": "If you are the system administrator, please refer to the official product documentation/ README for configuration guidance.", "if_you_are_the_system_administrator_please_refer_to_the_official_product_documentation_readme_for_configuration_guidance": "If you are the system administrator, please refer to the official product documentation/ README for configuration guidance.",
"imprint": "Imprint", "imprint": "Imprint",
"lap_time": "Lap time", "lap_time": "Lap time",
@@ -13,6 +16,7 @@
"main_page_text": "Here you can register for the Lauf Für Kaya! or manage your runner profile.", "main_page_text": "Here you can register for the Lauf Für Kaya! or manage your runner profile.",
"mittelname": "Middlename", "mittelname": "Middlename",
"nachname": "Lastname", "nachname": "Lastname",
"no_laps_scans_were_recorded_yet": "No laps/ scans were recorded yet...",
"ort": "City", "ort": "City",
"phone_number": "Phone Number", "phone_number": "Phone Number",
"please_provide_a_valid_zipcode": "Please provide a valid zipcode...", "please_provide_a_valid_zipcode": "Please provide a valid zipcode...",
@@ -31,6 +35,7 @@
"strasse": "Street/ Block", "strasse": "Street/ Block",
"the_system_is_not_properly_configured_please_contact_the_system_administrator_for_help": "The system is not properly configured. Please contact the system administrator for help.", "the_system_is_not_properly_configured_please_contact_the_system_administrator_for_help": "The system is not properly configured. Please contact the system administrator for help.",
"this_is_not_a_valid_international_phone_number": "This is not a valid international phone number", "this_is_not_a_valid_international_phone_number": "This is not a valid international phone number",
"tos": "Terms of Service",
"view_my_data": "View my data", "view_my_data": "View my data",
"vorname": "Firstname" "vorname": "Firstname"
} }

View File

@@ -25,7 +25,7 @@ const EnvError = import('./components/EnvError.vue');
const Home = import('./components/Home.vue'); const Home = import('./components/Home.vue');
const Imprint = import('./components/Imprint.vue'); const Imprint = import('./components/Imprint.vue');
const Privacy = import('./components/Privacy.vue'); const Privacy = import('./components/Privacy.vue');
const Register = import('./components/Register.vue'); const Register = () => import('./components/Register.vue');
const Profile = () => import('./components/Profile.vue'); const Profile = () => import('./components/Profile.vue');
// //
let routes = [ { path: '/:pathMatch(.*)*', component: EnvError } ]; let routes = [ { path: '/:pathMatch(.*)*', component: EnvError } ];
@@ -36,7 +36,8 @@ if (typeof config !== 'undefined') {
{ path: '/imprint', component: Imprint }, { path: '/imprint', component: Imprint },
{ path: '/privacy', component: Privacy }, { path: '/privacy', component: Privacy },
{ path: '/register', component: Register }, { path: '/register', component: Register },
{ path: '/profile', component: Profile } { path: '/register/:token', component: Register, props: true },
{ path: '/profile/:token', component: Profile, props: true }
]; ];
} }
} }