renamed login and home view, restructured login view and router, added logo

This commit is contained in:
Sascha Kühl 2025-04-11 15:55:34 +02:00
parent a18559a77a
commit 9071886991
7 changed files with 359 additions and 180 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -1,45 +1,24 @@
import { createRouter, createWebHistory } from '@ionic/vue-router';
import { useStore } from 'vuex'
import { RouteRecordRaw } from 'vue-router';
import PitcherList from '../views/PitcherList.vue'
import Login from '../views/Login.vue'
import Home from '../views/Home.vue'
import LoginView from '../views/LoginView.vue'
import HomeView from '../views/HomeView.vue'
import PreparePitch from "@/views/PreparePitch.vue";
import FinalizePitch from "@/views/FinalizePitch.vue";
import BullpenStats from "@/views/BullpenStats.vue";
import SetupView from '@/views/SetupView.vue';
const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/login'
}, {
path: '/login',
name: 'Login',
component: Login
}, {
path: '/home',
name: 'Home',
component: Home
}, {
path: '/pitchers',
name: 'Pitchers',
component: PitcherList
}, {
path: '/bullpen',
name: 'PreparePitch',
component: PreparePitch
}, {
path: '/prepare',
name: 'PreparePitch',
component: PreparePitch
}, {
path: '/finalize',
name: 'FinalizePitch',
component: FinalizePitch
}, {
path: '/stats',
name: 'BullpenStats',
component: BullpenStats
}
{ path: '/', redirect: '/login' },
{ path: '/login', component: LoginView },
{ path: '/setup', component: SetupView },
{ path: '/home', component: HomeView },
{ path: '/pitchers', component: PitcherList },
{ path: '/bullpen', component: PreparePitch },
{ path: '/prepare', component: PreparePitch },
{ path: '/finalize', component: FinalizePitch },
{ path: '/stats', component: BullpenStats }
]
const router = createRouter({
@ -47,4 +26,24 @@ const router = createRouter({
routes
})
router.beforeEach((to, from, next) => {
const store = useStore();
const serverAddress = localStorage.getItem('serverAddress');
const isAuthenticated = store.state.auth.isAuthenticated;
if (to.meta.requiresAuth && !isAuthenticated) {
return next('/login');
}
if (!serverAddress && to.path !== '/setup') {
return next('/setup');
}
if (serverAddress && to.path === '/setup') {
return next('/login');
}
next();
});
export default router

View File

@ -1,2 +1,50 @@
/* For information on how to create your own theme, please see:
http://ionicframework.com/docs/theming/ */
/*:root {*/
/* --ion-background-color: #7c3b6a;*/
/* --ion-text-color: white;*/
/* --ion-tab-bar-background: #7c3b6a;*/
/* --ion-toolbar-background: #7c3b6a;;*/
/* --ion-toolbar-color: white;*/
/*}*/
.custom-back {
background-color: rgba(255, 255, 255, 0.1) !important;
border-radius: 5px;
font-size: 0.8rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.custom-button {
--border-radius: 5px !important;
--background: rgb(236, 149, 35);
--background-activated: rgb(192, 125, 38);
--background-focused: rgb(192, 125, 38);
font-size: 1.2rem;
margin-top: 1rem;
}
.custom-input {
--background: #834e76;
--padding-bottom: 1rem;
--padding-top: 1rem;
--padding-start: 1rem;
--padding-end: 1rem;
border-radius: 10px !important;
margin-top: 0.25rem;
transition: all 0.2s linear;
}
.ion-label {
padding-left: 0.2rem;
padding-right: 0.5rem;
color: #d3a6c7;
display: flex;
justify-content: space-between;
align-content: center;
align-items: center;
}

View File

@ -1,146 +0,0 @@
<script setup lang="ts">
import { computed, ref, onMounted } from "vue";
import {
IonPage,
IonHeader,
IonToolbar,
IonButtons,
IonMenuButton,
IonButton,
IonContent,
IonList,
IonItem,
IonTitle,
IonRow,
IonCol,
IonInput,
IonIcon,
} from "@ionic/vue";
// Todo: https://github.com/alanmontgomery/ionic-react-login
import { lockClosedOutline, personOutline } from 'ionicons/icons';
import { useForm } from 'vee-validate';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex'
import * as yup from 'yup';
const loading = ref(false);
const { meta, errors, handleSubmit, defineField } = useForm({
validationSchema: yup.object({
email: yup.string().email().required('No email provided.'),
password: yup.string().required('No password provided.').min(8)
})
});
const [email, emailAttrs] = defineField('email');
const [password, passwordAttrs] = defineField('password');
const router = useRouter();
const store = useStore();
const loggedIn = computed(() => store.state.auth.isAuthenticated);
onMounted(() => {
if (loggedIn.value) {
onLogin();
}
});
const submit = handleSubmit((values, { resetForm }) => {
store.dispatch('auth/login', {
email: values.email,
password: values.password,
}).then(
() => {
resetForm();
onLogin();
},
error => {
loading.value = false;
console.log(error);
// message.value =
// (error.response && error.response.data && error.response.data.message) ||
// error.message ||
// error.toString();
}
);
}, ({errors}) => {
console.log(errors);
loading.value = false;
});
const onLogin = () => {
console.log('check if pitch types are available.');
store.dispatch('pitchTypes/initialize').then(() => {
router.push({ path: '/home'});
}, error => {
console.log(error);
});
}
</script>
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>Login</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<form @submit="submit">
<ion-row>
<ion-col>
<ion-list>
<ion-item>
<ion-icon :icon="personOutline" class="icon-login"></ion-icon>
<ion-input name="user" v-model="email" v-bind="emailAttrs" type="text" required placeholder="Username"></ion-input>
</ion-item>
<br />
<ion-item>
<ion-icon :icon="lockClosedOutline" class="icon-login"></ion-icon>
<ion-input name="password" v-model="password" v-bind="passwordAttrs" type="password" required placeholder="Password"></ion-input>
</ion-item>
</ion-list>
</ion-col>
</ion-row>
<ion-row>
<ion-col>
<ion-button :disabled="!meta.valid" type="submit" fill="solid" expand="full">
Login
</ion-button>
</ion-col>
</ion-row>
<ion-row>
<ion-col>
<pre>errors: {{ errors }}</pre>
</ion-col>
</ion-row>
</form>
</ion-content>
</ion-page>
</template>
<style scoped>
.login-logo {
padding: 20px 0;
min-height: 200px;
text-align: center;
}
.login-logo img {
max-width: 150px;
}
.list {
margin-bottom: 0;
}
</style>

240
app/src/views/LoginView.vue Normal file
View File

@ -0,0 +1,240 @@
<script setup lang="ts">
import { computed, ref, onMounted } from "vue";
import {
IonPage,
IonIcon,
IonButton,
IonContent,
IonInput
} from "@ionic/vue";
// Todo: https://github.com/alanmontgomery/ionic-react-login
import {
lockClosedOutline,
personOutline,
// arrowBack,
// shapesOutline
} from 'ionicons/icons';
import {Field, useForm} from 'vee-validate';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex'
import * as yup from 'yup';
const loading = ref(false);
const schema = yup.object({
email: yup.string().email('Invalid email address').required('Email is required'),
password: yup
.string()
.min(8, 'Minimum 8 characters')
.matches(/\d/, 'Must include a number')
.matches(/[^A-Za-z\d]/, 'Must include a special character')
.required('Password is required'),
});
const { meta, errors, handleSubmit, defineField } = useForm({
validationSchema: schema
});
const [email, emailAttrs] = defineField('email');
const [password, passwordAttrs] = defineField('password');
const router = useRouter();
const store = useStore();
const loggedIn = computed(() => store.state.auth.isAuthenticated);
onMounted(() => {
if (loggedIn.value) {
onLogin();
}
});
const submit = handleSubmit((values, { resetForm }) => {
store.dispatch('auth/login', {
email: values.email,
password: values.password,
}).then(
() => {
resetForm();
onLogin();
},
error => {
loading.value = false;
console.log(error);
// message.value =
// (error.response && error.response.data && error.response.data.message) ||
// error.message ||
// error.toString();
}
);
}, ({errors}) => {
console.log(errors);
loading.value = false;
});
const onLogin = () => {
console.log('check if pitch types are available.');
store.dispatch('pitchTypes/initialize').then(() => {
router.push({ path: '/home'});
}, error => {
console.log(error);
});
}
</script>
<template>
<ion-page>
<ion-content>
<div class="login-container">
<div class="top-section">
<h1 class="login-title">Login</h1>
<div class="logo-container">
<img src="../assets/Bonn_Capitals_Insignia.png" alt="Logo" class="logo" />
</div>
</div>
<form @submit="submit" class="form-container">
<div class="input-group">
<div class="label-row">
<label class="input-label" for="email">Email</label>
<small v-if="!meta.valid" class="error-message">{{ errors.email }}</small>
</div>
<Field id="email" name="email" type="email" >
<div class="input-with-icon">
<ion-icon :icon="personOutline" class="icon-login"></ion-icon>
<ion-input
name="email"
v-model="email"
v-bind="emailAttrs"
class="rounded-input"
:class="{ 'input-invalid': !meta.valid && errors.email }"
type="text" required placeholder="Email"></ion-input>
</div>
</Field>
</div>
<div class="input-group">
<div class="label-row">
<label class="input-label" for="password">Password</label>
<small v-if="!meta.valid" class="error-message">{{ errors.password }}</small>
</div>
<Field id="password" name="password" type="password">
<div class="input-with-icon">
<ion-icon :icon="lockClosedOutline" class="icon-login"></ion-icon>
<ion-input
name="password"
v-model="password"
v-bind="passwordAttrs"
type="password"
class="rounded-input"
:class="{ 'input-invalid': !meta.valid && errors.password }"
required placeholder="Password"></ion-input>
</div>
</Field>
</div>
<ion-button expand="block" class="rounded-button" type="submit">
Login
</ion-button>
</form>
</div>
</ion-content>
</ion-page>
</template>
<style scoped>
.login-container {
display: flex;
flex-direction: column;
align-items: center;
}
.top-section {
background-color: #cbeec9;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.login-title {
font-size: 2rem;
margin-bottom: 1rem;
}
.logo-container {
width: 100%;
display: flex;
justify-content: center;
height: 25vh;
padding-bottom: 1rem;
}
.logo {
height: 100%;
width: auto;
object-fit: cover;
}
.form-container {
padding-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
width: 100%;
max-width: 400px;
}
.input-group {
width: 100%;
display: flex;
flex-direction: column;
position: relative;
margin-bottom: 16px;
}
.label-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.input-label {
font-size: 0.9rem;
margin-bottom: 4px;
text-align: left;
}
.input-with-icon {
display: flex;
align-items: center;
padding: 8px 0;
gap: 8px;
}
.rounded-input {
--border-radius: 50%;
padding: 12px;
background-color: #ffffff;
}
.input-invalid {
background-color: lightcoral;
}
.error-message {
color: red;
font-size: 0.75rem;
text-align: right;
margin-top: 4px;
}
.rounded-button {
--border-radius: 12px;
width: 100%;
}
</style>

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import {IonButton, IonContent, IonCol, IonHeader, IonInput, IonPage, IonTitle, IonToolbar} from '@ionic/vue';
const serverAddress = ref('');
const router = useRouter();
const saveServerAddress = () => {
localStorage.setItem('serverAddress', serverAddress.value);
router.push('/login');
};
</script>
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Setup Backend Server</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-row>
<ion-col class="ion-padding">
<ion-input name="ipaddress" v-model="serverAddress" type="text" required placeholder="Enter server address"></ion-input>
</ion-col>
<ion-col>
<ion-button type="button" fill="solid" expand="full" @click="saveServerAddress">Save</ion-button>
</ion-col>
</ion-row>
</ion-content>
</ion-page>
</template>
<style scoped>
</style>