Refine admin content flow and improve Q&A mobile UX

This commit is contained in:
nmemmert
2026-05-07 14:23:18 -04:00
parent 5f479402a0
commit f4adeb2844
3 changed files with 179 additions and 12 deletions
+58 -8
View File
@@ -1829,9 +1829,9 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
<section className="admin-panel-section"> <section className="admin-panel-section">
<div className="admin-panel-head"> <div className="admin-panel-head">
<h2>Current Series</h2> <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> </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}> <div className="admin-field" key={key}>
<label htmlFor={`field-${key}`}>{label}</label> <label htmlFor={`field-${key}`}>{label}</label>
{multiline ? ( {multiline ? (
@@ -1844,6 +1844,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
)} )}
</div> </div>
))} ))}
<p className="admin-stats-note">Companion study guide title, description, and Amazon link are now managed in Downloads.</p>
{renderSaveStatus()} {renderSaveStatus()}
</section> </section>
)} )}
@@ -2075,11 +2076,6 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
<label htmlFor="resources-study-guide-description">Description</label> <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)} /> <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>
<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"> <div className="admin-field">
<label htmlFor="resources-study-guide-download-url">Primary Download URL</label> <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)} /> <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"> <section className="admin-panel-section">
<div className="admin-panel-head"> <div className="admin-panel-head">
<h2>Custom Links</h2> <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> </div>
{(form.customLinks ?? []).filter(link => link.placement !== 'resources').length === 0 && ( {(form.customLinks ?? []).filter(link => link.placement !== 'resources').length === 0 && (
<p className="admin-stats-note">No custom links yet.</p> <p className="admin-stats-note">No custom links yet.</p>
@@ -2267,6 +2268,55 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
</div> </div>
))} ))}
<button type="button" className="btn-admin-add" onClick={addLink}>+ Add Link</button> <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()} {renderSaveStatus()}
</section> </section>
)} )}
+102
View File
@@ -3285,6 +3285,15 @@
gap: 0.5rem; 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 { .qa-topic-btn {
background: none; background: none;
border: 1px solid rgba(201, 168, 76, 0.35); border: 1px solid rgba(201, 168, 76, 0.35);
@@ -3350,6 +3359,25 @@
border-color: var(--brand-gold); 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 { .qa-cards {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -3481,6 +3509,80 @@
text-align: center; 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 { @keyframes slideDown {
from { from {
opacity: 0; opacity: 0;
+19 -4
View File
@@ -89,6 +89,12 @@ export default function QASection() {
setPage(0) setPage(0)
} }
const clearFilters = () => {
setSelectedTopic(null)
setSearchQuery('')
setPage(0)
}
const getRelatedQuestions = (current: PublicQuestion) => { const getRelatedQuestions = (current: PublicQuestion) => {
const currentTokens = new Set(tokenizeForRelated(`${current.question} ${current.answer}`)) const currentTokens = new Set(tokenizeForRelated(`${current.question} ${current.answer}`))
@@ -125,6 +131,7 @@ export default function QASection() {
<div className="qa-filters"> <div className="qa-filters">
<div className="qa-topics"> <div className="qa-topics">
<button <button
type="button"
className={`qa-topic-btn${selectedTopic === null ? ' qa-topic-btn--active' : ''}`} className={`qa-topic-btn${selectedTopic === null ? ' qa-topic-btn--active' : ''}`}
onClick={() => handleTopic(null)} onClick={() => handleTopic(null)}
> >
@@ -132,6 +139,7 @@ export default function QASection() {
</button> </button>
{topics.map(topic => ( {topics.map(topic => (
<button <button
type="button"
key={topic} key={topic}
className={`qa-topic-btn${selectedTopic === topic ? ' qa-topic-btn--active' : ''}`} className={`qa-topic-btn${selectedTopic === topic ? ' qa-topic-btn--active' : ''}`}
onClick={() => handleTopic(topic)} onClick={() => handleTopic(topic)}
@@ -151,6 +159,11 @@ export default function QASection() {
/> />
</label> </label>
</div> </div>
{(searchQuery || selectedTopic) && (
<div className="qa-filter-actions">
<button type="button" className="qa-clear-btn" onClick={clearFilters}>Clear filters</button>
</div>
)}
</div> </div>
{filteredQuestions.length === 0 ? ( {filteredQuestions.length === 0 ? (
@@ -160,7 +173,9 @@ export default function QASection() {
) : ( ) : (
<> <>
<div className="qa-cards"> <div className="qa-cards">
{pagedQuestions.map(question => ( {pagedQuestions.map(question => {
const relatedQuestions = getRelatedQuestions(question)
return (
<div key={question.id} className="qa-card-scene"> <div key={question.id} className="qa-card-scene">
<div <div
className={`qa-card-inner ${expanded[question.id] ? 'flipped' : ''}`} className={`qa-card-inner ${expanded[question.id] ? 'flipped' : ''}`}
@@ -181,11 +196,11 @@ export default function QASection() {
<div className="qa-card-face qa-card-back"> <div className="qa-card-face qa-card-back">
<span className="qa-face-label">A</span> <span className="qa-face-label">A</span>
<div className="qa-answer-text">{renderTextWithLinks(question.answer)}</div> <div className="qa-answer-text">{renderTextWithLinks(question.answer)}</div>
{getRelatedQuestions(question).length > 0 && ( {relatedQuestions.length > 0 && (
<div className="qa-related-wrap"> <div className="qa-related-wrap">
<p className="qa-related-label">Related questions</p> <p className="qa-related-label">Related questions</p>
<div className="qa-related-list"> <div className="qa-related-list">
{getRelatedQuestions(question).map(related => ( {relatedQuestions.map(related => (
<button <button
key={related.id} key={related.id}
type="button" type="button"
@@ -209,7 +224,7 @@ export default function QASection() {
</div> </div>
</div> </div>
</div> </div>
))} )})}
</div> </div>
{totalPages > 1 && ( {totalPages > 1 && (