diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b127b86..22154a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: [main] pull_request: - branches: [main] permissions: contents: write @@ -20,7 +19,9 @@ jobs: node-version: 22 cache: 'npm' - run: npm ci + - run: npx playwright install --with-deps - run: npm run check + - run: npm run test:e2e deploy: name: 'Build and Deploy' diff --git a/.gitignore b/.gitignore index 5ef6a52..5a9d324 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Playwright +/playwright-report/ +/test-results/ diff --git a/AGENTS.md b/AGENTS.md index 68626b5..e9ef6dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,12 @@ Before writing any implementation code, agents must: 2. **Plan (Spec)**: Create a new specification file in `specs/pending/`. 3. **Approve**: Present the plan to the user and wait for explicit approval. 4. **Execute**: Implement surgical changes following the approved plan. -5. **Validate**: Run `npm run check` to ensure zero regressions. + - **Branching**: Always create a feature branch (`feat/name`) or fix branch (`fix/name`) before execution. + - **Incrementality**: Extract/Implement one logical unit (e.g., one component) at a time. + - **Test-Driven**: Write unit tests for each unit immediately after creation. + - **Atomic Commits**: Commit and push each successful "Unit + Test" cycle. + - **No Release Files**: NEVER create separate Markdown files for release notes in the repository. Provide release notes in the final task summary or PR description only. +5. **Validate**: Run `npm run check` to ensure zero regressions before final PR. ## 🛠 Tech Stack Constraints diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f75dbd9..642994a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,15 +54,24 @@ If working without an agent, follow these steps to keep the project state synchr | `npm run check:fast` | Fast checks: Lint, Format, and Type-check | | `npm run fix` | Automatically fix linting and formatting issues | | `npm run test` | Run unit tests with Vitest | +| `npm run test:e2e` | Run E2E tests with Playwright | | `npm run type-check` | Validate TypeScript types | ## 🧪 Testing Strategy Beyond end-to-end testing, we use a multi-tiered strategy for component, accessibility, and visual validation: -1. **Unit Tests (`*.test.[ts|tsx]`)**: Validate logic, utilities, and basic component interaction using Vitest. - - **Unit Tests**: Must be co-located with the source file (e.g., `src/utils/share.test.ts` for `src/utils/share.ts`). -2. **E2E Tests**: Reserved for the `e2e/` directory (to be created when needed). +1. **Unit Tests (`*.test.[ts|tsx]`)**: Validate logic, utilities, and basic component interaction using Vitest and JSDOM. These are fast and do not require a browser. + - **Co-location**: Unit tests MUST be co-located with the source file they test (e.g., `src/components/Button.test.tsx` for `src/components/Button.tsx`). + - **Command**: `npm run test` (or `npm run check` for the full suite). +2. **Visual Regression Tests (`*.spec.[ts|tsx]`)**: Validate component-level rendering and pixel-perfect consistency in a real browser (Chromium) using Playwright. + - **Location**: Co-located with the component, similar to unit tests, but using the `.spec` suffix. + - **Command**: `npm run test:visual` (Future capability). +3. E2E Tests (`e2e/*.spec.ts`): Validate full user workflows in a real browser environment. + - **Location**: Dedicated `e2e/` directory at the project root. + - **Command**: `npm run test:e2e` +4. **Storybook Interaction & A11y Tests**: Validate visual/accessibility compliance (e.g., color contrast) and component interactions in isolation. + - **Command**: `npm run test:storybook` (Future capability). ## ✅ The "Definition of Done" @@ -91,7 +100,7 @@ Use the `npm version [patch|minor|major]` command before committing. ## 📝 Release Note Best Practices -To maintain consistent, high-quality release notes, follow this structure: +To maintain consistent, high-quality release notes, follow the structure below. **IMPORTANT**: Do NOT create separate Markdown files in the repository for release notes. Instead, include them in your **Pull Request description**. - **Format**: `Release [Version] - [Short Descriptive Title]` - **Punchline**: 2-3 sentence summary. diff --git a/README.md b/README.md index baca142..b4b0bac 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ This project follows **Spec-Driven Development (SDD)** to maintain a clear roadm ```bash npm run dev ``` -5. **Open the App**: Visit `http://localhost:3000` and enter your API key in the top bar. +5. **Open the App**: Visit `http://localhost:3000` (development) or `http://localhost:3000/quick-tripper` (production-emulated) and enter your API key in the top bar. ## 🏗️ Architecture @@ -64,8 +64,9 @@ To maintain a robust "zero-backend" application, we rely on a multi-tiered valid 1. **Architectural Design**: High-level designs and data contracts are documented in `ARCHITECTURE.md`. 2. **Static Analysis**: ESLint and Prettier ensure code consistency and catch early errors. 3. **Type Safety**: Strict TypeScript is enforced at the commit level via Husky hooks. -4. **Unit Testing**: Vitest and JSDOM validate core logic and utility functions (e.g., URL compression). -5. **Spec-First Implementation**: Every change is traced back to a technical specification in `specs/`, ensuring architectural alignment. +4. **Unit Testing**: Vitest and JSDOM validate core logic, utility functions, and presentational components (co-located tests). +5. **E2E Testing**: Playwright validates full user workflows and "golden paths" in a real browser environment. +6. **Spec-First Implementation**: Every change is traced back to a technical specification in `specs/`, ensuring architectural alignment. ## 📦 Deployment diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 67e9986..31852fd 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -40,6 +40,26 @@ When the `Trip` data structure changes in a breaking way, follow these steps to 5. **Test**: - Add test cases in `src/tests/migration.test.ts` to verify the upgrade path from `v` to `v`. +## Component Architecture + +The project follows a **Smart Container & Dumb Presenter** pattern (Clean Architecture) to ensure high testability and separation of concerns. + +### Smart Containers (Pages/Hooks) + +- **Role**: Data orchestration, service invocation (API calls), and state management. +- **Location**: Found in `src/app/` (Pages) and `src/hooks/`. +- **Responsibility**: They do not contain complex UI logic or styling. They pass data and event handlers down to presentational components. + +### Dumb Presenters (Components) + +- **Role**: Visual representation and user interaction. +- **Location**: Found in `src/components/`. +- **Responsibility**: They are "stateless" (logic-lite) and rely entirely on props. They must be easily unit-testable in isolation using Vitest and React Testing Library. + +### Testing Co-location + +To maintain architectural clarity, every presentational component MUST have a co-located unit test file (e.g., `src/components/MyComponent.test.tsx`). + ## SSR and Hydration Strategy To maintain a "zero-backend" architecture using Next.js (SSR), we must ensure that the server-rendered HTML and client-rendered UI are identical during the initial mount. diff --git a/e2e/golden-path.spec.ts b/e2e/golden-path.spec.ts new file mode 100644 index 0000000..2aea617 --- /dev/null +++ b/e2e/golden-path.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from '@playwright/test'; + +test('golden path - generate trip', async ({ page }) => { + await page.goto('/'); + // Mock API + await page.route('https://router.huggingface.co/v1/chat/completions', (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + choices: [ + { + message: { + content: '# Golden Trip\nThis is a mocked itinerary.', + }, + }, + ], + }), + }), + ); + + // Enter API Key + await page.getByPlaceholder('HuggingFace API Token').fill('dummy-key'); + // Enter Destination + await page.getByPlaceholder('Ex: A 4-day hike').fill('Swiss Alps'); + // Send + await page.locator('.join').getByRole('button').click(); // Send icon button within join component + + // Verify + await expect(page.getByText('Golden Trip')).toBeVisible(); +}); diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts new file mode 100644 index 0000000..ab4e3e7 --- /dev/null +++ b/e2e/smoke.spec.ts @@ -0,0 +1,7 @@ +import { test, expect } from '@playwright/test'; + +test('homepage should have correct branding and show EmptyState', async ({ page }) => { + await page.goto('/'); + await expect(page.getByText('Quick-tripper')).toBeVisible(); + await expect(page.getByText('No active itineraries loaded')).toBeVisible(); +}); diff --git a/next.config.ts b/next.config.ts index bb49c21..c95f024 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,8 +1,10 @@ /** @type {import('next').NextConfig} */ +const isProd = process.env.NODE_ENV === 'production'; + const nextConfig = { output: 'export', // Indispensable pour GitHub Pages - basePath: '/quick-tripper', - assetPrefix: '/quick-tripper', + basePath: isProd ? '/quick-tripper' : '', + assetPrefix: isProd ? '/quick-tripper/' : '', images: { unoptimized: true, // Recommandé pour l'export statique }, diff --git a/package-lock.json b/package-lock.json index 2dc9dbe..a8235be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "quick-tripper", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quick-tripper", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "dependencies": { "@tailwindcss/typography": "^0.5.20", @@ -20,6 +20,7 @@ "rehype-raw": "^7.0.0" }, "devDependencies": { + "@playwright/test": "^1.60.0", "@tailwindcss/postcss": "^4", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", @@ -1518,6 +1519,22 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", @@ -8155,6 +8172,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index 60eff9b..d543406 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "quick-tripper", "type": "module", - "version": "0.1.0", + "version": "0.2.0", "private": true, "description": "Privacy-first, zero-backend AI travel companion", "author": "GehDoc", @@ -38,6 +38,7 @@ "format:fix": "prettier --write .", "type-check": "tsc --noEmit", "test": "vitest run", + "test:e2e": "playwright test", "test:watch": "vitest", "check": "npm run lint && npm run format && npm run type-check && npm run test", "check:fast": "npm run lint && npm run format && npm run type-check", @@ -62,6 +63,7 @@ "rehype-raw": "^7.0.0" }, "devDependencies": { + "@playwright/test": "^1.60.0", "@tailwindcss/postcss": "^4", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..a633d20 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'list', + use: { + baseURL: 'http://localhost:3001', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'npx next dev -p 3001', + url: 'http://localhost:3001', + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, +}); diff --git a/specs/completed/e2e-testing-setup-v1.md b/specs/completed/e2e-testing-setup-v1.md new file mode 100644 index 0000000..31a880b --- /dev/null +++ b/specs/completed/e2e-testing-setup-v1.md @@ -0,0 +1,31 @@ +# Spec: E2E Testing Setup with Playwright (v1) + +## Status + +🟢 Completed + +## Overview + +Initialize Playwright E2E testing for Quick-tripper to ensure core functionality works end-to-end. + +## Scope + +- [x] **Installation**: Install `@playwright/test`. +- [x] **Configuration**: Create initial Playwright configuration in the root. +- [x] **Directory**: Create `e2e/` folder for test files. +- [x] **Tests**: + - [x] `e2e/smoke.spec.ts`: Verify homepage loading, branding, and EmptyState. + - [x] `e2e/golden-path.spec.ts`: Mock Hugging Face API, simulate user trip generation, verify output. +- [x] **Scripts**: Add `test:e2e` to `package.json`. +- [x] **Documentation**: Update `CONTRIBUTING.md` to include the new E2E testing strategy. + +## Verification + +- [x] Run tests using `npx playwright test`. +- [x] Ensure tests pass. +- [x] Add `test:e2e` to `package.json` and run. +- [x] Update `CONTRIBUTING.md`. + +## Change Log + +- 2026-06-13: E2E suite initialized, smoke and golden-path tests implemented, `package.json` and `CONTRIBUTING.md` updated. Tests verified passing. diff --git a/specs/pending/ci-e2e-integration-v1.md b/specs/pending/ci-e2e-integration-v1.md new file mode 100644 index 0000000..96b2cf2 --- /dev/null +++ b/specs/pending/ci-e2e-integration-v1.md @@ -0,0 +1,15 @@ +# Spec: Integrate E2E Tests into CI/CD (v1) + +## Overview + +Include E2E tests (`npm run test:e2e`) in the `npm run check` script and ensure they run in GitHub Actions CI. + +## Scope + +1. **Scripts**: Add `test:e2e` to `npm run check` in `package.json`. +2. **CI**: Update `.github/workflows/ci.yml` to run Playwright E2E tests. + +## Verification + +1. Run `npm run check` locally to ensure it now includes E2E tests. +2. Verify CI workflow runs E2E tests. diff --git a/specs/pending/refactor-page-components-v1.md b/specs/pending/refactor-page-components-v1.md new file mode 100644 index 0000000..8bad73b --- /dev/null +++ b/specs/pending/refactor-page-components-v1.md @@ -0,0 +1,81 @@ +# Specification: Page Component Refactoring + +## Goal + +Refactor `src/app/page.tsx` to follow a clean architecture by extracting logic-heavy UI sections into "dumb", presentational components. This will improve testability, maintainability, and readability. + +## Current State + +The `Home` component in `src/app/page.tsx` is a "God Component" handling: + +- State orchestration (via `useTrips`). +- API Key management and local storage syncing. +- AI Service calls. +- File I/O (JSON Import/Export). +- Complex Markdown rendering logic (including iframe security). +- Layout and styling. + +## Proposed Architecture + +Extract UI logic into 6 functional components in `src/components/`. The `Home` page will act as a "Smart Container" that manages data and passes handlers. + +### 1. `Navbar` + +- **Props**: `apiKey: string`, `onApiKeyChange: (val: string) => void`. +- **Content**: Logo, versioning, GitHub link, and API Token input. + +### 2. `GenerationForm` + +- **Props**: `prompt: string`, `onPromptChange: (val: string) => void`, `onGenerate: () => void`, `isLoading: boolean`, `error: string`. +- **Content**: Prompt input field and generation button. + +### 3. `WorkspaceActions` + +- **Props**: `totalTrips: number`, `onExport: () => void`, `onImport: (e: React.ChangeEvent) => void`, `onShare: () => void`. +- **Content**: Export, Import (with hidden file input), and Share buttons. + +### 4. `TripNavigator` + +- **Props**: `activeTrip: Trip`, `activeIndex: number`, `totalTrips: number`, `onNext: () => void`, `onPrev: () => void`. +- **Content**: Pagination controls and trip metadata (destination/date). + +### 5. `TripViewer` (formerly TripContent) + +- **Props**: `content: string`, `onDelete: () => void`. +- **Content**: Markdown renderer with custom components for Google Maps iframes and specialized link styling. + +### 6. `EmptyState` + +- **Props**: None. +- **Content**: Placeholder UI when no trips are present. + +## Implementation Plan + +This refactoring will be executed incrementally. For each component identified in the architecture: + +1. **Extract**: Create the component in `src/components/`. +2. **Test**: Create a corresponding unit test (e.g., `src/components/__tests__/ComponentName.test.tsx`). +3. **Integrate**: Update `src/app/page.tsx` to use the new component. +4. **Verify & Commit**: Run `npm run check`, then commit and push the changes for that specific component. + +### Execution Order: + +1. [x] `EmptyState` +2. [x] `Navbar` +3. [x] `GenerationForm` +4. [x] `WorkspaceActions` +5. [x] `TripNavigator` +6. [x] `TripViewer` + +## Branching Strategy + +- **Branch**: `feat/refactor-page-components` +- **Commits**: Atomic commits for each component extraction + test suite. + +## Testing Strategy + +1. **Unit Tests**: Create test files for each new component in `src/components/__tests__/` (if following project convention, or alongside the file). + - Verify `GenerationForm` displays the loading spinner when `isLoading` is true. + - Verify `TripViewer` correctly blocks non-Google Maps iframes. + - Verify `WorkspaceActions` buttons are disabled when `totalTrips` is 0. +2. **Regression Testing**: Run `npm run check` to ensure no type errors or linting regressions were introduced. diff --git a/src/app/page.tsx b/src/app/page.tsx index 3eccbff..2bef040 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,26 +1,17 @@ 'use client'; import React, { useState } from 'react'; -import ReactMarkdown from 'react-markdown'; -import rehypeRaw from 'rehype-raw'; -import { FaKey as Key } from 'react-icons/fa'; -import { IoSend as Send } from 'react-icons/io5'; -import { - FiDownload as Download, - FiUpload as Upload, - FiShare2 as Share2, - FiTrash2 as Trash2, - FiChevronLeft as ChevronLeft, - FiChevronRight as ChevronRight, -} from 'react-icons/fi'; -import { SiGithub as GitHub } from 'react-icons/si'; // Standardized Hook, Service, and Share Utilities import { useTrips } from '@/hooks/useTrips'; import { generateItinerary } from '@/services/ai'; import { generateShareUrl } from '@/utils/share'; -import { VERSION, REPO_URL } from '@/utils/version'; -import { Logo } from '@/components/Logo'; +import { EmptyState } from '@/components/EmptyState'; +import { Navbar } from '@/components/Navbar'; +import { GenerationForm } from '@/components/GenerationForm'; +import { WorkspaceActions } from '@/components/WorkspaceActions'; +import { TripNavigator } from '@/components/TripNavigator'; +import { TripViewer } from '@/components/TripViewer'; export default function Home() { const { @@ -118,193 +109,36 @@ export default function Home() { return (
-
-
- -
- Quick-tripper -
- v{VERSION} - - GitHub - -
-
-
-
- -
-
+
-
-
-

