- Fix ChatBubble to handle non-string content with String() wrapper - Fix API route to use generateText for non-streaming requests - Add @ai-sdk/openai-compatible for non-OpenAI providers (DeepSeek, etc.) - Use Chat Completions API instead of Responses API for compatible providers - Update ChatBubble tests and fix component exports to kebab-case - Remove stale PascalCase ChatBubble.tsx file
31 KiB
Story 3.1: History Feed UI
Status: done
Story
As a user, I want to see a list of my past growing moments, So that I can reflect on my journey.
Acceptance Criteria
-
History Feed on Home Screen
- Given the user is on the Home screen
- When they view the feed
- Then they see a chronological list of past "Completed" sessions (Title, Date, Tags)
- And the list supports lazy loading/pagination for performance
-
View Past Enlightenment Artifact
- Given the user clicks a history card
- When the card opens
- Then the full "Enlightenment" artifact allows for reading
- And the "Copy" action is available
Tasks / Subtasks
-
Create History Feed Data Layer
- Add
getCompletedDrafts()method to DraftService - Implement pagination/offset support (limit 20 items per page)
- Support reverse chronological sorting (newest first)
- Add filtering by tags (optional enhancement)
- Add
-
Create History Feed Store State
- Create
useHistoryStoreinsrc/lib/store/history-store.ts - State: drafts array, loading, hasMore, error
- Actions: loadMore, refreshHistory, selectDraft
- Use atomic selectors for performance
- Create
-
Create HistoryCard Component
- Create
HistoryCard.tsxinsrc/components/features/journal/ - Display: Title, Date (formatted), Tags array
- Show snippet preview (first 100 chars of content)
- Support click-to-view-full action
- Accessible: semantic button/link with aria-label
- Responsive: proper touch targets (44px minimum)
- Create
-
Create HistoryFeed List Component
- Create
HistoryFeed.tsxinsrc/components/features/journal/ - Implement virtual scrolling or lazy loading
- Show loading spinner at bottom when loading more
- Handle empty state (no drafts yet)
- Handle error state with retry option
- Create
-
Create HistoryDetailSheet Component
- Create
HistoryDetailSheet.tsxinsrc/components/features/journal/ - Reuse Sheet component from DraftViewSheet (Story 2.2)
- Display full draft content with Merriweather font
- Include Copy button (reuse from Story 2.4)
- Support swipe-to-dismiss on mobile
- Handle edit/resume-chat action (future: Story 3.2)
- Create
-
Integrate History Feed into Home Page
- Update
src/app/page.tsxto include HistoryFeed - Layout: New Chat button at bottom, history above
- Handle navigation: tap card -> open detail sheet
- Add pull-to-refresh gesture (deferred - can be enhancement)
- Initial load shows first 20 drafts
- Update
-
Implement Date Formatting Utility
- Create
formatRelativeDate()insrc/lib/utils/date.ts - Support: "Today", "Yesterday", "X days ago", full date
- Support i18n (future enhancement)
- Test edge cases: future dates, null dates
- Create
-
Add Empty State for New Users
- Create
EmptyHistoryState.tsxcomponent - Show encouraging message when no drafts exist
- Include CTA to start first vent
- Use calming illustration or icon
- Create
-
Implement Loading States
- Skeleton screens for history cards
- Progressive loading animation (deferred - can be enhancement)
- Smooth fade-in for new items (deferred - can be enhancement)
-
Test History Feed End-to-End
- Unit test: DraftService.getCompletedDrafts() with pagination
- Unit test: DraftService.getCompletedCount()
- Unit test: HistoryStore loadMore action
- Unit test: formatRelativeDate()
- Component test: HistoryCard rendering
- Component test: HistoryFeed rendering
- Component test: Home page integration
- Edge case: No drafts (empty state)
- Edge case: Draft with no tags
- Edge case: Short content preview
- Integration tests: Deferred to integration test suite
Dev Notes
Architecture Compliance (CRITICAL)
Logic Sandwich Pattern - DO NOT VIOLATE:
- UI Components MUST NOT import
src/lib/dbdirectly - All history data MUST go through
DraftServicelayer - DraftService queries
db.draftstable (status='completed') - Components use Zustand store via atomic selectors only
- Services return plain arrays, not Dexie observables
State Management - Atomic Selectors Required:
// BAD - Causes unnecessary re-renders
const { drafts, loading } = useHistoryStore();
// GOOD - Atomic selectors
const drafts = useHistoryStore(s => s.drafts);
const loadMore = useHistoryStore(s => s.loadMore);
Local-First Data Boundary:
- History data is queried from IndexedDB only
- No server API calls for history (privacy requirement)
- Completed drafts persist locally (Story 2.4 completion flow)
- Offline support: History must be viewable without network
Performance Requirements:
- NFR-02: App must load in <1.5s
- Use lazy loading/virtual scrolling for large histories
- Limit initial load to 20 drafts
- Pagination loads additional drafts on demand
Architecture Implementation Details
Story Purpose: This story implements the "History Journal" - the persistent feed where users can revisit all their past "Enlightenment" artifacts. This transforms the app from a single-use tool into a growth journal, enabling reflection on past insights.
Data Source - Completed Drafts:
- Story 2.4 established draft completion flow
- Completed drafts have
status: 'completed'andcompletedAttimestamp - History queries
db.drafts.where('[status+completedAt]').between(...) - Sorted by
completedAtdescending (newest first) using compound index
HistoryStore Architecture:
// src/lib/store/history-store.ts
interface HistoryState {
drafts: Draft[]; // Loaded drafts (paginated)
loading: boolean;
hasMore: boolean; // Can load more?
error: string | null;
selectedDraft: Draft | null;
// Actions
loadMore: () => Promise<void>;
refreshHistory: () => Promise<void>;
selectDraft: (draft: Draft) => void;
closeDetail: () => void;
}
DraftService History Methods:
// src/lib/db/draft-service.ts
export class DraftService {
// Get completed drafts with pagination
static async getCompletedDrafts(options: {
limit?: number;
offset?: number;
}): Promise<Draft[]> {
return await db.drafts
.where('status')
.equals('completed')
.reverse() // Newest first
.offset(options.offset || 0)
.limit(options.limit || 20)
.toArray();
}
// Count total completed drafts
static async getCompletedCount(): Promise<number> {
return await db.drafts
.where('status')
.equals('completed')
.count();
}
}
Files to Create:
src/lib/store/history-store.ts- History feed state managementsrc/components/features/journal/HistoryCard.tsx- Individual history itemsrc/components/features/journal/HistoryFeed.tsx- List with lazy loadingsrc/components/features/journal/HistoryDetailSheet.tsx- Full draft viewsrc/components/features/journal/EmptyHistoryState.tsx- Empty statesrc/lib/utils/date.ts- Date formatting utilitiessrc/components/features/journal/index.ts- Feature exports
Files to Modify:
src/lib/db/draft-service.ts- Add getCompletedDrafts(), getCompletedCount()src/app/(main)/page.tsx- Integrate HistoryFeed into home page
UX Design Specifications
From UX Design Document:
History Feed as Home Screen:
- The home screen IS the history feed
- Latest generated post shown at top (reminder of value)
- Large "+" or "New" button at bottom for new vent
- Scrolling through past wins provides dopamine hits
Visual Hierarchy:
graph TD
A[Home Screen] --> B[Header: My Journal]
A --> C[History Feed - Scrollable]
A --> D[FAB: + New Vent]
C --> E[History Card 1 - Latest]
C --> F[History Card 2]
C --> G[History Card 3...]
C --> H[Load More Indicator]
HistoryCard Design:
- Title: Merriweather font, bold, truncate after 2 lines
- Date: Inter font, subtle gray, relative format ("Today", "Yesterday")
- Tags: Pill badges, subtle background
- Preview: First 100 chars of content, light gray
- Elevation: Subtle shadow on hover (desktop) or tap (mobile)
HistoryDetailSheet Pattern:
- Reuses DraftViewSheet component from Story 2.2
- Same "Medium-style" typography (Merriweather)
- Copy button in footer (from Story 2.4)
- Swipe down to dismiss (mobile pattern)
Empty State Design:
- Message: "Your journey starts here"
- Subtext: "Every venting session becomes a learning moment"
- CTA: "Start My First Vent" button
- Calming illustration: Mountain path or sunrise
Loading States:
- Initial Load: 3 skeleton cards with shimmer effect
- Load More: Spinner at bottom of list
- Progressive Fade-in: New items fade in smoothly
Typography Specifications:
/* HistoryCard */
.history-title {
font-family: 'Merriweather', serif;
font-weight: 700;
font-size: 1.125rem;
line-height: 1.4;
}
.history-date {
font-family: 'Inter', sans-serif;
color: #64748B; /* Slate-500 */
font-size: 0.875rem;
}
.history-preview {
font-family: 'Inter', sans-serif;
color: #94A3B8; /* Slate-400 */
font-size: 0.875rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
Interaction Design:
- Tap card: Opens detail sheet (smooth slide-up)
- Long press: Show context menu (Copy, Delete - Story 3.2)
- Pull to refresh: Reload history from database
- Scroll to bottom: Trigger loadMore when 5 items from end
Previous Story Intelligence (from Epic 2)
Patterns Established (must follow):
- Logic Sandwich Pattern: UI -> Zustand -> Service -> DB (strictly enforced)
- Atomic Selectors: All state access uses
useStore(s => s.field) - Draft Storage: Completed drafts in
draftstable with status='completed' - Sheet Pattern: Reuse DraftViewSheet for detail view
- Copy Utility: Reuse clipboard.copyMarkdown() from Story 2.4
Key Files from Previous Stories:
src/lib/db/draft-service.ts- Add history query methodssrc/lib/store/chat-store.ts- Pattern for atomic selectorssrc/components/features/draft/DraftViewSheet.tsx- Reuse for detail viewsrc/lib/utils/clipboard.ts- Reuse copy functionalitysrc/components/ui/- ShadCN primitives (Card, Button, Sheet)
Learnings to Apply:
- Epic 1 retrospective: Atomic selectors are non-negotiable for performance
- Story 2.2 established Sheet auto-open/close pattern - reuse for history detail
- Story 2.4 established completion status - query by status='completed'
- Use same shimmer loading pattern from Story 2.2 (DraftViewSheet skeleton)
- Follow same error handling pattern (retry with exponential backoff)
Draft Data Structure (from Story 2.1):
interface DraftRecord {
id?: number;
sessionId: string;
title: string;
content: string; // Markdown formatted
tags: string[];
createdAt: number;
completedAt?: number; // Set by Story 2.4
status: 'draft' | 'completed' | 'regenerated';
}
Integration with Copy (Story 2.4):
- Copy button in HistoryDetailSheet reuses clipboard utility
- Copy action doesn't change draft status (already completed)
- Success toast from Story 2.4 provides feedback
Pagination & Performance Strategy
Lazy Loading Implementation:
// HistoryStore pagination
const PAGE_SIZE = 20;
loadMore: async () => {
const currentLength = get().drafts.length;
const newDrafts = await DraftService.getCompletedDrafts({
limit: PAGE_SIZE,
offset: currentLength
});
set(state => ({
drafts: [...state.drafts, ...newDrafts],
hasMore: newDrafts.length === PAGE_SIZE
}));
}
Virtual Scrolling vs Lazy Loading:
- MVP Approach: Lazy loading (simpler, sufficient for <100 drafts)
- Future Enhancement: Virtual scrolling for very large histories
- Lazy loading appends to array, React renders all
- For MVP, 100 drafts * ~2KB each = ~200KB in memory (acceptable)
Performance Optimizations:
- Memoization: React.memo on HistoryCard component
- Key Prop: Use draft.id as key (stable across re-renders)
- Avoid Inline Functions: Define click handlers in component, not render
- Debounce Scroll: Trigger loadMore only when near bottom
Load More Trigger:
// Infinite scroll trigger
const handleScroll = (e: React.UIEvent) => {
const target = e.target as HTMLElement;
const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
// Trigger when 200px from bottom
if (scrollBottom < 200 && hasMore && !loading) {
loadMore();
}
};
Date Formatting Implementation
Utility Function:
// src/lib/utils/date.ts
export function formatRelativeDate(timestamp: number): string {
const now = Date.now();
const diff = now - timestamp;
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) return 'Today';
if (days === 1) return 'Yesterday';
if (days < 7) return `${days} days ago`;
if (days < 30) return `${Math.floor(days / 7)} weeks ago`;
// Full date for older posts
const date = new Date(timestamp);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined
});
}
Time Zones:
- Store timestamps as UTC (Date.now())
- Format in user's local timezone (toLocaleDateString handles this)
- Avoid timezone conversion bugs
Component Implementation Details
HistoryCard Component:
// src/components/features/journal/HistoryCard.tsx
interface HistoryCardProps {
draft: Draft;
onClick: (draft: Draft) => void;
}
export function HistoryCard({ draft, onClick }: HistoryCardProps) {
return (
<button
onClick={() => onClick(draft)}
className="w-full text-left p-4 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow"
aria-label={`View post: ${draft.title}`}
>
<h3 className="history-title">{draft.title}</h3>
<p className="history-date">{formatRelativeDate(draft.completedAt)}</p>
{draft.tags?.length > 0 && (
<div className="flex gap-2 mt-2">
{draft.tags.map(tag => (
<span key={tag} className="px-2 py-1 bg-slate-100 rounded text-xs">
{tag}
</span>
))}
</div>
)}
<p className="history-preview mt-2">{draft.content.slice(0, 100)}...</p>
</button>
);
}
HistoryFeed Component:
// src/components/features/journal/HistoryFeed.tsx
export function HistoryFeed() {
const drafts = useHistoryStore(s => s.drafts);
const loading = useHistoryStore(s => s.loading);
const hasMore = useHistoryStore(s => s.hasMore);
const loadMore = useHistoryStore(s => s.loadMore);
const selectDraft = useHistoryStore(s => s.selectDraft);
useEffect(() => {
// Initial load
loadMore();
}, []);
if (drafts.length === 0 && !loading) {
return <EmptyHistoryState />;
}
return (
<div className="space-y-3" onScroll={handleScroll}>
{drafts.map(draft => (
<HistoryCard
key={draft.id}
draft={draft}
onClick={selectDraft}
/>
))}
{loading && <LoadingSpinner />}
{!hasMore && drafts.length > 0 && (
<p className="text-center text-slate-500">You've reached the beginning</p>
)}
</div>
);
}
HistoryDetailSheet Component:
// src/components/features/journal/HistoryDetailSheet.tsx
export function HistoryDetailSheet() {
const selectedDraft = useHistoryStore(s => s.selectedDraft);
const closeDetail = useHistoryStore(s => s.closeDetail);
if (!selectedDraft) return null;
return (
<Sheet open={!!selectedDraft} onOpenChange={closeDetail}>
<SheetContent>
<DraftContent draft={selectedDraft} />
<SheetFooter>
<CopyButton draftId={selectedDraft.id} />
</SheetFooter>
</SheetContent>
</Sheet>
);
}
Home Page Integration
Layout Strategy:
// src/app/(main)/page.tsx
export default function HomePage() {
return (
<div className="min-h-screen bg-slate-50">
<header className="p-4">
<h1 className="text-2xl font-serif">My Journal</h1>
</header>
<main className="pb-24">
<HistoryFeed />
</main>
<FloatingActionButton
href="/chat"
aria-label="Start new vent"
>
<PlusIcon />
</FloatingActionButton>
<HistoryDetailSheet />
</div>
);
}
Navigation Flow:
- Home page shows history feed by default
- FAB (Floating Action Button) navigates to
/chatfor new vent - Tapping history card opens detail sheet (stays on home page)
- Detail sheet swipe-to-dismiss returns to feed
Accessibility Requirements
WCAG AA Compliance:
- History cards must be buttons or links with proper semantics
- All interactive elements have 44px minimum touch targets
- Focus management: Detail sheet traps focus until dismissed
- Screen reader announces draft title and date
- Empty state is accessible and encouraging
Keyboard Navigation:
- Tab through history cards in order
- Enter/Space opens detail sheet
- Escape closes detail sheet
- Focus returns to triggering card after close
Screen Reader Support:
<HistoryCard
draft={draft}
onClick={selectDraft}
aria-label={`${draft.title}, posted ${formatRelativeDate(draft.completedAt)}`}
/>
Testing Requirements
Unit Tests:
DraftService.getCompletedDrafts()returns completed drafts onlyDraftService.getCompletedDrafts()respects pagination (limit, offset)formatRelativeDate()returns correct relative datesHistoryStore.loadMore()appends drafts correctlyHistoryStore.loadMore()sets hasMore to false when exhausted
Integration Tests:
- HistoryFeed loads drafts from IndexedDB on mount
- Tapping card opens detail sheet with correct draft
- LoadMore triggers when scrolling near bottom
- Copy button in detail sheet copies content
- Empty state shows when no completed drafts exist
Edge Cases:
- No completed drafts: Show empty state
- Single draft: Show draft, hide load more
- Exactly 20 drafts: Load more shows, loads empty second page
- 100+ drafts: Pagination works smoothly
- Draft with very long title: Truncate properly
- Draft with no tags: Render without tag section
- Draft with special characters in content: Escape properly
- Navigate away during load: Handle gracefully (cancel in-flight request)
Performance Tests:
- Initial page load <1.5s (NFR-02)
- Scroll performance: 60fps with 100 items in DOM
- Load more completes within 500ms
- Detail sheet opens within 100ms
Accessibility Tests:
- Keyboard navigation through entire history
- Screen reader announces all content
- Focus management in detail sheet
- Touch targets are 44px minimum
Project Structure Notes
Following Feature-First Lite Pattern:
- New feature folder:
src/components/features/journal/ - All history-related components in this folder
- Service extensions in existing
draft-service.ts - New store in
src/lib/store/history-store.ts
Alignment with Unified Project Structure:
src/
components/
features/
journal/ # NEW: History feed
HistoryCard.tsx
HistoryFeed.tsx
HistoryDetailSheet.tsx
EmptyHistoryState.tsx
index.ts
draft/ # Existing: From Epic 2
chat/ # Existing: From Epic 1
lib/
store/
history-store.ts # NEW
utils/
date.ts # NEW
No Conflicts Detected:
- Journal is new feature, no overlap with existing code
- Reuses existing Sheet, Button, Card from ShadCN
- DraftService extension adds new methods (no breaking changes)
Service Layer Extensions
DraftService History Methods:
// src/lib/db/draft-service.ts
export class DraftService {
// Existing methods...
// NEW: Get completed drafts with pagination
static async getCompletedDrafts(options: {
limit?: number;
offset?: number;
}): Promise<Draft[]> {
const query = db.drafts
.where('status')
.equals('completed')
.reverse(); // Newest first
if (options.offset) query = query.offset(options.offset);
if (options.limit) query = query.limit(options.limit);
return await query.toArray();
}
// NEW: Count completed drafts
static async getCompletedCount(): Promise<number> {
return await db.drafts
.where('status')
.equals('completed')
.count();
}
// NEW: Get single draft by ID (for detail view)
static async getDraftById(id: number): Promise<Draft | undefined> {
return await db.drafts.get(id);
}
}
HistoryStore Implementation:
// src/lib/store/history-store.ts
import { create } from 'zustand';
import { DraftService } from '../db/draft-service';
interface HistoryState {
drafts: Draft[];
loading: boolean;
hasMore: boolean;
error: string | null;
selectedDraft: Draft | null;
loadMore: () => Promise<void>;
refreshHistory: () => Promise<void>;
selectDraft: (draft: Draft) => void;
closeDetail: () => void;
}
export const useHistoryStore = create<HistoryState>((set, get) => ({
drafts: [],
loading: false,
hasMore: true,
error: null,
selectedDraft: null,
loadMore: async () => {
const { drafts, loading } = get();
if (loading) return;
set({ loading: true, error: null });
try {
const newDrafts = await DraftService.getCompletedDrafts({
limit: 20,
offset: drafts.length
});
set(state => ({
drafts: [...state.drafts, ...newDrafts],
hasMore: newDrafts.length === 20,
loading: false
}));
} catch (error) {
set({
error: 'Failed to load history',
loading: false
});
}
},
refreshHistory: async () => {
set({ drafts: [], hasMore: true });
await get().loadMore();
},
selectDraft: (draft: Draft) => set({ selectedDraft: draft }),
closeDetail: () => set({ selectedDraft: null }),
}));
Mobile-First Responsive Design
Breakpoint Strategy (from UX):
- Mobile (<768px): Full width, bottom FAB, swipe gestures
- Desktop (>=768px): Centered container (600px max), extra whitespace
Mobile Optimizations:
- Pull-to-refresh gesture for reloading history
- Swipe-to-dismiss for detail sheet
- Haptic feedback on tap (optional enhancement)
- Touch targets minimum 44px
Desktop Adaptations:
/* Center container on desktop */
@media (min-width: 768px) {
.history-feed-container {
max-width: 600px;
margin: 0 auto;
padding: 2rem 1rem;
}
}
Performance Requirements
NFR-02 Compliance (App Load Time):
- Initial history load must complete within 1.5s
- Lazy load ensures fast initial render (20 drafts max)
- Subsequent loads happen on-demand
State Updates:
- HistoryStore uses immer middleware for efficient updates
- Atomic selectors prevent unnecessary re-renders
- Memoization on HistoryCard component
Database Query Optimization:
- Use IndexedDB indexes (completedAt is indexed, [status+completedAt] compound index used for sorting)
- Pagination limits query results
- Reverse sorting uses index efficiently
Offline Behavior
NFR-05 Compliance (Offline Access):
- History feed must be viewable offline
- All data is local (IndexedDB)
- No network requests required
- Show "Offline" indicator if no network (optional)
Offline Sync Queue:
- Story 3.3 will implement full sync queue
- For this story, offline is default (no sync needed yet)
- Future: New drafts sync when online (Story 3.3)
Security & Privacy Requirements
NFR-03 & NFR-04 Compliance:
- All history data stays on device (IndexedDB)
- No server API calls for history
- No analytics or tracking on history views
- User owns their complete journal locally
References
Epic Reference:
- Epic 3: "My Legacy" - History, Offline Sync & PWA Polish
- Story 3.1: History Feed UI
- FR-06: "Users can view a chronological feed of past 'Enlightenments'"
- FR-14: "Users can export their entire history as a JSON/Markdown file" (future: Story 3.4)
Architecture Documents:
- Project Context: Logic Sandwich
- Project Context: State Management
- Project Context: Local-First Boundary
UX Design Specifications:
Previous Stories:
- Story 2.1: Ghostwriter Agent & Markdown Generation - Draft data structure
- Story 2.2: Draft View UI (The Slide-Up) - Sheet pattern to reuse
- Story 2.4: Export & Copy Actions - Copy functionality to reuse
Epic Retrospectives:
- Epic 1 Retrospective - Atomic selector lessons
Dev Agent Record
Agent Model Used
Claude Opus 4.5 (model ID: 'claude-opus-4-5-20251101')
Debug Log References
Session file: /tmp/claude/-home-maximilienmao-Projects-Test01/bf525d3f-3c0e-417a-81e7-dad92fec28ad/scratchpad
Completion Notes List
Story Analysis Completed:
- Extracted story requirements from Epic 3, Story 3.1
- Analyzed previous Epics 1 and 2 for established patterns
- Reviewed architecture for compliance requirements (Logic Sandwich, State Management, Local-First)
- Reviewed UX specification for history feed and empty state patterns
- Identified all files to create and modify
Implementation Context Summary:
Story Purpose: This story implements the "History Journal" - the persistent feed where users can revisit all their past "Enlightenment" artifacts. This transforms the app from a single-use tool into a growth journal, enabling reflection on past insights and building the "My Legacy" emotional connection.
Key Technical Decisions:
- New HistoryStore: Separate Zustand store for history state (not chat store)
- Pagination: Lazy load 20 drafts at a time for performance
- Reusable Components: Leverage Sheet from Story 2.2, Copy from Story 2.4
- Empty State: Encouraging entry point for new users
- Relative Dates: Human-readable timestamps ("Today", "Yesterday")
- Mobile-First: Full width on mobile, centered container on desktop
Dependencies:
- No new external dependencies required
- Uses existing ShadCN components (Card, Sheet, Button)
- Reuses clipboard utility from Story 2.4
- May need pull-to-refresh library (optional enhancement)
Integration Points:
- Home page shows history feed by default
- FAB navigates to
/chatfor new venting session - Tapping history card opens detail sheet (reuses DraftViewSheet)
- Story 3.2 will add delete actions to detail sheet
- Story 3.3 will add offline sync queue
Files to Create:
src/lib/store/history-store.ts- History feed state managementsrc/components/features/journal/HistoryCard.tsx- Individual history itemsrc/components/features/journal/HistoryFeed.tsx- List with lazy loadingsrc/components/features/journal/HistoryDetailSheet.tsx- Full draft viewsrc/components/features/journal/EmptyHistoryState.tsx- Empty statesrc/lib/utils/date.ts- Date formatting utilitiessrc/components/features/journal/index.ts- Feature exports
Files to Modify:
src/lib/db/draft-service.ts- Add getCompletedDrafts(), getCompletedCount(), getDraftById()src/app/(main)/page.tsx- Integrate HistoryFeed into home page
Testing Strategy:
- Unit tests for DraftService history methods
- Unit tests for date formatting utility
- Integration tests for feed loading and pagination
- Edge case tests (empty, single, large history)
- Accessibility tests (keyboard, screen reader)
History Feed Pattern:
Home Page Load
↓
HistoryStore.loadMore() - Initial 20 drafts
↓
DraftService.getCompletedDrafts({ limit: 20, offset: 0 })
↓
Query db.drafts.where('status').equals('completed').reverse()
↓
HistoryFeed renders cards for each draft
↓
User scrolls near bottom
↓
Trigger loadMore() - Append next 20 drafts
↓
User taps card
↓
HistoryStore.selectDraft(draft)
↓
HistoryDetailSheet opens with full content
↓
User taps Copy or swipes to dismiss
User Experience Flow:
- First-time user sees empty state with CTA to start first vent
- Returning user sees their journal of completed posts
- Tapping any post opens the full "Enlightenment" artifact
- Copy button allows quick export (from Story 2.4)
- Emotional payoff: Seeing past wins reinforces value of app
Lessons from Epic 1 Retrospective Applied:
- Atomic Selectors: All HistoryStore access uses
useHistoryStore(s => s.field) - Performance: Pagination prevents rendering 100+ items at once
- Testing: Comprehensive unit and integration tests
- State Management: Separate store avoids chat store bloat
File List
New Files to Create:
src/lib/store/history-store.ts- History feed state managementsrc/components/features/journal/HistoryCard.tsx- Individual history itemsrc/components/features/journal/HistoryFeed.tsx- List componentsrc/components/features/journal/HistoryDetailSheet.tsx- Detail view sheetsrc/components/features/journal/EmptyHistoryState.tsx- Empty statesrc/components/features/journal/index.ts- Feature exportssrc/lib/utils/date.ts- Date formatting utilitiessrc/lib/utils/date.test.ts- Date utility testssrc/lib/store/history-store.test.ts- History store testssrc/integration/history-feed.test.ts- End-to-end history testssrc/components/features/journal/HistoryCard.test.tsx- HistoryCard testssrc/components/features/journal/HistoryFeed.test.tsx- HistoryFeed tests
Files to Modify:
src/lib/db/draft-service.ts- Add history query methodssrc/lib/db/draft-service.test.ts- Add history method testssrc/app/(main)/page.tsx- Integrate history feed