mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
fix(planner): make route tools reachable in mobile day plan sheet (#1142)
* wiki: update dev env * wiki: small precision in dev env * fix(planner): make route tools reachable in mobile day plan sheet On mobile, selecting a day closes the plan sheet immediately, so the route tools footer (Route toggle / Optimize / routing profile) - gated on the selected day - was never reachable. Desktop was unaffected. - Add showRouteToolsWhenExpanded prop to DayPlanSidebar: when set, route tools render on any expanded day with 2+ assigned places - Make handleOptimize accept an explicit dayId (defaulting to selectedDayId, preserving desktop behavior) - Keep the distance/duration pill gated on the selected day, since routeInfo belongs to the selected day's calculated route - Enable the prop on the mobile plan sheet in TripPlannerPage * fix(planner): correct route-tools prop doc and dev-environment wiki - Reword the showRouteToolsWhenExpanded JSDoc to list the controls the footer actually renders (Route toggle / Optimize / travel profile); there is no "Open in Google Maps" action in that block. - Wiki: drop the non-existent server test:parity script, document the real shared i18n:parity checks, and fix the i18n note (the translation layer already lives in @trek/shared, it is not "upcoming"). --------- Co-authored-by: jubnl <jgunther021@gmail.com> Co-authored-by: Maurice <mauriceboe@icloud.com>
This commit is contained in:
@@ -1708,4 +1708,49 @@ describe('DayPlanSidebar', () => {
|
|||||||
expect(onEditTransport).toHaveBeenCalledWith(res)
|
expect(onEditTransport).toHaveBeenCalledWith(res)
|
||||||
expect(onEditReservation).not.toHaveBeenCalled()
|
expect(onEditReservation).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ── showRouteToolsWhenExpanded (mobile route tools) ───────────────────────
|
||||||
|
|
||||||
|
it('FE-PLANNER-DAYPLAN-099: showRouteToolsWhenExpanded shows route tools on expanded day without selection', () => {
|
||||||
|
const places = [
|
||||||
|
buildPlace({ id: 1, name: 'A', lat: 48.85, lng: 2.35 }),
|
||||||
|
buildPlace({ id: 2, name: 'B', lat: 48.86, lng: 2.36 }),
|
||||||
|
]
|
||||||
|
const day = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' })
|
||||||
|
const assigns = {
|
||||||
|
'10': [
|
||||||
|
buildAssignment({ id: 1, day_id: 10, order_index: 0, place: places[0] }),
|
||||||
|
buildAssignment({ id: 2, day_id: 10, order_index: 1, place: places[1] }),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
render(<DayPlanSidebar {...makeDefaultProps({
|
||||||
|
days: [day], places, assignments: assigns, selectedDayId: null, showRouteToolsWhenExpanded: true,
|
||||||
|
})} />)
|
||||||
|
// Days are expanded by default, so route tools must be visible even with no selected day
|
||||||
|
expect(screen.getByRole('button', { name: /optimize/i })).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('FE-PLANNER-DAYPLAN-100: optimize via showRouteToolsWhenExpanded reorders the expanded day', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
const onReorder = vi.fn().mockResolvedValue(undefined)
|
||||||
|
const places = [
|
||||||
|
buildPlace({ id: 1, name: 'A', lat: 48.85, lng: 2.35 }),
|
||||||
|
buildPlace({ id: 2, name: 'B', lat: 48.86, lng: 2.36 }),
|
||||||
|
buildPlace({ id: 3, name: 'C', lat: 48.87, lng: 2.37 }),
|
||||||
|
]
|
||||||
|
const day = buildDay({ id: 10, date: '2025-06-01', title: 'Day 1' })
|
||||||
|
const assigns = {
|
||||||
|
'10': [
|
||||||
|
buildAssignment({ id: 1, day_id: 10, order_index: 0, place: places[0] }),
|
||||||
|
buildAssignment({ id: 2, day_id: 10, order_index: 1, place: places[1] }),
|
||||||
|
buildAssignment({ id: 3, day_id: 10, order_index: 2, place: places[2] }),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
render(<DayPlanSidebar {...makeDefaultProps({
|
||||||
|
days: [day], places, assignments: assigns, selectedDayId: null, onReorder, showRouteToolsWhenExpanded: true,
|
||||||
|
})} />)
|
||||||
|
const optimizeBtn = screen.getByRole('button', { name: /optimize/i })
|
||||||
|
await user.click(optimizeBtn)
|
||||||
|
await waitFor(() => expect(onReorder).toHaveBeenCalledWith(10, expect.any(Array)))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ interface DayPlanSidebarProps {
|
|||||||
onAddBookingToAssignment?: (dayId: number, assignmentId: number) => void
|
onAddBookingToAssignment?: (dayId: number, assignmentId: number) => void
|
||||||
initialScrollTop?: number
|
initialScrollTop?: number
|
||||||
onScrollTopChange?: (top: number) => void
|
onScrollTopChange?: (top: number) => void
|
||||||
|
/** Mobile: show the route tools footer (Route toggle / Optimize / travel profile) on expanded days, since selecting a day closes the sheet */
|
||||||
|
showRouteToolsWhenExpanded?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,6 +127,7 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
|
|||||||
onAddBookingToAssignment,
|
onAddBookingToAssignment,
|
||||||
initialScrollTop,
|
initialScrollTop,
|
||||||
onScrollTopChange,
|
onScrollTopChange,
|
||||||
|
showRouteToolsWhenExpanded = false,
|
||||||
} = props
|
} = props
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { t, language, locale } = useTranslation()
|
const { t, language, locale } = useTranslation()
|
||||||
@@ -742,9 +745,9 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
|
|||||||
pushUndo?.(t('undo.lock'), () => { setLockedIds(prevLocked) })
|
pushUndo?.(t('undo.lock'), () => { setLockedIds(prevLocked) })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOptimize = async () => {
|
const handleOptimize = async (dayId: number | null = selectedDayId) => {
|
||||||
if (!selectedDayId) return
|
if (!dayId) return
|
||||||
const da = getDayAssignments(selectedDayId)
|
const da = getDayAssignments(dayId)
|
||||||
if (da.length < 3) return
|
if (da.length < 3) return
|
||||||
|
|
||||||
const prevIds = da.map(a => a.id)
|
const prevIds = da.map(a => a.id)
|
||||||
@@ -764,7 +767,7 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
|
|||||||
const unlockedNoCoords = unlocked.filter(a => !a.place?.lat || !a.place?.lng)
|
const unlockedNoCoords = unlocked.filter(a => !a.place?.lat || !a.place?.lng)
|
||||||
// Anchor the route on the day's accommodation (when enabled): a loop out from and back to the
|
// Anchor the route on the day's accommodation (when enabled): a loop out from and back to the
|
||||||
// hotel, or — on a transfer day — a run from the hotel you leave to the one you arrive at.
|
// hotel, or — on a transfer day — a run from the hotel you leave to the one you arrive at.
|
||||||
const day = days.find(d => d.id === selectedDayId)
|
const day = days.find(d => d.id === dayId)
|
||||||
const anchors = day && useSettingsStore.getState().settings.optimize_from_accommodation !== false
|
const anchors = day && useSettingsStore.getState().settings.optimize_from_accommodation !== false
|
||||||
? getAccommodationAnchors(day, days, accommodations)
|
? getAccommodationAnchors(day, days, accommodations)
|
||||||
: {}
|
: {}
|
||||||
@@ -781,10 +784,10 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
|
|||||||
if (!result[i]) result[i] = optimizedQueue[qi++]
|
if (!result[i]) result[i] = optimizedQueue[qi++]
|
||||||
}
|
}
|
||||||
|
|
||||||
await onReorder(selectedDayId, result.map(a => a.id))
|
await onReorder(dayId, result.map(a => a.id))
|
||||||
const usedHotel = !!(anchors.start || anchors.end)
|
const usedHotel = !!(anchors.start || anchors.end)
|
||||||
toast.success(usedHotel ? t('dayplan.toast.routeOptimizedFromHotel') : t('dayplan.toast.routeOptimized'))
|
toast.success(usedHotel ? t('dayplan.toast.routeOptimizedFromHotel') : t('dayplan.toast.routeOptimized'))
|
||||||
const capturedDayId = selectedDayId
|
const capturedDayId = dayId
|
||||||
pushUndo?.(t('undo.optimize'), async () => {
|
pushUndo?.(t('undo.optimize'), async () => {
|
||||||
await tripActions.reorderAssignments(tripId, capturedDayId, prevIds)
|
await tripActions.reorderAssignments(tripId, capturedDayId, prevIds)
|
||||||
})
|
})
|
||||||
@@ -901,6 +904,7 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
|
|||||||
onAddBookingToAssignment,
|
onAddBookingToAssignment,
|
||||||
initialScrollTop,
|
initialScrollTop,
|
||||||
onScrollTopChange,
|
onScrollTopChange,
|
||||||
|
showRouteToolsWhenExpanded,
|
||||||
toast,
|
toast,
|
||||||
t,
|
t,
|
||||||
language,
|
language,
|
||||||
@@ -1047,6 +1051,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
|||||||
onAddBookingToAssignment,
|
onAddBookingToAssignment,
|
||||||
initialScrollTop,
|
initialScrollTop,
|
||||||
onScrollTopChange,
|
onScrollTopChange,
|
||||||
|
showRouteToolsWhenExpanded,
|
||||||
toast,
|
toast,
|
||||||
t,
|
t,
|
||||||
language,
|
language,
|
||||||
@@ -2096,7 +2101,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Routen-Werkzeuge (ausgewählter Tag, 2+ Orte) */}
|
{/* Routen-Werkzeuge (ausgewählter Tag, 2+ Orte) */}
|
||||||
{isSelected && getDayAssignments(day.id).length >= 2 && (
|
{(isSelected || (showRouteToolsWhenExpanded && isExpanded)) && getDayAssignments(day.id).length >= 2 && (
|
||||||
<div style={{ padding: '10px 16px 12px', borderTop: '1px solid var(--border-faint)', display: 'flex', flexDirection: 'column', gap: 7 }}>
|
<div style={{ padding: '10px 16px 12px', borderTop: '1px solid var(--border-faint)', display: 'flex', flexDirection: 'column', gap: 7 }}>
|
||||||
<div style={{ display: 'flex', gap: 6, alignItems: 'stretch' }}>
|
<div style={{ display: 'flex', gap: 6, alignItems: 'stretch' }}>
|
||||||
<button
|
<button
|
||||||
@@ -2112,7 +2117,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
|||||||
<RouteIcon size={12} strokeWidth={2} />
|
<RouteIcon size={12} strokeWidth={2} />
|
||||||
{t('dayplan.route')}
|
{t('dayplan.route')}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleOptimize} className="bg-surface-hover text-content-secondary" style={{
|
<button onClick={() => handleOptimize(day.id)} className="bg-surface-hover text-content-secondary" style={{
|
||||||
flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5,
|
flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5,
|
||||||
padding: '6px 0', fontSize: 11, fontWeight: 500, borderRadius: 8, border: 'none',
|
padding: '6px 0', fontSize: 11, fontWeight: 500, borderRadius: 8, border: 'none',
|
||||||
cursor: 'pointer', fontFamily: 'inherit',
|
cursor: 'pointer', fontFamily: 'inherit',
|
||||||
@@ -2141,7 +2146,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{routeInfo && (
|
{isSelected && routeInfo && (
|
||||||
<div className="text-content-secondary bg-surface-hover" style={{ display: 'flex', justifyContent: 'center', gap: 12, fontSize: 12, borderRadius: 8, padding: '5px 10px' }}>
|
<div className="text-content-secondary bg-surface-hover" style={{ display: 'flex', justifyContent: 'center', gap: 12, fontSize: 12, borderRadius: 8, padding: '5px 10px' }}>
|
||||||
<span>{routeInfo.distance}</span>
|
<span>{routeInfo.distance}</span>
|
||||||
<span className="text-content-faint">·</span>
|
<span className="text-content-faint">·</span>
|
||||||
|
|||||||
@@ -616,7 +616,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
<div style={{ flex: 1, overflow: 'auto' }}>
|
||||||
{mobileSidebarOpen === 'left'
|
{mobileSidebarOpen === 'left'
|
||||||
? <DayPlanSidebar tripId={tripId} trip={trip} days={days} places={places} categories={categories} assignments={assignments} selectedDayId={selectedDayId} selectedPlaceId={selectedPlaceId} selectedAssignmentId={selectedAssignmentId} onSelectDay={(id) => { handleSelectDay(id); setMobileSidebarOpen(null) }} onPlaceClick={(placeId, assignmentId) => { handlePlaceClick(placeId, assignmentId) }} onReorder={handleReorder} onReorderDays={handleReorderDays} onAddDay={handleAddDay} onUpdateDayTitle={handleUpdateDayTitle} onAssignToDay={handleAssignToDay} onRouteCalculated={(r) => { if (r) { setRoute([r.coordinates]); setRouteInfo(r) } }} reservations={reservations} visibleConnectionIds={visibleConnections} onToggleConnection={toggleConnection} onAddReservation={(dayId) => { setEditingReservation(null); tripActions.setSelectedDay(dayId); setShowReservationModal(true); setMobileSidebarOpen(null) }} onAddTransport={can('day_edit', trip) ? (dayId) => { setTransportModalDayId(dayId); setEditingTransport(null); setShowTransportModal(true); setMobileSidebarOpen(null) } : undefined} onAddPlace={() => { setEditingPlace(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onDayDetail={(day) => { setShowDayDetail(day); setSelectedPlaceId(null); selectAssignment(null) }} onRemoveAssignment={handleRemoveAssignment} onEditPlace={(place, assignmentId) => { setEditingPlace(place); setEditingAssignmentId(assignmentId || null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onDeletePlace={(placeId) => handleDeletePlace(placeId)} accommodations={tripAccommodations} routeShown={routeShown} routeProfile={routeProfile} onToggleRoute={() => setRouteShown(v => !v)} onSetRouteProfile={setRouteProfile} onNavigateToFiles={() => { setMobileSidebarOpen(null); handleTabChange('dateien') }} onExpandedDaysChange={setExpandedDayIds} pushUndo={pushUndo} canUndo={canUndo} lastActionLabel={lastActionLabel} onUndo={handleUndo} onEditTransport={can('day_edit', trip) ? (reservation) => { setEditingTransport(reservation); setTransportModalDayId(reservation.day_id ?? null); setShowTransportModal(true); setMobileSidebarOpen(null) } : undefined} onEditReservation={can('reservation_edit', trip) ? (r) => { setEditingReservation(r); setShowReservationModal(true); setMobileSidebarOpen(null) } : undefined} initialScrollTop={mobilePlanScrollTopRef.current} onScrollTopChange={(top) => { mobilePlanScrollTopRef.current = top }} />
|
? <DayPlanSidebar tripId={tripId} trip={trip} days={days} places={places} categories={categories} assignments={assignments} selectedDayId={selectedDayId} selectedPlaceId={selectedPlaceId} selectedAssignmentId={selectedAssignmentId} onSelectDay={(id) => { handleSelectDay(id); setMobileSidebarOpen(null) }} onPlaceClick={(placeId, assignmentId) => { handlePlaceClick(placeId, assignmentId) }} onReorder={handleReorder} onReorderDays={handleReorderDays} onAddDay={handleAddDay} onUpdateDayTitle={handleUpdateDayTitle} onAssignToDay={handleAssignToDay} onRouteCalculated={(r) => { if (r) { setRoute([r.coordinates]); setRouteInfo(r) } }} reservations={reservations} visibleConnectionIds={visibleConnections} onToggleConnection={toggleConnection} onAddReservation={(dayId) => { setEditingReservation(null); tripActions.setSelectedDay(dayId); setShowReservationModal(true); setMobileSidebarOpen(null) }} onAddTransport={can('day_edit', trip) ? (dayId) => { setTransportModalDayId(dayId); setEditingTransport(null); setShowTransportModal(true); setMobileSidebarOpen(null) } : undefined} onAddPlace={() => { setEditingPlace(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onDayDetail={(day) => { setShowDayDetail(day); setSelectedPlaceId(null); selectAssignment(null) }} onRemoveAssignment={handleRemoveAssignment} onEditPlace={(place, assignmentId) => { setEditingPlace(place); setEditingAssignmentId(assignmentId || null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onDeletePlace={(placeId) => handleDeletePlace(placeId)} accommodations={tripAccommodations} routeShown={routeShown} routeProfile={routeProfile} onToggleRoute={() => setRouteShown(v => !v)} onSetRouteProfile={setRouteProfile} onNavigateToFiles={() => { setMobileSidebarOpen(null); handleTabChange('dateien') }} onExpandedDaysChange={setExpandedDayIds} pushUndo={pushUndo} canUndo={canUndo} lastActionLabel={lastActionLabel} onUndo={handleUndo} onEditTransport={can('day_edit', trip) ? (reservation) => { setEditingTransport(reservation); setTransportModalDayId(reservation.day_id ?? null); setShowTransportModal(true); setMobileSidebarOpen(null) } : undefined} onEditReservation={can('reservation_edit', trip) ? (r) => { setEditingReservation(r); setShowReservationModal(true); setMobileSidebarOpen(null) } : undefined} initialScrollTop={mobilePlanScrollTopRef.current} onScrollTopChange={(top) => { mobilePlanScrollTopRef.current = top }} showRouteToolsWhenExpanded />
|
||||||
: <PlacesSidebar tripId={tripId} places={places} categories={categories} assignments={assignments} selectedDayId={selectedDayId} selectedPlaceId={selectedPlaceId} onPlaceClick={(placeId) => { handlePlaceClick(placeId); setMobileSidebarOpen(null) }} onAddPlace={() => { setEditingPlace(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onAssignToDay={handleAssignToDay} onEditPlace={(place) => { setEditingPlace(place); setEditingAssignmentId(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onDeletePlace={(placeId) => handleDeletePlace(placeId)} onBulkDeletePlaces={(ids) => setDeletePlaceIds(ids)} onBulkDeleteConfirm={(ids) => confirmDeletePlaces(ids)} days={days} isMobile onCategoryFilterChange={setMapCategoryFilter} onPlacesFilterChange={setMapPlacesFilter} pushUndo={pushUndo} initialScrollTop={mobilePlacesScrollTopRef.current} onScrollTopChange={(top) => { mobilePlacesScrollTopRef.current = top }} />
|
: <PlacesSidebar tripId={tripId} places={places} categories={categories} assignments={assignments} selectedDayId={selectedDayId} selectedPlaceId={selectedPlaceId} onPlaceClick={(placeId) => { handlePlaceClick(placeId); setMobileSidebarOpen(null) }} onAddPlace={() => { setEditingPlace(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onAssignToDay={handleAssignToDay} onEditPlace={(place) => { setEditingPlace(place); setEditingAssignmentId(null); setShowPlaceForm(true); setMobileSidebarOpen(null) }} onDeletePlace={(placeId) => handleDeletePlace(placeId)} onBulkDeletePlaces={(ids) => setDeletePlaceIds(ids)} onBulkDeleteConfirm={(ids) => confirmDeletePlaces(ids)} days={days} isMobile onCategoryFilterChange={setMapCategoryFilter} onPlacesFilterChange={setMapPlacesFilter} pushUndo={pushUndo} initialScrollTop={mobilePlacesScrollTopRef.current} onScrollTopChange={(top) => { mobilePlacesScrollTopRef.current = top }} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ git rebase upstream/dev # or: git merge upstream/dev
|
|||||||
Working on a dedicated branch keeps your changes isolated and makes PRs easier to review:
|
Working on a dedicated branch keeps your changes isolated and makes PRs easier to review:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Create a new branch off of dev
|
||||||
git checkout -b fix/my-changes origin/dev
|
git checkout -b fix/my-changes origin/dev
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -70,16 +71,10 @@ Branch naming conventions:
|
|||||||
|
|
||||||
## 5. Install Dependencies
|
## 5. Install Dependencies
|
||||||
|
|
||||||
Install dependencies for both the client and server:
|
The repo is an npm workspace monorepo. One command at the root installs everything:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Client
|
npm ci
|
||||||
cd client
|
|
||||||
npm i
|
|
||||||
|
|
||||||
# Server
|
|
||||||
cd ../server
|
|
||||||
npm i
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -127,31 +122,70 @@ You can override `KITINERARY_EXTRACTOR_PATH` if you installed the binary to a di
|
|||||||
|
|
||||||
## 7. Available Scripts
|
## 7. Available Scripts
|
||||||
|
|
||||||
|
### Root (`/`)
|
||||||
|
|
||||||
|
These commands run across all workspaces at once and are the recommended way to work:
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|----------------------|---------------------------------------------------------------------|
|
||||||
|
| `npm run dev` | Build shared, then start shared (watch), server, and client together via `concurrently` |
|
||||||
|
| `npm run build` | Build shared → server → client in order |
|
||||||
|
| `npm test` | Run tests in shared, server, and client |
|
||||||
|
| `npm run test:cov` | Run coverage for server and client |
|
||||||
|
| `npm run test:e2e` | Run end-to-end tests (server) |
|
||||||
|
| `npm run lint` | Lint shared, server, and client |
|
||||||
|
| `npm run format` | Format shared, server, and client |
|
||||||
|
| `npm run format:check` | Check formatting across all workspaces |
|
||||||
|
|
||||||
|
### Shared (`/shared`)
|
||||||
|
|
||||||
|
The `@trek/shared` package is the single source of truth for code shared between the client and server. It holds the **Zod schemas that define the API contracts** (request/response shapes, common primitives, pagination) and the **i18n translation layer** (per-language keys and types). Both workspaces import from it, so schema and translation changes propagate to both sides from one place.
|
||||||
|
|
||||||
|
> **Tip:** run `npm run i18n:parity` (or `i18n:parity:strict`) in this package to verify every locale exposes the same translation keys — the CI parity gate runs the strict variant.
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|-----------------------------|--------------------------------------|
|
||||||
|
| `npm run build` | Compile shared package (tsup) |
|
||||||
|
| `npm run build:watch` | Compile in watch mode |
|
||||||
|
| `npm test` | Run tests |
|
||||||
|
| `npm run typecheck` | Type-check without emitting |
|
||||||
|
| `npm run i18n:parity` | Check locale key parity |
|
||||||
|
| `npm run i18n:parity:strict`| Strict locale key parity (CI gate) |
|
||||||
|
| `npm run lint` | Lint source |
|
||||||
|
| `npm run format` | Format source |
|
||||||
|
|
||||||
### Server (`/server`)
|
### Server (`/server`)
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|----------------------------|------------------------------------------|
|
|----------------------------|------------------------------------------|
|
||||||
| `npm start` | Start the server (production) |
|
| `npm start` | Start the server (production) |
|
||||||
| `npm run dev` | Start the server in watch mode (tsx) |
|
| `npm run dev` | Start the server in watch mode |
|
||||||
|
| `npm run build` | Compile server |
|
||||||
|
| `npm run typecheck` | Type-check without emitting |
|
||||||
| `npm test` | Run all tests |
|
| `npm test` | Run all tests |
|
||||||
| `npm run test:unit` | Run unit tests only |
|
| `npm run test:unit` | Run unit tests only |
|
||||||
| `npm run test:integration` | Run integration tests |
|
| `npm run test:integration` | Run integration tests |
|
||||||
| `npm run test:ws` | Run WebSocket tests |
|
| `npm run test:ws` | Run WebSocket tests |
|
||||||
|
| `npm run test:e2e` | Run end-to-end tests |
|
||||||
| `npm run test:watch` | Run tests in watch mode |
|
| `npm run test:watch` | Run tests in watch mode |
|
||||||
| `npm run test:coverage` | Run tests with coverage report |
|
| `npm run test:coverage` | Run tests with coverage report |
|
||||||
|
| `npm run lint` | Lint source |
|
||||||
|
| `npm run format` | Format source |
|
||||||
|
|
||||||
### Client (`/client`)
|
### Client (`/client`)
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|--------------------------|------------------------------------------------------|
|
|----------------------------|------------------------------------------------------|
|
||||||
| `npm run dev` | Start the Vite dev server |
|
| `npm run dev` | Start the Vite dev server |
|
||||||
| `npm run build` | Build for production (runs icon generation first) |
|
| `npm run build` | Build for production (runs icon generation first) |
|
||||||
| `npm run preview` | Preview the production build locally |
|
| `npm run preview` | Preview the production build locally |
|
||||||
| `npm test` | Run all tests |
|
| `npm test` | Run all tests |
|
||||||
| `npm run test:unit` | Run unit tests only |
|
| `npm run test:unit` | Run unit tests only |
|
||||||
| `npm run test:integration` | Run integration tests |
|
| `npm run test:integration` | Run integration tests |
|
||||||
| `npm run test:watch` | Run tests in watch mode |
|
| `npm run test:watch` | Run tests in watch mode |
|
||||||
| `npm run test:coverage` | Run tests with coverage report |
|
| `npm run test:coverage` | Run tests with coverage report |
|
||||||
|
| `npm run lint` | Lint source |
|
||||||
|
| `npm run format` | Format source |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -162,7 +196,7 @@ git add .
|
|||||||
git commit -m "fix: describe your change"
|
git commit -m "fix: describe your change"
|
||||||
|
|
||||||
# Push to your fork's dev branch
|
# Push to your fork's dev branch
|
||||||
git push origin fix/my-changes:dev
|
git push origin fix/my-changes
|
||||||
|
|
||||||
# Or if working directly on dev
|
# Or if working directly on dev
|
||||||
git push origin dev
|
git push origin dev
|
||||||
@@ -175,5 +209,5 @@ Then open a Pull Request from your fork to `mauriceboe/TREK` targeting the `dev`
|
|||||||
## Tips
|
## Tips
|
||||||
|
|
||||||
- Always branch off from an up-to-date `dev` — run `git fetch upstream && git rebase upstream/dev` before starting new work.
|
- Always branch off from an up-to-date `dev` — run `git fetch upstream && git rebase upstream/dev` before starting new work.
|
||||||
- Run tests before pushing: `npm run test` in both `client/` and `server/`.
|
- Run tests before pushing: `npm test` at the repo root runs all workspaces.
|
||||||
- Follow the commit message conventions described in the [[Contributing]] guidelines.
|
- Follow the commit message conventions described in the [[Contributing]] guidelines.
|
||||||
Reference in New Issue
Block a user