diff --git a/client/src/components/shared/CopyTripDialog.tsx b/client/src/components/shared/CopyTripDialog.tsx
new file mode 100644
index 00000000..efea7afb
--- /dev/null
+++ b/client/src/components/shared/CopyTripDialog.tsx
@@ -0,0 +1,108 @@
+import React, { useEffect, useCallback } from 'react'
+import { Check, X } from 'lucide-react'
+import { useTranslation } from '../../i18n'
+
+interface CopyTripDialogProps {
+ isOpen: boolean
+ tripTitle: string
+ onClose: () => void
+ onConfirm: () => void
+}
+
+const WILL_COPY_KEYS = [
+ 'dashboard.confirm.copy.will1',
+ 'dashboard.confirm.copy.will2',
+ 'dashboard.confirm.copy.will3',
+ 'dashboard.confirm.copy.will4',
+ 'dashboard.confirm.copy.will5',
+ 'dashboard.confirm.copy.will6',
+]
+
+const WONT_COPY_KEYS = [
+ 'dashboard.confirm.copy.wont1',
+ 'dashboard.confirm.copy.wont2',
+ 'dashboard.confirm.copy.wont3',
+ 'dashboard.confirm.copy.wont4',
+]
+
+export default function CopyTripDialog({ isOpen, tripTitle, onClose, onConfirm }: CopyTripDialogProps) {
+ const { t } = useTranslation()
+
+ const handleEsc = useCallback((e: KeyboardEvent) => {
+ if (e.key === 'Escape') onClose()
+ }, [onClose])
+
+ useEffect(() => {
+ if (isOpen) document.addEventListener('keydown', handleEsc)
+ return () => document.removeEventListener('keydown', handleEsc)
+ }, [isOpen, handleEsc])
+
+ if (!isOpen) return null
+
+ return (
+
+
e.stopPropagation()}
+ >
+
+ {t('dashboard.confirm.copy.title')}
+
+
+ {tripTitle}
+
+
+
+
+
+ {t('dashboard.confirm.copy.willCopy')}
+
+
+ {WILL_COPY_KEYS.map(key => (
+ -
+
+ {t(key)}
+
+ ))}
+
+
+
+
+
+ {t('dashboard.confirm.copy.wontCopy')}
+
+
+ {WONT_COPY_KEYS.map(key => (
+ -
+
+ {t(key)}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts
index e444d957..a50ffcc3 100644
--- a/client/src/i18n/translations/en.ts
+++ b/client/src/i18n/translations/en.ts
@@ -122,6 +122,20 @@ const en: Record = {
'dashboard.toast.copied': 'Trip copied!',
'dashboard.toast.copyError': 'Failed to copy trip',
'dashboard.confirm.delete': 'Delete trip "{title}"? All places and plans will be permanently deleted.',
+ 'dashboard.confirm.copy.title': 'Copy this trip?',
+ 'dashboard.confirm.copy.willCopy': 'Will be copied',
+ 'dashboard.confirm.copy.will1': 'Days, places & day assignments',
+ 'dashboard.confirm.copy.will2': 'Accommodations & reservations',
+ 'dashboard.confirm.copy.will3': 'Budget items & category order',
+ 'dashboard.confirm.copy.will4': 'Packing lists (unchecked)',
+ 'dashboard.confirm.copy.will5': 'TODOs (unassigned & unchecked)',
+ 'dashboard.confirm.copy.will6': 'Day notes',
+ 'dashboard.confirm.copy.wontCopy': "Won't be copied",
+ 'dashboard.confirm.copy.wont1': 'Collaborators & member assignments',
+ 'dashboard.confirm.copy.wont2': 'Collab notes, polls & messages',
+ 'dashboard.confirm.copy.wont3': 'Files & photos',
+ 'dashboard.confirm.copy.wont4': 'Share tokens',
+ 'dashboard.confirm.copy.confirm': 'Copy trip',
'dashboard.editTrip': 'Edit Trip',
'dashboard.createTrip': 'Create New Trip',
'dashboard.tripTitle': 'Title',
diff --git a/client/src/pages/DashboardPage.test.tsx b/client/src/pages/DashboardPage.test.tsx
index 4aac3121..2a3530ee 100644
--- a/client/src/pages/DashboardPage.test.tsx
+++ b/client/src/pages/DashboardPage.test.tsx
@@ -401,6 +401,10 @@ describe('DashboardPage', () => {
const copyButtons = screen.getAllByRole('button', { name: /copy/i });
await user.click(copyButtons[0]);
+ // Confirm the copy dialog
+ const confirmButton = await screen.findByRole('button', { name: /copy trip/i });
+ await user.click(confirmButton);
+
await waitFor(() => {
expect(screen.getAllByText('Paris Adventure (Copy)')[0]).toBeInTheDocument();
});
@@ -766,6 +770,10 @@ describe('DashboardPage', () => {
expect(copyButtons.length).toBeGreaterThan(0);
await user.click(copyButtons[0]);
+ // Confirm the copy dialog
+ const confirmButton = await screen.findByRole('button', { name: /copy trip/i });
+ await user.click(confirmButton);
+
await waitFor(() => {
expect(screen.getAllByText('Paris Adventure (Copy)').length).toBeGreaterThan(0);
});
diff --git a/client/src/pages/DashboardPage.tsx b/client/src/pages/DashboardPage.tsx
index 321ba3ef..c3e4fa85 100644
--- a/client/src/pages/DashboardPage.tsx
+++ b/client/src/pages/DashboardPage.tsx
@@ -12,6 +12,7 @@ import CurrencyWidget from '../components/Dashboard/CurrencyWidget'
import TimezoneWidget from '../components/Dashboard/TimezoneWidget'
import TripFormModal from '../components/Trips/TripFormModal'
import ConfirmDialog from '../components/shared/ConfirmDialog'
+import CopyTripDialog from '../components/shared/CopyTripDialog'
import { useToast } from '../components/shared/Toast'
import { useCountUp } from '../hooks/useCountUp'
import {
@@ -699,6 +700,7 @@ export default function DashboardPage(): React.ReactElement {
const [showWidgetSettings, setShowWidgetSettings] = useState(false)
const [viewMode, setViewMode] = useState<'grid' | 'list'>(() => (localStorage.getItem('trek_dashboard_view') as 'grid' | 'list') || 'grid')
const [deleteTrip, setDeleteTrip] = useState(null)
+ const [copyTrip, setCopyTrip] = useState(null)
const toggleViewMode = () => {
setViewMode(prev => {
@@ -815,14 +817,18 @@ export default function DashboardPage(): React.ReactElement {
setArchivedTrips(prev => prev.map(update))
}
- const handleCopy = async (trip: DashboardTrip) => {
+ const handleCopy = (trip: DashboardTrip) => setCopyTrip(trip)
+
+ const confirmCopy = async () => {
+ if (!copyTrip) return
try {
- const data = await tripsApi.copy(trip.id, { title: `${trip.title} (${t('dashboard.copySuffix')})` })
+ const data = await tripsApi.copy(copyTrip.id, { title: `${copyTrip.title} (${t('dashboard.copySuffix')})` })
setTrips(prev => sortTrips([data.trip, ...prev]))
toast.success(t('dashboard.toast.copied'))
} catch {
toast.error(t('dashboard.toast.copyError'))
}
+ setCopyTrip(null)
}
const today = new Date().toISOString().split('T')[0]
@@ -1205,6 +1211,13 @@ export default function DashboardPage(): React.ReactElement {
message={t('dashboard.confirm.delete', { title: deleteTrip?.title || '' })}
/>
+ setCopyTrip(null)}
+ onConfirm={confirmCopy}
+ />
+