frontend and backend progress

This commit is contained in:
Sascha Kühl 2025-04-28 22:49:22 +02:00
parent 888cdae3cd
commit 787b9879fc
10 changed files with 166 additions and 14 deletions

13
app/Dockerfile Normal file
View File

@ -0,0 +1,13 @@
# build stage
FROM node:lts-alpine AS build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# production stage
FROM nginx:stable-alpine AS production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

2
app/package-lock.json generated
View File

@ -17,6 +17,7 @@
"@ionic/vue": "^8.0.0", "@ionic/vue": "^8.0.0",
"@ionic/vue-router": "^8.0.0", "@ionic/vue-router": "^8.0.0",
"axios": "^1.8.1", "axios": "^1.8.1",
"dayjs": "^1.11.13",
"ionicons": "^7.0.0", "ionicons": "^7.0.0",
"vee-validate": "^4.15.0", "vee-validate": "^4.15.0",
"vue": "^3.3.0", "vue": "^3.3.0",
@ -4962,7 +4963,6 @@
"version": "1.11.13", "version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/de-indent": { "node_modules/de-indent": {

View File

@ -21,6 +21,7 @@
"@ionic/vue": "^8.0.0", "@ionic/vue": "^8.0.0",
"@ionic/vue-router": "^8.0.0", "@ionic/vue-router": "^8.0.0",
"axios": "^1.8.1", "axios": "^1.8.1",
"dayjs": "^1.11.13",
"ionicons": "^7.0.0", "ionicons": "^7.0.0",
"vee-validate": "^4.15.0", "vee-validate": "^4.15.0",
"vue": "^3.3.0", "vue": "^3.3.0",

View File

@ -5,6 +5,7 @@ import { useStore } from 'vuex'
import { AppStore } from '@/store'; import { AppStore } from '@/store';
import { onMounted, onBeforeUnmount, ref } from "vue"; import { onMounted, onBeforeUnmount, ref } from "vue";
import EventBus from "./common/EventBus"; import EventBus from "./common/EventBus";
import backendService from '@/services/BackendService'
const router = useRouter(); const router = useRouter();
const store = useStore<AppStore>(); const store = useStore<AppStore>();
@ -19,6 +20,8 @@ const isOnline = ref(navigator.onLine);
onMounted(() => { onMounted(() => {
EventBus.on("logout", logout); EventBus.on("logout", logout);
backendService.updateServer(backendService.getServer());
window.addEventListener("online", () => isOnline.value = true); window.addEventListener("online", () => isOnline.value = true);
window.addEventListener("offline", () => isOnline.value = false); window.addEventListener("offline", () => isOnline.value = false);

View File

@ -9,12 +9,14 @@ import BullpenSummaryView from "@/views/BullpenSummaryView.vue";
import SetupView from '@/views/SetupView.vue'; import SetupView from '@/views/SetupView.vue';
import backendService from '@/services/BackendService' import backendService from '@/services/BackendService'
import BullpenListView from "@/views/BullpenListView.vue"; import BullpenListView from "@/views/BullpenListView.vue";
import ProfileView from "@/views/ProfileView.vue";
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ path: '/', redirect: '/login' }, { path: '/', redirect: '/login' },
{ path: '/login', component: LoginView }, { path: '/login', component: LoginView },
{ path: '/setup', component: SetupView }, { path: '/setup', component: SetupView },
{ path: '/home', component: HomeView }, { path: '/home', component: HomeView },
{ path: '/profile', component: ProfileView },
{ path: '/pitchers', component: PitcherList }, { path: '/pitchers', component: PitcherList },
{ path: '/bullpen', component: BullpenView }, { path: '/bullpen', component: BullpenView },
{ path: '/stats', component: BullpenListView }, { path: '/stats', component: BullpenListView },

View File

@ -5,8 +5,8 @@ class BackendService {
return localStorage.getItem("server") !== null; return localStorage.getItem("server") !== null;
} }
getServer(): Server { getServer(): Server {
return JSON.parse(localStorage.getItem("server") || '{"protocol":"http","host":"localhost","port":8080}'); // return JSON.parse(localStorage.getItem("server") || '{"protocol":"http","host":"localhost","port":8080}');
// return JSON.parse(localStorage.getItem("server") || '{"protocol":"https","host":"bullpen-api.palaeomatiker.home64.de","port":443}'); return JSON.parse(localStorage.getItem("server") || '{"protocol":"https","host":"bullpen-api.palaeomatiker.home64.de","port":443}');
} }
updateServer(server: Server): void { updateServer(server: Server): void {
localStorage.setItem("server", JSON.stringify(server)); localStorage.setItem("server", JSON.stringify(server));

View File

@ -2,17 +2,24 @@
import { import {
IonButton, IonButton,
IonCard, IonCard,
IonCardTitle,
IonCardSubtitle,
IonContent, IonContent,
IonFooter, IonFooter,
IonHeader, IonHeader,
IonLabel,
IonPage, IonPage,
IonTitle, IonTitle,
IonToolbar IonToolbar, IonLabel, IonList, IonItem, IonIcon, IonBadge
} from "@ionic/vue"; } from "@ionic/vue";
import {
baseballOutline
} from 'ionicons/icons';
import PitchType from "@/types/PitchType";
import {useStore} from 'vuex'; import {useStore} from 'vuex';
import {useRouter} from 'vue-router'; import {useRouter} from 'vue-router';
import {computed} from "vue"; import {computed} from "vue";
import dayjs from 'dayjs';
const router = useRouter(); const router = useRouter();
const store = useStore(); const store = useStore();
@ -20,6 +27,17 @@ const store = useStore();
const isAuthenticated = computed(() => store.state.auth.isAuthenticated); const isAuthenticated = computed(() => store.state.auth.isAuthenticated);
const pitcher = computed(() => store.state.auth.user); const pitcher = computed(() => store.state.auth.user);
const bullpens = computed(() => store.state.bullpen.bullpens); const bullpens = computed(() => store.state.bullpen.bullpens);
const pitchTypes = computed(() => store.state.pitchTypes.pitchTypes);
const formatDate = (date: Date) => {
return dayjs(date).format('YYYY.MM.DD HH:mm');
}
const determinePitchTypeName = (id: number): string => {
const pitchType = pitchTypes.value.find((pitchType: PitchType) => pitchType.id === id);
return pitchType?.name ?? 'Unknown';
}
const gotoHome = () => { const gotoHome = () => {
router.push({path: '/home'}); router.push({path: '/home'});
@ -37,7 +55,21 @@ const gotoHome = () => {
</ion-header> </ion-header>
<ion-content> <ion-content>
<ion-card v-for="(bullpen, index) in bullpens.data" :key="index"> <ion-card v-for="(bullpen, index) in bullpens.data" :key="index">
<ion-label>Bullpen {{bullpen.id}} ({{bullpen.startedAt}})</ion-label> <ion-card-header>
<ion-card-title>Bullpen from ({{formatDate(bullpen.startedAt)}})</ion-card-title>
<ion-card-subtitle></ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<ion-list v-for="(pitch, index) in bullpen.pitches" :key="index">
<ion-item>
<ion-icon slot="start" :icon="baseballOutline" class="icon-login"></ion-icon>
<ion-label>{{determinePitchTypeName(pitch.pitchTypeId)}}</ion-label>
<ion-badge>{{pitch.aimedArea}}</ion-badge>
<ion-badge>{{pitch.hitArea}}</ion-badge>
</ion-item>
</ion-list>
</ion-card-content>
</ion-card> </ion-card>
</ion-content> </ion-content>
<ion-footer> <ion-footer>

View File

@ -3,7 +3,7 @@ import { computed, ref, onMounted } from "vue";
import { import {
IonPage, IonPage,
IonIcon, IonIcon,
IonLabel, // IonLabel,
IonButton, IonButton,
IonContent, IonContent,
IonInput IonInput
@ -20,11 +20,10 @@ import { useRouter } from 'vue-router';
import { useStore } from 'vuex' import { useStore } from 'vuex'
import * as yup from 'yup'; import * as yup from 'yup';
import PitchTypeService from "@/services/PitchTypeService"; import PitchTypeService from "@/services/PitchTypeService";
import EventBus from "@/common/EventBus";
import PitchType from "@/types/PitchType"; import PitchType from "@/types/PitchType";
const loading = ref(false); const loading = ref(false);
const server = JSON.parse(localStorage.getItem("server") || '""'); // const server = JSON.parse(localStorage.getItem("server") || '""');
const schema = yup.object({ const schema = yup.object({
email: yup.string().email('Invalid email address').required('Email is required'), email: yup.string().email('Invalid email address').required('Email is required'),
@ -100,7 +99,7 @@ const changeServer = () => {
<div class="logo-container"> <div class="logo-container">
<img src="../assets/Bonn_Capitals_Insignia.png" alt="Logo" class="logo" /> <img src="../assets/Bonn_Capitals_Insignia.png" alt="Logo" class="logo" />
</div> </div>
<ion-label>{{server.protocol}}://{{server.host}}:{{server.port}}</ion-label> <!-- <ion-label>{{server.protocol}}://{{server.host}}:{{server.port}}</ion-label>-->
</div> </div>
<form @submit="submit" class="form-container"> <form @submit="submit" class="form-container">
<div class="input-group"> <div class="input-group">

View File

@ -0,0 +1,102 @@
<script setup lang="ts">
import {logOutOutline, personOutline, playOutline, statsChartOutline} from "ionicons/icons";
import {
IonAvatar,
IonButton,
IonContent,
IonFooter,
IonHeader,
IonIcon,
IonPage,
IonTitle,
IonToolbar
} from "@ionic/vue";
import {computed, ref} from "vue";
import {useStore} from "vuex";
import {useRouter} from "vue-router";
const userImage = ref(null);
const router = useRouter();
const store = useStore();
const pitcher = computed(() => store.state.auth.user);
const isAuthenticated = computed(() => store.state.auth.isAuthenticated);
const gotoHome = () => {
router.push({path: '/home'});
}
</script>
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title v-if="isAuthenticated">Home of {{ pitcher.firstName }} {{ pitcher.lastName }}</ion-title>
<ion-title v-else>Home</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="user-container">
<ion-avatar class="user-avatar">
<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>
<div v-if="isAuthenticated" class="user-name">{{ pitcher.firstName }} {{ pitcher.lastName }}</div>
</div>
</ion-content>
<ion-footer>
<ion-button expand="full" color="success" @click="gotoHome">Home</ion-button>
</ion-footer>
</ion-page>
</template>
<style scoped>
.user-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 30vh;
background-color: #f0f0f0;
padding: 5px;
}
.user-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
border: 4px solid #ccc;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
.avatar-frame {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.avatar-placeholder {
font-size: 60px;
color: #888;
}
.user-name {
margin-top: 10px;
font-size: 18px;
font-weight: bold;
color: #333;
}
</style>

View File

@ -1,10 +1,10 @@
module.exports = { module.exports = {
secret: "bullpen-secret-key", secret: "bullpen-secret-key",
// jwtExpiration: 3600, // 1 hour jwtExpiration: 3600, // 1 hour
// jwtRefreshExpiration: 86400, // 24 hours jwtRefreshExpiration: 86400 * 7, // 7 days
/* for test */ /* for test */
jwtExpiration: 30, // 30 seconds // jwtExpiration: 30, // 30 seconds
jwtRefreshExpiration: 60 // 1 minute // jwtRefreshExpiration: 60 // 1 minute
}; };