This commit is contained in:
2023-06-13 11:39:18 +02:00
commit 872c089e4b
27 changed files with 10246 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
<script setup>
import Chart from 'chart.js/auto'
const props = defineProps({
muscle: {
type: String,
required: true,
},
exercise: {
type: String,
required: true,
},
fromDate: {
type: String,
required: true,
},
toDate: {
type: String,
required: true,
},
})
const weightInput = useWeightInputStore()
// Convert fromDate and toDate to Date objects
const fromDate = computed(() => new Date(props.fromDate))
const toDate = computed(() => new Date(props.toDate))
// Retrieve exercise data based on the date range
const exerciseData = weightInput.exercises
// Filter the exercise data within the specified date range
const filteredData = computed(() =>
Object.entries(exerciseData)
.filter(([date]) => {
const currentDate = new Date(date)
return currentDate >= fromDate.value && currentDate <= toDate.value
})
.map(([, muscleData]) => muscleData[props.muscle]?.[props.exercise])
.filter(Boolean),
)
// Prepare the chart data
const chartData = computed(() =>
filteredData.value.map((exercise, index) => {
const date = new Date(Object.keys(exerciseData)[index])
const formattedDate = `${date.getMonth() + 1}/${date.getDate()}`
const maxWarmUpSetWeight = Math.max(
...exercise.warmUpSet.map(set => set.warmSetWeight),
)
const maxWorkingSetWeight = Math.max(
...exercise.workingSet.map(set => set.workingSetWeight),
)
return {
date: formattedDate,
warmUpSet: isNaN(maxWarmUpSetWeight) ? 0 : maxWarmUpSetWeight,
workingSet: isNaN(maxWorkingSetWeight) ? 0 : maxWorkingSetWeight,
}
}),
)
const labels = computed(() => chartData.value.map(data => data.date))
const warmUpSetWeights = computed(() => chartData.value.map(data => data.warmUpSet))
const workingSetWeights = computed(() => chartData.value.map(data => data.workingSet))
const chartOptions = computed(() => ({
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
suggestedMax: Math.max(
Math.max(...warmUpSetWeights.value),
Math.max(...workingSetWeights.value),
),
},
},
}))
const chartConfig = computed(() => ({
type: 'line',
data: {
labels: labels.value,
datasets: [
{
label: 'Warm-Up Sets',
data: warmUpSetWeights.value,
fill: false,
borderColor: 'rgba(75, 192, 192, 1)',
},
{
label: 'Working Sets',
data: workingSetWeights.value,
fill: false,
borderColor: 'rgba(192, 75, 192, 1)',
},
],
},
options: chartOptions.value,
}))
const exerciseChart = ref(null)
let chartInstance = null
onMounted(() => {
const ctx = exerciseChart.value.getContext('2d')
chartInstance = new Chart(ctx, chartConfig.value)
})
watch([filteredData, chartOptions], () => {
if (chartInstance) {
chartInstance.data.labels = labels.value
chartInstance.data.datasets[0].data = warmUpSetWeights.value
chartInstance.data.datasets[1].data = workingSetWeights.value
chartInstance.options.scales.y.suggestedMax = Math.max(
Math.max(...warmUpSetWeights.value),
Math.max(...workingSetWeights.value),
)
chartInstance.update()
}
})
</script>
<template>
<div>
<canvas ref="exerciseChart" :style="{ height: '400px', width: '600px' }" />
</div>
</template>

105
components/ExerciseList.vue Normal file
View File

