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 (
+
+
+
+ Searching for information on {exerciseName}…
+
+
+ )
+}
+
+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 (
-
-
-