diff --git a/CHANGELOG.md b/CHANGELOG.md index 386461d..63f7e94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,10 @@ All notable changes are documented here. Format based on [Keep a Changelog](http and export controls (#555) - `/stream/[id]` — **PDF** button triggers `window.print()` to produce a formatted single-stream summary document via the print stylesheet; actions and back-link hidden in print view (#571) +- `/stream/[id]` — **Calendar** button downloads a standard `.ics` invitation for the stream's + end date (one-hour event, one-hour reminder, deterministic UID so re-importing updates the + existing entry instead of duplicating it), built by the new `lib/calendar.ts`; rendered only + for bounded streams, since an open-ended stream has no end date to schedule (#566) - `app/globals.css` — `@media print` block scoped to `.print-receipt` class: resets body to white/black, collapses table borders, hides interactive chrome, preserves monospace addresses and amounts, renders status badges in monochrome, adds a print footer with the diff --git a/__mocks__/lucide-react.tsx b/__mocks__/lucide-react.tsx index 7821913..9779f72 100644 --- a/__mocks__/lucide-react.tsx +++ b/__mocks__/lucide-react.tsx @@ -28,6 +28,7 @@ export const AlertCircle = createIconStub('AlertCircle'); export const ArrowDownToLine = createIconStub('ArrowDownToLine'); export const ArrowLeft = createIconStub('ArrowLeft'); export const ArrowRight = createIconStub('ArrowRight'); +export const CalendarPlus = createIconStub('CalendarPlus'); export const Check = createIconStub('Check'); export const CheckCircle = createIconStub('CheckCircle'); export const Clock = createIconStub('Clock'); diff --git a/app/stream/[id]/__tests__/page.test.tsx b/app/stream/[id]/__tests__/page.test.tsx index 3078d67..69f3c99 100644 --- a/app/stream/[id]/__tests__/page.test.tsx +++ b/app/stream/[id]/__tests__/page.test.tsx @@ -45,6 +45,14 @@ vi.mock('@/components/stream/StreamActions', () => ({ StreamActions: ({ token }: { token: string }) => React.createElement('div', { 'data-testid': 'stream-actions' }, token), })); +vi.mock('@/components/stream/AddToCalendarButton', () => ({ + AddToCalendarButton: ({ streamId, endTime }: { streamId: string; endTime: number }) => + React.createElement('div', { + 'data-testid': 'add-to-calendar', + 'data-stream-id': streamId, + 'data-end-time': String(endTime), + }), +})); import { useWallet } from '@/contexts/WalletContext'; import { getStreamAddress, getStreamInfo, getWithdrawable } from '@/lib/stream'; @@ -269,4 +277,46 @@ describe('StreamPage (app/stream/[id]/page.tsx)', () => { // silently stale "active" state from the first request. expect(container.querySelector('[data-testid="badge"]')?.textContent).toBe('cancelled'); }); + + it('#566 — offers the calendar action with the stream end date when the stream is bounded', async () => { + mockUseWallet.mockReturnValue({ + publicKey: 'GRECIPIENT', + connected: true, + } as unknown as ReturnType); + mockAddr.mockResolvedValue('STREAM_ADDR'); + const endTime = Math.floor(Date.now() / 1000) + 3600; + mockInfo.mockResolvedValue(makeInfo({ endTime })); + mockWithdrawable.mockResolvedValue(0n); + + await act(async () => { + root.render(React.createElement(StreamPage)); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + }); + + const action = container.querySelector('[data-testid="add-to-calendar"]'); + expect(action).not.toBeNull(); + expect(action?.getAttribute('data-stream-id')).toBe('42'); + expect(action?.getAttribute('data-end-time')).toBe(String(endTime)); + }); + + it('#566 — omits the calendar action for an open-ended stream (nothing to schedule)', async () => { + mockUseWallet.mockReturnValue({ + publicKey: 'GRECIPIENT', + connected: true, + } as unknown as ReturnType); + mockAddr.mockResolvedValue('STREAM_ADDR'); + mockInfo.mockResolvedValue(makeInfo({ endTime: 0 })); + mockWithdrawable.mockResolvedValue(0n); + + await act(async () => { + root.render(React.createElement(StreamPage)); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(container.querySelector('[data-testid="add-to-calendar"]')).toBeNull(); + }); }); diff --git a/app/stream/[id]/page.tsx b/app/stream/[id]/page.tsx index 29f338f..fde07fb 100644 --- a/app/stream/[id]/page.tsx +++ b/app/stream/[id]/page.tsx @@ -16,6 +16,7 @@ import { StreamActions } from "@/components/stream/StreamActions"; import { AddToCalendarButton } from "@/components/stream/AddToCalendarButton"; import { OperatorInfo } from "@/components/stream/OperatorInfo"; import { StreamNoteEditor } from "@/components/stream/StreamNoteEditor"; +import { AddToCalendarButton } from "@/components/stream/AddToCalendarButton"; import { useWallet } from "@/contexts/WalletContext"; import { useStreamNote } from "@/hooks/useStreamNote"; import { @@ -324,6 +325,17 @@ export default function StreamPage() {