diff --git a/src/renderer/components/Dashboard/Dashboard.tsx b/src/renderer/components/Dashboard/Dashboard.tsx index 8c6d0b7..4860752 100644 --- a/src/renderer/components/Dashboard/Dashboard.tsx +++ b/src/renderer/components/Dashboard/Dashboard.tsx @@ -6,6 +6,11 @@ function daysSince(iso: string): number { return Math.floor((Date.now() - new Date(iso).getTime()) / 86_400_000) } +type StoryRow = + | { kind: 'out'; storyId: string; storyTitle: string; storyPath: string; marketName: string; days: number; overdue: boolean } + | { kind: 'ready'; storyId: string; storyTitle: string; storyPath: string; wordCount: number } + | { kind: 'accepted' | 'rejected'; storyId: string; storyTitle: string; storyPath: string; marketName: string } + export function Dashboard(): JSX.Element { const { stories, submissions, markets, setActiveStory, markSaved, isDirty, activeStoryPath, activeStoryContent } = useBorgesStore() @@ -19,26 +24,45 @@ export function Dashboard(): JSX.Element { setActiveStory(path, id, content) } - // Stories ready to submit: no active pending, never submitted or last status rejected - const readyToSubmit = stories.filter((story) => { + const rows: StoryRow[] = [] + + // Out: stories with active pending submissions + const pendingSubs = submissions + .filter((s) => s.status === 'pending' || s.status === 'pending-revision') + .sort((a, b) => a.submittedAt.localeCompare(b.submittedAt)) + for (const sub of pendingSubs) { + const story = stories.find((s) => s.id === sub.storyId) + const market = markets.find((m) => m.id === sub.marketId) + if (!story) continue + const days = daysSince(sub.submittedAt) + const overdue = !!(market?.responseTimeWeeks && days > market.responseTimeWeeks * 7) + rows.push({ kind: 'out', storyId: story.id, storyTitle: story.meta.title || story.id, storyPath: story.path, marketName: market?.name ?? sub.marketId, days, overdue }) + } + + // Ready: no active pending + const outIds = new Set(pendingSubs.map((s) => s.storyId)) + const readyStories = stories.filter((story) => { + if (outIds.has(story.id)) return false const storySubs = submissions.filter((s) => s.storyId === story.id) - const hasActive = storySubs.some((s) => s.status === 'pending' || s.status === 'pending-revision') - if (hasActive) return false if (storySubs.length === 0) return true const last = storySubs.sort((a, b) => b.submittedAt.localeCompare(a.submittedAt))[0] return last.status === 'rejected' || last.status === 'withdrawn' }) + for (const story of readyStories) { + rows.push({ kind: 'ready', storyId: story.id, storyTitle: story.meta.title || story.id, storyPath: story.path, wordCount: story.wordCount }) + } - // Pending submissions - const pending = submissions - .filter((s) => s.status === 'pending' || s.status === 'pending-revision') - .sort((a, b) => a.submittedAt.localeCompare(b.submittedAt)) - - // Recent activity (last 10 non-pending changes) - const recent = submissions + // Recent activity + const recentSubs = submissions .filter((s) => s.status === 'accepted' || s.status === 'rejected') .sort((a, b) => b.statusUpdatedAt.localeCompare(a.statusUpdatedAt)) - .slice(0, 10) + .slice(0, 8) + for (const sub of recentSubs) { + const story = stories.find((s) => s.id === sub.storyId) + const market = markets.find((m) => m.id === sub.marketId) + if (!story) continue + rows.push({ kind: sub.status as 'accepted' | 'rejected', storyId: story.id, storyTitle: story.meta.title || story.id, storyPath: story.path, marketName: market?.name ?? sub.marketId }) + } const totalWords = stories.reduce((s, story) => s + story.wordCount, 0) @@ -52,73 +76,44 @@ export function Dashboard(): JSX.Element {
- {/* Ready to submit */} + {/* Stories — all statuses in one card */}
-
Ready to submit ({readyToSubmit.length})
- {readyToSubmit.length === 0 &&
All stories are out or in progress.
} - {readyToSubmit.slice(0, 8).map((story) => ( -
openStory(story.path, story.id)}> - {story.meta.title || story.id} - {story.wordCount.toLocaleString()}w +
Stories
+ {rows.length === 0 &&
No stories yet.
} + {rows.map((row, i) => ( +
openStory(row.storyPath, row.storyId)}> + {row.storyTitle} + {row.kind === 'out' && ( + <> + {row.marketName} + {row.days}d{row.overdue ? ' ⚠' : ''} + out + + )} + {row.kind === 'ready' && ( + <> + {row.wordCount.toLocaleString()}w + ready + + )} + {row.kind === 'accepted' && ( + <> + {row.marketName} + accepted + + )} + {row.kind === 'rejected' && ( + <> + {row.marketName} + rejected + + )}
))}
- {/* Pending submissions */} -
-
Out ({pending.length})
- {pending.length === 0 &&
Nothing currently submitted.
} - {pending.map((sub) => { - const story = stories.find((s) => s.id === sub.storyId) - const market = markets.find((m) => m.id === sub.marketId) - const days = daysSince(sub.submittedAt) - const isOverdue = market?.responseTimeWeeks && days > market.responseTimeWeeks * 7 - return ( -
story && openStory(story.path, story.id)}> - {story?.meta.title || sub.storyId} → {market?.name ?? sub.marketId} - {days}d{isOverdue ? ' ⚠' : ''} -
- ) - })} -
- - {/* Recent activity */} -
-
Recent activity
- {recent.length === 0 &&
No acceptances or rejections yet.
} - {recent.map((sub) => { - const story = stories.find((s) => s.id === sub.storyId) - const market = markets.find((m) => m.id === sub.marketId) - return ( -
story && openStory(story.path, story.id)}> - {story?.meta.title || sub.storyId} - - {sub.status === 'accepted' ? '✓' : '✗'} {market?.name ?? sub.marketId} - -
- ) - })} -
- {/* Writing stats */} - - {/* Word count overview */} -
-
Word counts
- {stories.length === 0 &&
No stories yet.
} - {[...stories].sort((a, b) => b.wordCount - a.wordCount).slice(0, 10).map((story) => { - const target = story.meta.wordCountTarget - return ( -
openStory(story.path, story.id)}> - {story.meta.title || story.id} - - {story.wordCount.toLocaleString()}{target ? `/${target.toLocaleString()}` : ''} - -
- ) - })} -
) diff --git a/src/renderer/styles/app.css b/src/renderer/styles/app.css index 791364a..a268647 100644 --- a/src/renderer/styles/app.css +++ b/src/renderer/styles/app.css @@ -376,6 +376,11 @@ textarea { resize: vertical; } .dashboard-row-meta { font-size: 11px; color: var(--text3); flex-shrink: 0; } .dashboard-row-flag { font-size: 11px; color: var(--warn); flex-shrink: 0; } .dashboard-empty { font-size: 13px; color: var(--text3); padding: 8px 0; } +.dashboard-status-badge { font-size: 10px; font-weight: 600; letter-spacing: 0.04em; padding: 2px 6px; border-radius: 4px; flex-shrink: 0; text-transform: uppercase; } +.dashboard-status-out { background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); } +.dashboard-status-ready { background: color-mix(in srgb, var(--text3) 15%, transparent); color: var(--text2); } +.dashboard-status-accepted { background: color-mix(in srgb, var(--success) 15%, transparent); color: var(--success); } +.dashboard-status-rejected { background: color-mix(in srgb, var(--text3) 10%, transparent); color: var(--text3); } .writing-stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px 8px; margin-bottom: 4px; } .writing-stat { display: flex; flex-direction: column; gap: 2px; } .writing-stat-value { font-size: 22px; font-weight: 300; color: var(--text); line-height: 1; }