From 6472eb8c6ced0a0fc010abcdab8f25cbb534ef60 Mon Sep 17 00:00:00 2001 From: Clawd Agent Date: Mon, 2 Mar 2026 20:38:14 +0100 Subject: [PATCH] feat(05-03): ResearchDisplay component + dark-theme encyclopedia UI - Add ResearchDisplay.jsx: pure presentational component for research data with loading skeleton, accessible error state, and source cards - Refactor ExerciseResearchPanel to delegate rendering to ResearchDisplay (separates fetch/state logic from display) - Add ExerciseEncyclopediaPage.css: full dark-theme stylesheet using CSS variables (--bg-*, --text-*, --accent, --border, --radius-*) replacing the light-theme WorkoutEditPage.css import - Update ExerciseEncyclopediaPage.jsx: new semantic class names, keyboard-accessible card toggle (Enter key + role=button + aria-expanded) - Mobile-responsive at 600px breakpoint Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/ExerciseResearchPanel.jsx | 80 +-- frontend/src/components/ResearchDisplay.jsx | 104 ++++ .../src/pages/ExerciseEncyclopediaPage.css | 565 ++++++++++++++++++ .../src/pages/ExerciseEncyclopediaPage.jsx | 130 ++-- 4 files changed, 751 insertions(+), 128 deletions(-) create mode 100644 frontend/src/components/ResearchDisplay.jsx create mode 100644 frontend/src/pages/ExerciseEncyclopediaPage.css diff --git a/frontend/src/components/ExerciseResearchPanel.jsx b/frontend/src/components/ExerciseResearchPanel.jsx index 3145577..4fdb640 100644 --- a/frontend/src/components/ExerciseResearchPanel.jsx +++ b/frontend/src/components/ExerciseResearchPanel.jsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import ResearchDisplay from './ResearchDisplay' const API_URL = '/api' @@ -33,73 +34,22 @@ function ExerciseResearchPanel({ exerciseId, exerciseName }) {

Research

- {!research && ( - - )} - {research && ( - - )} +
- {loading && ( -
-
- Searching for information on {exerciseName}... -
- )} - - {error && ( -
- {error} - -
- )} - - {research && !loading && ( -
- {research.summary && ( -
-

Summary

-

{research.summary}

-
- )} - - {research.results && research.results.length > 0 && ( -
-

Sources

-
    - {research.results.map((result, i) => ( -
  • - - {result.title} - - {result.snippet && ( -

    {result.snippet}

    - )} -
  • - ))} -
-
- )} -
- )} + setError(null)} + />
) } diff --git a/frontend/src/components/ResearchDisplay.jsx b/frontend/src/components/ResearchDisplay.jsx new file mode 100644 index 0000000..227c5d7 --- /dev/null +++ b/frontend/src/components/ResearchDisplay.jsx @@ -0,0 +1,104 @@ +function ResearchLoadingSkeleton({ exerciseName }) { + return ( +
+ + ) +} + +function ResearchError({ message, onDismiss }) { + return ( +
+ + {message} + {onDismiss && ( + + )} +
+ ) +} + +function ResearchSourceCard({ result, index }) { + return ( +
  • + + {index + 1} + {result.title} + + + {result.snippet && ( +

    {result.snippet}

    + )} +
  • + ) +} + +/** + * ResearchDisplay — pure presentational component. + * + * Props: + * loading {boolean} Show loading skeleton + * error {string} Error message to display + * data {object} Research data: { summary, results } + * name {string} Exercise name (shown during loading) + * onDismiss {function} Clear error callback + */ +function ResearchDisplay({ loading, error, data, name, onDismiss }) { + if (loading) { + return + } + + if (error) { + return + } + + if (!data) return null + + const hasSummary = Boolean(data.summary) + const hasSources = Array.isArray(data.results) && data.results.length > 0 + + return ( +
    + {hasSummary && ( +
    +

    + + Summary +

    +

    {data.summary}

    +
    + )} + + {hasSources && ( +
    +

    + + Sources + {data.results.length} +

    +
      + {data.results.map((result, i) => ( + + ))} +
    +
    + )} + + {!hasSummary && !hasSources && ( +

    No research data found for this exercise.

    + )} +
    + ) +} + +export default ResearchDisplay diff --git a/frontend/src/pages/ExerciseEncyclopediaPage.css b/frontend/src/pages/ExerciseEncyclopediaPage.css new file mode 100644 index 0000000..2a6cdd2 --- /dev/null +++ b/frontend/src/pages/ExerciseEncyclopediaPage.css @@ -0,0 +1,565 @@ +/* ============================================ + EXERCISE ENCYCLOPEDIA — Dark Theme + Uses CSS variables from index.css + ============================================ */ + +/* Page shell */ +.encyclopedia-page { + display: flex; + flex-direction: column; + min-height: 100vh; + background: var(--bg-primary); + color: var(--text-primary); +} + +/* Header */ +.encyclopedia-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--space-4) var(--space-5); + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 10; +} + +.encyclopedia-header h1 { + flex: 1; + text-align: center; + font-size: var(--font-xl); + font-weight: 700; + color: var(--text-primary); + margin: 0; +} + +.encyclopedia-back-btn { + padding: var(--space-2) var(--space-4); + background: var(--bg-card); + color: var(--text-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + font-size: var(--font-sm); + font-weight: 500; + transition: all var(--transition-fast); + min-height: 40px; + display: flex; + align-items: center; + gap: var(--space-1); +} + +.encyclopedia-back-btn:hover { + background: var(--bg-card-hover); + color: var(--text-primary); + border-color: var(--border-hover); +} + +/* Spacer keeps header balanced */ +.encyclopedia-header-spacer { + width: 80px; +} + +/* Main scrollable area */ +.encyclopedia-main { + flex: 1; + overflow-y: auto; + padding: var(--space-4) var(--space-4) var(--space-8); + display: flex; + flex-direction: column; + gap: var(--space-3); + max-width: 720px; + width: 100%; + margin: 0 auto; +} + +/* Search bar */ +.encyclopedia-search-wrap { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: var(--space-4); +} + +.encyclopedia-search { + width: 100%; + padding: var(--space-3) var(--space-4); + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + color: var(--text-primary); + font-size: 16px; + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); + box-sizing: border-box; +} + +.encyclopedia-search::placeholder { + color: var(--text-tertiary); +} + +.encyclopedia-search:hover { + border-color: var(--border-hover); +} + +.encyclopedia-search:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-subtle); +} + +/* State messages */ +.encyclopedia-state { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: var(--space-6) var(--space-4); + text-align: center; + color: var(--text-muted); + font-size: var(--font-sm); +} + +.encyclopedia-error { + background: var(--error-subtle); + border: 1px solid rgba(239, 68, 68, 0.25); + border-radius: var(--radius-lg); + padding: var(--space-4); + color: var(--error); + font-size: var(--font-sm); +} + +/* Exercise list */ +.encyclopedia-list { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +/* Exercise card */ +.exercise-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; + transition: border-color var(--transition-base); +} + +.exercise-card:hover { + border-color: var(--border-hover); +} + +.exercise-card.exercise-card--open { + border-color: var(--accent); + box-shadow: 0 0 0 1px var(--accent-subtle); +} + +.exercise-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: var(--space-4) var(--space-5); + cursor: pointer; + gap: var(--space-3); + user-select: none; +} + +.exercise-card-header:hover .exercise-chevron { + color: var(--accent); +} + +.exercise-card-info { + flex: 1; + min-width: 0; +} + +.exercise-card-info h3 { + margin: 0 0 var(--space-2); + font-size: var(--font-base); + font-weight: 600; + color: var(--text-primary); +} + +.exercise-card-tags { + display: flex; + gap: var(--space-2); + flex-wrap: wrap; + margin-bottom: var(--space-2); +} + +.exercise-tag { + padding: var(--space-1) var(--space-2); + background: var(--bg-elevated); + color: var(--text-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-full); + font-size: var(--font-xs); + font-weight: 500; +} + +.exercise-tag.exercise-tag--difficulty { + background: var(--accent-subtle); + color: var(--accent); + border-color: var(--accent-subtle); +} + +.exercise-card-description { + margin: 0; + font-size: var(--font-sm); + color: var(--text-muted); + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.exercise-chevron { + color: var(--text-muted); + font-size: var(--font-base); + transition: transform var(--transition-fast), color var(--transition-fast); + flex-shrink: 0; + margin-top: 2px; +} + +.exercise-chevron--open { + transform: rotate(180deg); + color: var(--accent); +} + +/* Expanded detail area */ +.exercise-detail { + border-top: 1px solid var(--border); + padding: var(--space-4) var(--space-5); + display: flex; + flex-direction: column; + gap: var(--space-4); + background: var(--bg-elevated); +} + +.exercise-instructions h4 { + margin: 0 0 var(--space-2); + font-size: var(--font-xs); + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.exercise-instructions p { + margin: 0; + font-size: var(--font-sm); + color: var(--text-secondary); + line-height: 1.7; +} + +/* ============================================ + RESEARCH PANEL — Dark Theme + ============================================ */ + +.research-panel { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.research-panel-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.research-panel-title { + margin: 0; + font-size: var(--font-xs); + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.research-btn { + padding: var(--space-2) var(--space-4); + border-radius: var(--radius-md); + font-size: var(--font-sm); + font-weight: 600; + min-height: 36px; + transition: all var(--transition-base); +} + +.btn-primary.research-btn { + background: var(--accent); + color: #fff; + border: none; + box-shadow: 0 2px 8px rgba(255, 107, 74, 0.3); +} + +.btn-primary.research-btn:hover:not(:disabled) { + background: var(--accent-hover); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(255, 107, 74, 0.4); +} + +.btn-secondary.research-btn { + background: var(--bg-elevated); + color: var(--text-secondary); + border: 1px solid var(--border); +} + +.btn-secondary.research-btn:hover:not(:disabled) { + background: var(--bg-card-hover); + color: var(--text-primary); + border-color: var(--border-hover); +} + +.btn-primary.research-btn:disabled, +.btn-secondary.research-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +/* ============================================ + RESEARCH DISPLAY — rd- prefix + ============================================ */ + +/* Loading state */ +.rd-loading { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) 0; + color: var(--text-secondary); + font-size: var(--font-sm); +} + +.rd-spinner { + width: 18px; + height: 18px; + border: 2px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: rd-spin 0.8s linear infinite; + flex-shrink: 0; +} + +@keyframes rd-spin { + to { transform: rotate(360deg); } +} + +.rd-loading-text em { + color: var(--text-primary); + font-style: normal; + font-weight: 500; +} + +/* Error state */ +.rd-error { + display: flex; + align-items: flex-start; + gap: var(--space-3); + background: var(--error-subtle); + border: 1px solid rgba(239, 68, 68, 0.25); + border-radius: var(--radius-md); + padding: var(--space-3) var(--space-4); +} + +.rd-error-icon { + font-size: var(--font-base); + flex-shrink: 0; +} + +.rd-error-message { + flex: 1; + font-size: var(--font-sm); + color: var(--error); + line-height: 1.5; +} + +.rd-dismiss { + background: transparent; + border: none; + color: var(--error); + font-size: var(--font-xl); + line-height: 1; + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.7; + transition: opacity var(--transition-fast); + flex-shrink: 0; +} + +.rd-dismiss:hover { + opacity: 1; +} + +/* Results */ +.rd-results { + display: flex; + flex-direction: column; + gap: var(--space-4); +} + +.rd-section-title { + margin: 0 0 var(--space-3); + font-size: var(--font-sm); + font-weight: 600; + color: var(--text-secondary); + display: flex; + align-items: center; + gap: var(--space-2); +} + +.rd-section-icon { + font-size: var(--font-base); +} + +.rd-count { + margin-left: auto; + background: var(--bg-elevated); + color: var(--text-muted); + border: 1px solid var(--border); + border-radius: var(--radius-full); + padding: 1px var(--space-2); + font-size: var(--font-xs); + font-weight: 600; +} + +/* Summary */ +.rd-summary { + padding: var(--space-4); + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); +} + +.rd-summary-text { + margin: 0; + font-size: var(--font-sm); + color: var(--text-secondary); + line-height: 1.7; +} + +/* Sources */ +.rd-sources-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.rd-source-item { + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); + overflow: hidden; + transition: border-color var(--transition-fast); +} + +.rd-source-item:hover { + border-color: var(--border-hover); +} + +.rd-source-link { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-3) var(--space-4); + text-decoration: none; + color: var(--accent); + transition: background var(--transition-fast); +} + +.rd-source-link:hover { + background: var(--accent-subtle); +} + +.rd-source-index { + width: 20px; + height: 20px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-full); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-xs); + font-weight: 700; + color: var(--text-muted); + flex-shrink: 0; +} + +.rd-source-title { + flex: 1; + font-size: var(--font-sm); + font-weight: 500; + line-height: 1.4; + word-break: break-word; +} + +.rd-source-arrow { + font-size: var(--font-sm); + opacity: 0.6; + flex-shrink: 0; +} + +.rd-source-snippet { + margin: 0; + padding: 0 var(--space-4) var(--space-3); + font-size: var(--font-xs); + color: var(--text-muted); + line-height: 1.6; + border-top: 1px solid var(--border); + padding-top: var(--space-2); +} + +/* Empty state */ +.rd-empty { + margin: 0; + padding: var(--space-4); + text-align: center; + font-size: var(--font-sm); + color: var(--text-muted); + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: var(--radius-md); +} + +/* ============================================ + MOBILE + ============================================ */ + +@media (max-width: 600px) { + .encyclopedia-header { + padding: var(--space-3) var(--space-4); + } + + .encyclopedia-main { + padding: var(--space-3) var(--space-3) var(--space-8); + } + + .exercise-card-header { + padding: var(--space-3) var(--space-4); + } + + .exercise-detail { + padding: var(--space-3) var(--space-4); + } + + .rd-source-link { + padding: var(--space-3); + } + + .rd-source-snippet { + padding: var(--space-2) var(--space-3) var(--space-3); + } +} diff --git a/frontend/src/pages/ExerciseEncyclopediaPage.jsx b/frontend/src/pages/ExerciseEncyclopediaPage.jsx index 66ccf96..95b15cf 100644 --- a/frontend/src/pages/ExerciseEncyclopediaPage.jsx +++ b/frontend/src/pages/ExerciseEncyclopediaPage.jsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' import ExerciseResearchPanel from '../components/ExerciseResearchPanel' -import './WorkoutEditPage.css' +import './ExerciseEncyclopediaPage.css' const API_URL = '/api' @@ -31,96 +31,100 @@ function ExerciseEncyclopediaPage({ onBack }) { ex.name.toLowerCase().includes(search.toLowerCase()) ) + const toggle = (exercise) => + setSelected(prev => (prev?.id === exercise.id ? null : exercise)) + return ( -
    -
    -

    Exercise Encyclopedia

    -
    -
    +
    + -
    -
    +
    +
    setSearch(e.target.value)} className="encyclopedia-search" + aria-label="Search exercises" />
    {loading && ( -
    -

    Loading exercises...

    -
    +
    Loading exercises…
    )} {error && ( -
    - {error} -
    +
    {error}
    )} {!loading && !error && ( -
    +
    {filtered.length === 0 && ( -
    -

    No exercises found.

    -
    +
    No exercises found.
    )} - {filtered.map(exercise => ( -
    + {filtered.map(exercise => { + const isOpen = selected?.id === exercise.id + return (
    setSelected(selected?.id === exercise.id ? null : exercise)} - style={{ cursor: 'pointer' }} + key={exercise.id} + className={`exercise-card${isOpen ? ' exercise-card--open' : ''}`} > -
    -

    {exercise.name}

    -
    - {exercise.difficulty && ( - {exercise.difficulty} - )} - {(exercise.muscle_groups || []).map(mg => ( - {mg} - ))} -
    - {exercise.description && ( -

    - {exercise.description} -

    - )} -
    - - {selected?.id === exercise.id ? '▲' : '▼'} - -
    - - {selected?.id === exercise.id && ( -
    - {exercise.instructions && ( -
    -

    Instructions

    -

    {exercise.instructions}

    +
    toggle(exercise)} + role="button" + aria-expanded={isOpen} + tabIndex={0} + onKeyDown={e => e.key === 'Enter' && toggle(exercise)} + > +
    +

    {exercise.name}

    +
    + {exercise.difficulty && ( + + {exercise.difficulty} + + )} + {(exercise.muscle_groups || []).map(mg => ( + {mg} + ))}
    - )} - + {exercise.description && ( +

    {exercise.description}

    + )} +
    + + ▼ +
    - )} -
    - ))} + + {isOpen && ( +
    + {exercise.instructions && ( +
    +

    Instructions

    +

    {exercise.instructions}

    +
    + )} + +
    + )} +
    + ) + })}
    )} -
    +
    ) }