- Describe your next journey -

-
- setPrompt(e.target.value)} - disabled={isLoading} - className="input input-bordered join-item w-full input-md focus:outline-none" - /> - -
- {error &&

{error}

} -
-
-
-
-

- Serverless Workspace -

-
- - -
- -
-
+ +
{activeTrip ? (
-
- -
-
{activeTrip.destination}
-
- {activeIndex + 1} / {totalTrips} — {activeTrip.createdAt} -
-
- -
- -
-
- - -
- ( - - {children} - - ), - iframe: ({ src, ...props }) => { - const isGoogleMap = - src?.startsWith('https://www.google.com/maps/embed') || - src?.startsWith('https://www.google.com/maps?') || - src?.startsWith('https://maps.google.com/'); - - if (!isGoogleMap) { - return ( -
- Blocked unsafe iframe: {src} -
- ); - } - - return ( -
- ', + createdAt: '2023-10-27', +}; + +describe('TripViewer', () => { + it('renders markdown content correctly', () => { + render( {}} />); + expect(screen.getByText('Tokyo Trip')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /Google Maps link/i })).toBeInTheDocument(); + }); + + it('calls onDelete when delete button is clicked', () => { + const handleDelete = vi.fn(); + render(); + + const deleteBtn = screen.getByTitle(/Remove data entry/i); + fireEvent.click(deleteBtn); + expect(handleDelete).toHaveBeenCalledWith('trip-1'); + }); + + it('renders whitelisted Google Maps iframe', () => { + const { container } = render( {}} />); + const iframe = container.querySelector('iframe'); + expect(iframe).toBeInTheDocument(); + expect(iframe).toHaveAttribute('src', 'https://www.google.com/maps/embed?pb=1'); + }); + + it('blocks unsafe iframes', () => { + const unsafeTrip = { + ...mockTrip, + content: '', + }; + const { container } = render( {}} />); + expect(screen.getByText(/Blocked unsafe iframe/i)).toBeInTheDocument(); + expect(container.querySelector('iframe')).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/TripViewer.tsx b/src/components/TripViewer.tsx new file mode 100644 index 0000000..f962b88 --- /dev/null +++ b/src/components/TripViewer.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import ReactMarkdown from 'react-markdown'; +import rehypeRaw from 'rehype-raw'; +import { FiTrash2 as Trash2 } from 'react-icons/fi'; +import { Logo } from '@/components/Logo'; +import { Trip } from '@/types/trip'; + +interface TripViewerProps { + trip: Trip; + onDelete: (id: string) => void; +} + +export const TripViewer: React.FC = ({ trip, onDelete }) => { + return ( +
+
+ + +
+ ( + + {children} + + ), + iframe: ({ src, ...props }) => { + const isGoogleMap = + src?.startsWith('https://www.google.com/maps/embed') || + src?.startsWith('https://www.google.com/maps?') || + src?.startsWith('https://maps.google.com/'); + + if (!isGoogleMap) { + return ( +
+ Blocked unsafe iframe: {src} +
+ ); + } + + return ( +
+