Admin reorganization: nav groups, streak/progress display in Study Users; v1.1.1
- Split "Manage" nav group into Audience, Insights, Tools - Moved Email Templates into Configure group - Added Testimonials to About jump nav - Added RSS Feed tab to Podcast Hub - Updated Study Users expanded card: shows last studied date, streak (current + longest), and per-study lesson completion count - Streak pill shown in collapsed header when > 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+64
-5
@@ -141,7 +141,10 @@ interface StudyUserRecord {
|
||||
displayName: string
|
||||
createdAt: string | null
|
||||
lastLoginAt: string | null
|
||||
enrolledStudies: Array<{ slug: string; title: string }>
|
||||
lastStudiedDate: string | null
|
||||
currentStreak: number
|
||||
longestStreak: number
|
||||
enrolledStudies: Array<{ slug: string; title: string; completedLessons?: number; totalLessons?: number }>
|
||||
noteCount: number
|
||||
subscribeNewsletter: boolean
|
||||
studyRemindersEnabled: boolean
|
||||
@@ -276,6 +279,7 @@ function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) {
|
||||
<div className="admin-study-user-header-right">
|
||||
<span className="admin-study-user-pill">{user.enrolledStudies.length} enrolled</span>
|
||||
<span className="admin-study-user-pill">{user.noteCount} notes</span>
|
||||
{(user.currentStreak ?? 0) > 0 && <span className="admin-study-user-pill">{user.currentStreak} 🔥</span>}
|
||||
<span style={{ color: '#5a5440', fontSize: '0.8rem' }}>{isExpanded ? '▲' : '▼'}</span>
|
||||
</div>
|
||||
</button>
|
||||
@@ -285,6 +289,8 @@ function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) {
|
||||
<div className="admin-study-user-meta">
|
||||
<span>Joined: {user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '—'}</span>
|
||||
<span>Last login: {user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleDateString() : '—'}</span>
|
||||
<span>Last studied: {user.lastStudiedDate ?? '—'}</span>
|
||||
<span>Streak: {user.currentStreak ?? 0} 🔥 (best: {user.longestStreak ?? 0})</span>
|
||||
<span>Newsletter: {user.subscribeNewsletter ? 'Yes' : 'No'}</span>
|
||||
<span>Reminders: {user.studyRemindersEnabled ? 'On' : 'Off'}</span>
|
||||
</div>
|
||||
@@ -329,7 +335,14 @@ function StudyUsersPanel({ studies }: { studies: StudyProgram[] }) {
|
||||
<ul className="admin-study-user-enrollments">
|
||||
{user.enrolledStudies.map(e => (
|
||||
<li key={e.slug}>
|
||||
<span>{e.title}</span>
|
||||
<span>
|
||||
{e.title}
|
||||
{e.totalLessons != null && (
|
||||
<span style={{ color: '#8a7a3a', fontSize: '0.8em', marginLeft: '0.5rem' }}>
|
||||
{e.completedLessons ?? 0}/{e.totalLessons} lessons
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<button type="button" className="btn-admin-remove" style={{ fontSize: '0.78rem', padding: '0.2rem 0.6rem' }} onClick={() => handleUnenroll(user, e.slug)}>Remove</button>
|
||||
</li>
|
||||
))}
|
||||
@@ -748,7 +761,7 @@ const ADMIN_VIEW_OPTIONS: Array<{ group: string; options: Array<{ value: AdminVi
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Manage',
|
||||
group: 'Audience',
|
||||
options: [
|
||||
{ value: 'questions', label: 'Questions' },
|
||||
{ value: 'study-comments', label: 'Study Comments' },
|
||||
@@ -756,8 +769,17 @@ const ADMIN_VIEW_OPTIONS: Array<{ group: string; options: Array<{ value: AdminVi
|
||||
{ value: 'contacts', label: 'Contacts' },
|
||||
{ value: 'subscribers', label: 'Subscribers' },
|
||||
{ value: 'study-users', label: 'Study Users' },
|
||||
{ value: 'email-templates', label: 'Email Templates' },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Insights',
|
||||
options: [
|
||||
{ value: 'analytics', label: 'Analytics' },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Tools',
|
||||
options: [
|
||||
{ value: 'assets', label: 'Asset Manager' },
|
||||
{ value: 'qr-codes', label: 'QR Codes' },
|
||||
],
|
||||
@@ -767,6 +789,7 @@ const ADMIN_VIEW_OPTIONS: Array<{ group: string; options: Array<{ value: AdminVi
|
||||
options: [
|
||||
{ value: 'global', label: 'Footer & Global' },
|
||||
{ value: 'seo', label: 'SEO & Redirects' },
|
||||
{ value: 'email-templates', label: 'Email Templates' },
|
||||
{ value: 'legal', label: 'Legal Pages' },
|
||||
{ value: 'security', label: 'Security' },
|
||||
{ value: 'brand', label: 'Brand Kit' },
|
||||
@@ -792,6 +815,7 @@ const ADMIN_SECTION_LINKS: Partial<Record<AdminView, AdminSectionLink[]>> = {
|
||||
{ id: 'about-show-copy', label: 'Show Copy' },
|
||||
{ id: 'about-nate-bio', label: 'Nate Bio' },
|
||||
{ id: 'about-images', label: 'Images' },
|
||||
{ id: 'about-testimonials', label: 'Testimonials' },
|
||||
],
|
||||
contact: [
|
||||
{ id: 'contact-profile', label: 'Profile' },
|
||||
@@ -820,7 +844,7 @@ const ADMIN_SECTION_LINKS: Partial<Record<AdminView, AdminSectionLink[]>> = {
|
||||
],
|
||||
}
|
||||
|
||||
type PodcastTab = 'current-series' | 'episode-highlights' | 'finished-books' | 'podcast-checklist' | 'episode-scripts'
|
||||
type PodcastTab = 'current-series' | 'episode-highlights' | 'finished-books' | 'podcast-checklist' | 'episode-scripts' | 'rss-feed'
|
||||
|
||||
type MainContentSection = 'hero' | 'start-here' | 'about' | 'contact' | 'series' | 'share' | 'prism' | 'global' | 'email-templates'
|
||||
|
||||
@@ -3815,6 +3839,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
<button type="button" className={`admin-tab${podcastTab === 'finished-books' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('finished-books')}>Finished Books</button>
|
||||
<button type="button" className={`admin-tab${podcastTab === 'podcast-checklist' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('podcast-checklist')}>Production Checklist</button>
|
||||
<button type="button" className={`admin-tab${podcastTab === 'episode-scripts' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('episode-scripts')}>Episode Scripts</button>
|
||||
<button type="button" className={`admin-tab${podcastTab === 'rss-feed' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('rss-feed')}>RSS Feed</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
@@ -4816,6 +4841,40 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* RSS FEED */}
|
||||
{(adminView === 'podcast' && podcastTab === 'rss-feed') && (() => {
|
||||
const canonicalUrl = (form as { seo?: { canonicalUrl?: string } }).seo?.canonicalUrl?.replace(/\/$/, '') ?? ''
|
||||
const feedUrl = canonicalUrl ? `${canonicalUrl}/feed.xml` : '/feed.xml'
|
||||
const anchorFeedUrl = 'https://anchor.fm/s/11068d290/podcast/rss'
|
||||
return (
|
||||
<section className="admin-panel-section">
|
||||
<div className="admin-panel-head">
|
||||
<h2>RSS Feed</h2>
|
||||
<p>Your podcast feed is available at your own domain and proxies the Anchor source feed.</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||
<div style={{ background: '#1a1a16', border: '1px solid #2a2518', borderRadius: '10px', padding: '1.25rem 1.5rem' }}>
|
||||
<p style={{ margin: '0 0 0.35rem', fontSize: '0.78rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', color: '#7a7060' }}>Your Feed URL</p>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<code style={{ fontSize: '0.95rem', color: '#e0d5b5', wordBreak: 'break-all' }}>{feedUrl}</code>
|
||||
<button type="button" className="btn-admin-save" onClick={() => navigator.clipboard.writeText(feedUrl)}>Copy</button>
|
||||
<a href={feedUrl} target="_blank" rel="noopener noreferrer" className="btn-admin-reset">Open ↗</a>
|
||||
</div>
|
||||
<p style={{ margin: '0.75rem 0 0', fontSize: '0.85rem', color: '#7a7060' }}>Submit this URL to Apple Podcasts, Spotify, and other directories to keep your feed on your own domain.</p>
|
||||
</div>
|
||||
<div style={{ background: '#1a1a16', border: '1px solid #2a2518', borderRadius: '10px', padding: '1.25rem 1.5rem' }}>
|
||||
<p style={{ margin: '0 0 0.35rem', fontSize: '0.78rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', color: '#7a7060' }}>Anchor Source Feed</p>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<code style={{ fontSize: '0.88rem', color: '#7a7060', wordBreak: 'break-all' }}>{anchorFeedUrl}</code>
|
||||
<a href={anchorFeedUrl} target="_blank" rel="noopener noreferrer" className="btn-admin-reset">Open ↗</a>
|
||||
</div>
|
||||
<p style={{ margin: '0.75rem 0 0', fontSize: '0.85rem', color: '#7a7060' }}>Your feed.xml proxies this source and rewrites the channel link to your domain. Episodes are cached for 30 minutes.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* STUDY COMMENTS */}
|
||||
{adminView === 'study-comments' && (() => {
|
||||
const filtered = studyComments.filter(c => {
|
||||
|
||||
Reference in New Issue
Block a user