added circular progress bar

This commit is contained in:
Sascha Kühl 2025-06-13 16:21:42 +02:00
parent 464cf60153
commit ea80c8b95d
3 changed files with 248 additions and 47 deletions

View File

@ -0,0 +1,183 @@
:root {
--cpb-color: #53077A;
--cpb-bg-color: #F2E9E1;
--cpb-outer: 4em;
--cpb-thickness: 0.5em;
}
.progress-circle {
font-size: 20px;
margin: 0;
position: relative; /* so that children can be absolutely positioned */
padding: 0;
width: var(--cpb-outer);
height: var(--cpb-outer);
background-color: var(--cpb-bg-color);
border-radius: 50%;
line-height: var(--cpb-outer);
}
.progress-circle:after{
border: none;
position: absolute;
top: var(--cpb-thickness);
left: var(--cpb-thickness);
text-align: center;
display: block;
border-radius: 50%;
width: calc(var(--cpb-outer) - var(--cpb-thickness) * 2);
height: calc(var(--cpb-outer) - var(--cpb-thickness) * 2);
background-color: white;
content: " ";
}
/* Text inside the control */
.progress-circle span {
position: absolute;
line-height: var(--cpb-outer);
width: var(--cpb-outer);
text-align: center;
display: block;
color: var(--cpb-color);
z-index: 2;
}
.left-half-clipper {
/* a round circle */
border-radius: 50%;
width: var(--cpb-outer);
height: var(--cpb-outer);
position: absolute; /* needed for clipping */
clip: rect(0, var(--cpb-outer), var(--cpb-outer), calc(var(--cpb-outer) / 2)); /* clips the whole left half*/
}
/* when p>50, don't clip left half*/
.progress-circle.over50 .left-half-clipper {
clip: rect(auto,auto,auto,auto);
}
.value-bar {
/*This is an overlayed square, that is made round with the border radius,
then it is cut to display only the left half, then rotated clockwise
to escape the outer clipping path.*/
position: absolute; /*needed for clipping*/
clip: rect(0, calc(var(--cpb-outer) / 2), var(--cpb-outer), 0);
width: var(--cpb-outer);
height: var(--cpb-outer);
border-radius: 50%;
border: 0.45em solid var(--cpb-color); /*The border is 0.35 but making it larger removes visual artifacts */
/*background-color: #4D642D;*/ /* for debug */
box-sizing: border-box;
}
/* Progress bar filling the whole right half for values above 50% */
.progress-circle.over50 .first50-bar {
/*Progress bar for the first 50%, filling the whole right half*/
position: absolute; /*needed for clipping*/
clip: rect(0, var(--cpb-outer), var(--cpb-outer), calc(var(--cpb-outer) / 2));
background-color: var(--cpb-color);
border-radius: 50%;
width: var(--cpb-outer);
height: var(--cpb-outer);
}
.progress-circle:not(.over50) .first50-bar{ display: none; }
/* Progress bar rotation position */
.progress-circle.p0 .value-bar { display: none; }
.progress-circle.p1 .value-bar { transform: rotate(4deg); }
.progress-circle.p2 .value-bar { transform: rotate(7deg); }
.progress-circle.p3 .value-bar { transform: rotate(11deg); }
.progress-circle.p4 .value-bar { transform: rotate(14deg); }
.progress-circle.p5 .value-bar { transform: rotate(18deg); }
.progress-circle.p6 .value-bar { transform: rotate(22deg); }
.progress-circle.p7 .value-bar { transform: rotate(25deg); }
.progress-circle.p8 .value-bar { transform: rotate(29deg); }
.progress-circle.p9 .value-bar { transform: rotate(32deg); }
.progress-circle.p10 .value-bar { transform: rotate(36deg); }
.progress-circle.p11 .value-bar { transform: rotate(40deg); }
.progress-circle.p12 .value-bar { transform: rotate(43deg); }
.progress-circle.p13 .value-bar { transform: rotate(47deg); }
.progress-circle.p14 .value-bar { transform: rotate(50deg); }
.progress-circle.p15 .value-bar { transform: rotate(54deg); }
.progress-circle.p16 .value-bar { transform: rotate(58deg); }
.progress-circle.p17 .value-bar { transform: rotate(61deg); }
.progress-circle.p18 .value-bar { transform: rotate(65deg); }
.progress-circle.p19 .value-bar { transform: rotate(68deg); }
.progress-circle.p20 .value-bar { transform: rotate(72deg); }
.progress-circle.p21 .value-bar { transform: rotate(76deg); }
.progress-circle.p22 .value-bar { transform: rotate(79deg); }
.progress-circle.p23 .value-bar { transform: rotate(83deg); }
.progress-circle.p24 .value-bar { transform: rotate(86deg); }
.progress-circle.p25 .value-bar { transform: rotate(90deg); }
.progress-circle.p26 .value-bar { transform: rotate(94deg); }
.progress-circle.p27 .value-bar { transform: rotate(97deg); }
.progress-circle.p28 .value-bar { transform: rotate(101deg); }
.progress-circle.p29 .value-bar { transform: rotate(104deg); }
.progress-circle.p30 .value-bar { transform: rotate(108deg); }
.progress-circle.p31 .value-bar { transform: rotate(112deg); }
.progress-circle.p32 .value-bar { transform: rotate(115deg); }
.progress-circle.p33 .value-bar { transform: rotate(119deg); }
.progress-circle.p34 .value-bar { transform: rotate(122deg); }
.progress-circle.p35 .value-bar { transform: rotate(126deg); }
.progress-circle.p36 .value-bar { transform: rotate(130deg); }
.progress-circle.p37 .value-bar { transform: rotate(133deg); }
.progress-circle.p38 .value-bar { transform: rotate(137deg); }
.progress-circle.p39 .value-bar { transform: rotate(140deg); }
.progress-circle.p40 .value-bar { transform: rotate(144deg); }
.progress-circle.p41 .value-bar { transform: rotate(148deg); }
.progress-circle.p42 .value-bar { transform: rotate(151deg); }
.progress-circle.p43 .value-bar { transform: rotate(155deg); }
.progress-circle.p44 .value-bar { transform: rotate(158deg); }
.progress-circle.p45 .value-bar { transform: rotate(162deg); }
.progress-circle.p46 .value-bar { transform: rotate(166deg); }
.progress-circle.p47 .value-bar { transform: rotate(169deg); }
.progress-circle.p48 .value-bar { transform: rotate(173deg); }
.progress-circle.p49 .value-bar { transform: rotate(176deg); }
.progress-circle.p50 .value-bar { transform: rotate(180deg); }
.progress-circle.p51 .value-bar { transform: rotate(184deg); }
.progress-circle.p52 .value-bar { transform: rotate(187deg); }
.progress-circle.p53 .value-bar { transform: rotate(191deg); }
.progress-circle.p54 .value-bar { transform: rotate(194deg); }
.progress-circle.p55 .value-bar { transform: rotate(198deg); }
.progress-circle.p56 .value-bar { transform: rotate(202deg); }
.progress-circle.p57 .value-bar { transform: rotate(205deg); }
.progress-circle.p58 .value-bar { transform: rotate(209deg); }
.progress-circle.p59 .value-bar { transform: rotate(212deg); }
.progress-circle.p60 .value-bar { transform: rotate(216deg); }
.progress-circle.p61 .value-bar { transform: rotate(220deg); }
.progress-circle.p62 .value-bar { transform: rotate(223deg); }
.progress-circle.p63 .value-bar { transform: rotate(227deg); }
.progress-circle.p64 .value-bar { transform: rotate(230deg); }
.progress-circle.p65 .value-bar { transform: rotate(234deg); }
.progress-circle.p66 .value-bar { transform: rotate(238deg); }
.progress-circle.p67 .value-bar { transform: rotate(241deg); }
.progress-circle.p68 .value-bar { transform: rotate(245deg); }
.progress-circle.p69 .value-bar { transform: rotate(248deg); }
.progress-circle.p70 .value-bar { transform: rotate(252deg); }
.progress-circle.p71 .value-bar { transform: rotate(256deg); }
.progress-circle.p72 .value-bar { transform: rotate(259deg); }
.progress-circle.p73 .value-bar { transform: rotate(263deg); }
.progress-circle.p74 .value-bar { transform: rotate(266deg); }
.progress-circle.p75 .value-bar { transform: rotate(270deg); }
.progress-circle.p76 .value-bar { transform: rotate(274deg); }
.progress-circle.p77 .value-bar { transform: rotate(277deg); }
.progress-circle.p78 .value-bar { transform: rotate(281deg); }
.progress-circle.p79 .value-bar { transform: rotate(284deg); }
.progress-circle.p80 .value-bar { transform: rotate(288deg); }
.progress-circle.p81 .value-bar { transform: rotate(292deg); }
.progress-circle.p82 .value-bar { transform: rotate(295deg); }
.progress-circle.p83 .value-bar { transform: rotate(299deg); }
.progress-circle.p84 .value-bar { transform: rotate(302deg); }
.progress-circle.p85 .value-bar { transform: rotate(306deg); }
.progress-circle.p86 .value-bar { transform: rotate(310deg); }
.progress-circle.p87 .value-bar { transform: rotate(313deg); }
.progress-circle.p88 .value-bar { transform: rotate(317deg); }
.progress-circle.p89 .value-bar { transform: rotate(320deg); }
.progress-circle.p90 .value-bar { transform: rotate(324deg); }
.progress-circle.p91 .value-bar { transform: rotate(328deg); }
.progress-circle.p92 .value-bar { transform: rotate(331deg); }
.progress-circle.p93 .value-bar { transform: rotate(335deg); }
.progress-circle.p94 .value-bar { transform: rotate(338deg); }
.progress-circle.p95 .value-bar { transform: rotate(342deg); }
.progress-circle.p96 .value-bar { transform: rotate(346deg); }
.progress-circle.p97 .value-bar { transform: rotate(349deg); }
.progress-circle.p98 .value-bar { transform: rotate(353deg); }
.progress-circle.p99 .value-bar { transform: rotate(356deg); }
.progress-circle.p100 .value-bar { transform: rotate(360deg); }

