Refine admin content flow and improve Q&A mobile UX
This commit is contained in:
+58
-8
@@ -1829,9 +1829,9 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
<section className="admin-panel-section">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Current Series</h2>
|
||||
<p>Update the active study card and companion guide content.</p>
|
||||
<p>Update the active series card shown on the homepage and episodes page.</p>
|
||||
</div>
|
||||
{FIELDS.filter(f => f.section === 'series').map(({ key, label, multiline }) => (
|
||||
{FIELDS.filter(f => f.section === 'series' && !['studyGuideTitle', 'studyGuideDescription', 'studyGuideUrl'].includes(f.key)).map(({ key, label, multiline }) => (
|
||||
<div className="admin-field" key={key}>
|
||||
<label htmlFor={`field-${key}`}>{label}</label>
|
||||
{multiline ? (
|
||||
@@ -1844,6 +1844,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<p className="admin-stats-note">Companion study guide title, description, and Amazon link are now managed in Downloads.</p>
|
||||
{renderSaveStatus()}
|
||||
</section>
|
||||
)}
|
||||
@@ -2075,11 +2076,6 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
<label htmlFor="resources-study-guide-description">Description</label>
|
||||
<textarea id="resources-study-guide-description" rows={3} value={form.studyGuideDescription} placeholder="Describe the study guide download." onChange={e => handleChange('studyGuideDescription', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="resources-series-image-url">Cover Image URL</label>
|
||||
<input id="resources-series-image-url" type="text" value={form.seriesImageUrl} placeholder="/uploads/study-guide-cover.png" onChange={e => handleChange('seriesImageUrl', e.target.value)} />
|
||||
{renderImageAssetSelector(form.seriesImageUrl, value => handleChange('seriesImageUrl', value), 'resources-series-image-url-asset')}
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="resources-study-guide-download-url">Primary Download URL</label>
|
||||
<input id="resources-study-guide-download-url" type="url" value={form.studyGuideDownloadUrl} placeholder="/uploads/new-guide.pdf or https://..." onChange={e => handleChange('studyGuideDownloadUrl', e.target.value)} />
|
||||
@@ -2225,7 +2221,12 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
<section className="admin-panel-section">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Custom Links</h2>
|
||||
<p>Add links to show in the platform buttons row or footer navigation.</p>
|
||||
<p>Add links to show in platform buttons, footer navigation, or the More Resources download library.</p>
|
||||
</div>
|
||||
|
||||
<div className="admin-panel-subhead">
|
||||
<h3>Platform + Footer Links</h3>
|
||||
<p>Links that appear on the listen buttons row or footer nav.</p>
|
||||
</div>
|
||||
{(form.customLinks ?? []).filter(link => link.placement !== 'resources').length === 0 && (
|
||||
<p className="admin-stats-note">No custom links yet.</p>
|
||||
@@ -2267,6 +2268,55 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
</div>
|
||||
))}
|
||||
<button type="button" className="btn-admin-add" onClick={addLink}>+ Add Link</button>
|
||||
|
||||
<div className="admin-panel-subhead">
|
||||
<h3>More Resources Links</h3>
|
||||
<p>These create cards in Downloads → More guides, worksheets, and past study downloads.</p>
|
||||
</div>
|
||||
{(form.customLinks ?? []).filter(link => link.placement === 'resources').length === 0 && (
|
||||
<p className="admin-stats-note">No resource links yet.</p>
|
||||
)}
|
||||
{(form.customLinks ?? []).filter(link => link.placement === 'resources').map(link => (
|
||||
<div key={link.id} className="admin-array-row">
|
||||
<div className="admin-array-fields">
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`resources-link-label-${link.id}`}>Label</label>
|
||||
<input id={`resources-link-label-${link.id}`} type="text" value={link.label} placeholder="Resource title" onChange={e => updateLink(link.id, 'label', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`resources-link-url-${link.id}`}>Download URL</label>
|
||||
<input id={`resources-link-url-${link.id}`} type="url" value={link.url} placeholder="https://..." onChange={e => updateLink(link.id, 'url', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`resources-link-description-${link.id}`}>Description</label>
|
||||
<textarea id={`resources-link-description-${link.id}`} rows={3} value={link.description ?? ''} placeholder="Description shown on the download page" onChange={e => updateLink(link.id, 'description', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`resources-link-image-${link.id}`}>Image URL</label>
|
||||
<input id={`resources-link-image-${link.id}`} type="text" value={link.imageUrl ?? ''} placeholder="/uploads/example.png" onChange={e => updateLink(link.id, 'imageUrl', e.target.value)} />
|
||||
{renderImageAssetSelector(link.imageUrl, value => updateLink(link.id, 'imageUrl', value), `resources-link-image-${link.id}-asset`)}
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`resources-link-amazon-url-${link.id}`}>Amazon URL (optional)</label>
|
||||
<input id={`resources-link-amazon-url-${link.id}`} type="url" value={link.amazonUrl ?? ''} placeholder="https://amazon.com/..." onChange={e => updateLink(link.id, 'amazonUrl', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`resources-link-amazon-label-${link.id}`}>Amazon Button Label</label>
|
||||
<input id={`resources-link-amazon-label-${link.id}`} type="text" value={link.amazonLabel ?? ''} placeholder="Get it on Amazon" onChange={e => updateLink(link.id, 'amazonLabel', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`resources-link-tags-${link.id}`}>Tags</label>
|
||||
<input id={`resources-link-tags-${link.id}`} type="text" value={(link.tags ?? []).join(', ')} placeholder="worksheet, study, pdf" onChange={e => updateLink(link.id, 'tags', e.target.value.split(',').map(tag => tag.trim()).filter(Boolean))} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label>Quick Action</label>
|
||||
<button type="button" className="btn-admin-apply" onClick={() => updateLink(link.id, 'placement', 'platforms')}>Move to Platform Buttons</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" className="btn-admin-remove" onClick={() => removeLink(link.id)}>Remove</button>
|
||||
</div>
|
||||
))}
|
||||
<button type="button" className="btn-admin-add" onClick={addResource}>+ Add Resource Link</button>
|
||||
{renderSaveStatus()}
|
||||
</section>
|
||||
)}
|
||||
|
||||
+102
@@ -3285,6 +3285,15 @@
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.qa-topics::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.qa-topics::-webkit-scrollbar-thumb {
|
||||
background: rgba(201, 168, 76, 0.4);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.qa-topic-btn {
|
||||
background: none;
|
||||
border: 1px solid rgba(201, 168, 76, 0.35);
|
||||
@@ -3350,6 +3359,25 @@
|
||||
border-color: var(--brand-gold);
|
||||
}
|
||||
|
||||
.qa-filter-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.qa-clear-btn {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(201, 168, 76, 0.38);
|
||||
color: #d7bd83;
|
||||
border-radius: 999px;
|
||||
padding: 0.35rem 0.8rem;
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.qa-clear-btn:hover {
|
||||
background: rgba(201, 168, 76, 0.12);
|
||||
}
|
||||
|
||||
.qa-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -3481,6 +3509,80 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.qa-filters {
|
||||
gap: 0.65rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.qa-topics {
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding-bottom: 0.2rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.qa-topic-btn {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8rem;
|
||||
padding: 0.32rem 0.72rem;
|
||||
}
|
||||
|
||||
.qa-search input {
|
||||
padding: 0.68rem 0.82rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.qa-card-face {
|
||||
padding: 1rem 0.9rem;
|
||||
}
|
||||
|
||||
.qa-card-front {
|
||||
align-items: flex-start;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.qa-face-label {
|
||||
width: 1.2rem;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.qa-question-text {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.qa-answer-text {
|
||||
font-size: 0.98rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.qa-pagination {
|
||||
gap: 0.5rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.qa-page-btn {
|
||||
flex: 1;
|
||||
min-height: 42px;
|
||||
padding: 0.45rem 0.7rem;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.qa-page-info {
|
||||
min-width: 3rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.qa-related-btn {
|
||||
font-size: 0.78rem;
|
||||
padding: 0.3rem 0.62rem;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
@@ -89,6 +89,12 @@ export default function QASection() {
|
||||
setPage(0)
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
setSelectedTopic(null)
|
||||
setSearchQuery('')
|
||||
setPage(0)
|
||||
}
|
||||
|
||||
const getRelatedQuestions = (current: PublicQuestion) => {
|
||||
const currentTokens = new Set(tokenizeForRelated(`${current.question} ${current.answer}`))
|
||||
|
||||
@@ -125,6 +131,7 @@ export default function QASection() {
|
||||
<div className="qa-filters">
|
||||
<div className="qa-topics">
|
||||
<button
|
||||
type="button"
|
||||
className={`qa-topic-btn${selectedTopic === null ? ' qa-topic-btn--active' : ''}`}
|
||||
onClick={() => handleTopic(null)}
|
||||
>
|
||||
@@ -132,6 +139,7 @@ export default function QASection() {
|
||||
</button>
|
||||
{topics.map(topic => (
|
||||
<button
|
||||
type="button"
|
||||
key={topic}
|
||||
className={`qa-topic-btn${selectedTopic === topic ? ' qa-topic-btn--active' : ''}`}
|
||||
onClick={() => handleTopic(topic)}
|
||||
@@ -151,6 +159,11 @@ export default function QASection() {
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{(searchQuery || selectedTopic) && (
|
||||
<div className="qa-filter-actions">
|
||||
<button type="button" className="qa-clear-btn" onClick={clearFilters}>Clear filters</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{filteredQuestions.length === 0 ? (
|
||||
@@ -160,7 +173,9 @@ export default function QASection() {
|
||||
) : (
|
||||
<>
|
||||
<div className="qa-cards">
|
||||
{pagedQuestions.map(question => (
|
||||
{pagedQuestions.map(question => {
|
||||
const relatedQuestions = getRelatedQuestions(question)
|
||||
return (
|
||||
<div key={question.id} className="qa-card-scene">
|
||||
<div
|
||||
className={`qa-card-inner ${expanded[question.id] ? 'flipped' : ''}`}
|
||||
@@ -181,11 +196,11 @@ export default function QASection() {
|
||||
<div className="qa-card-face qa-card-back">
|
||||
<span className="qa-face-label">A</span>
|
||||
<div className="qa-answer-text">{renderTextWithLinks(question.answer)}</div>
|
||||
{getRelatedQuestions(question).length > 0 && (
|
||||
{relatedQuestions.length > 0 && (
|
||||
<div className="qa-related-wrap">
|
||||
<p className="qa-related-label">Related questions</p>
|
||||
<div className="qa-related-list">
|
||||
{getRelatedQuestions(question).map(related => (
|
||||
{relatedQuestions.map(related => (
|
||||
<button
|
||||
key={related.id}
|
||||
type="button"
|
||||
@@ -209,7 +224,7 @@ export default function QASection() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)})}
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
|
||||
Reference in New Issue
Block a user