Compare commits

...

3 Commits

Author SHA1 Message Date
Sascha Kühl 16717f2405 added member entity to backend (progress) 2025-03-31 15:55:34 +02:00
Sascha Kühl f2cfe0c4e3 restrutcured files 2025-03-31 15:55:12 +02:00
Sascha Kühl 983ca5a25a stored pitch types in local storage 2025-03-31 15:54:52 +02:00
9 changed files with 305 additions and 325 deletions

View File

@ -1,29 +1,14 @@
import PitchType from "@/types/PitchType"; import PitchType from "@/types/PitchType";
import api from './Api';
import authHeader from './AuthHeader';
class PitchTypeService { class PitchTypeService {
private static instance: PitchTypeService; getLocalPitchTypes(): PitchType[] {
return JSON.parse(localStorage.getItem("pitchTypes") || '""');
private constructor() {}
// Static method to get the instance of the service
public static getInstance(): PitchTypeService {
if (!PitchTypeService.instance) {
PitchTypeService.instance = new PitchTypeService();
}
return PitchTypeService.instance;
} }
async getAllPitchTypes(): Promise<PitchType[]> { updateLocalPitchTypes(pitchTypes: PitchType[]): void {
const users = await api.get('/pitch_types', { headers: authHeader() }); localStorage.setItem("pitchTypes", JSON.stringify(pitchTypes));
return users.data;
} }
async getPitchType(id: number): Promise<PitchType> {
const pitchType = await api.get(`/pitch_types/${id}`, { headers: authHeader() });
return pitchType.data;
}
} }
export const pitchTypeService = PitchTypeService.getInstance(); export default new PitchTypeService();

View File

