progress on player view

This commit is contained in:
Sascha Kühl 2025-05-27 16:24:44 +02:00
parent 3fa9ff42b5
commit 8d829975a6
7 changed files with 326 additions and 18 deletions

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Ionic App</title>
<title>Bullpen App</title>
<base href="/" />

View File

@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from '@ionic/vue-router';
import { useStore } from 'vuex'
import { RouteRecordRaw } from 'vue-router';
import PlayerList from '../views/PlayerList.vue'
import PlayerView from '../views/PlayerView.vue'
import LoginView from '../views/LoginView.vue'
import HomeView from '../views/HomeView.vue'
import BullpenView from "@/views/BullpenView.vue";
@ -17,7 +18,8 @@ const routes: Array<RouteRecordRaw> = [
{ path: '/setup', component: SetupView },
{ path: '/home', component: HomeView },
{ path: '/profile', component: ProfileView },
{ path: '/player', component: PlayerList },
{ path: '/players', component: PlayerList },
{ path: '/player', component: PlayerView },
{ path: '/bullpen', component: BullpenView },
{ path: '/stats', component: BullpenListView },
{ path: '/summary', component: BullpenSummaryView }

View File

@ -15,12 +15,15 @@ const pitchTypes: Module<PlayerState, RootState> = {
namespaced: true,
state: {player: null},
actions: {
determinePlayer({commit}: PlayerActionContext, user: User) {
selectPlayer({commit}: PlayerActionContext, user: User) {
if (user.roles.includes('ROLE_PLAYER')) {
playerService.fetchByUserId(user.id).then(player => {
return playerService.fetchByUserId(user.id).then(player => {
commit('select', player);
return Promise.resolve(player);
});
}
return null;
}
},
mutations: {

View File

@ -27,7 +27,7 @@ const store = useStore();
const pitch = ref<Pitch>(BullpenSessionService.createPitch());
const currentStep = ref<BullpenStep>(BullpenStep.Prepare);
const pitcher = computed(() => store.state.auth.user);
const player = computed(() => store.state.player.player);
const isAuthenticated = computed(() => store.state.auth.isAuthenticated);
const pitchTypes = computed(() => store.state.pitchTypes.pitchTypes);
const bullpen = computed(() => store.state.bullpen);
@ -82,7 +82,7 @@ const pitchAreaCssClasses = (area: number, name: string) => {
<ion-page>
<ion-header :translucent="true">
<ion-toolbar>
<ion-title v-if="isAuthenticated">Pitching {{ pitcher.firstName }} {{ pitcher.lastName }}</ion-title>
<ion-title v-if="isAuthenticated">Pitching {{ player.user.firstName }} {{ player.user.lastName }}</ion-title>
<ion-title v-else>Pitching</ion-title>
</ion-toolbar>
</ion-header>

View File

@ -13,7 +13,6 @@ const store = useStore();
const userImage = ref(null);
const user = computed(() => store.state.auth.user);
const player = computed(() => store.state.player.player);
const isAuthenticated = computed(() => store.state.auth.isAuthenticated);
if (user.value === undefined || user.value === null || user.value === '') {
@ -21,11 +20,13 @@ if (user.value === undefined || user.value === null || user.value === '') {
}
const startBullpen = () => {
if (player.value === undefined || player.value === null || player.value === '') {
router.push({ path: '/player' });
} else {
store.dispatch("bullpen/start", user.value);
router.push({ path: '/bullpen' });
if (TokenService.isPlayer()) {
store.dispatch('player/selectPlayer', user).then(() => {
store.dispatch("bullpen/start", user.value);
router.push({ path: '/bullpen' });
});
} else if (TokenService.isCoach()) {
router.push({ path: '/players' });
}
};
@ -48,7 +49,9 @@ const logout = () => {
});
}
const addPlayer = () => {}
const addPlayer = () => {
router.push({ path: '/player' });
}
</script>

View File

@ -41,6 +41,7 @@ const [password, passwordAttrs] = defineField('password');
const router = useRouter();
const store = useStore();
const loginError = ref('');
const loggedIn = computed(() => store.state.auth.isAuthenticated);
@ -50,18 +51,19 @@ onMounted(() => {
}
});
const submit = handleSubmit((values, { /*resetForm*/ }) => {
const submit = handleSubmit((values, { resetForm }) => {
store.dispatch('auth/login', {
email: values.email,
password: values.password,
}).then((user: User) => {
return store.dispatch('player/determinePlayer', user);
}, error => {
}).then(() => {
resetForm();
onLogin();
}).catch(error => {
loading.value = false;
console.log(error);
}).then(() => {
// resetForm();
onLogin();
loginError.value = 'Invalid login data. Please try again.';
});
}, ({errors}) => {
console.log(errors);
@ -135,6 +137,7 @@ const changeServer = () => {
</ion-item>
<ion-button expand="block" class="ion-margin-top" type="submit">Login</ion-button>
</form>
<p v-if="loginError" class="error-text">{{ loginError }}</p>
</ion-card-content>
</ion-card>
</div>

View File

@ -0,0 +1,297 @@
<script setup lang="ts">
import {
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonItem,
IonInput,
IonButton,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardContent,
IonSelect,
IonSelectOption
} from '@ionic/vue';
import { Field, useForm } from 'vee-validate';
import * as yup from 'yup';
import dayjs from 'dayjs';
import { ref, computed, watch } from 'vue';
import Player from '@/types/Player';
const props = defineProps<{ player?: Player }>();
// Todo: create default player
// ...
// Computed reactive player object: Either use the provided player or initialize with default
const player = ref<Player>(props.player ? { ...props.player } : { ...defaultPlayer });
const schema = yup.object({
email: yup
.string()
.email('Invalid email address')
.required('E-Mail is required'),
firstName: yup.string().required('First name is required'),
lastName: yup.string().required('Last name is required'),
dateOfBirth: yup
.date()
.max(dayjs().subtract(5, 'year').toDate(), 'Player must be at least 5 years old')
.required('Geburtsdatum ist erforderlich'),
gender: yup.string().required('Gender is required'),
bats: yup
.string()
.oneOf(['R', 'L', 'S'], 'Invalid selection')
.required('Batting hand is required'),
throws: yup
.string()
.oneOf(['R', 'L'], 'Invalid selection')
.required('Throwing hand is required'),
jerseyNumber: yup
.number()
.integer('Must be an integer')
.min(0, 'Must be zero or positive')
.max(99, 'Maximum is 99')
.required('Jersey number is required'),
weight: yup
.number()
.min(30, 'Minimum weight is 30kg')
.max(200, 'Maximum weight is 200kg')
.required('Weight is required'),
height: yup
.number()
.min(100, 'Minimum height is 100cm')
.max(250, 'Maximum height is 250cm')
.required('Height is required'),
});
const { errors, handleSubmit, defineField } = useForm({
validationSchema: schema,
initialValues: player.value
});
// Watch for changes in props.player to reset form when editing
watch(
() => props.player,
(newPlayer) => {
if (newPlayer) {
resetForm({ values: { ...newPlayer } });
} else {
resetForm({ values: { ...defaultPlayer } });
}
}
);
// Submit logic: Differentiate between create and update
const submit = handleSubmit((values: Player) => {
if (player.value.id) {
console.log('Updating player:', values); // Replace with update logic
} else {
console.log('Creating player:', values); // Replace with create logic
}
});
const isEdit = computed(() => !!player.value.id);
const [email, emailAttrs] = defineField('email');
const [firstName, firstNameAttrs] = defineField('firstName');
const [lastName, lastNameAttrs] = defineField('lastName');
const [dateOfBirth, dateOfBirthAttrs] = defineField('dateOfBirth');
const [gender, genderAttrs] = defineField('gender');
const [bats, batsAttrs] = defineField('bats');
const [throws, throwsAttrs] = defineField('throws');
const [jerseyNumber, jerseyNumberAttrs] = defineField('jerseyNumber');
const [weight, weightAttrs] = defineField('weight');
const [height, heightAttrs] = defineField('height');
</script>
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>{{ isEdit ? 'Update Player' : 'Create Player' }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<form @submit="submit">
<ion-card>
<ion-card-header>
<ion-card-title>Personal Data</ion-card-title>
</ion-card-header>
<ion-card-content>
<ion-item>
<Field name="email">
<ion-input
label="E-Mail"
label-placement="stacked"
v-model="email"
v-bind="emailAttrs"
type="email"
>
<small v-if="errors.email" class="error-message">{{ errors.email }}</small>
</ion-input>
</Field>
</ion-item>
<ion-item>
<Field name="firstName">
<ion-input
label="First Name"
label-placement="stacked"
v-model="firstName"
v-bind="firstNameAttrs"
type="text"
>
<small v-if="errors.firstName" class="error-message">{{ errors.firstName }}</small>
</ion-input>
</Field>
</ion-item>
<ion-item>
<Field name="lastName">
<ion-input
label="Last Name"
label-placement="stacked"
v-model="lastName"
v-bind="lastNameAttrs"
type="text"
>
<small v-if="errors.lastName" class="error-message">{{ errors.lastName }}</small>
</ion-input>
</Field>
</ion-item>
<ion-item>
<Field name="dateOfBirth">
<ion-input
label="Date of Birth"
label-placement="stacked"
v-model="dateOfBirth"
v-bind="dateOfBirthAttrs"
type="date"
>
<small v-if="errors.dateOfBirth" class="error-message">{{ errors.dateOfBirth }}</small>
</ion-input>
</Field>
</ion-item>
</ion-card-content>
</ion-card>
<ion-card>
<ion-card-header>
<ion-card-title>Player Data</ion-card-title>
</ion-card-header>
<ion-card-content>
<ion-item>
<Field name="gender">
<ion-select
label="Gender"
label-placement="stacked"
v-model="gender"
v-bind="genderAttrs"
interface="action-sheet"
>
<ion-select-option value="male">Male</ion-select-option>
<ion-select-option value="female">Female</ion-select-option>
<ion-select-option value="other">Diverse</ion-select-option>
</ion-select>
</Field>
</ion-item>
<ion-item>
<Field name="bats">
<ion-select
label="Bats"
label-placement="stacked"
v-model="bats"
v-bind="batsAttrs"
interface="action-sheet"
>
<ion-select-option value="R">Right</ion-select-option>
<ion-select-option value="L">Left</ion-select-option>
<ion-select-option value="S">Switch</ion-select-option>
</ion-select>
</Field>
</ion-item>
<ion-item>
<Field name="throws">
<ion-select
label="Throws"
label-placement="stacked"
v-model="throws"
v-bind="throwsAttrs"
>
<ion-select-option value="R">Right</ion-select-option>
<ion-select-option value="L">Left</ion-select-option>
</ion-select>
</Field>
</ion-item>
<ion-item>
<Field name="jerseyNumber">
<ion-input
label="Jersey Number"
label-placement="stacked"
v-model="jerseyNumber"
v-bind="jerseyNumberAttrs"
type="number"
>
<small v-if="errors.jerseyNumber" class="error-message">{{ errors.jerseyNumber }}</small>
</ion-input>
</Field>
</ion-item>
<ion-item>
<Field name="weight">
<ion-input
label="Weight (kg)"
label-placement="stacked"
v-model="weight"
v-bind="weightAttrs"
type="number"
>
<small v-if="errors.weight" class="error-message">{{ errors.weight }}</small>
</ion-input>
</Field>
</ion-item>
<ion-item>
<Field name="height">
<ion-input
label="Height (cm)"
label-placement="stacked"
v-model="height"
v-bind="heightAttrs"
type="number"
>
<small v-if="errors.height" class="error-message">{{ errors.height }}</small>
</ion-input>
</Field>
</ion-item>
</ion-card-content>
</ion-card>
<div class="ion-padding-vertical">
<ion-button type="submit" expand="block">Save</ion-button>
</div>
</form>
</ion-content>
</ion-page>
</template>
<style scoped>
.error-message {
color: var(--ion-color-danger);
font-size: 0.75rem;
margin-top: 4px;
}
ion-card {
margin-bottom: 1rem;
}
</style>