Skip to content

Commit

Permalink
Add a daily agenda to front page
Browse files Browse the repository at this point in the history
  • Loading branch information
ajuvonen committed Mar 22, 2024
1 parent ae808ae commit 9886e17
Show file tree
Hide file tree
Showing 10 changed files with 341 additions and 25 deletions.
48 changes: 48 additions & 0 deletions src/components/DailyAgenda.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script lang="ts" setup>
import {computed} from 'vue';
import {storeToRefs} from 'pinia';
import {DateTime} from 'luxon';
import {useScheduleStore} from '@/stores/schedule';
import TrainingCard from '@/components/TrainingCard.vue';
const scheduleStore = useScheduleStore();
const {weeks, settings} = storeToRefs(scheduleStore);
const dailyTrainings = computed(() => {
const startDate = DateTime.fromJSDate(settings.value.startDate!);
const difference = Math.ceil(startDate.diffNow('weeks').weeks);
if (difference < 1) {
const targetWeek = weeks.value[Math.abs(difference)];
if (targetWeek) {
const targetDay = (DateTime.now().weekday - (settings.value.startsOnSunday ? 0 : 1)) % 7;
return targetWeek.trainings.filter(({dayIndex}) => dayIndex === targetDay);
}
}
return [];
});
</script>
<template>
<div class="daily-agenda has-scroll mt-10 d-flex">
<TrainingCard
v-for="training in dailyTrainings"
:key="training.id"
:training="training"
simple
/>
<p v-if="!dailyTrainings.length" class="mx-auto">{{ $t('home.noTrainings') }}</p>
</div>
</template>
<style lang="scss" scoped>
.daily-agenda {
gap: 1rem;
overflow-x: scroll;
}
.training-card__container:first-child {
margin-left: auto;
}
.training-card__container:last-child {
margin-right: auto;
}
</style>
12 changes: 8 additions & 4 deletions src/components/TrainingCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ import TrainingCardRating from '@/components/TrainingCardRating.vue';
import TrainingCardInstructions from '@/components/TrainingCardInstructions.vue';
import {useAppStateStore} from '@/stores/appState';
const props = defineProps<{
const props = withDefaults(defineProps<{
training: Training;
}>();
simple?: boolean;
}>(), {
simple: false,
});
const {settings} = storeToRefs(useScheduleStore());
Expand All @@ -39,7 +42,7 @@ const {isSmallScreen} = useScreen();
}"
>
<v-card-title class="ml-10 training-card__title">
<v-icon icon="mdi-drag-vertical-variant" size="normal" />
<v-icon v-if="!simple" icon="mdi-drag-vertical-variant" size="normal" />
<span class="text-truncate">{{
training.title || $t(`activities.${training.activity}`)
}}</span>
Expand Down Expand Up @@ -75,7 +78,7 @@ const {isSmallScreen} = useScreen();
</div>
<TrainingCardRating :training="training" :disabled="isDescriptionOpen" />
</v-card-text>
<TrainingCardActions :training="training" :disabled="isDescriptionOpen" />
<TrainingCardActions :training="training" :disabled="isDescriptionOpen" :simple="simple" />
<TrainingCardInstructions :training="training" :show="isDescriptionOpen" />
</v-card>
</div>
Expand All @@ -85,6 +88,7 @@ const {isSmallScreen} = useScreen();
background-size: cover !important;
border-radius: 4px;
width: fit-content;
height: fit-content;
}
.training-card {
Expand Down
15 changes: 14 additions & 1 deletion src/components/TrainingCardActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import TrainingCardActionsMenuGroup from '@/components/TrainingCardActionsMenuGr
const props = defineProps<{
training: Training;
disabled: boolean;
simple: boolean;
}>();
const menuOpen = ref(false);
Expand Down Expand Up @@ -43,7 +44,19 @@ watch(
@click="toggleShowInstructions(training.id)"
>{{ $t('general.more') }}</v-btn
>
<v-menu v-model="menuOpen" location="top center" :close-on-content-click="false">
<v-btn
v-if="simple"
:prepend-icon="training.completed ? 'mdi-progress-alert' : 'mdi-progress-check'"
:disabled="disabled"
class="training-card__complete-button"
@click="toggleCompletion(training)"
>{{
$t(
!training.completed ? 'trainingCard.completeTraining' : 'trainingCard.uncompleteTraining',
)
}}</v-btn
>
<v-menu v-else v-model="menuOpen" location="top center" :close-on-content-click="false">
<template v-slot:activator="{props}">
<v-btn
:aria-label="$t('trainingCard.actionsLabel', training.activity)"
Expand Down
49 changes: 49 additions & 0 deletions src/components/__tests__/DailyAgenda.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {mount} from '@vue/test-utils';
import {describe, it, expect, beforeEach} from 'vitest';
import {v4 as uuid} from 'uuid';
import {DateTime} from 'luxon';
import DailyAgenda from '@/components/DailyAgenda.vue';
import {useScheduleStore} from '@/stores/schedule';
import {getEmptyTraining} from '@/utils';

describe('DailyAgenda', () => {
let scheduleStore: ReturnType<typeof useScheduleStore>;

beforeEach(() => {
scheduleStore = useScheduleStore();
});

it('displays training data', () => {
const weekId = uuid();
const todayIndex = DateTime.now().weekday - 1;
scheduleStore.settings.startDate = DateTime.now().startOf('week').toJSDate();
scheduleStore.weeks.push({
id: weekId,
trainings: [
getEmptyTraining({
activity: 'boxing',
dayIndex: todayIndex,
weekId,
}),
getEmptyTraining({
activity: 'swimming',
dayIndex: todayIndex,
weekId,
}),
getEmptyTraining({
dayIndex: (todayIndex + 1) % 7,
weekId,
}),
getEmptyTraining({
dayIndex: (todayIndex + 2) % 7,
weekId,
}),
],
});
const wrapper = mount(DailyAgenda);

expect(wrapper.findAll('.training-card__title').length).toBe(2);
expect(wrapper.findAll('.training-card__title')[0].text()).toBe('Boxing');
expect(wrapper.findAll('.training-card__title')[1].text()).toBe('Swimming');
});
});
29 changes: 29 additions & 0 deletions src/components/__tests__/HomeView.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {it, expect, describe, beforeEach} from 'vitest';
import {mount} from '@vue/test-utils';
import {DateTime} from 'luxon';
import {v4 as uuid} from 'uuid';
import {useScheduleStore} from '@/stores/schedule';
import HomeView from '@/views/HomeView.vue';

describe('HomeView', () => {
let scheduleStore: ReturnType<typeof useScheduleStore>;

beforeEach(() => {
scheduleStore = useScheduleStore();
});

it('mounts', () => {
const wrapper = mount(HomeView);
expect(wrapper.html()).toMatchSnapshot();
});

it('mounts with agenda', () => {
scheduleStore.weeks.push({
id: uuid(),
trainings: [],
});
scheduleStore.settings.startDate = DateTime.now().startOf('week').toJSDate();
const wrapper = mount(HomeView);
expect(wrapper.html()).toMatchSnapshot();
});
});
10 changes: 10 additions & 0 deletions src/components/__tests__/TrainingCard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ describe('TrainingCard', () => {
expect(wrapper.html()).toMatchSnapshot();
});

it('mounts in simple mode', () => {
const wrapper = mount(TrainingCard, {
props: {
training: basicTraining,
simple: true,
},
});
expect(wrapper.html()).toMatchSnapshot();
});

it('shows basic data', () => {
const wrapper = mount(TrainingCard, {
props: {
Expand Down
35 changes: 32 additions & 3 deletions src/components/__tests__/TrainingCardActions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('TrainingCardActions', () => {
props: {
training: getEmptyTraining(),
disabled: false,
simple: false,
},
});
expect(wrapper.html()).toMatchSnapshot();
Expand All @@ -29,8 +30,9 @@ describe('TrainingCardActions', () => {
it('actions work', async () => {
const wrapper = mount(TrainingCardActions, {
props: {
training: getEmptyTraining({instructions: 'Test Instructions'}),
training: getEmptyTraining(),
disabled: false,
simple: false,
},
});
await wrapper.find('.training-card__action-button').trigger('click');
Expand All @@ -49,21 +51,48 @@ describe('TrainingCardActions', () => {
props: {
training: getEmptyTraining({id: trainingId, instructions: 'Test Instructions'}),
disabled: false,
simple: false,
},
});
expect(wrapper.find('.training-card__instructions').exists()).toBe(false);
await wrapper.find('.training-card__more-button').trigger('click');
expect(appStateStore.toggleShowInstructions).toHaveBeenCalledWith(trainingId);
});

it('disabled prop works', async () => {
it('disabled prop works', () => {
const wrapper = mount(TrainingCardActions, {
props: {
training: getEmptyTraining({instructions: 'Test Instructions'}),
disabled: true,
simple: false,
},
});
expect(wrapper.findAll('button:not([disabled])').length).toBe(0);
expect(wrapper.findAll('button[disabled]').length).toBe(2);
});

it('disabled and simple props work', () => {
const wrapper = mount(TrainingCardActions, {
props: {
training: getEmptyTraining({instructions: 'Test Instructions'}),
disabled: true,
simple: true,
},
});
expect(wrapper.findAll('button:not([disabled])').length).toBe(0);
expect(wrapper.findAll('button[disabled]').length).toBe(2);
})
});

it('simple card can be completed', async () => {
const wrapper = mount(TrainingCardActions, {
props: {
training: getEmptyTraining(),
disabled: false,
simple: true,
},
});

await wrapper.find('.training-card__complete-button').trigger('click');
expect(scheduleStore.toggleCompletion).toHaveBeenCalledOnce();
});
});
33 changes: 33 additions & 0 deletions src/components/__tests__/__snapshots__/HomeView.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`HomeView > mounts 1`] = `
"<div data-v-b4e148ca="" class="home__overlay text-body-1 text-grey-lighten-5">
<h1 data-v-b4e148ca="" class="text-h3 text-center">Let's GetFit </h1>
<p data-v-b4e148ca="" class="mt-10">GetFit is a free, easy-to-use exercise program planner that allows you to plan a schedule spanning multiple weeks.</p>
<p data-v-b4e148ca="" class="mt-4">To get started, navigate to settings and fill some basic info. Then, go to your schedule and start planning activities.</p>
<p data-v-b4e148ca="" class="mt-4">If you give your schedule a starting date, a daily agenda will appear on this screen. All data persists in your browser's local storage only, and is not sent anywhere.</p>
<p data-v-b4e148ca="" class="mt-10"><i data-v-b4e148ca="" class="mdi-github mdi v-icon notranslate v-theme--light v-icon--size-default" aria-hidden="true"></i><a data-v-b4e148ca="" class="text-grey-lighten-5 ml-2" href="https://www.github.com/ajuvonen/getfit">GitHub</a></p><button data-v-b4e148ca="" type="button" class="v-btn v-theme--light v-btn--density-default v-btn--size-x-small v-btn--variant-text home__photo-credit"><span class="v-btn__overlay"></span><span class="v-btn__underlay"></span>
<!----><span class="v-btn__content" data-no-activator="">Photo credits</span>
<!---->
<!---->
</button>
<!---->
<!---->
</div>"
`;

exports[`HomeView > mounts with agenda 1`] = `
"<div data-v-b4e148ca="" class="home__overlay text-body-1 text-grey-lighten-5">
<h1 data-v-b4e148ca="" class="text-h3 text-center">Today:</h1>
<div data-v-c4ee72cd="" data-v-b4e148ca="" class="daily-agenda has-scroll mt-10 d-flex">
<p data-v-c4ee72cd="" class="mx-auto">No trainings. Enjoy your day off!</p>
</div>
<p data-v-b4e148ca="" class="mt-10"><i data-v-b4e148ca="" class="mdi-github mdi v-icon notranslate v-theme--light v-icon--size-default" aria-hidden="true"></i><a data-v-b4e148ca="" class="text-grey-lighten-5 ml-2" href="https://www.github.com/ajuvonen/getfit">GitHub</a></p><button data-v-b4e148ca="" type="button" class="v-btn v-theme--light v-btn--density-default v-btn--size-x-small v-btn--variant-text home__photo-credit"><span class="v-btn__overlay"></span><span class="v-btn__underlay"></span>
<!----><span class="v-btn__content" data-no-activator="">Photo credits</span>
<!---->
<!---->
</button>
<!---->
<!---->
</div>"
`;
Loading

0 comments on commit 9886e17

Please sign in to comment.