@@ -0,0 +1,105 @@
<script setup>
const props = defineProps({
muscle: String,
})
const exerciseStore = useExerciseStore()
const weightInput = useWeightInputStore()
const exercises = ref([])
const selectedExercise = ref(null)
const selectedDate = ref(new Date().toISOString().substr(0, 10))
onMounted(() => {
exercises.value = exerciseStore.getExercisesByMuscle(props.muscle)
},
)
// filter exercises with help from https://blog.logrocket.com/create-search-bar-vue/ last accessed 05.05.2023
const input = ref('')
function filterExercises() {
return exercises.value.filter((exercise) => {
return exercise.name.toLowerCase().includes(input.value.toLowerCase())
})
}
function isActiveExercise(exercise) {
// TODO: Implement (exercise in weightInput.exercises) to show which exercise are edited, or put them in a proper seperate list
return { 'exercise-list-button exerciseItem': selectedExercise.value !== exercise, 'exercise-list-button-active exerciseItem': selectedExercise.value === exercise }
}
function exerciseClick(exercise) {
selectedExercise.value = exercise.name
}
function initSetInput() {
weightInput.initSetsInputs(selectedDate.value, props.muscle, selectedExercise.value)
}
</script>
<template>
<div class="flex flex-col">
<div class="flex">
<div class="w-1/4">
<input
v-model="input"
class="text-black py-2 px-2 mt-5 rounded-md mb-4 w-full"
type="text"
placeholder="Search..."
>
</div>
<div class="w-3/4 flex justify-end">
<input
id="date"
v-model="selectedDate"
type="date"
class="text-black py-2 px-2 mt-5 rounded-md mb-4 w-2/4"
>
</div>
</div>
<div>
<div class="flex flex-col">
<div class="flex flex-row">
<div class="w-1/4">
<button v-for="exercise in filterExercises()" :key="exercise.name" :class="isActiveExercise(exercise.name)" @click="exerciseClick(exercise); initSetInput()">
{{ exercise.name }}
</button>
</div>
<div v-if="selectedExercise" class="w-3/4">
<WeightForm :date="selectedDate" :muscle="muscle" :selected-exercise="selectedExercise" />
</div>
</div>
</div>
</div>
</div>
</template>
<style>
.exercise-list-button {
@apply bg-green-500 hover:bg-green-400 text-white font-bold py-2 px-2 rounded my-1 w-full;
}
.exercise-list-button-active {
@apply bg-green-600 text-white font-bold py-2 px-2 rounded mb-1 w-full;
}
.weights{
display: flex;
flex-direction: row;
gap: 0.25rem;
margin-top: 0.5rem;
margin-left: 5rem;
color: white;
}
.set{
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 0.25rem;
margin-top: 0.5rem;
color: white;
}
</style>

81
components/NavBar.vue Normal file
View File

@@ -0,0 +1,81 @@
<script setup lang="ts">
const isNavOpen = ref(false)
const exerciseStore = useExerciseStore()
const route = useRoute()
const muscleGroups = exerciseStore.getAllMuscles()
</script>
<template>
<div class="min-h-full">
<div class="pb-32 bg-gray-800">
<nav class="bg-gray-800">
<div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
<div class="relative flex h-16 items-center justify-between">
<div class="absolute inset-y-0 left-0 flex items-center sm:hidden">
<!-- Mobile menu button -->
<button type="button" class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" aria-controls="mobile-menu" aria-expanded="false">
<span class="sr-only">Open main menu</span>
<!--
Icon when menu is closed.
Menu open: "hidden", Menu closed: "block"
-->
<svg v-show="isNavOpen === false" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" @click="isNavOpen = true">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
<!--
Icon when menu is open.
Menu open: "block", Menu closed: "hidden"
-->
<svg v-show="isNavOpen === true" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" @click="isNavOpen = false;">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
<div class="flex flex-shrink-0 items-center">
<img class="block h-8 w-auto lg:hidden" src="#" alt="Logo">
<img class="hidden h-8 w-auto lg:block" src="#" alt="Logo">
</div>
<div class="hidden sm:ml-6 sm:block">
<div class="flex space-x-4">
<!-- Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" -->
<NuxtLink to="/" active-class="bg-gray-900 text-white" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium" aria-current="page">
Dashboard
</NuxtLink>
<NuxtLink v-for="muscle in muscleGroups" :key="muscle" :to="`/muscles/${muscle.toLowerCase()}`" active-class="bg-gray-900 text-white" class="text-gray-300 hover:bg-gray-700 hover:text-white rounded-md px-3 py-2 text-sm font-medium">
{{ muscle }}
</NuxtLink>
</div>
</div>
</div>
</div>
</div>
<!-- Mobile menu, show/hide based on menu state. -->
<div v-if="isNavOpen" id="mobile-menu" class="sm:hidden">
<div class="space-y-1 px-2 pb-3 pt-2">
<!-- Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" -->
<a href="#" class="bg-gray-900 text-white block rounded-md px-3 py-2 text-base font-medium" aria-current="page">Dashboard</a>
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium">Team</a>
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium">Projects</a>
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white block rounded-md px-3 py-2 text-base font-medium">Calendar</a>
</div>
</div>
</nav>
<header class="py-10 bg-gray-800">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold tracking-tight text-white">
{{ route.meta.name }}
</h1>
</div>
</header>
</div>
</div>
</template>
<style scoped>
</style>

