Files
brachnha-insight/_bmad-output/implementation-artifacts/3-1-history-feed-ui.md
Max e9e6fadb1d fix: ChatBubble crash and DeepSeek API compatibility
- 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
2026-01-26 16:55:05 +07:00

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

  1. 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
  2. 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)
  • Create History Feed Store State

    • Create useHistoryStore in src/lib/store/history-store.ts
    • State: drafts array, loading, hasMore, error
    • Actions: loadMore, refreshHistory, selectDraft
    • Use atomic selectors for performance
  • Create HistoryCard Component

    • Create HistoryCard.tsx in src/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 HistoryFeed List Component

    • Create HistoryFeed.tsx in src/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 HistoryDetailSheet Component

    • Create HistoryDetailSheet.tsx in src/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)
  • Integrate History Feed into Home Page

    • Update src/app/page.tsx to 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
  • Implement Date Formatting Utility

    • Create formatRelativeDate() in src/lib/utils/date.ts
    • Support: "Today", "Yesterday", "X days ago", full date
    • Support i18n (future enhancement)
    • Test edge cases: future dates, null dates
  • Add Empty State for New Users

    • Create EmptyHistoryState.tsx component
    • Show encouraging message when no drafts exist
    • Include CTA to start first vent
    • Use calming illustration or icon
  • 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/db directly
  • All history data MUST go through DraftService layer
  • DraftService queries db.drafts table (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' and completedAt timestamp
  • History queries db.drafts.where('[status+completedAt]').between(...)
  • Sorted by completedAt descending (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 management
  • src/components/features/journal/HistoryCard.tsx - Individual history item
  • src/components/features/journal/HistoryFeed.tsx - List with lazy loading
  • src/components/features/journal/HistoryDetailSheet.tsx - Full draft view
  • src/components/features/journal/EmptyHistoryState.tsx - Empty state
  • src/lib/utils/date.ts - Date formatting utilities
  • src/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 drafts table 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 methods
  • src/lib/store/chat-store.ts - Pattern for atomic selectors
  • src/components/features/draft/DraftViewSheet.tsx - Reuse for detail view
  • src/lib/utils/clipboard.ts - Reuse copy functionality
  • src/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 /chat for 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 only
  • DraftService.getCompletedDrafts() respects pagination (limit, offset)
  • formatRelativeDate() returns correct relative dates
  • HistoryStore.loadMore() appends drafts correctly
  • HistoryStore.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:

Architecture Documents:

UX Design Specifications:

Previous Stories:

Epic Retrospectives:

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:

  1. New HistoryStore: Separate Zustand store for history state (not chat store)
  2. Pagination: Lazy load 20 drafts at a time for performance
  3. Reusable Components: Leverage Sheet from Story 2.2, Copy from Story 2.4
  4. Empty State: Encouraging entry point for new users
  5. Relative Dates: Human-readable timestamps ("Today", "Yesterday")
  6. 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 /chat for 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 management
  • src/components/features/journal/HistoryCard.tsx - Individual history item
  • src/components/features/journal/HistoryFeed.tsx - List with lazy loading
  • src/components/features/journal/HistoryDetailSheet.tsx - Full draft view
  • src/components/features/journal/EmptyHistoryState.tsx - Empty state
  • src/lib/utils/date.ts - Date formatting utilities
  • src/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 management
  • src/components/features/journal/HistoryCard.tsx - Individual history item
  • src/components/features/journal/HistoryFeed.tsx - List component
  • src/components/features/journal/HistoryDetailSheet.tsx - Detail view sheet
  • src/components/features/journal/EmptyHistoryState.tsx - Empty state
  • src/components/features/journal/index.ts - Feature exports
  • src/lib/utils/date.ts - Date formatting utilities
  • src/lib/utils/date.test.ts - Date utility tests
  • src/lib/store/history-store.test.ts - History store tests
  • src/integration/history-feed.test.ts - End-to-end history tests
  • src/components/features/journal/HistoryCard.test.tsx - HistoryCard tests
  • src/components/features/journal/HistoryFeed.test.tsx - HistoryFeed tests

Files to Modify:

  • src/lib/db/draft-service.ts - Add history query methods
  • src/lib/db/draft-service.test.ts - Add history method tests
  • src/app/(main)/page.tsx - Integrate history feed