View File

@ -1,5 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import {computed, onMounted} from "vue";
import {
IonButton
} from '@ionic/vue';
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useStore } from "vuex"; import { useStore } from "vuex";
@ -8,6 +11,12 @@ const router = useRouter();
const user = computed(() => store.state.auth.user); const user = computed(() => store.state.auth.user);
const emit = defineEmits(['updateTitle']);
onMounted(() => {
emit('updateTitle', 'Bullpens');
});
const managePlayers = () => { const managePlayers = () => {
router.push({ path: "/players" }); router.push({ path: "/players" });
}; };

View File

@ -10,8 +10,7 @@ import {
IonItem, IonItem,
IonLabel, IonLabel,
IonList, IonList,
IonNote, IonNote
IonProgressBar
} from '@ionic/vue'; } from '@ionic/vue';
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useStore } from "vuex"; import { useStore } from "vuex";
@ -20,6 +19,7 @@ import Bullpen from '@/types/Bullpen';
import BullpenSessionService from "@/services/BullpenSessionService"; import BullpenSessionService from "@/services/BullpenSessionService";
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import PitchType from '@/types/PitchType'; import PitchType from '@/types/PitchType';
import "../assets/css/circular-progress-bar.css";
const store = useStore(); const store = useStore();
const router = useRouter(); const router = useRouter();
@ -63,6 +63,10 @@ const determinePitchTypeName = (id: number): string => {
return pitchType?.name ?? 'Unknown'; return pitchType?.name ?? 'Unknown';
} }
const percentageClass = (value: number) => {
return "p" + value;
};
const ionInfinite = (event: InfiniteScrollCustomEvent) => { const ionInfinite = (event: InfiniteScrollCustomEvent) => {
generateItems(); generateItems();
setTimeout(() => event.target.complete(), 500); setTimeout(() => event.target.complete(), 500);
@ -73,33 +77,45 @@ generateItems();
</script> </script>
<template> <template>
<ion-list inset="false"> <ion-list :inset="false">
<ion-item v-for="(bullpen, index) in items" :key="index" class="custom-item" :button="true" detail="false"> <ion-item v-for="(bullpen, index) in items" :key="index" class="custom-item" :button="true" :detail="false">
<!-- Top-left icon --> <!-- Top-left icon -->
<ion-icon :icon="baseballOutline" slot="start" class="item-icon"></ion-icon> <ion-icon :icon="baseballOutline" slot="start" class="item-icon"></ion-icon>
<ion-label> <ion-label>
<strong>{{formatDate(bullpen.startedAt)}}</strong> <strong>{{formatDate(bullpen.startedAt)}}</strong>
<ion-label class="item-labels"> <ion-label class="item-labels">
<div class="progress-section"> <!-- Horizontale Anordnung der Kreise -->
<div class="top-row"> <div class="circle-container">
<span class="left-label">Strike</span> <div class="circle-wrapper">
<span class="right-label">{{ (0.57 * 100).toFixed(0) }}%</span> <div class="progress-circle" :class="percentageClass(57)">
<span>{{ (0.57 * 100).toFixed(0) }}%</span>
<div class="left-half-clipper">
<div class="first50-bar"></div>
<div class="value-bar"></div>
</div> </div>
<ion-progress-bar :value="0.57" color="success"></ion-progress-bar>
</div> </div>
<div class="progress-section"> <label>Precision</label>
<div class="top-row">
<span class="left-label">Balls</span>
<span class="right-label">{{ (0.33 * 100).toFixed(0) }}%</span>
</div> </div>
<ion-progress-bar :value="0.33" color="warning"></ion-progress-bar> <div class="circle-wrapper">
<div class="progress-circle" :class="percentageClass(77)">
<span>{{ (0.77 * 100).toFixed(0) }}%</span>
<div class="left-half-clipper">
<div class="first50-bar"></div>
<div class="value-bar"></div>
</div> </div>
<div class="progress-section">
<div class="top-row">
<span class="left-label">Precision</span>
<span class="right-label">{{ (0.77 * 100).toFixed(0) }}%</span>
</div> </div>
<ion-progress-bar :value="0.77" color="danger"></ion-progress-bar> <label>Strike</label>
</div>
<div class="circle-wrapper">
<div class="progress-circle" :class="percentageClass(33)">
<span>{{ (0.33 * 100).toFixed(0) }}%</span>
<div class="left-half-clipper">
<div class="first50-bar"></div>
<div class="value-bar"></div>
</div>
</div>
<label>Balls</label>
</div>
</div> </div>
</ion-label> </ion-label>
</ion-label> </ion-label>
@ -108,19 +124,6 @@ generateItems();
<ion-icon color="medium" :icon="chevronForward"></ion-icon> <ion-icon color="medium" :icon="chevronForward"></ion-icon>
</div> </div>
</ion-item> </ion-item>
<!-- <ion-item v-for="(bullpen, index) in items" :key="index">-->
<!-- <ion-icon aria-hidden="true" :icon="baseball" slot="start"></ion-icon>-->
<!-- <ion-label>Bullpen from ({{formatDate(bullpen.startedAt)}})</ion-label>-->
<!-- <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-item>-->
</ion-list> </ion-list>
<ion-infinite-scroll @ionInfinite="ionInfinite"> <ion-infinite-scroll @ionInfinite="ionInfinite">
<ion-infinite-scroll-content></ion-infinite-scroll-content> <ion-infinite-scroll-content></ion-infinite-scroll-content>
@ -166,27 +169,33 @@ ion-label strong{
.item-labels { .item-labels {
width: 100%; width: 100%;
display: flex;
align-items: start;
} }
.progress-section {
margin-bottom: 12px;
}
.top-row { /* Circle progress bar */
padding: 2px 0 0 0; .circle-container {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 6px;
} }
.left-label { .circle-wrapper {
font-weight: normal; flex: 1;
font-size: 16px; text-align: center;
margin: 10px 10px 10px 0;
display: flex; /* Flexbox für vertikale Ausrichtung */
flex-direction: column; /* Elemente übereinander anordnen */
justify-content: center; /* Vertikale Zentrierung */
align-items: center; /* Horizontale Zentrierung */
} }
.right-label { .circle-wrapper label {
font-weight: normal; display: block;
font-size: 14px; font-size: 12px;
font-weight: bold;
color: var(--ion-color-medium);
margin-top: 8px;
} }
</style> </style>