Merge branch 'release/0.1.0'

This commit is contained in:
Philipp Dormann 2021-03-26 18:00:50 +01:00
commit 5876e0c9df
21 changed files with 498 additions and 241 deletions

1
.gitignore vendored
View File

@ -183,3 +183,4 @@ test/
/package-lock.json /package-lock.json
/yarn.lock /yarn.lock
/public/env.js

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,3 +1,22 @@
# @lfk/selfservice # @lfk/selfservice
runner selfservice portal runner selfservice portal
## ⚡ Development
### Requirements
- Node.js v14.15.0 or newer
- yarn package manager >= v1.22.10 < 2
### Recommended Extensions
- will be automatically recommended via `./vscode/extensions.json`
- we also provide a config for i18n-ally in the `./vscode/` folder
### Fastest Dev Environment
- You can install the [Remote - Containers](https://github.com/Microsoft/vscode-remote-release) extension and use all recommended extensions and editor settings via the provided `./devcontainer/` config
## 🔨 environment config
- copy the `/public/env.sample.js` file to `/public/env.js`
- set the required environment variables
- `documentserver_key`: url to the [document server](https://git.odit.services/lfk/document-server) instance
- `baseurl`: url to the main instance
- see [@lfk/deployment](https://git.odit.services/lfk/deployment) for a complete deployment guide

View File

@ -10,6 +10,7 @@
<body class="dark:bg-gray-900 text-black dark:text-white p-0"> <body class="dark:bg-gray-900 text-black dark:text-white p-0">
<div id="app"></div> <div id="app"></div>
<script src="/env.js"></script>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>

View File

@ -1,6 +1,6 @@
{ {
"name": "@odit/lfk-selfservice", "name": "@odit/lfk-selfservice",
"version": "0.0.0", "version": "0.1.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
@ -8,6 +8,7 @@
}, },
"dependencies": { "dependencies": {
"got": "^11.8.2", "got": "^11.8.2",
"marked": "^2.0.1",
"redaxios": "^0.4.1", "redaxios": "^0.4.1",
"toastify-js": "^1.9.3", "toastify-js": "^1.9.3",
"validator": "^13.5.2", "validator": "^13.5.2",
@ -19,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": {

10
public/env.sample.js Normal file
View File

@ -0,0 +1,10 @@
const config = {
// required
documentserver_key: '',
// required
baseurl: '',
// optional, will fallback to /imprint
url_imprint: '',
// optional, will fallback to /privacy
url_privacy: ''
};

1
public/imprint_en.md Normal file
View File

@ -0,0 +1 @@
TODO:

1
public/privacy_en.md Normal file
View File

@ -0,0 +1 @@
TODO:

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@ -0,0 +1,29 @@
<template>
<section class="container px-4 py-32 mx-auto">
<div class="w-full mx-auto lg:w-1/3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-20 feather feather-alert-triangle"
viewBox="0 0 24 24"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0zM12 9v4M12 17h.01"
/>
</svg>
<p
class="mt-5 mb-3 text-xl font-bold text-black dark:text-gray-50 md:text-2xl"
>{{ $t('configuration_error') }}</p>
<p class="mb-3 text-base font-medium text-gray-700 dark:text-gray-400">
{{ $t('the_system_is_not_properly_configured_please_contact_the_system_administrator_for_help') }}
<br />
<br />
{{ $t('if_you_are_the_system_administrator_please_refer_to_the_official_product_documentation_readme_for_configuration_guidance') }}
</p>
</div>
</section>
</template>

View File

@ -14,16 +14,26 @@
<a <a
target="_blank" target="_blank"
rel="noopener,noreferrer" rel="noopener,noreferrer"
href="/impressum/" :href="[[imprint_url]]"
class="ml-3 text-gray-400 underline" class="ml-3 text-gray-400 underline"
>Impressum</a> >Impressum</a>
<a <a
target="_blank" target="_blank"
rel="noopener,noreferrer" rel="noopener,noreferrer"
href="/datenschutz/" :href="[[privacy_url]]"
class="ml-3 text-gray-400 underline" class="ml-3 text-gray-400 underline"
>Datenschutzerklärung</a> >Datenschutzerklärung</a>
</span> </span>
</div> </div>
</footer> </footer>
</template> </template>
<script>
export default {
data() {
return {
imprint_url: config.url_imprint || "/imprint/"
, privacy_url: config.url_privacy || "/privacy/"
}
},
}
</script>

View File

@ -0,0 +1,38 @@
<template>
<section class="container px-4 py-24 mx-auto">
<div class="simplecontent">
<div class="mb-24 text-left md:text-center">
<h1
class="mb-4 text-4xl font-bold leading-tight text-gray-900 dark:text-gray-50 md:text-5xl"
>{{$t('imprint')}}</h1>
</div>
<div class="mx-auto prose" v-html="content"></div>
</div>
</section>
</template>
<style src="./simple.css">
</style>
<script>
import marked from "marked";
export default {
data() {
return {
content: ""
}
},
async beforeMount() {
const browserlocale = ((navigator.languages && navigator.languages[0]) || '').substr(0, 2);
let md = "";
try {
md = await fetch(`/imprint_${browserlocale}.md`);
} catch (error) {
try {
md = await fetch(`/imprint_en.md`);
} catch (error) {
md = "Error loading Imprint";
}
}
this.content = marked(await md.text());
},
}
</script>

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

@ -0,0 +1,38 @@
<template>
<section class="container px-4 py-24 mx-auto">
<div class="simplecontent">
<div class="mb-24 text-left md:text-center">
<h1
class="mb-4 text-4xl font-bold leading-tight text-gray-900 dark:text-gray-50 md:text-5xl"
>{{ $t('privacy_policy') }}</h1>
</div>
<div class="mx-auto prose" v-html="content"></div>
</div>
</section>
</template>
<style src="./simple.css">
</style>
<script>
import marked from "marked";
export default {
data() {
return {
content: ""
}
},
async beforeMount() {
const browserlocale = ((navigator.languages && navigator.languages[0]) || '').substr(0, 2);
let md = "";
try {
md = await fetch(`/privacy_${browserlocale}.md`);
} catch (error) {
try {
md = await fetch(`/privacy_en.md`);
} catch (error) {
md = "Error loading Privacy Policy";
}
}
this.content = marked(await md.text());
},
}
</script>

View File

@ -1,115 +1,110 @@
<template> <template>
<div class="min-h-screen w-full p-4"> <div class="min-h-screen w-full p-4">
<!-- <div class="section-title w-full mb-6 pt-3"> <section class="text-white body-font">
<div class="flex flex-row items-center justify-between mb-4"> <div class="container mx-auto flex items-center md:flex-row flex-col">
<div class="flex flex-col"> <div
<div class="text-xs uppercase font-light text-gray-500">Pages</div> class="flex flex-col md:pr-10 md:mb-0 mb-6 pr-0 w-full md:w-auto md:text-left text-center text-black dark:text-gray-200"
<div class="text-xl font-bold">User profile</div> >
<p
class="text-3xl font-bold whitespace-nowrap"
v-text="(state.firstname || '') + ' ' + (state.middlename || '') + ' ' + (state.lastname || '')"
></p>
<p class="text-md whitespace-nowrap">{{ state.group }}</p>
</div>
<div class="inline-flex md:ml-auto md:mr-0 mx-auto items-center">
<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"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-download"
style="display: inline;height: 1rem;vertical-align: sub;"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
{{ $t('download_certificate') }}
</button>
</div> </div>
</div> </div>
</div> </section>
<div
class="w-full p-4 mb-4 rounded-lg bg-white border border-gray-100 dark:bg-gray-900 dark:border-gray-800"
>-->
<div class="flex flex-row items-center justify-start p-4">
<div class="py-2 px-2">
<p class="text-3xl font-bold whitespace-nowrap">Max Mustermann</p>
<p class="text-md text-gray-500 whitespace-nowrap">Musterfirma > PR</p>
</div>
<div class="ml-auto flex-shrink-0 space-x-2 hidden lg:flex">
<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"
>
<svg
style="display:inline;"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-download"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg> Urkunde herunterladen
</button>
<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-green-600 hover:bg-green-800 hover:shadow-lg opacity-50"
>Änderungen speichern</button>
</div>
</div>
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<div class="w-full p-4"> <div class="w-full p-4">
<div class="flex flex-wrap flex-col w-full tabs"> <div class="flex flex-wrap flex-col w-full tabs">
<div class="flex lg:flex-wrap flex-row lg:space-x-2"> <div class="flex lg:flex-wrap flex-row lg:space-x-2">
<div class="flex-none"> <div class="flex-none">
<button <button
class="tab tab-underline tab-active py-4 px-6 block border-b-2 font-medium border-blue-500" @click="() => { state.activetab = 'profile' }"
:class="{ 'tab-active border-b-2 font-medium border-blue-500': (state.activetab === 'profile') }"
class="tab tab-underline py-4 px-6 block"
type="button" type="button"
>Mein Profil</button> >{{ $t('profile') }}</button>
</div> </div>
<div class="flex-none"> <div class="flex-none">
<button class="tab tab-underline py-4 px-6 block" type="button">Meine Rundenzeiten</button> <button
@click="() => { state.activetab = 'laptimes' }"
:class="{ 'tab-active border-b-2 font-medium border-blue-500': (state.activetab === 'laptimes') }"
class="tab tab-underline py-4 px-6 block"
type="button"
>{{ $t('lap_times') }}</button>
</div> </div>
<div class="flex-none"> <div class="flex-none">
<button class="tab tab-underline py-4 px-6 block" type="button">Meine Spender</button> <button
@click="() => { state.activetab = 'sponsorings' }"
:class="{ 'tab-active border-b-2 font-medium border-blue-500': (state.activetab === 'sponsorings') }"
class="tab tab-underline py-4 px-6 block"
type="button"
>{{ $t('sponsoring') }}</button>
</div> </div>
</div> </div>
<div class="tab-content block"> <div v-if="(state.activetab === 'profile')" class="tab-content block">
<div class="py-4 w-full"> <div class="py-4 w-full">
<div class="flex flex-col"> <div class="flex flex-col">
<form class="form flex flex-wrap w-full"> <form class="form flex flex-wrap w-full">
<div class="w-full"> <div class="w-full">
<div class="form-element"> <div class="form-element">
<div class="form-label">First name</div> <div class="text-lg">{{ $t('vorname') }}</div>
<input <p
name="first-name" 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"
class="w-full dark:bg-gray-800 rounded border border-gray-700 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-900 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"
type="text"
placeholder="first name"
/> />
</div> </div>
<div class="form-element"> <div class="form-element">
<div class="form-label">middle name</div> <div class="text-lg">{{ $t('mittelname') }}</div>
<input <p
name="middle-name" 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"
class="w-full dark:bg-gray-800 rounded border border-gray-700 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-900 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"
type="text"
placeholder="middle name"
/> />
</div> </div>
<div class="form-element"> <div class="form-element">
<div class="form-label">Last name</div> <div class="text-lg">{{ $t('nachname') }}</div>
<input <p
name="last-name" 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"
type="text" v-text="state.lastname"
class="w-full dark:bg-gray-800 rounded border border-gray-700 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-900 text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
placeholder="last name"
/> />
</div> </div>
<div class="form-element"> <div class="form-element">
<div class="form-label">Email address</div> <div class="text-lg">{{ $t('e_mail_adress') }}</div>
<input <p
name="email" 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"
type="email" v-text="state.email"
class="w-full dark:bg-gray-800 rounded border border-gray-700 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-900 text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
placeholder="email address"
/> />
</div> </div>
<div class="form-element"> <div class="form-element">
<div class="form-label">phone number</div> <div class="text-lg">{{ $t('phone_number') }}</div>
<input <p
name="tel" 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"
type="tel" v-text="state.phone"
class="w-full dark:bg-gray-800 rounded border border-gray-700 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-900 text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
placeholder="phone number"
/> />
</div> </div>
</div> </div>
@ -117,134 +112,101 @@
</div> </div>
</div> </div>
</div> </div>
<div class="tab-content hidden"> <div v-if="(state.activetab === 'laptimes')" class="tab-content block">
<div class="py-4 w-full lg:w-1/2"> <div class="py-4 w-full">
<div class="flex flex-col"> <section class="text-gray-400 dark:bg-gray-900 body-font">
<form class="form flex flex-wrap w-full"> <div class="container mx-auto">
<div class="w-full"> <div class="lg:w-2/3 w-full mx-auto overflow-auto">
<div class="form-element"> <table
<div class="form-label">Current email</div> v-if="state.scans.length > 0"
<input class="table-auto w-full text-left whitespace-no-wrap"
name="email" >
type="email" <thead
class="w-full dark:bg-gray-800 rounded border border-gray-700 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-900 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="text-black bg-gray-300 dark:text-white text-sm dark:bg-gray-800"
placeholder="current email address" >
<tr>
<th
class="px-4 py-3 title-font tracking-wider font-medium"
>{{ $t('distance') }}</th>
<th
class="px-4 py-3 title-font tracking-wider font-medium"
>{{ $t('lap_time') }}</th>
</tr>
</thead>
<tbody class="text-gray-900 dark:text-gray-50">
<tr v-for="s in state.scans" :key="s.id">
<td class="px-4 py-3">
<span v-text="s.distance"></span>m
</td>
<td class="px-4 py-3" v-text="s.lapTime"></td>
</tr>
</tbody>
</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')]]"
/> />
</div> {{ $t('no_laps_scans_were_recorded_yet') }}
<div class="form-element">
<div class="form-label">New email</div>
<input
name="email"
type="email"
class="w-full dark:bg-gray-800 rounded border border-gray-700 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-900 text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
placeholder="new email address"
/>
</div>
<div class="form-element">
<div class="form-label">Daily updates</div>
<div class="flex items-center justify-start space-x-2">
<label class="flex items-center justify-start space-x-2">
<input
type="radio"
name="daily-updates"
class="form-radio h-4 w-4"
value="yes"
/>
<span class>Yes</span>
</label>
<label class="flex items-center justify-start space-x-2">
<input
type="radio"
name="daily-updates"
class="form-radio h-4 w-4"
value="no"
/>
<span class>No</span>
</label>
</div>
</div>
<div class="form-element">
<div class="form-label">Weekly updates</div>
<div class="flex items-center justify-start space-x-2">
<label class="flex items-center justify-start space-x-2">
<input
type="radio"
name="weekle-updates"
class="form-radio h-4 w-4"
value="yes"
/>
<span class>Yes</span>
</label>
<label class="flex items-center justify-start space-x-2">
<input
type="radio"
name="weekle-updates"
class="form-radio h-4 w-4"
value="no"
/>
<span class>No</span>
</label>
</div>
</div> </div>
</div> </div>
<input </div>
type="submit" </section>
class="btn btn-default bg-blue-500 hover:bg-blue-600 text-white btn-rounded"
/>
</form>
</div>
</div> </div>
</div> </div>
<div class="tab-content hidden"> <div v-if="(state.activetab === 'sponsorings')" class="tab-content block">
<div class="py-4 w-full lg:w-1/2"> <div class="py-4 w-full">coming soon...</div>
<div class="flex flex-col">
<form class="form flex flex-wrap w-full">
<div class="w-full">
<div class="form-element">
<div class="form-label">Current password</div>
<input
name="current-password"
type="password"
class="w-full dark:bg-gray-800 rounded border border-gray-700 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-900 text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
placeholder="Enter your current password"
/>
</div>
<div class="form-element">
<div class="form-label">New password</div>
<input
name="new-password"
type="password"
class="w-full dark:bg-gray-800 rounded border border-gray-700 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-900 text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
placeholder="Enter your new password"
/>
</div>
<div class="form-element">
<div class="form-label">Confirm new password</div>
<input
name="confirm-new-password"
type="password"
class="w-full dark:bg-gray-800 rounded border border-gray-700 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-900 text-base outline-none dark:text-gray-100 text-gray-600 py-1 px-3 leading-8 transition-colors duration-200 ease-in-out"
placeholder="Enter your new password confirmation"
/>
</div>
</div>
<input
type="submit"
class="btn btn-default bg-blue-500 hover:bg-blue-600 text-white btn-rounded"
/>
</form>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- </div> -->
</template> </template>
<script setup> <script setup>
import { ref } from "vue"; import { ref, reactive } from "vue";
import { useToast } from "vue-toastification";
import axios from "redaxios"; import axios from "redaxios";
import Toastify from "toastify-js"; // import isEmail from 'validator/es/lib/isEmail';
// import isMobilePhone from 'validator/es/lib/isMobilePhone';
// import isPostalCode from 'validator/es/lib/isPostalCode';
//
const state = reactive({
phone: "",
email: "",
firstname: "",
middlename: "",
lastname: "",
scans: [],
group: "",
activetab: "profile",
})
const toast = useToast();
const props = defineProps({
token: String
})
const accesstoken = atob(props.token);
axios.get(`${config.baseurl}api/runners/me/${accesstoken}`)
.then(({ data }) => {
state.phone = data.phone;
state.email = data.email;
state.firstname = data.firstname;
state.middlename = data.middlename;
state.lastname = data.lastname;
state.group = data.group;
}).catch((error) => {
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') }}
@ -64,10 +69,7 @@
/> />
<p v-if="!isEmail(userdetails.mail)" class="text-sm">{{ $t('please_provide_valid_mail') }}</p> <p v-if="!isEmail(userdetails.mail)" class="text-sm">{{ $t('please_provide_valid_mail') }}</p>
<!-- --> <!-- -->
<label <label for="phone" class="select-none block font-medium">{{ $t('phone_number') }}</label>
for="phone"
class="select-none block font-medium"
>{{ $t('phone_number') }}</label>
<input <input
v-model="userdetails.phone" v-model="userdetails.phone"
name="phone" name="phone"
@ -78,6 +80,10 @@
:class="{ 'border-red-500': (!isMobilePhone(userdetails.phone) && userdetails.phone.trim()), 'border-green-300': (isMobilePhone(userdetails.phone) && userdetails.phone.trim()) }" :class="{ 'border-red-500': (!isMobilePhone(userdetails.phone) && userdetails.phone.trim()), 'border-green-300': (isMobilePhone(userdetails.phone) && userdetails.phone.trim()) }"
class="dark:bg-gray-800 mt-1 block w-full shadow-sm sm:text-sm border-2 bg-gray-50 text-gray-500 rounded-md p-2" class="dark:bg-gray-800 mt-1 block w-full shadow-sm sm:text-sm border-2 bg-gray-50 text-gray-500 rounded-md p-2"
/> />
<p
v-if="(!isMobilePhone(userdetails.phone) && userdetails.phone.trim())"
class="text-sm"
>{{ $t('this_is_not_a_valid_international_phone_number') }}</p>
<!-- --> <!-- -->
<div class="grid grid-cols-6 mt-6"> <div class="grid grid-cols-6 mt-6">
<div class="col-span-6"></div> <div class="col-span-6"></div>
@ -94,43 +100,74 @@
<div class="ml-3 text-sm"> <div class="ml-3 text-sm">
<label <label
for="address_activated" for="address_activated"
class="font-medium text-gray-400" class="font-medium text-gray-400 select-none"
>{{ $t('provide_address') }}</label> >{{ $t('provide_address') }}</label>
</div> </div>
</div> </div>
<div v-if="provide_address === true" class="col-span-6"> <div v-if="provide_address === true" class="col-span-6">
<div class="col-span-6"> <div class="col-span-6">
<label for="street" class="block font-medium">{{ $t('strasse') }}</label> <label for="street" class="block font-medium">
{{ $t('strasse') }}
<span class="font-bold">*</span>
</label>
<input <input
v-model="userdetails.address.street"
type="text" type="text"
name="street" name="street"
:placeholder="[[$t('strasse')]]" :placeholder="[[$t('strasse')]]"
id="street" id="street"
autocomplete="street-address" autocomplete="street-address"
:class="{ 'border-red-500': (!userdetails.address.street.trim()), 'border-green-300': (userdetails.address.street.trim()) }"
class="dark:bg-gray-800 mt-1 block w-full shadow-sm sm:text-sm border-gray-300 border-2 bg-gray-50 text-gray-500 rounded-md p-2"
/>
</div>
<div class="col-span-6">
<label for="address2" class="block font-medium">{{ $t('apartment_suite_etc') }}</label>
<input
v-model="userdetails.address.address2"
type="text"
name="address2"
:placeholder="[[$t('apartment_suite_etc')]]"
id="address2"
autocomplete="street-address"
class="dark:bg-gray-800 mt-1 block w-full shadow-sm sm:text-sm border-gray-300 border-2 bg-gray-50 text-gray-500 rounded-md p-2" class="dark:bg-gray-800 mt-1 block w-full shadow-sm sm:text-sm border-gray-300 border-2 bg-gray-50 text-gray-500 rounded-md p-2"
/> />
</div> </div>
<div class="col-span-6 sm:col-span-6 lg:col-span-2"> <div class="col-span-6 sm:col-span-6 lg:col-span-2">
<label for="city" class="block font-medium">{{ $t('ort') }}</label> <label for="city" class="block font-medium">
{{ $t('ort') }}
<span class="font-bold">*</span>
</label>
<input <input
v-model="userdetails.address.city"
type="text" type="text"
name="city" name="city"
:placeholder="[[$t('ort')]]" :placeholder="[[$t('ort')]]"
id="city" id="city"
:class="{ 'border-red-500': (!userdetails.address.city.trim()), 'border-green-300': (userdetails.address.city.trim()) }"
class="dark:bg-gray-800 mt-1 block w-full shadow-sm sm:text-sm border-gray-300 border-2 bg-gray-50 text-gray-500 rounded-md p-2" class="dark:bg-gray-800 mt-1 block w-full shadow-sm sm:text-sm border-gray-300 border-2 bg-gray-50 text-gray-500 rounded-md p-2"
/> />
</div> </div>
<div class="col-span-6 sm:col-span-3 lg:col-span-2"> <div class="col-span-6 sm:col-span-3 lg:col-span-2">
<label for="postal_code" class="block font-medium">{{ $t('plz') }}</label> <label for="postal_code" class="block font-medium">
{{ $t('plz') }}
<span class="font-bold">*</span>
</label>
<input <input
v-model="userdetails.address.zipcode"
type="text" type="text"
name="postal_code" name="postal_code"
:placeholder="[[$t('plz')]]" :placeholder="[[$t('plz')]]"
id="postal_code" id="postal_code"
autocomplete="postal-code" autocomplete="postal-code"
:class="{ 'border-red-500': (!isPostalCode(userdetails.address.zipcode, 'DE')), 'border-green-300': (isPostalCode(userdetails.address.zipcode, 'DE')) }"
class="dark:bg-gray-800 mt-1 block w-full shadow-sm sm:text-sm border-gray-300 border-2 bg-gray-50 text-gray-500 rounded-md p-2" class="dark:bg-gray-800 mt-1 block w-full shadow-sm sm:text-sm border-gray-300 border-2 bg-gray-50 text-gray-500 rounded-md p-2"
/> />
</div> </div>
<p
v-if="!isPostalCode(userdetails.address.zipcode, 'DE')"
class="text-sm"
>{{ $t('please_provide_a_valid_zipcode') }}</p>
</div> </div>
</div> </div>
<div class="flex items-start mt-6"> <div class="flex items-start mt-6">
@ -145,16 +182,40 @@
</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>
</label> </label>
</div> </div>
</div> </div>
<div class="mt-6"> <div class="mt-6">
<button <button
@click="login" @click="login"
:disabled="(agb_accepted === false)" :disabled="(!state.submit_enabled)"
:class="{ 'opacity-50': (agb_accepted === false), 'cursor-not-allowed': (agb_accepted === false) }" :class="{ 'opacity-50': (!state.submit_enabled), 'cursor-not-allowed': (!state.submit_enabled) }"
class="text-white block w-full text-center py-2 px-3 border-2 border-gray-300 rounded-md p-1 bg-blue-800 font-medium hover:border-gray-400 focus:outline-none focus:border-gray-400 sm:text-sm" class="text-white block w-full text-center py-2 px-3 border-2 border-gray-300 rounded-md p-1 bg-blue-800 font-medium hover:border-gray-400 focus:outline-none focus:border-gray-400 sm:text-sm"
>{{ $t('registrieren') }}</button> >{{ $t('registrieren') }}</button>
</div> </div>
@ -180,16 +241,38 @@
</template> </template>
<script setup> <script setup>
import { ref } from "vue"; import { computed, ref, reactive, defineProps } from "vue";
import axios from "redaxios"; import axios from "redaxios";
import Toastify from "toastify-js";
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 { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
let userdetails = ref({ firstname: "", lastname: "", middlename: "", mail: "", phone: "" }); 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 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({
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.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"))))
})
const toast = useToast(); const toast = useToast();
function login() { function login() {
userdetails = userdetails.value; userdetails = userdetails.value;
@ -205,14 +288,24 @@ function login() {
if (isMobilePhone(userdetails.phone)) { if (isMobilePhone(userdetails.phone)) {
postdata.phone = userdetails.phone; postdata.phone = userdetails.phone;
} }
if (provide_address.value === true) {
postdata.address = {
address1: userdetails.address.street,
address2: userdetails.address.address2 || "",
city: userdetails.address.city,
postalcode: userdetails.address.zipcode,
country: "DE",
}
}
toast("registration in progress..."); toast("registration in progress...");
axios.post('https://dev.lauf-fuer-kaya.de/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!");
}) })

File diff suppressed because one or more lines are too long

View File

@ -1,21 +1,41 @@
{ {
"already_have_an_account": "Sie haben bereits einen Account?", "already_have_an_account": "Sie haben bereits einen Account?",
"apartment_suite_etc": "Addresszeile 2",
"configuration_error": "Konfigurationsfehler",
"confirm_personal_data": "Hiermit bestätige ich die Vollständigkeit und Richtigkeit der oben genannten Angaben",
"distance": "Distanz",
"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.",
"imprint": "Impressum",
"lap_time": "Rundenzeit",
"lap_times": "Rundenzeiten",
"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_valid_mail": "Bitte geben Sie eine gültige E-Mail Adresse an", "please_provide_valid_mail": "Bitte geben Sie eine gültige E-Mail Adresse an",
"plz": "PLZ", "plz": "PLZ",
"privacy_policy": "Datenschutzerklärung",
"profile": "Profil",
"provide_address": "Adresse angeben?", "provide_address": "Adresse angeben?",
"register": { "register": {
"register_now": "Jetzt für den Lauf für Kaya! 2021 registrieren." "register_now": "Jetzt für den Lauf für Kaya! 2021 registrieren."
}, },
"register_now": "Jetzt registrieren!", "register_now": "Jetzt registrieren!",
"registrieren": "Registrieren", "registrieren": "Registrieren",
"save_changes": "Änderungen speichern",
"sponsoring": "Sponsoring",
"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.",
"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

@ -1,21 +1,41 @@
{ {
"already_have_an_account": "Already have an account?", "already_have_an_account": "Already have an account?",
"apartment_suite_etc": "Apartment, suite, etc.",
"configuration_error": "Configuration error",
"confirm_personal_data": "I hereby confirm that the above information is complete and correct",
"distance": "Distance",
"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.",
"imprint": "Imprint",
"lap_time": "Lap time",
"lap_times": "Lap times",
"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_valid_mail": "Please provide a valid mail address.", "please_provide_valid_mail": "Please provide a valid mail address.",
"plz": "zipcode", "plz": "zipcode",
"privacy_policy": "Privacy Policy",
"profile": "Profile",
"provide_address": "Provide a postal address?", "provide_address": "Provide a postal address?",
"register": { "register": {
"register_now": "Register now for Lauf für Kaya! 2021." "register_now": "Register now for Lauf für Kaya! 2021."
}, },
"register_now": "Register now!", "register_now": "Register now!",
"registrieren": "Register Now", "registrieren": "Register Now",
"save_changes": "Save changes",
"sponsoring": "Sponsoring",
"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.",
"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

@ -21,17 +21,29 @@ const i18n = createI18n({
}); });
// --------------- // ---------------
const EnvError = import('./components/EnvError.vue');
const Home = import('./components/Home.vue'); const Home = import('./components/Home.vue');
const Register = import('./components/Register.vue'); const Imprint = import('./components/Imprint.vue');
const Profile = import('./components/Profile.vue'); const Privacy = import('./components/Privacy.vue');
const Register = () => import('./components/Register.vue');
const Profile = () => import('./components/Profile.vue');
// //
let routes = [ { path: '/:pathMatch(.*)*', component: EnvError } ];
if (typeof config !== 'undefined') {
if (config.baseurl && config.documentserver_key) {
routes = [
{ path: '/', component: Home },
{ path: '/imprint', component: Imprint },
{ path: '/privacy', component: Privacy },
{ path: '/register', component: Register },
{ path: '/register/:token', component: Register, props: true },
{ path: '/profile/:token', component: Profile, props: true }
];
}
}
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes: [ routes
{ path: '/', component: Home },
{ path: '/register', component: Register },
{ path: '/profile', component: Profile }
]
}); });
// --------------- // ---------------
createApp(App).use(Toast).use(i18n).use(router).mount('#app'); createApp(App).use(Toast).use(i18n).use(router).mount('#app');