feature/05-exercise-encyclopedia #4
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import ResearchDisplay from './ResearchDisplay'
|
||||||
|
|
||||||
const API_URL = '/api'
|
const API_URL = '/api'
|
||||||
|
|
||||||
@@ -33,73 +34,22 @@ function ExerciseResearchPanel({ exerciseId, exerciseName }) {
|
|||||||
<div className="research-panel">
|
<div className="research-panel">
|
||||||
<div className="research-panel-header">
|
<div className="research-panel-header">
|
||||||
<h3 className="research-panel-title">Research</h3>
|
<h3 className="research-panel-title">Research</h3>
|
||||||
{!research && (
|
<button
|
||||||
<button
|
className={`btn ${research ? 'btn-secondary' : 'btn-primary'} research-btn`}
|
||||||
className="btn btn-primary research-btn"
|
onClick={fetchResearch}
|
||||||
onClick={fetchResearch}
|
disabled={loading}
|
||||||
disabled={loading}
|
>
|
||||||
>
|
{loading ? 'Fetching…' : research ? 'Refresh' : 'Get Research'}
|
||||||
{loading ? 'Fetching...' : 'Get Research'}
|
</button>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{research && (
|
|
||||||
<button
|
|
||||||
className="btn btn-secondary research-btn"
|
|
||||||
onClick={fetchResearch}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? 'Fetching...' : 'Refresh'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading && (
|
<ResearchDisplay
|
||||||
<div className="research-loading">
|
loading={loading}
|
||||||
<div className="research-spinner"></div>
|
error={error}
|
||||||
<span>Searching for information on {exerciseName}...</span>
|
data={research}
|
||||||
</div>
|
name={exerciseName}
|
||||||
)}
|
onDismiss={() => setError(null)}
|
||||||
|
/>
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
</div>
|
</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 { useState, useEffect } from 'react'
|
||||||
import ExerciseResearchPanel from '../components/ExerciseResearchPanel'
|
import ExerciseResearchPanel from '../components/ExerciseResearchPanel'
|
||||||
import './WorkoutEditPage.css'
|
import './ExerciseEncyclopediaPage.css'
|
||||||
|
|
||||||
const API_URL = '/api'
|
const API_URL = '/api'
|
||||||
|
|
||||||
@@ -31,96 +31,100 @@ function ExerciseEncyclopediaPage({ onBack }) {
|
|||||||
ex.name.toLowerCase().includes(search.toLowerCase())
|
ex.name.toLowerCase().includes(search.toLowerCase())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const toggle = (exercise) =>
|
||||||
|
setSelected(prev => (prev?.id === exercise.id ? null : exercise))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="edit-page">
|
<div className="encyclopedia-page">
|
||||||
<div className="page-header">
|
<header className="encyclopedia-header">
|
||||||
<button className="back-btn" onClick={onBack}>
|
<button className="encyclopedia-back-btn" onClick={onBack}>
|
||||||
← Back
|
← Back
|
||||||
</button>
|
</button>
|
||||||
<h1>Exercise Encyclopedia</h1>
|
<h1>Exercise Encyclopedia</h1>
|
||||||
<div style={{ width: 70 }} />
|
<div className="encyclopedia-header-spacer" />
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
<div className="edit-main">
|
<main className="encyclopedia-main">
|
||||||
<div className="workout-meta-card">
|
<div className="encyclopedia-search-wrap">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search exercises..."
|
placeholder="Search exercises…"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => setSearch(e.target.value)}
|
onChange={e => setSearch(e.target.value)}
|
||||||
className="encyclopedia-search"
|
className="encyclopedia-search"
|
||||||
|
aria-label="Search exercises"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="workout-meta-card">
|
<div className="encyclopedia-state">Loading exercises…</div>
|
||||||
<p>Loading exercises...</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="error-banner">
|
<div className="encyclopedia-error" role="alert">{error}</div>
|
||||||
<span>{error}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && !error && (
|
{!loading && !error && (
|
||||||
<div className="edit-exercises-list">
|
<div className="encyclopedia-list">
|
||||||
{filtered.length === 0 && (
|
{filtered.length === 0 && (
|
||||||
<div className="workout-meta-card">
|
<div className="encyclopedia-state">No exercises found.</div>
|
||||||
<p>No exercises found.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{filtered.map(exercise => (
|
{filtered.map(exercise => {
|
||||||
<div
|
const isOpen = selected?.id === exercise.id
|
||||||
key={exercise.id}
|
return (
|
||||||
className={`edit-exercise-card${selected?.id === exercise.id ? ' exercise-selected' : ''}`}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="edit-card-header"
|
key={exercise.id}
|
||||||
onClick={() => setSelected(selected?.id === exercise.id ? null : exercise)}
|
className={`exercise-card${isOpen ? ' exercise-card--open' : ''}`}
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
>
|
>
|
||||||
<div className="edit-card-info">
|
<div
|
||||||
<h3>{exercise.name}</h3>
|
className="exercise-card-header"
|
||||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
onClick={() => toggle(exercise)}
|
||||||
{exercise.difficulty && (
|
role="button"
|
||||||
<span className="muscle-group">{exercise.difficulty}</span>
|
aria-expanded={isOpen}
|
||||||
)}
|
tabIndex={0}
|
||||||
{(exercise.muscle_groups || []).map(mg => (
|
onKeyDown={e => e.key === 'Enter' && toggle(exercise)}
|
||||||
<span key={mg} className="muscle-group">{mg}</span>
|
>
|
||||||
))}
|
<div className="exercise-card-info">
|
||||||
</div>
|
<h3>{exercise.name}</h3>
|
||||||
{exercise.description && (
|
<div className="exercise-card-tags">
|
||||||
<p style={{ margin: '0.5rem 0 0', fontSize: '0.875rem', color: '#666' }}>
|
{exercise.difficulty && (
|
||||||
{exercise.description}
|
<span className="exercise-tag exercise-tag--difficulty">
|
||||||
</p>
|
{exercise.difficulty}
|
||||||
)}
|
</span>
|
||||||
</div>
|
)}
|
||||||
<span style={{ color: '#999', fontSize: '1.25rem' }}>
|
{(exercise.muscle_groups || []).map(mg => (
|
||||||
{selected?.id === exercise.id ? '▲' : '▼'}
|
<span key={mg} className="exercise-tag">{mg}</span>
|
||||||
</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>
|
</div>
|
||||||
)}
|
{exercise.description && (
|
||||||
<ExerciseResearchPanel
|
<p className="exercise-card-description">{exercise.description}</p>
|
||||||
exerciseId={exercise.id}
|
)}
|
||||||
exerciseName={exercise.name}
|
</div>
|
||||||
/>
|
<span className={`exercise-chevron${isOpen ? ' exercise-chevron--open' : ''}`}>
|
||||||
|
▼
|
||||||
|
</span>
|
||||||
</div>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user