@ -1,8 +1,11 @@
import {pitchTypeService} from '@/services/PitchTypeService' import pitchTypeService from '@/services/PitchTypeService'
import PitchType from "@/types/PitchType"; import PitchType from "@/types/PitchType";
import api from "@/services/Api";
import authHeader from '@/services/AuthHeader';
import { Module, ActionContext } from 'vuex'; import { Module, ActionContext } from 'vuex';
import { RootState } from './index'; import { RootState } from './index';
import {AxiosResponse} from 'axios';
export interface PitchTypeState { export interface PitchTypeState {
pitchTypes: PitchType[]; pitchTypes: PitchType[];
@ -15,10 +18,10 @@ const pitchTypes: Module<PitchTypeState, RootState> = {
state: {pitchTypes: []}, state: {pitchTypes: []},
actions: { actions: {
initialize({ commit }: PitchTypeActionContext) { initialize({ commit }: PitchTypeActionContext) {
return pitchTypeService.getAllPitchTypes().then( api.get('/pitch_types', { headers: authHeader() }).then(
(pitchTypeList: PitchType[]) => { (response: AxiosResponse) => {
commit('initializedPitchTypes', pitchTypeList); commit('initializedPitchTypes', response.data);
return Promise.resolve(pitchTypeList); return Promise.resolve(response.data);
}, },
error => { error => {
return Promise.reject(error); return Promise.reject(error);
@ -28,7 +31,7 @@ const pitchTypes: Module<PitchTypeState, RootState> = {
}, },
mutations: { mutations: {
initializedPitchTypes(state, pitchTypeList: PitchType[]) { initializedPitchTypes(state, pitchTypeList: PitchType[]) {
localStorage.setItem("pitchTypes", JSON.stringify(pitchTypeList)); pitchTypeService.updateLocalPitchTypes(pitchTypeList);
state.pitchTypes = pitchTypeList; state.pitchTypes = pitchTypeList;
} }
} }

View File

@ -1,8 +1,38 @@
<script setup lang="ts">
import {
IonButton,
IonBadge,
IonLabel,
IonList,
IonItem,
IonFooter,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardContent,
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar
} from '@ionic/vue';
import {ref} from 'vue'
import {useRouter} from 'vue-router';
import {BullpenSessionService, bullpenSessionService} from "@/services/BullpenSessionService";
const router = useRouter();
const bps = ref<BullpenSessionService>(bullpenSessionService);
const gotoHome = () => {
router.push('/home');
};
</script>
<template> <template>
<ion-page> <ion-page>
<ion-header :translucent="true"> <ion-header :translucent="true">
<ion-toolbar> <ion-toolbar>
<ion-title>Bullpen Stats for {{ bps.getPitcher().first_name }} {{ bps.getPitcher().last_name }}</ion-title> <ion-title>Bullpen Stats for {{ bps.getPitcher().firstName }} {{ bps.getPitcher().lastName }}</ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
@ -34,61 +64,6 @@
</ion-page> </ion-page>
</template> </template>
<script lang="ts">
import {
IonButton,
IonBadge,
IonLabel,
IonList,
IonItem,
IonFooter,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardContent,
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar
} from '@ionic/vue';
import {defineComponent, ref} from 'vue'
import {useRouter} from 'vue-router';
import {BullpenSessionService, bullpenSessionService} from "@/services/BullpenSessionService";
export default defineComponent({
name: 'BullpenStats',
components: {
IonButton,
IonBadge,
IonFooter,
IonLabel,
IonList,
IonItem,
IonPage,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardContent,
IonContent,
IonHeader,
IonToolbar,
IonTitle,
},
setup() {
const router = useRouter();
const bps = ref<BullpenSessionService>(bullpenSessionService);
const gotoHome = () => {
router.push('/home');
}
return {bps, gotoHome};
}
});
</script>
<style scoped> <style scoped>
</style> </style>

View File

@ -1,8 +1,46 @@
<script setup lang="ts">
import {defineComponent, ref} from "vue";
import {
IonContent,
IonHeader,
IonFooter,
IonToolbar,
IonTitle,
IonPage,
IonCard,
IonCardContent,
IonCardHeader,
IonCardTitle, IonButton,
IonGrid, IonRow, IonCol
} from "@ionic/vue";
import {BullpenSessionService, bullpenSessionService} from "@/services/BullpenSessionService";
import {useRouter} from "vue-router";
const router = useRouter();
const bps = ref<BullpenSessionService>(bullpenSessionService);
const finalizeAndNextPitch = () => {
bullpenSessionService.nextPitch();
router.push({ name: 'PreparePitch' });
}
const finalizeAndEndBullpen = () => {
bullpenSessionService.nextPitch();
bullpenSessionService.finishSession();
router.push({ name: 'BullpenStats' });
}
const setRealPitchArea = (hitArea: number) => {
bps.value.currentBullpenPitch().realPitchArea = hitArea;
};
</script>
<template> <template>
<ion-page> <ion-page>
<ion-header :translucent="true"> <ion-header :translucent="true">
<ion-toolbar> <ion-toolbar>
<ion-title>Finalize Pitch for {{ bps.getPitcher().first_name }} {{ bps.getPitcher().last_name }}</ion-title> <ion-title>Finalize Pitch for {{ bps.getPitcher().firstName }} {{ bps.getPitcher().lastName }}</ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
@ -82,65 +120,6 @@
</ion-page> </ion-page>
</template> </template>
<script lang="ts">
import {defineComponent, ref} from "vue";
import {
IonContent,
IonHeader,
IonFooter,
IonToolbar,
IonTitle,
IonPage,
IonCard,
IonCardContent,
IonCardHeader,
IonCardTitle, IonButton,
IonGrid, IonRow, IonCol
} from "@ionic/vue";
import {BullpenSessionService, bullpenSessionService} from "@/services/BullpenSessionService";
import {useRouter} from "vue-router";
export default defineComponent({
name: "FinalizePitch",
components: {
IonButton,
IonPage,
IonFooter,
IonContent,
IonHeader,
IonToolbar,
IonTitle,
IonCard,
IonCardContent,
IonCardHeader,
IonCardTitle,
IonGrid, IonRow, IonCol
},
setup() {
const router = useRouter();
const bps = ref<BullpenSessionService>(bullpenSessionService);
const finalizeAndNextPitch = () => {
bullpenSessionService.nextPitch();
router.push({ name: 'PreparePitch' });
}
const finalizeAndEndBullpen = () => {
bullpenSessionService.nextPitch();
bullpenSessionService.finishSession();
router.push({ name: 'BullpenStats' });
}
return {bps, finalizeAndNextPitch, finalizeAndEndBullpen};
},
methods: {
setRealPitchArea(hitArea: number) {
this.bps.currentBullpenPitch().realPitchArea = hitArea;
}
}
});
</script>
<style scoped> <style scoped>
.wasted { .wasted {
fill: firebrick; fill: firebrick;

View File

@ -1,12 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
// import { ref } from 'vue'; import { ref } from 'vue';
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonAvatar, IonButton, IonIcon } from '@ionic/vue'; import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonAvatar, IonButton, IonIcon } from '@ionic/vue';
import { playOutline, statsChartOutline, personOutline } from 'ionicons/icons'; import { playOutline, statsChartOutline, personOutline } from 'ionicons/icons';
import img from '../assets/groot.jpg' // import userImage from '../assets/groot.jpg'
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
const router = useRouter(); const router = useRouter();
// const userImage = ref('../assets/groot.jpg'); // Replace with actual user image URL const userImage = ref(null);
// const userImage = ref('../assets/groot.jpg');
const startBullpen = () => { const startBullpen = () => {
console.log('Starting bullpen session'); console.log('Starting bullpen session');
@ -35,7 +36,12 @@ const showProfile = () => {
<ion-content class="ion-padding"> <ion-content class="ion-padding">
<div class="user-container"> <div class="user-container">
<ion-avatar class="user-avatar"> <ion-avatar class="user-avatar">
<img :src="img" alt="User Profile" class="avatar-frame" /> <template v-if="userImage">
<img :src="userImage" alt="User Profile" class="avatar-frame" />
</template>
<template v-else>
<ion-icon :icon="personOutline" class="avatar-placeholder" />
</template>
</ion-avatar> </ion-avatar>
</div> </div>

View File

@ -1,66 +1,3 @@
<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>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, onMounted } from "vue"; import { computed, ref, onMounted } from "vue";
import { import {
@ -140,3 +77,66 @@ const onLogin = () => {
}); });
} }
</script> </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>

View File

@ -1,3 +1,27 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonList, IonItem, IonLabel } from '@ionic/vue';
import { pitcherService } from "@/services/PitcherService";
import Pitcher from "@/types/Pitcher";
import {bullpenSessionService} from "@/services/BullpenSessionService";
const pitchers = ref<Pitcher[]>([]);
const router = useRouter();
bullpenSessionService.clear();
onMounted(async () => {
pitchers.value = await pitcherService.getAllPitchers();
});
const goToPreparePitch = (pitcher: Pitcher) => {
bullpenSessionService.startSession( pitcher );
router.push({ name: 'PreparePitch' });
};
</script>
<template> <template>
<ion-page> <ion-page>
<ion-header :translucent="true"> <ion-header :translucent="true">
@ -22,48 +46,6 @@
</ion-page> </ion-page>
</template> </template>
<script lang="ts">
import { defineComponent, ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonList, IonItem, IonLabel } from '@ionic/vue';
import { pitcherService } from "@/services/PitcherService";
import Pitcher from "@/types/Pitcher";
import {bullpenSessionService} from "@/services/BullpenSessionService";
export default defineComponent({
name: "PitcherList",
components: {
IonContent,
IonHeader,
IonToolbar,
IonPage,
IonTitle,
IonList,
IonItem,
IonLabel,
},
setup() {
// Define a reactive array to hold colors
const pitchers = ref<Pitcher[]>([]);
const router = useRouter();
bullpenSessionService.clear();
// Fetch colors from the service when the component is mounted
onMounted(async () => {
pitchers.value = await pitcherService.getAllPitchers();
});
const goToPreparePitch = (pitcher: Pitcher) => {
bullpenSessionService.startSession( pitcher );
router.push({ name: 'PreparePitch' });
};
return { pitchers, goToPreparePitch };
},
});
</script>
<style scoped> <style scoped>
#container { #container {
text-align: center; text-align: center;

View File

@ -1,8 +1,51 @@
<script setup lang="ts">
import {onMounted, ref} from "vue";
import {pitchTypeService} from "@/services/PitchTypeService";
import {
IonContent,
IonHeader,
IonFooter,
IonToolbar,
IonTitle,
IonLabel,
IonButton,
IonPage,
IonCard,
IonCardContent,
IonCardHeader,
IonCardTitle,
} from "@ionic/vue";
import PitchType from "@/types/PitchType";
import {BullpenSessionService, bullpenSessionService} from "@/services/BullpenSessionService";
import {useRouter} from "vue-router";
const router = useRouter();
const pitchTypes = ref<PitchType[]>([]);
const bps = ref<BullpenSessionService>(bullpenSessionService);
onMounted(async () => {
pitchTypes.value = await pitchTypeService.getAllPitchTypes();
bps.value.currentBullpenPitch().pitchType = await pitchTypeService.getPitchType(1);
});
const setPitchType = (pitchType: PitchType) => {
bps.value.currentBullpenPitch().pitchType = pitchType;
}
const gotoFinalizePitch = () => {
router.push({ name: 'FinalizePitch' });
}
const setPlannedPitchArea = (hitArea: number) => {
bps.value.currentBullpenPitch().plannedPitchArea = hitArea;
};
</script>
<template> <template>
<ion-page> <ion-page>
<ion-header :translucent="true"> <ion-header :translucent="true">
<ion-toolbar> <ion-toolbar>
<ion-title>Prepare Pitch for {{ bps.getPitcher().first_name }} {{ bps.getPitcher().last_name }}</ion-title> <ion-title>Prepare Pitch for {{ bps.getPitcher().firstName }} {{ bps.getPitcher().lastName }}</ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content>
@ -87,71 +130,6 @@
</ion-page> </ion-page>
</template> </template>
<script lang="ts">
import {defineComponent, onMounted, ref} from "vue";
import {pitchTypeService} from "@/services/PitchTypeService";
import {
IonContent,
IonHeader,
IonFooter,
IonToolbar,
IonTitle,
IonLabel,
IonButton,
IonPage,
IonCard,
IonCardContent,
IonCardHeader,
IonCardTitle,
} from "@ionic/vue";
import PitchType from "@/types/PitchType";
import {BullpenSessionService, bullpenSessionService} from "@/services/BullpenSessionService";
import {useRouter} from "vue-router";
export default defineComponent({
name: "PreparePitch",
components: {
IonPage,
IonLabel,
IonFooter,
IonButton,
IonContent,
IonHeader,
IonToolbar,
IonTitle,
IonCard,
IonCardContent,
IonCardHeader,
IonCardTitle
},
setup() {
const router = useRouter();
// const pitcher = ref<Pitcher>(bullpenSessionService.getPitcher());
// const pitch = ref<BullpenPitch>(bullpenSessionService.currentBullpenPitch());
const pitchTypes = ref<PitchType[]>([]);
const bps = ref<BullpenSessionService>(bullpenSessionService);
onMounted(async () => {
pitchTypes.value = await pitchTypeService.getAllPitchTypes();
bps.value.currentBullpenPitch().pitchType = await pitchTypeService.getPitchType(1);
});
const setPitchType = (pitchType: PitchType) => {
bps.value.currentBullpenPitch().pitchType = pitchType;
}
const gotoFinalizePitch = () => {
router.push({ name: 'FinalizePitch' });
}
return {bps, pitchTypes, setPitchType, gotoFinalizePitch};
},
methods: {
setPlannedPitchArea(hitArea: number) {
this.bps.currentBullpenPitch().plannedPitchArea = hitArea;
}
}
});
</script>
<style scoped> <style scoped>
.wasted { .wasted {
fill: firebrick; fill: firebrick;

72
backend/models/member.js Normal file
View File

@ -0,0 +1,72 @@
const { Model } = require('sequelize');
const bcrypt = require("bcryptjs");
module.exports = (sequelize, DataTypes) => {
class Member extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
Member.belongsTo(models.User, {
foreignKey: 'loginId',
targetKey: 'id'
});
}
}
User.init({
firstName: {
type: DataTypes.STRING,
allowNull: false
},
lastName: {
type: DataTypes.STRING,
allowNull: false
},
dateOfBirth: {
type: DataTypes.DATE,
allowNull: false
},
height: {
type: DataTypes.INTEGER
},
weight: {
type: DataTypes.INTEGER
},
handedness: {
type: DataTypes.ENUM('LeftHandedness', 'RightHandedness'),
},
position: {
type: DataTypes.ENUM
},
preferredPosition: {
type: DataTypes.ENUM
}
}, {
sequelize,
modelName: 'User',
tableName: "Users",
hooks: {
beforeCreate: async (user) => {
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(user.password, salt);
}
}
}, {
defaultScope: {
attributes: { exclude: ['password'] },
},
scopes: {
withSecretColumns: {
attributes: { include: ['password'] },
},
},
});
User.prototype.validPassword = function (password) {
return bcrypt.compareSync(password, this.password);
};
return User;
};