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} + /> +