Init
This commit is contained in:
129
components/ExerciseChart.vue
Normal file
129
components/ExerciseChart.vue
Normal 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
105
components/ExerciseList.vue
Normal 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
81
components/NavBar.vue
Normal 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
111
components/WeightForm.vue
Normal 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>
|
||||
Reference in New Issue
Block a user