Story 31: View Upcoming Stories
Add GET /workflow/upcoming endpoint that reads .story_kit/stories/upcoming/ and returns story IDs with names parsed from frontmatter. Add UpcomingPanel component wired into Chat view with loading, error, empty, and list states. 12 new tests (3 backend, 9 frontend) all passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
169
frontend/src/components/UpcomingPanel.tsx
Normal file
169
frontend/src/components/UpcomingPanel.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import type { UpcomingStory } from "../api/workflow";
|
||||
|
||||
interface UpcomingPanelProps {
|
||||
stories: UpcomingStory[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
lastRefresh: Date | null;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
const formatTimestamp = (value: Date | null): string => {
|
||||
if (!value) return "—";
|
||||
return value.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
export function UpcomingPanel({
|
||||
stories,
|
||||
isLoading,
|
||||
error,
|
||||
lastRefresh,
|
||||
onRefresh,
|
||||
}: UpcomingPanelProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #333",
|
||||
borderRadius: "10px",
|
||||
padding: "12px 16px",
|
||||
background: "#1f1f1f",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: "12px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 600 }}>Upcoming Stories</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading}
|
||||
style={{
|
||||
padding: "4px 10px",
|
||||
borderRadius: "999px",
|
||||
border: "1px solid #333",
|
||||
background: isLoading ? "#2a2a2a" : "#2f2f2f",
|
||||
color: isLoading ? "#777" : "#aaa",
|
||||
cursor: isLoading ? "not-allowed" : "pointer",
|
||||
fontSize: "0.75em",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-end",
|
||||
gap: "2px",
|
||||
fontSize: "0.85em",
|
||||
color: "#aaa",
|
||||
}}
|
||||
>
|
||||
<div>{stories.length} stories</div>
|
||||
<div style={{ fontSize: "0.8em", color: "#777" }}>
|
||||
Updated {formatTimestamp(lastRefresh)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div style={{ fontSize: "0.85em", color: "#aaa" }}>
|
||||
Loading upcoming stories...
|
||||
</div>
|
||||
) : error ? (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.85em",
|
||||
color: "#ff7b72",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<span>{error} Use Refresh to try again.</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading}
|
||||
style={{
|
||||
padding: "4px 10px",
|
||||
borderRadius: "999px",
|
||||
border: "1px solid #333",
|
||||
background: "#2f2f2f",
|
||||
color: "#aaa",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.75em",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
) : stories.length === 0 ? (
|
||||
<div style={{ fontSize: "0.85em", color: "#aaa" }}>
|
||||
No upcoming stories.
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "6px",
|
||||
}}
|
||||
>
|
||||
{stories.map((story) => (
|
||||
<div
|
||||
key={`upcoming-${story.story_id}`}
|
||||
style={{
|
||||
border: "1px solid #2a2a2a",
|
||||
borderRadius: "8px",
|
||||
padding: "8px 12px",
|
||||
background: "#191919",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 600, fontSize: "0.9em" }}>
|
||||
{story.name ?? story.story_id}
|
||||
</div>
|
||||
{story.name && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.75em",
|
||||
color: "#777",
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
{story.story_id}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user