From 47034e111c4584b2cb6d504c5a0e65837f00fa36 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 4 Sep 2026 10:35:06 +0200 Subject: [PATCH 1/2] fix: List item UI improvements --- src/api/__tests__/lists.spec.ts | 14 ++-- src/api/lists.ts | 21 ++--- src/components/ConfirmDeleteModal.vue | 51 ++++++++++++ src/components/DeleteListItemModal.vue | 31 ++++++++ src/components/DeleteListModal.vue | 37 ++------- src/components/ListItemRow.vue | 43 ++++++++-- .../__tests__/ConfirmDeleteModal.spec.ts | 79 +++++++++++++++++++ .../__tests__/DeleteListItemModal.spec.ts | 50 ++++++++++++ src/components/__tests__/ListItemRow.spec.ts | 70 +++++++++++++++- src/database/db.ts | 4 +- src/stores/__tests__/lists.spec.ts | 15 ++-- src/stores/lists.ts | 24 +++--- src/types/list.ts | 10 +-- src/views/__tests__/ListDetailView.spec.ts | 11 ++- 14 files changed, 374 insertions(+), 86 deletions(-) create mode 100644 src/components/ConfirmDeleteModal.vue create mode 100644 src/components/DeleteListItemModal.vue create mode 100644 src/components/__tests__/ConfirmDeleteModal.spec.ts create mode 100644 src/components/__tests__/DeleteListItemModal.spec.ts diff --git a/src/api/__tests__/lists.spec.ts b/src/api/__tests__/lists.spec.ts index 4e3accf..0233853 100644 --- a/src/api/__tests__/lists.spec.ts +++ b/src/api/__tests__/lists.spec.ts @@ -5,7 +5,7 @@ import { createListApi, getListItemsApi, createListItemApi, - updateListItemApi, + updateListItemTitleApi, setListItemCompletedApi, addUserToListApi, removeUserFromListApi, @@ -77,7 +77,7 @@ describe('lists API', () => { expect(result).toEqual(mockItems) }) - it('setListItemCompletedApi sends POST to /lists/items/{id}', async () => { + it('setListItemCompletedApi sends POST to /lists/items/{id}/complete', async () => { const fetchMock = vi.fn().mockResolvedValueOnce({ ok: true, status: 204, @@ -87,7 +87,7 @@ describe('lists API', () => { await setListItemCompletedApi('item-1', { is_completed: true }) expect(fetchMock).toHaveBeenCalledWith( - `${API_BASE_URL}/lists/items/item-1`, + `${API_BASE_URL}/lists/items/item-1/complete`, expect.objectContaining({ method: 'POST' }), ) }) @@ -137,18 +137,18 @@ describe('lists API', () => { expect(result).toEqual(mockItem) }) - it('updateListItemApi sends PUT to /lists/items', async () => { + it('updateListItemTitleApi sends POST to /lists/items/{id}/title', async () => { const fetchMock = vi.fn().mockResolvedValueOnce({ ok: true, status: 204, } as unknown as Response) global.fetch = fetchMock - await updateListItemApi({ list_item_id: 'item-1', is_completed: true }) + await updateListItemTitleApi('item-1', { title: 'Free-range eggs' }) expect(fetchMock).toHaveBeenCalledWith( - `${API_BASE_URL}/lists/items`, - expect.objectContaining({ method: 'PUT' }), + `${API_BASE_URL}/lists/items/item-1/title`, + expect.objectContaining({ method: 'POST' }), ) }) diff --git a/src/api/lists.ts b/src/api/lists.ts index 2451165..5999e2a 100644 --- a/src/api/lists.ts +++ b/src/api/lists.ts @@ -5,8 +5,8 @@ import type { ListItem, CreateListPayload, CreateListItemPayload, - UpdateListItemPayload, SetListItemCompletedPayload, + SetListItemTitlePayload, AddUserToListPayload, RemoveUserFromListPayload, } from '@/types/list' @@ -43,23 +43,26 @@ export async function createListItemApi(payload: CreateListItemPayload): Promise return response.json() } -export async function updateListItemApi(payload: UpdateListItemPayload): Promise { - const response = await apiClient.put('/lists/items', payload) - if (!response.ok) { - throw new Error(await extractErrorMessage(response, 'Failed to update list item')) - } -} - export async function setListItemCompletedApi( itemId: string, payload: SetListItemCompletedPayload, ): Promise { - const response = await apiClient.post(`/lists/items/${itemId}`, payload) + const response = await apiClient.post(`/lists/items/${itemId}/complete`, payload) if (!response.ok) { throw new Error(await extractErrorMessage(response, 'Failed to update list item status')) } } +export async function updateListItemTitleApi( + itemId: string, + payload: SetListItemTitlePayload, +): Promise { + const response = await apiClient.post(`/lists/items/${itemId}/title`, payload) + if (!response.ok) { + throw new Error(await extractErrorMessage(response, 'Failed to update list item title')) + } +} + export async function addUserToListApi(payload: AddUserToListPayload): Promise { const response = await apiClient.post('/lists/user', payload) if (!response.ok) { diff --git a/src/components/ConfirmDeleteModal.vue b/src/components/ConfirmDeleteModal.vue new file mode 100644 index 0000000..99a6b7d --- /dev/null +++ b/src/components/ConfirmDeleteModal.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/src/components/DeleteListItemModal.vue b/src/components/DeleteListItemModal.vue new file mode 100644 index 0000000..afd973f --- /dev/null +++ b/src/components/DeleteListItemModal.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/components/DeleteListModal.vue b/src/components/DeleteListModal.vue index 38dc474..a9f8c3f 100644 --- a/src/components/DeleteListModal.vue +++ b/src/components/DeleteListModal.vue @@ -1,8 +1,8 @@ - - diff --git a/src/components/ListItemRow.vue b/src/components/ListItemRow.vue index 78eca80..3455fd2 100644 --- a/src/components/ListItemRow.vue +++ b/src/components/ListItemRow.vue @@ -3,6 +3,7 @@ import { ref } from 'vue' import type { LocalListItem } from '@/database/db' import { useListsStore } from '@/stores/lists' import { useDismissableMenu } from '@/composables/useDismissableMenu' +import DeleteListItemModal from '@/components/DeleteListItemModal.vue' const props = defineProps<{ item: LocalListItem }>() @@ -16,6 +17,7 @@ const editedTitle = ref(props.item.title) // diffing against the live (possibly just-changed) prop and overwriting the // remote edit with the untouched original text. const originalTitle = ref(props.item.title) +const showDeleteModal = ref(false) const { isOpen: isMenuOpen, containerRef: menuContainerRef, @@ -27,6 +29,7 @@ function toggleCompleted() { } function startEditing() { + isMenuOpen.value = false editedTitle.value = props.item.title originalTitle.value = props.item.title isEditing.value = true @@ -35,15 +38,22 @@ function startEditing() { function saveTitle() { const title = editedTitle.value.trim() if (title && title !== originalTitle.value) { - listsStore.updateListItem(props.item.id, { title }) + listsStore.updateListItemTitle(props.item.id, title) } isEditing.value = false } -function handleDelete(event: Event) { - event.preventDefault() - event.stopPropagation() +function handleOpenDelete() { isMenuOpen.value = false + showDeleteModal.value = true +} + +function handleCloseDelete() { + showDeleteModal.value = false +} + +function handleConfirmDelete() { + showDeleteModal.value = false listsStore.deleteListItem(props.item.id) } @@ -68,7 +78,7 @@ function handleDelete(event: Event) { @keyup.escape="isEditing = false" @blur="saveTitle" /> - {{ item.title }} + {{ item.title }} @@ -90,11 +100,25 @@ function handleDelete(event: Event) { + + diff --git a/src/components/__tests__/ConfirmDeleteModal.spec.ts b/src/components/__tests__/ConfirmDeleteModal.spec.ts new file mode 100644 index 0000000..4bc62f9 --- /dev/null +++ b/src/components/__tests__/ConfirmDeleteModal.spec.ts @@ -0,0 +1,79 @@ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import ConfirmDeleteModal from '../ConfirmDeleteModal.vue' + +describe('ConfirmDeleteModal', () => { + it('renders title, description, and default confirm label', () => { + const wrapper = mount(ConfirmDeleteModal, { + props: { + title: 'Delete "Groceries"?', + description: 'Are you sure?', + }, + }) + + expect(wrapper.text()).toContain('Delete "Groceries"?') + expect(wrapper.text()).toContain('Are you sure?') + expect(wrapper.find('.confirm-delete-btn').text()).toBe('Delete') + expect(wrapper.find('.cancel-btn').text()).toBe('Cancel') + }) + + it('renders a custom confirm label', () => { + const wrapper = mount(ConfirmDeleteModal, { + props: { + title: 'Delete "Apples"?', + description: 'Are you sure?', + confirmLabel: 'Delete item', + }, + }) + + expect(wrapper.find('.confirm-delete-btn').text()).toBe('Delete item') + }) + + it('emits confirm event when Delete button is clicked', async () => { + const wrapper = mount(ConfirmDeleteModal, { + props: { + title: 'Delete "Groceries"?', + description: 'Are you sure?', + }, + }) + + await wrapper.find('.confirm-delete-btn').trigger('click') + expect(wrapper.emitted('confirm')).toBeTruthy() + }) + + it('emits close event when Cancel button is clicked', async () => { + const wrapper = mount(ConfirmDeleteModal, { + props: { + title: 'Delete "Groceries"?', + description: 'Are you sure?', + }, + }) + + await wrapper.find('.cancel-btn').trigger('click') + expect(wrapper.emitted('close')).toBeTruthy() + }) + + it('emits close event when close icon button is clicked', async () => { + const wrapper = mount(ConfirmDeleteModal, { + props: { + title: 'Delete "Groceries"?', + description: 'Are you sure?', + }, + }) + + await wrapper.find('.close-btn').trigger('click') + expect(wrapper.emitted('close')).toBeTruthy() + }) + + it('emits close event when clicking overlay background', async () => { + const wrapper = mount(ConfirmDeleteModal, { + props: { + title: 'Delete "Groceries"?', + description: 'Are you sure?', + }, + }) + + await wrapper.find('.modal-overlay').trigger('click') + expect(wrapper.emitted('close')).toBeTruthy() + }) +}) diff --git a/src/components/__tests__/DeleteListItemModal.spec.ts b/src/components/__tests__/DeleteListItemModal.spec.ts new file mode 100644 index 0000000..ef0cda1 --- /dev/null +++ b/src/components/__tests__/DeleteListItemModal.spec.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest' +import { mount } from '@vue/test-utils' +import DeleteListItemModal from '../DeleteListItemModal.vue' +import type { LocalListItem } from '@/database/db' + +describe('DeleteListItemModal', () => { + const sampleItem: LocalListItem = { + id: 'item-1', + list_id: 'list-1', + title: 'Apples', + is_completed: false, + created_at: '2026-08-21T00:00:00.000Z', + modified_at: '2026-08-21T00:00:00.000Z', + } + + it('renders modal with item title and confirmation prompt', () => { + const wrapper = mount(DeleteListItemModal, { + props: { + item: sampleItem, + }, + }) + + expect(wrapper.text()).toContain('Delete "Apples"?') + expect(wrapper.text()).toContain('Are you sure you want to delete this item?') + expect(wrapper.find('.confirm-delete-btn').text()).toBe('Delete item') + expect(wrapper.find('.cancel-btn').text()).toBe('Cancel') + }) + + it('emits confirm event when Delete button is clicked', async () => { + const wrapper = mount(DeleteListItemModal, { + props: { + item: sampleItem, + }, + }) + + await wrapper.find('.confirm-delete-btn').trigger('click') + expect(wrapper.emitted('confirm')).toBeTruthy() + }) + + it('emits close event when Cancel button is clicked', async () => { + const wrapper = mount(DeleteListItemModal, { + props: { + item: sampleItem, + }, + }) + + await wrapper.find('.cancel-btn').trigger('click') + expect(wrapper.emitted('close')).toBeTruthy() + }) +}) diff --git a/src/components/__tests__/ListItemRow.spec.ts b/src/components/__tests__/ListItemRow.spec.ts index 719c5a5..6f4aa06 100644 --- a/src/components/__tests__/ListItemRow.spec.ts +++ b/src/components/__tests__/ListItemRow.spec.ts @@ -47,7 +47,39 @@ describe('ListItemRow', () => { expect(wrapper.find('.submenu-dropdown').exists()).toBe(false) }) - it('deletes item when Delete item is clicked in submenu', async () => { + it('toggles completed state when the title text is clicked', async () => { + const wrapper = mount(ListItemRow, { + props: { + item: sampleItem, + }, + }) + + const listsStore = useListsStore() + const toggleSpy = vi.spyOn(listsStore, 'setListItemCompleted').mockResolvedValue() + + await wrapper.find('.title').trigger('click') + + expect(toggleSpy).toHaveBeenCalledWith('item-1', true) + }) + + it('opens the title editor via the Edit title submenu item', async () => { + const wrapper = mount(ListItemRow, { + props: { + item: sampleItem, + }, + }) + + await wrapper.find('.menu-trigger-btn').trigger('click') + const editBtn = wrapper.find('.submenu-item:not(.submenu-item-danger)') + expect(editBtn.text()).toContain('Edit title') + + await editBtn.trigger('click') + + expect(wrapper.find('.title-input').exists()).toBe(true) + expect(wrapper.find('.submenu-dropdown').exists()).toBe(false) + }) + + it('opens a confirmation modal when Delete item is clicked in submenu', async () => { const wrapper = mount(ListItemRow, { props: { item: sampleItem, @@ -61,10 +93,42 @@ describe('ListItemRow', () => { await wrapper.find('.menu-trigger-btn').trigger('click') expect(wrapper.find('.submenu-dropdown').exists()).toBe(true) - // 2nd click: delete item + // 2nd click: opens confirmation modal, doesn't delete yet await wrapper.find('.submenu-item-danger').trigger('click') - expect(deleteSpy).toHaveBeenCalledWith('item-1') expect(wrapper.find('.submenu-dropdown').exists()).toBe(false) + expect(deleteSpy).not.toHaveBeenCalled() + + const modal = wrapper.findComponent({ name: 'DeleteListItemModal' }) + expect(modal.exists()).toBe(true) + expect(modal.text()).toContain('Delete "Apples"?') + + // Confirm deletion in modal + await modal.find('.confirm-delete-btn').trigger('click') + + expect(deleteSpy).toHaveBeenCalledWith('item-1') + expect(wrapper.findComponent({ name: 'DeleteListItemModal' }).exists()).toBe(false) + }) + + it('cancels item deletion when cancel is clicked in confirmation modal', async () => { + const wrapper = mount(ListItemRow, { + props: { + item: sampleItem, + }, + }) + + const listsStore = useListsStore() + const deleteSpy = vi.spyOn(listsStore, 'deleteListItem').mockResolvedValue() + + await wrapper.find('.menu-trigger-btn').trigger('click') + await wrapper.find('.submenu-item-danger').trigger('click') + + const modal = wrapper.findComponent({ name: 'DeleteListItemModal' }) + expect(modal.exists()).toBe(true) + + await modal.find('.cancel-btn').trigger('click') + + expect(deleteSpy).not.toHaveBeenCalled() + expect(wrapper.findComponent({ name: 'DeleteListItemModal' }).exists()).toBe(false) }) }) diff --git a/src/database/db.ts b/src/database/db.ts index 3ff14df..d66c2f1 100644 --- a/src/database/db.ts +++ b/src/database/db.ts @@ -4,8 +4,8 @@ import type { ListItem, CreateListPayload, CreateListItemPayload, - UpdateListItemPayload, SetListItemCompletedPayload, + SetListItemTitlePayload, } from '@/types/list' export interface LocalList extends List { @@ -36,7 +36,7 @@ interface SyncQueueEntryBase { type SyncOperationPayloads = { createList: CreateListPayload createListItem: CreateListItemPayload - updateListItem: UpdateListItemPayload + updateListItemTitle: SetListItemTitlePayload setListItemCompleted: SetListItemCompletedPayload deleteList: DeleteListPayload deleteListItem: DeleteListItemPayload diff --git a/src/stores/__tests__/lists.spec.ts b/src/stores/__tests__/lists.spec.ts index fd2be49..2b3713b 100644 --- a/src/stores/__tests__/lists.spec.ts +++ b/src/stores/__tests__/lists.spec.ts @@ -109,7 +109,7 @@ const listsApiMocks = vi.hoisted(() => ({ createListApi: vi.fn<() => Promise>(), getListItemsApi: vi.fn<() => Promise>(), createListItemApi: vi.fn<() => Promise>(), - updateListItemApi: vi.fn<() => Promise>(), + updateListItemTitleApi: vi.fn<() => Promise>(), setListItemCompletedApi: vi.fn<() => Promise>(), addUserToListApi: vi.fn<() => Promise>(), removeUserFromListApi: vi.fn<() => Promise>(), @@ -211,29 +211,28 @@ describe('useListsStore', () => { expect(store.error).toBe('Network error') }) - it('updates a list item locally and pushes the change to the server', async () => { + it('updates a list item title locally and pushes the change via the dedicated endpoint', async () => { listsApiMocks.createListItemApi.mockResolvedValueOnce({ id: 'server-item-2', list_id: '', title: 'Eggs', is_completed: false, }) - listsApiMocks.updateListItemApi.mockResolvedValueOnce(undefined) + listsApiMocks.updateListItemTitleApi.mockResolvedValueOnce(undefined) const store = useListsStore() await store.createListItem('list-1', 'Eggs') await store.sync() const created = store.listItems.find((entry) => entry.title === 'Eggs')! - await store.updateListItem(created.id, { is_completed: true }) + await store.updateListItemTitle(created.id, 'Free-range eggs') await store.sync() - expect(listsApiMocks.updateListItemApi).toHaveBeenCalledWith({ - list_item_id: created.id, - is_completed: true, + expect(listsApiMocks.updateListItemTitleApi).toHaveBeenCalledWith(created.id, { + title: 'Free-range eggs', }) const updated = store.listItems.find((entry) => entry.id === created.id) - expect(updated?.is_completed).toBe(true) + expect(updated?.title).toBe('Free-range eggs') expect(updated?.pendingSync).toBe(false) }) diff --git a/src/stores/lists.ts b/src/stores/lists.ts index 3e7cb07..b7f07e8 100644 --- a/src/stores/lists.ts +++ b/src/stores/lists.ts @@ -13,7 +13,7 @@ import { createListApi, getListItemsApi, createListItemApi, - updateListItemApi, + updateListItemTitleApi, setListItemCompletedApi, addUserToListApi, removeUserFromListApi, @@ -180,12 +180,9 @@ export const useListsStore = defineStore('lists', () => { return localItem } - async function updateListItem( - itemId: string, - changes: { title?: string; is_completed?: boolean }, - ) { + async function updateListItemTitle(itemId: string, title: string) { const patch = { - ...changes, + title, modified_at: new Date().toISOString(), pendingSync: true, } @@ -193,8 +190,8 @@ export const useListsStore = defineStore('lists', () => { const existingItem = listItems.value.find((entry) => entry.id === itemId) if (existingItem) Object.assign(existingItem, patch) await enqueue({ - type: 'updateListItem', - payload: { list_item_id: itemId, ...changes }, + type: 'updateListItemTitle', + payload: { title }, localListItemId: itemId, }) scheduleSync() @@ -357,11 +354,10 @@ export const useListsStore = defineStore('lists', () => { } break } - case 'updateListItem': { - await updateListItemApi(entry.payload) - if (entry.localListItemId) { - await markListItemSynced(entry.localListItemId) - } + case 'updateListItemTitle': { + if (!entry.localListItemId) break + await updateListItemTitleApi(entry.localListItemId, entry.payload) + await markListItemSynced(entry.localListItemId) break } case 'setListItemCompleted': { @@ -570,7 +566,7 @@ export const useListsStore = defineStore('lists', () => { refresh, createList, createListItem, - updateListItem, + updateListItemTitle, setListItemCompleted, addUserToList, removeUserFromList, diff --git a/src/types/list.ts b/src/types/list.ts index e65c2c3..b2273e8 100644 --- a/src/types/list.ts +++ b/src/types/list.ts @@ -25,16 +25,14 @@ export interface CreateListItemPayload { title: string } -export interface UpdateListItemPayload { - list_item_id: string - title?: string - is_completed?: boolean -} - export interface SetListItemCompletedPayload { is_completed: boolean } +export interface SetListItemTitlePayload { + title: string +} + export interface AddUserToListPayload { list_id: string email: string diff --git a/src/views/__tests__/ListDetailView.spec.ts b/src/views/__tests__/ListDetailView.spec.ts index bdcd134..46e19a1 100644 --- a/src/views/__tests__/ListDetailView.spec.ts +++ b/src/views/__tests__/ListDetailView.spec.ts @@ -121,7 +121,7 @@ describe('ListDetailView', () => { expect(wrapper.findComponent({ name: 'DeleteListModal' }).exists()).toBe(false) }) - it('allows deleting list items via two clicks on item row menu', async () => { + it('allows deleting list items via menu and confirmation modal', async () => { const listsStore = useListsStore() const deleteItemSpy = vi.spyOn(listsStore, 'deleteListItem').mockResolvedValue() const sampleList: LocalList = { @@ -156,9 +156,16 @@ describe('ListDetailView', () => { const deleteItemBtn = itemRow.find('.submenu-item-danger') expect(deleteItemBtn.exists()).toBe(true) - // 2nd click: delete item + // 2nd click: opens confirmation modal await deleteItemBtn.trigger('click') + const modal = itemRow.findComponent({ name: 'DeleteListItemModal' }) + expect(modal.exists()).toBe(true) + expect(deleteItemSpy).not.toHaveBeenCalled() + + // Confirm deletion in modal + await modal.find('.confirm-delete-btn').trigger('click') + expect(deleteItemSpy).toHaveBeenCalledWith('item-1') }) }) -- 2.54.0 From 7185730a87307cea7eb567e77ba353266d10bb77 Mon Sep 17 00:00:00 2001 From: Robin Dittmar Date: Fri, 4 Sep 2026 12:29:25 +0200 Subject: [PATCH 2/2] feat: Added first iteration of exercises view --- src/api/exercises.ts | 23 +++++ src/components/BottomNav.vue | 18 ++++ src/components/ExerciseCard.vue | 112 +++++++++++++++++++++++ src/router/index.ts | 6 ++ src/types/exercise.ts | 62 +++++++++++++ src/views/ExercisesView.vue | 157 ++++++++++++++++++++++++++++++++ 6 files changed, 378 insertions(+) create mode 100644 src/api/exercises.ts create mode 100644 src/components/ExerciseCard.vue create mode 100644 src/types/exercise.ts create mode 100644 src/views/ExercisesView.vue diff --git a/src/api/exercises.ts b/src/api/exercises.ts new file mode 100644 index 0000000..dc84adc --- /dev/null +++ b/src/api/exercises.ts @@ -0,0 +1,23 @@ +import { apiClient } from '@/api/client' +import { extractErrorMessage } from '@/api/http' +import type { PaginatedExercises } from '@/types/exercise' + +export interface GetExercisesParams { + page?: number + count?: number +} + +export async function getExercisesApi( + params: GetExercisesParams = {}, +): Promise { + const query = new URLSearchParams() + if (params.page !== undefined) query.set('page', String(params.page)) + if (params.count !== undefined) query.set('count', String(params.count)) + const queryString = query.toString() + + const response = await apiClient.get(`/exercises${queryString ? `?${queryString}` : ''}`) + if (!response.ok) { + throw new Error(await extractErrorMessage(response, 'Failed to load exercises')) + } + return response.json() +} diff --git a/src/components/BottomNav.vue b/src/components/BottomNav.vue index 16049a1..371e387 100644 --- a/src/components/BottomNav.vue +++ b/src/components/BottomNav.vue @@ -19,6 +19,24 @@ const listsStore = useListsStore() listsStore.pendingCount }} + + + + + + + + + Exercises + diff --git a/src/components/ExerciseCard.vue b/src/components/ExerciseCard.vue new file mode 100644 index 0000000..66a6106 --- /dev/null +++ b/src/components/ExerciseCard.vue @@ -0,0 +1,112 @@ + + +
+

{{ exercise.name }}

+

{{ exercise.notes }}

+
+ {{ loadLabel }} + {{ metricLabel }} + {{ + equipment + }} +
+
+ + + + + diff --git a/src/router/index.ts b/src/router/index.ts index dd40680..b9c7874 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -18,6 +18,12 @@ const router = createRouter({ meta: { requiresAuth: true }, props: true, }, + { + path: '/exercises', + name: 'exercises', + component: () => import('../views/ExercisesView.vue'), + meta: { requiresAuth: true }, + }, { path: '/login', name: 'login', diff --git a/src/types/exercise.ts b/src/types/exercise.ts new file mode 100644 index 0000000..cbc7aa8 --- /dev/null +++ b/src/types/exercise.ts @@ -0,0 +1,62 @@ +export enum Equipment { + Unknown = 0, + Floor = 1, + Rings = 2, + PullUpBar = 3, + ParallelBars = 4, + LowBar = 5, + Parallettes = 6, + ResistanceBand = 7, +} + +export enum Load { + Unknown = 0, + Bodyweight = 1, + External = 2, +} + +export enum Metric { + Unknown = 0, + Reps = 1, + Seconds = 2, +} + +export const EQUIPMENT_LABELS: Record = { + [Equipment.Unknown]: 'Unknown', + [Equipment.Floor]: 'Floor', + [Equipment.Rings]: 'Rings', + [Equipment.PullUpBar]: 'Pull-up bar', + [Equipment.ParallelBars]: 'Parallel bars', + [Equipment.LowBar]: 'Low bar', + [Equipment.Parallettes]: 'Parallettes', + [Equipment.ResistanceBand]: 'Resistance band', +} + +export const LOAD_LABELS: Record = { + [Load.Unknown]: 'Unknown', + [Load.Bodyweight]: 'Bodyweight', + [Load.External]: 'External', +} + +export const METRIC_LABELS: Record = { + [Metric.Unknown]: 'Unknown', + [Metric.Reps]: 'Reps', + [Metric.Seconds]: 'Seconds', +} + +export interface Exercise { + id: string + name: string + notes?: string + equipment?: Equipment[] + load?: Load + metric?: Metric + tags?: string[] + modified_at?: string +} + +export interface PaginatedExercises { + data: Exercise[] + total: number + count: number +} diff --git a/src/views/ExercisesView.vue b/src/views/ExercisesView.vue new file mode 100644 index 0000000..05422f7 --- /dev/null +++ b/src/views/ExercisesView.vue @@ -0,0 +1,157 @@ + + + + + -- 2.54.0