F9: Small polish batch

- getYouTubeId also recognizes /embed/<id> and /shorts/<id> URLs (test
  renders all three variants as thumbnails on the exercise detail view)
- useListNavigation JSDoc: itemCount is a Ref<number>, not a number
- collectionRepository.getAll: N+1 per-collection count queries folded
  into one LEFT JOIN ... GROUP BY

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David 2026-07-09 15:45:39 +02:00
parent 166cf87e95
commit 0bc20f81d8
4 changed files with 52 additions and 11 deletions

View file

@ -3,9 +3,9 @@ import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
/**
* Provides keyboard navigation for a list of items using arrow keys.
*
* @param {number} itemCount - The number of items in the list
* @param {import('vue').Ref<number>} itemCount - Ref holding the number of items in the list
* @param {Function} onSelect - Callback when Enter is pressed on a focused item (receives index)
* @returns {{ focusedIndex: Ref<number>, onKeydown: Function, focusItem: Function }}
* @returns {{ focusedIndex: import('vue').Ref<number>, onKeydown: Function, focusItem: Function }}
*/
export function useListNavigation(itemCount, onSelect) {
const focusedIndex = ref(-1)

View file

@ -20,15 +20,13 @@ export const collectionRepository = {
},
async getAll() {
const collections = await db.selectAll('SELECT * FROM collection ORDER BY label')
for (const c of collections) {
const row = await db.selectOne(
'SELECT COUNT(*) AS sessionCount FROM collection_session WHERE collection_id = ?',
[c.id],
)
c.sessionCount = row ? row.sessionCount : 0
}
return collections
return db.selectAll(
`SELECT c.*, COUNT(cs.session_id) AS sessionCount
FROM collection c
LEFT JOIN collection_session cs ON cs.collection_id = c.id
GROUP BY c.id
ORDER BY c.label`,
)
},
async getAllWithSessions() {

View file

@ -5,6 +5,8 @@ export function getYouTubeId(url) {
return u.pathname.slice(1)
}
if (u.hostname.includes('youtube.com')) {
const pathMatch = u.pathname.match(/^\/(?:embed|shorts)\/([^/?]+)/)
if (pathMatch) return pathMatch[1]
return u.searchParams.get('v')
}
} catch {

View file

@ -9,6 +9,7 @@ Step 3 — Exercise Management Tests
- test_delete_exercise: Delete an exercise with confirmation
- test_delete_exercise_cancel: Cancelling delete does not remove exercise
- test_delete_exercise_used_in_session: Delete confirm mentions usage; no session_item orphans
- test_exercise_video_embed_and_shorts_urls: embed/shorts YouTube URLs render as thumbnails
- test_search_exercises: Search filters exercises by title
- test_search_no_results: Search with no matches shows empty message
- test_search_clear_button: Clear button empties the search field and restores the full list
@ -172,6 +173,46 @@ def test_create_exercise_full(page: Page, app_url: str):
_clean_exercises(page)
def test_exercise_video_embed_and_shorts_urls(page: Page, app_url: str):
"""YouTube /embed/<id> and /shorts/<id> URLs are recognized like watch
URLs and render as thumbnails on the detail view."""
_go_to_exercises(page, app_url)
_clean_exercises(page)
exercise_id = page.evaluate(
"""async () => {
const uuid = await window.__repos.settings.get('user_uuid');
return window.__repos.exercises.create({
title: 'Video URL Variants',
description: '',
asymmetric: false,
alternate: false,
image_urls: [],
video_urls: [
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
'https://www.youtube.com/embed/dQw4w9WgXcQ',
'https://www.youtube.com/shorts/dQw4w9WgXcQ',
],
default_reps: 10,
default_weight: 0,
created_by: uuid
});
}"""
)
page.goto(f"{app_url}/#/exercises/{exercise_id}")
page.wait_for_selector('[data-testid="exercise-detail-video-0"]', timeout=5000)
for i in range(3):
video = page.locator(f'[data-testid="exercise-detail-video-{i}"]')
expect(video).to_have_class("youtube-thumb")
expect(video.locator("img")).to_have_attribute(
"src", "https://img.youtube.com/vi/dQw4w9WgXcQ/mqdefault.jpg"
)
_clean_exercises(page)
def test_exercise_appears_in_list(page: Page, app_url: str):
"""Created exercise appears in the list view with correct info."""
_go_to_exercises(page, app_url)