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:
@@ -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
|
||||
Reference in New Issue
Block a user