From bd58f17ec099b686cb9586c3a072fd7bde9e6962 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 13 Nov 2026 12:40:16 +0100 Subject: [PATCH] feat(trip-settings): edit the trip inside its settings, in one look across the areas and the trip form Edit trip is an area of the settings dialog, beside People, Sharing and Journal, instead of a second dialog. The side column names the areas only, the dialog keeps one height, and every area uses the same cards, fields and buttons; the trip form on its own takes the same head, cards and buttons. A trip that loads quickly opens without the splash on the desktop too. --- client/src/components/Trips/TripFormModal.tsx | 683 +++++++++--------- .../Trips/TripJournalLinkSection.test.tsx | 3 +- .../Trips/TripJournalLinkSection.tsx | 250 +++++-- .../Trips/TripMembersModal.test.tsx | 14 +- .../src/components/Trips/TripMembersModal.tsx | 6 +- .../src/components/Trips/TripShareDialog.tsx | 101 +-- client/src/components/shared/DialogShell.tsx | 19 +- client/src/components/shared/dialogParts.tsx | 16 + .../components/shared/useTripSplashBeats.ts | 8 + .../screens/trip/MTripLoadingSplash.tsx | 14 +- client/src/pages/TripPlannerPage.tsx | 14 +- .../pages/TripPlannerPage.wiring.fixture.ts | 3 +- .../src/pages/TripPlannerPage.wiring.test.tsx | 29 +- .../tripPlanner/usePlannerDialogs.test.ts | 10 - .../pages/tripPlanner/usePlannerDialogs.ts | 8 +- .../pages/tripPlanner/useTripPlanner.test.tsx | 5 +- .../src/pages/tripPlanner/useTripPlanner.ts | 25 +- client/tests/helpers/mobileTrip.ts | 2 +- .../unit/mobile/trip/MTripShell.test.tsx | 4 +- shared/src/i18n/ar/trip.ts | 7 + shared/src/i18n/az/trip.ts | 7 + shared/src/i18n/br/trip.ts | 7 + shared/src/i18n/ca/trip.ts | 7 + shared/src/i18n/cs/trip.ts | 7 + shared/src/i18n/de/trip.ts | 7 + shared/src/i18n/en/trip.ts | 8 + shared/src/i18n/es/trip.ts | 7 + shared/src/i18n/et/trip.ts | 7 + shared/src/i18n/fr/trip.ts | 7 + shared/src/i18n/gr/trip.ts | 8 + shared/src/i18n/he/trip.ts | 7 + shared/src/i18n/hu/trip.ts | 7 + shared/src/i18n/id/trip.ts | 8 + shared/src/i18n/it/trip.ts | 7 + shared/src/i18n/ja/trip.ts | 7 + shared/src/i18n/ko/trip.ts | 7 + shared/src/i18n/nl/trip.ts | 8 + shared/src/i18n/pl/trip.ts | 7 + shared/src/i18n/ru/trip.ts | 7 + shared/src/i18n/sk/trip.ts | 7 + shared/src/i18n/sv/trip.ts | 7 + shared/src/i18n/th/trip.ts | 7 + shared/src/i18n/tr/trip.ts | 7 + shared/src/i18n/uk/trip.ts | 7 + shared/src/i18n/vi/trip.ts | 7 + shared/src/i18n/zh-TW/trip.ts | 7 + shared/src/i18n/zh/trip.ts | 7 + 47 files changed, 892 insertions(+), 522 deletions(-) diff --git a/client/src/components/Trips/TripFormModal.tsx b/client/src/components/Trips/TripFormModal.tsx index a5bb59f7f..83113e617 100644 --- a/client/src/components/Trips/TripFormModal.tsx +++ b/client/src/components/Trips/TripFormModal.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect, useRef } from 'react' -import Modal from '../shared/Modal' -import { Calendar, Camera, Search, X, UserPlus, Bell } from 'lucide-react' +import { useId, useState, useEffect, useRef } from 'react' +import { DialogButton, DialogFooter, DialogHeader, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT } from '../shared/DialogShell' +import { CardHead, INPUT, LABEL, PANEL } from '../shared/dialogParts' +import { CalendarDays, Camera, Image as ImageIcon, Pencil, Plane, Search, Type, X } from 'lucide-react' import { tripsApi, authApi } from '../../api/client' import CustomSelect from '../shared/CustomSelect' import { useAuthStore } from '../../store/authStore' @@ -43,10 +44,17 @@ interface TripFormModalProps { onSave: (data: TripCreateRequest & { date_shift_mode?: DateShiftMode }) => Promise<{ trip?: Trip } | void> | void trip: Trip | null onCoverUpdate?: (tripId: number, coverUrl: string | null) => void + /** + * Drawn inside the trip settings rather than as a dialog of its own (#2811): no + * frame and no Cancel, the settings around it close it. One column there, since + * beside the settings' own side column two would cut the dates and wrap the chips. + */ + embedded?: boolean } -export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUpdate }: TripFormModalProps) { +export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUpdate, embedded = false }: TripFormModalProps) { const isEditing = !!trip + const labelId = useId() const fileRef = useRef(null) const toast = useToast() const { t } = useTranslation() @@ -256,352 +264,367 @@ export default function TripFormModal({ isOpen, onClose, onSave, trip, onCoverUp variables are reachable from here — what carries over is the geometry: generous radii, grouped panels instead of a single stack of labelled fields, and overline captions rather than sentence-case labels. */ - const inputCls = "w-full px-3.5 py-2.5 border border-edge rounded-xl bg-surface-input text-content placeholder:text-content-faint focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent text-body transition-shadow" - const labelCls = "flex items-center gap-1 text-caption font-semibold uppercase tracking-[0.14em] text-content-faint mb-2" - const ghostBtnCls = "px-4 py-2.5 text-body font-medium text-content-secondary hover:text-content border border-edge rounded-xl hover:bg-surface-hover transition-colors" - const primaryBtnCls = "px-5 py-2.5 text-body font-medium bg-accent hover:bg-accent-hover disabled:opacity-50 text-accent-text rounded-xl shadow-card transition-colors flex items-center gap-2" - const dangerBtnCls = "px-5 py-2.5 text-body font-medium whitespace-nowrap bg-danger hover:opacity-90 disabled:opacity-50 text-white rounded-xl shadow-card transition-opacity flex items-center gap-2" + // The dialogs' own language: their fields, eyebrows, panels and buttons, so the + // form reads alike on its own and inside the trip settings. + const inputCls = INPUT + const labelCls = `${LABEL.replace('block ', '')} flex items-center gap-1` /* Two columns once there is room for them: the form had grown to eight stacked blocks and the create button sat a full screen below the title. Left is what the trip looks like, right is when and with whom. One column below md. */ const columnCls = "flex flex-col gap-4" - const panelCls = "rounded-2xl border border-edge bg-surface-secondary p-4 space-y-3.5" + const panelCls = PANEL - return ( - { if (!busy) setPendingReview(null) } : onClose} - title={pendingReview - ? t(pendingReview.askShift ? 'dashboard.dateShiftTitle' : 'dashboard.shrinkTitle') - : isEditing ? t('dashboard.editTrip') : t('dashboard.createTrip')} - /* The review step is a few radio buttons and a short list; alone, either - takes the width of the day dialog, where the same list stands. With both, - they stand side by side at the form's own width. */ - size={pendingReview && !dateReviewIsWide(pendingReview.askShift, pendingReview.removal) ? 'lg' : '2xl'} - footer={ -
- {pendingReview ? ( - <> - - {/* Removing days with something on them is the one save here that - loses work, so it asks in the danger tone and says so. */} - - - ) : ( - <> - - - - )} -
- } - > - {pendingReview && ( - + const reviewTitle = pendingReview ? t(pendingReview.askShift ? 'dashboard.dateShiftTitle' : 'dashboard.shrinkTitle') : '' + const buttons = pendingReview ? ( + <> + setPendingReview(null)} disabled={busy}>{t('common.back')} + {/* Removing days with something on them is the one save here that + loses work, so it asks in the danger tone and says so. */} + performSave(pendingReview.askShift ? { ...pendingReview.payload, date_shift_mode: dateShiftMode } : pendingReview.payload)}> + {isLoading ? t('common.saving') : t(pendingReview.removal ? 'dashboard.shrinkConfirm' : 'common.update')} + + + ) : ( + <> + {!embedded && {t('common.cancel')}} + + {busy ? t('common.saving') : isEditing ? t('common.update') : t('dashboard.createTrip')} + + + ) + const content = ( + <> + {pendingReview && ( + + )} +
+ {error && ( +
{error}
)} - - {error && ( -
{error}
- )} -
+
- {/* Left column — what the trip is: its picture, its name, what it is about. */} -
+ {/* Left column — what the trip is: its picture, its name, what it is about. */} +
- {/* Cover image — gated by trip_cover_upload permission */} - {canUploadCover &&
- - - {coverPreview ? ( -
- -
- {/* Chrome sitting on top of a photo, so it is deliberately dark in - both themes rather than following the surface tokens. */} - - -
+ {/* Cover image — gated by trip_cover_upload permission */} + {canUploadCover &&
+ } title={t('trip.form.coverTitle')} hint={t('trip.form.coverHint')} /> + + {coverPreview ? ( +
+ +
+ {/* Chrome sitting on top of a photo, so it is deliberately dark in + both themes rather than following the surface tokens. */} + +
- ) : ( - - )} -
- setCoverSearchQuery(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); void handleCoverSearch() } }} - placeholder={t('dashboard.unsplashSearchPlaceholder')} - className={inputCls} - /> -
- {coverSearchError &&

{coverSearchError}

} - {coverSearchResults.length > 0 && ( -
- {coverSearchResults.map(photo => ( - + )} +
+ setCoverSearchQuery(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); void handleCoverSearch() } }} + placeholder={t('dashboard.unsplashSearchPlaceholder')} + className={inputCls} + /> + +
+ {coverSearchError &&

{coverSearchError}

} + {coverSearchResults.length > 0 && ( +
+ {coverSearchResults.map(photo => ( + + ))} +
+ )} +
} + +
+ } title={t('trip.form.aboutTitle')} hint={t('trip.form.aboutHint')} /> +
+ + canEditTrip && update('title', e.target.value)} + required readOnly={!canEditTrip} placeholder={t('dashboard.tripTitlePlaceholder')} className={inputCls} /> +
+ +
+ +