111
components/WeightForm.vue Normal file
View File

@@ -0,0 +1,111 @@
<script setup lang="ts">
import { defineProps, ref } from 'vue'
const props = defineProps({
date: String,
muscle: String,
selectedExercise: String,
})
const weightInput = useWeightInputStore()
weightInput.selectedExercise = ref(props.selectedExercise)
const addWorkingSet = () => weightInput.addWorkingSet(props.date, props.muscle, props.selectedExercise)
const removeWorkingSet = () => weightInput.removeWorkingSet(props.date, props.muscle, props.selectedExercise)
const getWorkingSetCount = weightInput.getWorkingSetCount(props.date, props.muscle, props.selectedExercise)
const addWarmUpSet = () => weightInput.addWarmUpSet(props.date, props.muscle, props.selectedExercise)
const removeWarmUpSet = () => weightInput.removeWarmUpSet(props.date, props.muscle, props.selectedExercise)
const getWarmUpSetCount = weightInput.getWarmUpSetCount(props.date, props.muscle, props.selectedExercise)
function restrictToNumbers(event) {
const charCode = event.which ? event.which : event.keyCode
if (charCode > 31 && (charCode < 48 || charCode > 57))
event.preventDefault()
}
</script>
<template>
<div class="w-2/3 mx-auto">
<div class="mt-2">
<label>Warm-Up Sets</label>
<button
class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-2 rounded ml-5"
@click="addWarmUpSet"
>
Add
</button>
<button
:disabled="getWarmUpSetCount <= 1"
class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-2 rounded ml-5 disabled:opacity-25"
@click="removeWarmUpSet"
>
Remove
</button>
<div
v-for="(warmUpSet, warmUpSetCount) in weightInput.exercises[date][muscle][selectedExercise].warmUpSet"
:key="warmUpSetCount"
class="flex ml-2"
>
<label>Set {{ warmUpSetCount + 1 }}</label>
<div class="ml-3">
<input
v-model="warmUpSet.warmSetWeight"
type="number"
class="mt-1 px-3 py-2 border shadow-sm weightInput.exercises[ focus:outline-none block rounded-md sm:text-sm"
placeholder="Weight (kg)"
@keypress="restrictToNumbers"
>
</div>
<div class="ml-3">
<input
v-model="warmUpSet.warmSetReps"
type="number"
class="mt-1 px-3 py-2 border shadow-sm weightInput.exercises[ focus:outline-none block rounded-md sm:text-sm"
placeholder="Reps"
@keypress="restrictToNumbers"
>
</div>
</div>
</div>
<div class="working-set mt-5">
<label>Working Sets</label>
<button
class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-2 rounded ml-7"
@click="addWorkingSet"
>
Add
</button>
<button
:disabled="getWorkingSetCount <= 1"
class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-2 rounded ml-5 disabled:opacity-25"
@click="removeWorkingSet"
>
Remove
</button>
<div v-for="(workingset, workingSetCount) in weightInput.exercises[date][muscle][selectedExercise].workingSet" :key="workingSetCount" class="item flex justify-smart mt-1">
<label class="">{{ workingSetCount + 1 }}. Set</label>
<div class="ml-3">
<input
v-model="workingset.workingSetWeight"
type="number"
class="mt-1 px-3 py-2 border shadow-sm weightInput.exercises[ focus:outline-none block rounded-md sm:text-sm"
placeholder="Weight (Kg)"
@keypress="restrictToNumbers"
>
</div>
<div class="ml-3">
<input
v-model="workingset.workingSetReps"
type="number"
class="mt-1 px-3 py-2 border shadow-sm focus:outline-none block rounded-md sm:text-sm"
placeholder="Reps"
@keypress="restrictToNumbers"
>
</div>
</div>
</div>
</div>
</template>