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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import ResearchDisplay from './ResearchDisplay'
|
||||
|
||||
const API_URL = '/api'
|
||||
|
||||
@@ -33,73 +34,22 @@ function ExerciseResearchPanel({ exerciseId, exerciseName }) {
|
||||
<div className="research-panel">
|
||||
<div className="research-panel-header">
|
||||
<h3 className="research-panel-title">Research</h3>
|
||||
{!research && (
|
||||
<button
|
||||
className="btn btn-primary research-btn"
|
||||
onClick={fetchResearch}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Fetching...' : 'Get Research'}
|
||||
</button>
|
||||
)}
|
||||
{research && (
|
||||
<button
|
||||
className="btn btn-secondary research-btn"
|
||||
onClick={fetchResearch}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Fetching...' : 'Refresh'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`btn ${research ? 'btn-secondary' : 'btn-primary'} research-btn`}
|
||||
onClick={fetchResearch}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Fetching…' : research ? 'Refresh' : 'Get Research'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="research-loading">
|
||||
<div className="research-spinner"></div>
|
||||
<span>Searching for information on {exerciseName}...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="research-error">
|
||||
<span>{error}</span>
|
||||
<button className="btn-close" onClick={() => setError(null)}>×</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{research && !loading && (
|
||||
<div className="research-results">
|
||||
{research.summary && (
|
||||
<div className="research-summary">
|
||||
<h4>Summary</h4>
|
||||
<p>{research.summary}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{research.results && research.results.length > 0 && (
|
||||
<div className="research-sources">
|
||||
<h4>Sources</h4>
|
||||
<ul className="research-sources-list">
|
||||
{research.results.map((result, i) => (
|
||||
<li key={i} className="research-source-item">
|
||||
<a
|
||||
href={result.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="research-source-link"
|
||||
>
|
||||
{result.title}
|
||||
</a>
|
||||
{result.snippet && (
|
||||
<p className="research-source-snippet">{result.snippet}</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ResearchDisplay
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={research}
|
||||
name={exerciseName}
|
||||
onDismiss={() => setError(null)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
function ResearchLoadingSkeleton({ exerciseName }) {
|
||||
return (
|
||||
<div className="rd-loading">
|
||||
<div className="rd-spinner" aria-hidden="true" />
|
||||
<span className="rd-loading-text">
|
||||
Searching for information on <em>{exerciseName}</em>…
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ResearchError({ message, onDismiss }) {
|
||||
return (
|
||||
<div className="rd-error" role="alert">
|
||||
<span className="rd-error-icon" aria-hidden="true">⚠</span>
|
||||
<span className="rd-error-message">{message}</span>
|
||||
{onDismiss && (
|
||||
<button className="rd-dismiss" onClick={onDismiss} aria-label="Dismiss error">
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ResearchSourceCard({ result, index }) {
|
||||
return (
|
||||
<li className="rd-source-item">
|
||||
<a
|
||||
href={result.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rd-source-link"
|
||||
>
|
||||
<span className="rd-source-index">{index + 1}</span>
|
||||
<span className="rd-source-title">{result.title}</span>
|
||||
<span className="rd-source-arrow" aria-hidden="true">↗</span>
|
||||
</a>
|
||||
{result.snippet && (
|
||||
<p className="rd-source-snippet">{result.snippet}</p>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <ResearchLoadingSkeleton exerciseName={name} />
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ResearchError message={error} onDismiss={onDismiss} />
|
||||
}
|
||||
|
||||
if (!data) return null
|
||||
|
||||
const hasSummary = Boolean(data.summary)
|
||||
const hasSources = Array.isArray(data.results) && data.results.length > 0
|
||||
|
||||
return (
|
||||
<div className="rd-results">
|
||||
{hasSummary && (
|
||||
<div className="rd-summary">
|
||||
<h4 className="rd-section-title">
|
||||
<span className="rd-section-icon" aria-hidden="true">📋</span>
|
||||
Summary
|
||||
</h4>
|
||||
<p className="rd-summary-text">{data.summary}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasSources && (
|
||||
<div className="rd-sources">
|
||||
<h4 className="rd-section-title">
|
||||
<span className="rd-section-icon" aria-hidden="true">🔗</span>
|
||||
Sources
|
||||
<span className="rd-count">{data.results.length}</span>
|
||||
</h4>
|
||||
<ul className="rd-sources-list" aria-label="Research sources">
|
||||
{data.results.map((result, i) => (
|
||||
<ResearchSourceCard key={i} result={result} index={i} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasSummary && !hasSources && (
|
||||
<p className="rd-empty">No research data found for this exercise.</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResearchDisplay
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="edit-page">
|
||||
<div className="page-header">
|
||||
<button className="back-btn" onClick={onBack}>
|
||||
<div className="encyclopedia-page">
|
||||
<header className="encyclopedia-header">
|
||||
<button className="encyclopedia-back-btn" onClick={onBack}>
|
||||
← Back
|
||||
</button>
|
||||
<h1>Exercise Encyclopedia</h1>
|
||||
<div style={{ width: 70 }} />
|
||||
</div>
|
||||
<div className="encyclopedia-header-spacer" />
|
||||
</header>
|
||||
|
||||
<div className="edit-main">
|
||||
<div className="workout-meta-card">
|
||||
<main className="encyclopedia-main">
|
||||
<div className="encyclopedia-search-wrap">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search exercises..."
|
||||
placeholder="Search exercises…"
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
className="encyclopedia-search"
|
||||
aria-label="Search exercises"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="workout-meta-card">
|
||||
<p>Loading exercises...</p>
|
||||
</div>
|
||||
<div className="encyclopedia-state">Loading exercises…</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="error-banner">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
<div className="encyclopedia-error" role="alert">{error}</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && (
|
||||
<div className="edit-exercises-list">
|
||||
<div className="encyclopedia-list">
|
||||
{filtered.length === 0 && (
|
||||
<div className="workout-meta-card">
|
||||
<p>No exercises found.</p>
|
||||
</div>
|
||||
<div className="encyclopedia-state">No exercises found.</div>
|
||||
)}
|
||||
{filtered.map(exercise => (
|
||||
<div
|
||||
key={exercise.id}
|
||||
className={`edit-exercise-card${selected?.id === exercise.id ? ' exercise-selected' : ''}`}
|
||||
>
|
||||
{filtered.map(exercise => {
|
||||
const isOpen = selected?.id === exercise.id
|
||||
return (
|
||||
<div
|
||||
className="edit-card-header"
|
||||
onClick={() => setSelected(selected?.id === exercise.id ? null : exercise)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
key={exercise.id}
|
||||
className={`exercise-card${isOpen ? ' exercise-card--open' : ''}`}
|
||||
>
|
||||
<div className="edit-card-info">
|
||||
<h3>{exercise.name}</h3>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
{exercise.difficulty && (
|
||||
<span className="muscle-group">{exercise.difficulty}</span>
|
||||
)}
|
||||
{(exercise.muscle_groups || []).map(mg => (
|
||||
<span key={mg} className="muscle-group">{mg}</span>
|
||||
))}
|
||||
</div>
|
||||
{exercise.description && (
|
||||
<p style={{ margin: '0.5rem 0 0', fontSize: '0.875rem', color: '#666' }}>
|
||||
{exercise.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span style={{ color: '#999', fontSize: '1.25rem' }}>
|
||||
{selected?.id === exercise.id ? '▲' : '▼'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{selected?.id === exercise.id && (
|
||||
<div className="exercise-detail-expanded">
|
||||
{exercise.instructions && (
|
||||
<div className="exercise-instructions">
|
||||
<h4>Instructions</h4>
|
||||
<p>{exercise.instructions}</p>
|
||||
<div
|
||||
className="exercise-card-header"
|
||||
onClick={() => toggle(exercise)}
|
||||
role="button"
|
||||
aria-expanded={isOpen}
|
||||
tabIndex={0}
|
||||
onKeyDown={e => e.key === 'Enter' && toggle(exercise)}
|
||||
>
|
||||
<div className="exercise-card-info">
|
||||
<h3>{exercise.name}</h3>
|
||||
<div className="exercise-card-tags">
|
||||
{exercise.difficulty && (
|
||||
<span className="exercise-tag exercise-tag--difficulty">
|
||||
{exercise.difficulty}
|
||||
</span>
|
||||
)}
|
||||
{(exercise.muscle_groups || []).map(mg => (
|
||||
<span key={mg} className="exercise-tag">{mg}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<ExerciseResearchPanel
|
||||
exerciseId={exercise.id}
|
||||
exerciseName={exercise.name}
|
||||
/>
|
||||
{exercise.description && (
|
||||
<p className="exercise-card-description">{exercise.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<span className={`exercise-chevron${isOpen ? ' exercise-chevron--open' : ''}`}>
|
||||
▼
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isOpen && (
|
||||
<div className="exercise-detail">
|
||||
{exercise.instructions && (
|
||||
<div className="exercise-instructions">
|
||||
<h4>Instructions</h4>
|
||||
<p>{exercise.instructions}</p>
|
||||
</div>
|
||||
)}
|
||||
<ExerciseResearchPanel
|
||||
exerciseId={exercise.id}
|
||||
exerciseName={exercise.name}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user