mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 06:26:26 +00:00
fix: clear every open Sonar security issue, and half the reliability ones (#2037)
* fix(security): mint idempotency keys properly, and resolve the extractor by path Three findings out of a sweep of every open Sonar security issue. The other 49 turned out to be false positives and are documented in sonar-project.properties rather than changed — each entry scoped to a file, never a whole rule, so a future Math.random() on an auth path still fails. The idempotency key is the one that can lose data. Both producers fell back to Math.random when crypto.randomUUID was missing, and randomUUID needs a secure context — TREK's own quickstart is http://localhost:3000 and LAN installs run on http://192.168.x.x, so on the deployments the README documents the fallback is what actually runs. The axios one was `Math.random().toString(36).slice(2)`, which is not length-stable: 0.5 yields a single character and 0 yields the empty string, which the interceptor reads as "no key" and skips deduplication for entirely. Two writes sharing a key collapse into one — the second gets the first one's cached response and the edit is gone with no error anywhere. Both now come from utils/randomId, which has the getRandomValues rung that works in insecure contexts too. The extractor probe stored the bare binary name, so every extraction re-resolved it through whatever PATH held at that moment. The official image sets KITINERARY_EXTRACTOR_PATH and never reaches that branch, but a bare-metal install without it ran an unqualified name from a PATH it does not control. It now walks the search path itself and keeps the absolute path of the file it verified; execFileSync also drops the /bin/sh hop the old probe needed. PATH comes through app-config because process.env is walled off in src/. * fix: four tests and sorts that were not doing what they claimed Sonar reported 43 high-impact reliability issues. Reading all of them, four are real; the rest are cosmetic or would be made worse by the suggested change. budget.test.ts hand-built `paid: true`, but both emitters send `paid: body.paid ? 1 : 0` and the handler stores the value verbatim — the test asserted a payload the server never sends. The line below it already used a number. admin.controller.test.ts had a `.resolves` assertion with no await. The promise was dropped, so that line could not fail the test. The two to-do category lists sorted with a bare .sort(), which is byte order: every accented name files behind the whole ASCII range, so in a German trip "Übernachtung" lands after "Zelt". TREK ships 23 locales, so this is real data. One side effect worth knowing: katColor picks a category's dot colour by its index, so re-sorting can move which category shows which colour — it already moves whenever a category is added, and the colour carries no meaning. Deliberately not changed: the two `| 0` truncations (Math.trunc is not equivalent — `| 0` wraps at 32 bits and these carry millisecond timestamps), the two .map() callbacks whose second parameter is declared and used, and the sha1 in thumbnail.service (its own comment says a swap would orphan every existing thumbnail). * refactor: give every button a type, and prefer the Number statics Two mechanical sweeps, no behaviour change intended in either. A <button> with no type attribute is type="submit" per the HTML spec. Outside a form that does nothing today, but it turns into a surprise the moment the markup is wrapped in one, which is what Sonar's S9011 is warning about. 870 buttons get an explicit type="button". Two things were left alone on purpose: buttons that take a {...spread}, because the spread may carry a type and the merge order decides which wins, and anything inside a <form> — there the answer is a judgement call, and checking all eleven form files found every real submit button already carrying type="submit". The Number sweep covers only the three globals that are exactly equivalent to their Number twin: parseInt, parseFloat and NaN. isNaN is NOT in it — the global coerces its argument, so isNaN('abc') is true where Number.isNaN('abc') is false, and renaming it would change what the code does. It reaches server and plugin-sdk as well as the client; both copies of egress-policy.ts got the identical edit, so the mirror the SDK's parity test guards stays intact.
This commit is contained in:
@@ -236,7 +236,7 @@ export default function AirTrailImportModal({ isOpen, onClose, tripId, pushUndo
|
||||
const isSelected = selected.has(f.id)
|
||||
const label = f.flightNumber ? `${f.airline ? `${f.airline} ` : ''}${f.flightNumber}` : `${f.fromCode ?? '?'} → ${f.toCode ?? '?'}`
|
||||
return (
|
||||
<button
|
||||
<button type="button"
|
||||
key={f.id}
|
||||
onClick={() => !already && toggle(f.id)}
|
||||
disabled={already}
|
||||
@@ -279,7 +279,7 @@ export default function AirTrailImportModal({ isOpen, onClose, tripId, pushUndo
|
||||
return (
|
||||
<div key={chainKey(chain)} style={{ border: '1px solid var(--border-primary)', borderRadius: 12, padding: '8px 8px 0', marginBottom: 8 }}>
|
||||
{chain.map(renderFlight)}
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => toggleJoin(chain)}
|
||||
className="bg-transparent"
|
||||
style={{
|
||||
@@ -327,7 +327,7 @@ export default function AirTrailImportModal({ isOpen, onClose, tripId, pushUndo
|
||||
<div style={{ flex: 1, fontSize: 'calc(15px * var(--fs-scale-subtitle, 1))', fontWeight: 700, color: 'var(--text-primary)' }}>
|
||||
{t('reservations.airtrail.title')}
|
||||
</div>
|
||||
<button onClick={handleClose} className="bg-transparent text-content-faint" style={{ border: 'none', cursor: 'pointer', padding: 4, borderRadius: 6, display: 'flex', alignItems: 'center' }}>
|
||||
<button type="button" onClick={handleClose} className="bg-transparent text-content-faint" style={{ border: 'none', cursor: 'pointer', padding: 4, borderRadius: 6, display: 'flex', alignItems: 'center' }}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -371,13 +371,13 @@ export default function AirTrailImportModal({ isOpen, onClose, tripId, pushUndo
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 14, paddingTop: 14, borderTop: '1px solid var(--border-faint)' }}>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleClose}
|
||||
style={{ padding: '8px 16px', borderRadius: 10, border: '1px solid var(--border-primary)', background: 'none', color: 'var(--text-primary)', fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 500, cursor: 'pointer', fontFamily: 'inherit' }}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleImport}
|
||||
disabled={selectableCount === 0 || importing}
|
||||
className={selectableCount > 0 && !importing ? 'bg-accent text-accent-text' : 'bg-surface-tertiary text-content-faint'}
|
||||
|
||||
@@ -130,7 +130,7 @@ export default function BookingImportModal({ isOpen, onClose, tripId }: BookingI
|
||||
<div style={{ flex: 1, fontSize: 'calc(15px * var(--fs-scale-subtitle, 1))', fontWeight: 700, color: 'var(--text-primary)' }}>
|
||||
{t('reservations.import.title')}
|
||||
</div>
|
||||
<button onClick={handleClose} className="bg-transparent text-content-faint" style={{ border: 'none', cursor: 'pointer', padding: 4, borderRadius: 6, display: 'flex', alignItems: 'center' }}>
|
||||
<button type="button" onClick={handleClose} className="bg-transparent text-content-faint" style={{ border: 'none', cursor: 'pointer', padding: 4, borderRadius: 6, display: 'flex', alignItems: 'center' }}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -184,13 +184,13 @@ export default function BookingImportModal({ isOpen, onClose, tripId }: BookingI
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 14, paddingTop: 14, borderTop: '1px solid var(--border-faint)' }}>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleClose}
|
||||
style={{ padding: '8px 16px', borderRadius: 10, border: '1px solid var(--border-primary)', background: 'none', color: 'var(--text-primary)', fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 500, cursor: 'pointer', fontFamily: 'inherit' }}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleParse}
|
||||
disabled={files.length === 0 || loading}
|
||||
className={files.length > 0 && !loading ? 'bg-accent text-accent-text' : 'bg-surface-tertiary text-content-faint'}
|
||||
|
||||
@@ -174,7 +174,7 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
||||
{day.title || t('planner.dayN', { n: (days.indexOf(day) + 1) || '?' })}
|
||||
</span>
|
||||
{canEditDays && onUpdateDayTitle && (
|
||||
<button onClick={startRename} aria-label={t('common.edit')} title={t('common.edit')} className="text-content-faint" style={{ border: 'none', background: 'none', padding: 3, cursor: 'pointer', display: 'flex', flexShrink: 0 }}>
|
||||
<button type="button" onClick={startRename} aria-label={t('common.edit')} title={t('common.edit')} className="text-content-faint" style={{ border: 'none', background: 'none', padding: 3, cursor: 'pointer', display: 'flex', flexShrink: 0 }}>
|
||||
<Pencil size={12} strokeWidth={1.8} />
|
||||
</button>
|
||||
)}
|
||||
@@ -182,14 +182,14 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri
|
||||
)}
|
||||
{!collapsed && formattedDate && <div className="text-content-muted" style={{ fontSize: 'calc(12px * var(--fs-scale-body, 1))', marginTop: 1 }}>{formattedDate}</div>}
|
||||
</div>
|
||||
<button onClick={(e) => { e.stopPropagation(); toggleCollapse() }} title={collapsed ? t('common.expand') : t('common.collapse')}
|
||||
<button type="button" onClick={(e) => { e.stopPropagation(); toggleCollapse() }} title={collapsed ? t('common.expand') : t('common.collapse')}
|
||||
className="bg-surface-secondary"
|
||||
style={{ border: 'none', borderRadius: 10, width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0, transition: 'all 0.15s ease' }}
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
|
||||
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-secondary)'}>
|
||||
{collapsed ? <ChevronsUp size={14} className="text-content-muted" /> : <ChevronsDown size={14} className="text-content-muted" />}
|
||||
</button>
|
||||
<button onClick={(e) => { e.stopPropagation(); onClose() }} className="bg-surface-secondary" style={{ border: 'none', borderRadius: 10, width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0 }}
|
||||
<button type="button" onClick={(e) => { e.stopPropagation(); onClose() }} className="bg-surface-secondary" style={{ border: 'none', borderRadius: 10, width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0 }}
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
|
||||
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-secondary)'}>
|
||||
<X size={14} className="text-content-muted" />
|
||||
@@ -409,11 +409,11 @@ function AccommodationList({ dayAccommodations, day, reservations, canEditDays,
|
||||
<div style={{ fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 600, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{acc.place_name}</div>
|
||||
{acc.place_address && <div style={{ fontSize: 'calc(10px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{acc.place_address}</div>}
|
||||
</div>
|
||||
{canEditDays && <button onClick={() => { setAccommodation(acc); setHotelForm({ check_in: acc.check_in || '', check_in_end: acc.check_in_end || '', check_out: acc.check_out || '', confirmation: acc.confirmation || '', place_id: acc.place_id }); setHotelDayRange({ start: acc.start_day_id, end: acc.end_day_id }); setShowHotelPicker('edit') }}
|
||||
{canEditDays && <button type="button" onClick={() => { setAccommodation(acc); setHotelForm({ check_in: acc.check_in || '', check_in_end: acc.check_in_end || '', check_out: acc.check_out || '', confirmation: acc.confirmation || '', place_id: acc.place_id }); setHotelDayRange({ start: acc.start_day_id, end: acc.end_day_id }); setShowHotelPicker('edit') }}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 3, flexShrink: 0 }}>
|
||||
<Pencil size={12} style={{ color: 'var(--text-faint)' }} />
|
||||
</button>}
|
||||
{canEditDays && <button onClick={() => { setAccommodation(acc); handleRemoveAccommodation() }} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 3, flexShrink: 0 }}>
|
||||
{canEditDays && <button type="button" onClick={() => { setAccommodation(acc); handleRemoveAccommodation() }} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 3, flexShrink: 0 }}>
|
||||
<X size={12} style={{ color: 'var(--text-faint)' }} />
|
||||
</button>}
|
||||
</div>
|
||||
@@ -468,7 +468,7 @@ function AccommodationList({ dayAccommodations, day, reservations, canEditDays,
|
||||
)
|
||||
})}
|
||||
{/* Add another hotel */}
|
||||
{canEditDays && <button onClick={() => setShowHotelPicker(true)} style={{
|
||||
{canEditDays && <button type="button" onClick={() => setShowHotelPicker(true)} style={{
|
||||
width: '100%', padding: 8, border: '1.5px dashed var(--border-primary)', borderRadius: 10,
|
||||
background: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5,
|
||||
fontSize: 'calc(10px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', fontFamily: 'inherit',
|
||||
@@ -477,7 +477,7 @@ function AccommodationList({ dayAccommodations, day, reservations, canEditDays,
|
||||
</button>}
|
||||
</div>
|
||||
) : (
|
||||
canEditDays ? <button onClick={() => setShowHotelPicker(true)} style={{
|
||||
canEditDays ? <button type="button" onClick={() => setShowHotelPicker(true)} style={{
|
||||
width: '100%', padding: 10, border: '1.5px dashed var(--border-primary)', borderRadius: 10,
|
||||
background: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5,
|
||||
fontSize: 'calc(11px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', fontFamily: 'inherit',
|
||||
@@ -546,7 +546,7 @@ function HotelPickerModal({ showHotelPicker, setShowHotelPicker, font, t, hotelD
|
||||
<div style={{ padding: '16px 18px 12px', borderBottom: '1px solid var(--border-faint)', display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<Hotel size={16} style={{ color: 'var(--text-primary)' }} />
|
||||
<span style={{ fontSize: 'calc(14px * var(--fs-scale-body, 1))', fontWeight: 700, color: 'var(--text-primary)', flex: 1 }}>{showHotelPicker === 'edit' ? t('day.editAccommodation') : t('day.addAccommodation')}</span>
|
||||
<button onClick={() => setShowHotelPicker(false)} style={{ background: 'var(--bg-secondary)', border: 'none', borderRadius: 8, width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}>
|
||||
<button type="button" onClick={() => setShowHotelPicker(false)} style={{ background: 'var(--bg-secondary)', border: 'none', borderRadius: 8, width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}>
|
||||
<X size={12} style={{ color: 'var(--text-muted)' }} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -584,7 +584,7 @@ function HotelPickerModal({ showHotelPicker, setShowHotelPicker, font, t, hotelD
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<button onClick={() => setHotelDayRange({ start: days[0]?.id, end: days[days.length - 1]?.id })} style={{
|
||||
<button type="button" onClick={() => setHotelDayRange({ start: days[0]?.id, end: days[days.length - 1]?.id })} style={{
|
||||
padding: '6px 14px', borderRadius: 8, border: 'none', fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 600, cursor: 'pointer', flexShrink: 0,
|
||||
background: hotelDayRange.start === days[0]?.id && hotelDayRange.end === days[days.length - 1]?.id ? 'var(--text-primary)' : 'var(--bg-card)',
|
||||
color: hotelDayRange.start === days[0]?.id && hotelDayRange.end === days[days.length - 1]?.id ? 'var(--bg-card)' : 'var(--text-muted)',
|
||||
@@ -618,14 +618,14 @@ function HotelPickerModal({ showHotelPicker, setShowHotelPicker, font, t, hotelD
|
||||
{/* Category Filter */}
|
||||
{categories.length > 0 && (
|
||||
<div style={{ padding: '8px 18px', borderBottom: '1px solid var(--border-faint)', display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
<button onClick={() => setHotelCategoryFilter('')} style={{
|
||||
<button type="button" onClick={() => setHotelCategoryFilter('')} style={{
|
||||
padding: '3px 10px', borderRadius: 6, border: 'none', fontSize: 'calc(10px * var(--fs-scale-caption, 1))', fontWeight: 600, cursor: 'pointer',
|
||||
background: !hotelCategoryFilter ? 'var(--text-primary)' : 'var(--bg-secondary)',
|
||||
color: !hotelCategoryFilter ? 'var(--bg-card)' : 'var(--text-muted)',
|
||||
}}>{t('day.allDays')}</button>
|
||||
|
||||
{categories.map(c => (
|
||||
<button key={c.id} onClick={() => setHotelCategoryFilter(c.id)} style={{
|
||||
<button type="button" key={c.id} onClick={() => setHotelCategoryFilter(c.id)} style={{
|
||||
padding: '3px 10px', borderRadius: 6, border: 'none', fontSize: 'calc(10px * var(--fs-scale-caption, 1))', fontWeight: 600, cursor: 'pointer',
|
||||
background: hotelCategoryFilter === c.id ? c.color || 'var(--text-primary)' : 'var(--bg-secondary)',
|
||||
color: hotelCategoryFilter === c.id ? '#fff' : 'var(--text-muted)',
|
||||
@@ -641,7 +641,7 @@ function HotelPickerModal({ showHotelPicker, setShowHotelPicker, font, t, hotelD
|
||||
return filtered.length === 0 ? (
|
||||
<div style={{ padding: 20, textAlign: 'center', fontSize: 'calc(12px * var(--fs-scale-body, 1))', color: 'var(--text-faint)' }}>{t('day.noPlacesForHotel')}</div>
|
||||
) : filtered.map(p => (
|
||||
<button key={p.id} onClick={() => handleSelectPlace(p.id)} style={{
|
||||
<button type="button" key={p.id} onClick={() => handleSelectPlace(p.id)} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10, width: '100%', padding: '10px 18px',
|
||||
border: 'none', borderBottom: '1px solid var(--border-faint)',
|
||||
background: hotelForm.place_id === p.id ? 'var(--bg-hover)' : 'none',
|
||||
@@ -671,10 +671,10 @@ function HotelPickerModal({ showHotelPicker, setShowHotelPicker, font, t, hotelD
|
||||
|
||||
{/* Save / Cancel */}
|
||||
<div style={{ padding: '12px 18px', borderTop: '1px solid var(--border-faint)', display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
<button onClick={() => setShowHotelPicker(false)} style={{ padding: '7px 16px', borderRadius: 8, border: '1px solid var(--border-primary)', background: 'none', fontSize: 'calc(12px * var(--fs-scale-body, 1))', cursor: 'pointer', fontFamily: 'inherit', color: 'var(--text-muted)' }}>
|
||||
<button type="button" onClick={() => setShowHotelPicker(false)} style={{ padding: '7px 16px', borderRadius: 8, border: '1px solid var(--border-primary)', background: 'none', fontSize: 'calc(12px * var(--fs-scale-body, 1))', cursor: 'pointer', fontFamily: 'inherit', color: 'var(--text-muted)' }}>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button onClick={() => { void saveHotelPicker() }} disabled={!hotelForm.place_id} style={{
|
||||
<button type="button" onClick={() => { void saveHotelPicker() }} disabled={!hotelForm.place_id} style={{
|
||||
padding: '7px 20px', borderRadius: 8, border: 'none', fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit',
|
||||
background: hotelForm.place_id ? 'var(--text-primary)' : 'var(--bg-tertiary)',
|
||||
color: hotelForm.place_id ? 'var(--bg-card)' : 'var(--text-faint)',
|
||||
|
||||
@@ -306,7 +306,7 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
|
||||
assignmentId: dragDataRef.current.assignmentId || '',
|
||||
noteId: dragDataRef.current.noteId || '',
|
||||
reservationId: dragDataRef.current.reservationId || '',
|
||||
fromDayId: parseInt(dragDataRef.current.fromDayId) || 0,
|
||||
fromDayId: Number.parseInt(dragDataRef.current.fromDayId) || 0,
|
||||
phase: (dragDataRef.current.phase || 'single') as 'single' | 'start' | 'middle' | 'end',
|
||||
}
|
||||
}
|
||||
@@ -990,7 +990,7 @@ function useDayPlanSidebar(props: DayPlanSidebarProps) {
|
||||
setDraggingId(null); setDropTargetKey(null); dragDataRef.current = null; window.__dragData = null; return
|
||||
}
|
||||
if (placeId) {
|
||||
onAssignToDay?.(parseInt(placeId), dayId)
|
||||
onAssignToDay?.(Number.parseInt(placeId), dayId)
|
||||
} else if (assignmentId && fromDayId !== dayId) {
|
||||
const srcAssignment = (useTripStore.getState().assignments[String(fromDayId)] || []).find(a => a.id === Number(assignmentId))
|
||||
const capturedFromDayId = fromDayId
|
||||
@@ -1739,7 +1739,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
)
|
||||
})()
|
||||
) : (
|
||||
<button onClick={e => toggleDay(day.id, e)} className="text-content-faint" style={{ alignSelf: 'flex-start', flexShrink: 0, background: 'none', border: 'none', padding: 6, cursor: 'pointer', display: 'flex', alignItems: 'center' }}>
|
||||
<button type="button" onClick={e => toggleDay(day.id, e)} className="text-content-faint" style={{ alignSelf: 'flex-start', flexShrink: 0, background: 'none', border: 'none', padding: 6, cursor: 'pointer', display: 'flex', alignItems: 'center' }}>
|
||||
{isExpanded ? <ChevronDown size={16} strokeWidth={1.8} /> : <ChevronRight size={16} strokeWidth={1.8} />}
|
||||
</button>
|
||||
)}
|
||||
@@ -1765,7 +1765,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
const toLegIndex = legPart ? Number(legPart.slice(3)) : null
|
||||
|
||||
if (placeId) {
|
||||
onAssignToDay?.(parseInt(placeId), day.id)
|
||||
onAssignToDay?.(Number.parseInt(placeId), day.id)
|
||||
} else if (fromReservationId && fromDayId !== day.id) {
|
||||
const r = reservations.find(x => x.id === Number(fromReservationId))
|
||||
if (r) { const update = computeMultiDayMove(r, day.id, phase); tripActions.updateReservation(tripId, r.id, update).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError'))) }
|
||||
@@ -1791,7 +1791,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
}
|
||||
if (!assignmentId && !noteId && !placeId) { dragDataRef.current = null; window.__dragData = null; return }
|
||||
if (placeId) {
|
||||
onAssignToDay?.(parseInt(placeId), day.id)
|
||||
onAssignToDay?.(Number.parseInt(placeId), day.id)
|
||||
setDropTargetKey(null); window.__dragData = null; return
|
||||
}
|
||||
if (assignmentId && fromDayId !== day.id) {
|
||||
@@ -1909,7 +1909,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
const { placeId, assignmentId: fromAssignmentId, noteId, reservationId: fromReservationId, fromDayId, phase } = getDragData(e)
|
||||
if (placeId) {
|
||||
const pos = placeItems.findIndex(i => i.data.id === assignment.id)
|
||||
onAssignToDay?.(parseInt(placeId), day.id, pos >= 0 ? pos : undefined)
|
||||
onAssignToDay?.(Number.parseInt(placeId), day.id, pos >= 0 ? pos : undefined)
|
||||
setDropTargetKey(null); window.__dragData = null
|
||||
} else if (fromReservationId && fromDayId !== day.id) {
|
||||
const r = reservations.find(x => x.id === Number(fromReservationId))
|
||||
@@ -2154,15 +2154,15 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
)}
|
||||
</div>
|
||||
{canEditDays && <div className="reorder-buttons" style={{ flexShrink: 0, display: 'flex', gap: 1, transition: 'opacity 0.15s' }}>
|
||||
<button onClick={moveUp} disabled={idx === 0} className={idx === 0 ? 'text-[var(--border-primary)]' : 'text-content-faint'} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: idx === 0 ? 'default' : 'pointer', display: 'flex', lineHeight: 1 }}>
|
||||
<button type="button" onClick={moveUp} disabled={idx === 0} className={idx === 0 ? 'text-[var(--border-primary)]' : 'text-content-faint'} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: idx === 0 ? 'default' : 'pointer', display: 'flex', lineHeight: 1 }}>
|
||||
<ChevronUp size={12} strokeWidth={2} />
|
||||
</button>
|
||||
<button onClick={moveDown} disabled={idx === merged.length - 1} className={idx === merged.length - 1 ? 'text-[var(--border-primary)]' : 'text-content-faint'} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: idx === merged.length - 1 ? 'default' : 'pointer', display: 'flex', lineHeight: 1 }}>
|
||||
<button type="button" onClick={moveDown} disabled={idx === merged.length - 1} className={idx === merged.length - 1 ? 'text-[var(--border-primary)]' : 'text-content-faint'} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: idx === merged.length - 1 ? 'default' : 'pointer', display: 'flex', lineHeight: 1 }}>
|
||||
<ChevronDown size={12} strokeWidth={2} />
|
||||
</button>
|
||||
</div>}
|
||||
{canEditDays && onAddBookingToAssignment && hoveredAssignmentId === assignment.id && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
onAddBookingToAssignment(day.id, assignment.id)
|
||||
@@ -2289,7 +2289,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
const insertAfter = e.clientY > rect.top + rect.height / 2
|
||||
const { placeId, assignmentId: fromAssignmentId, noteId, reservationId: fromReservationId, fromDayId, phase } = getDragData(e)
|
||||
if (placeId) {
|
||||
onAssignToDay?.(parseInt(placeId), day.id)
|
||||
onAssignToDay?.(Number.parseInt(placeId), day.id)
|
||||
} else if (fromReservationId && fromDayId !== day.id) {
|
||||
const r2 = reservations.find(x => x.id === Number(fromReservationId))
|
||||
if (r2) { const update = computeMultiDayMove(r2, day.id, phase); tripActions.updateReservation(tripId, r2.id, update).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError'))) }
|
||||
@@ -2464,7 +2464,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
const tm = getMergedItems(day.id)
|
||||
const noteIdx = tm.findIndex(i => i.type === 'note' && i.data.id === note.id)
|
||||
const pos = tm.slice(0, noteIdx).filter(i => i.type === 'place').length
|
||||
onAssignToDay?.(parseInt(placeId), day.id, pos)
|
||||
onAssignToDay?.(Number.parseInt(placeId), day.id, pos)
|
||||
setDropTargetKey(null); window.__dragData = null
|
||||
} else if (fromReservationId && fromDayId !== day.id) {
|
||||
const r = reservations.find(x => x.id === Number(fromReservationId))
|
||||
@@ -2549,12 +2549,12 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
every note a shorter line for the whole time you are
|
||||
not hovering. */}
|
||||
{canEditDays && <div className="note-edit-buttons bg-surface-card" style={{ position: 'absolute', right: 5, top: '50%', transform: 'translateY(-50%)', display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 3, padding: 4, borderRadius: 999, border: '1px solid var(--border-faint)', boxShadow: '0 4px 14px rgba(0,0,0,0.10)', opacity: 0, transition: 'opacity 0.15s' }}>
|
||||
<button onClick={e => openEditNote(day.id, note, e)} aria-label={t('dayplan.noteEdit')} title={t('dayplan.noteEdit')} className="text-content-faint note-edit-btn" style={{ background: 'none', border: 'none', width: 30, height: 30, borderRadius: 999, cursor: 'pointer', display: 'grid', placeItems: 'center', padding: 0 }}><Pencil size={15} /></button>
|
||||
<button onClick={e => { e.stopPropagation(); setPendingDeleteNote({ dayId: day.id, noteId: note.id }) }} aria-label={t('dayplan.noteDelete')} title={t('dayplan.noteDelete')} className="text-content-faint note-edit-btn note-edit-btn-danger" style={{ background: 'none', border: 'none', width: 30, height: 30, borderRadius: 999, cursor: 'pointer', display: 'grid', placeItems: 'center', padding: 0 }}><Trash2 size={15} /></button>
|
||||
<button type="button" onClick={e => openEditNote(day.id, note, e)} aria-label={t('dayplan.noteEdit')} title={t('dayplan.noteEdit')} className="text-content-faint note-edit-btn" style={{ background: 'none', border: 'none', width: 30, height: 30, borderRadius: 999, cursor: 'pointer', display: 'grid', placeItems: 'center', padding: 0 }}><Pencil size={15} /></button>
|
||||
<button type="button" onClick={e => { e.stopPropagation(); setPendingDeleteNote({ dayId: day.id, noteId: note.id }) }} aria-label={t('dayplan.noteDelete')} title={t('dayplan.noteDelete')} className="text-content-faint note-edit-btn note-edit-btn-danger" style={{ background: 'none', border: 'none', width: 30, height: 30, borderRadius: 999, cursor: 'pointer', display: 'grid', placeItems: 'center', padding: 0 }}><Trash2 size={15} /></button>
|
||||
</div>}
|
||||
{canEditDays && <div className="reorder-buttons" style={{ flexShrink: 0, display: 'flex', gap: 1, transition: 'opacity 0.15s' }}>
|
||||
<button onClick={e => { e.stopPropagation(); moveNote(day.id, note.id, 'up') }} disabled={noteIdx === 0} className={noteIdx === 0 ? 'text-[var(--border-primary)]' : 'text-content-faint'} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: noteIdx === 0 ? 'default' : 'pointer', display: 'flex', lineHeight: 1 }}><ChevronUp size={12} strokeWidth={2} /></button>
|
||||
<button onClick={e => { e.stopPropagation(); moveNote(day.id, note.id, 'down') }} disabled={noteIdx === merged.length - 1} className={noteIdx === merged.length - 1 ? 'text-[var(--border-primary)]' : 'text-content-faint'} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: noteIdx === merged.length - 1 ? 'default' : 'pointer', display: 'flex', lineHeight: 1 }}><ChevronDown size={12} strokeWidth={2} /></button>
|
||||
<button type="button" onClick={e => { e.stopPropagation(); moveNote(day.id, note.id, 'up') }} disabled={noteIdx === 0} className={noteIdx === 0 ? 'text-[var(--border-primary)]' : 'text-content-faint'} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: noteIdx === 0 ? 'default' : 'pointer', display: 'flex', lineHeight: 1 }}><ChevronUp size={12} strokeWidth={2} /></button>
|
||||
<button type="button" onClick={e => { e.stopPropagation(); moveNote(day.id, note.id, 'down') }} disabled={noteIdx === merged.length - 1} className={noteIdx === merged.length - 1 ? 'text-[var(--border-primary)]' : 'text-content-faint'} style={{ background: 'none', border: 'none', padding: '1px 2px', cursor: noteIdx === merged.length - 1 ? 'default' : 'pointer', display: 'flex', lineHeight: 1 }}><ChevronDown size={12} strokeWidth={2} /></button>
|
||||
</div>}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
@@ -2580,7 +2580,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
const { placeId, assignmentId, noteId, reservationId: fromReservationId, fromDayId, phase } = getDragData(e)
|
||||
// Neuer Ort von der Orte-Liste
|
||||
if (placeId) {
|
||||
onAssignToDay?.(parseInt(placeId), day.id)
|
||||
onAssignToDay?.(Number.parseInt(placeId), day.id)
|
||||
setDropTargetKey(null); window.__dragData = null; return
|
||||
}
|
||||
if (fromReservationId && fromDayId !== day.id) {
|
||||
@@ -2618,7 +2618,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
{(isSelected || (showRouteToolsWhenExpanded && isExpanded)) && routeToolsRoutable && (
|
||||
<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' }}>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => {
|
||||
if (showRouteToolsWhenExpanded) {
|
||||
// Mobile: toggle this day's inline leg distances in place.
|
||||
@@ -2647,7 +2647,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
{t('dayplan.route')}
|
||||
</button>
|
||||
{/* Open the day's stops as a route in Google Maps (planned order). #1255 */}
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => {
|
||||
const url = generateGoogleMapsUrl(dayExportStops())
|
||||
if (url) window.open(url, '_blank', 'noopener,noreferrer')
|
||||
@@ -2666,7 +2666,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
{/* The same day, handed to CoMaps for offline navigation (#1904). The
|
||||
day's own travel mode rides along, so the route it builds walks
|
||||
when the plan walks. */}
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => {
|
||||
const url = generateCoMapsUrl(dayExportStops(), day.default_transport_mode ?? routeProfile)
|
||||
if (url) window.open(url, '_blank', 'noopener,noreferrer')
|
||||
@@ -2688,7 +2688,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
sat against both edges as soon as the row filled up, and
|
||||
the row gained a button when CoMaps arrived. The label
|
||||
stays as the accessible name and the tooltip. */}
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => handleOptimize(day.id)}
|
||||
aria-label={t('dayplan.optimize')}
|
||||
title={t('dayplan.optimize')}
|
||||
@@ -2706,7 +2706,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
const ModeIcon = p.key === 'driving' ? Car : p.key === 'walking' ? Footprints : Zap
|
||||
const active = (day.default_transport_mode ?? routeProfile) === p.key
|
||||
return (
|
||||
<button
|
||||
<button type="button"
|
||||
key={p.key}
|
||||
onClick={() => setDayDefaultMode(day.id, p.key)}
|
||||
aria-label={p.label}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function MobileAddPlaceButton({ dayId, places, assignments, onAssign, onA
|
||||
return (
|
||||
<div className="md:hidden" style={{ padding: '8px 12px 12px' }}>
|
||||
{!open ? (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={e => { e.stopPropagation(); setOpen(true) }}
|
||||
style={{
|
||||
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
@@ -47,7 +47,7 @@ export function MobileAddPlaceButton({ dayId, places, assignments, onAssign, onA
|
||||
placeholder={t('dayplan.mobile.searchPlaces')}
|
||||
style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontFamily: 'inherit', color: 'var(--text-primary)' }}
|
||||
/>
|
||||
<button onClick={() => { setOpen(false); setSearch('') }} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2, color: 'var(--text-faint)' }}>
|
||||
<button type="button" onClick={() => { setOpen(false); setSearch('') }} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2, color: 'var(--text-faint)' }}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -58,7 +58,7 @@ export function MobileAddPlaceButton({ dayId, places, assignments, onAssign, onA
|
||||
</div>
|
||||
)}
|
||||
{filtered.slice(0, 20).map(p => (
|
||||
<button
|
||||
<button type="button"
|
||||
key={p.id}
|
||||
onClick={() => {
|
||||
onAssign?.(p.id, dayId)
|
||||
@@ -77,7 +77,7 @@ export function MobileAddPlaceButton({ dayId, places, assignments, onAssign, onA
|
||||
))}
|
||||
</div>
|
||||
{onAddNew && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => { onAddNew(); setOpen(false); setSearch('') }}
|
||||
style={{
|
||||
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
|
||||
@@ -47,11 +47,11 @@ export function DayPlanSidebarTimeConfirmModal({ timeConfirm, setTimeConfirm, co
|
||||
{t('dayplan.confirmRemoveTimeBody', { time: timeConfirm.time })}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 4 }}>
|
||||
<button onClick={() => setTimeConfirm(null)} className="text-content-muted" style={{
|
||||
<button type="button" onClick={() => setTimeConfirm(null)} className="text-content-muted" style={{
|
||||
fontSize: 'calc(12px * var(--fs-scale-body, 1))', background: 'none', border: '1px solid var(--border-primary)',
|
||||
borderRadius: 8, padding: '6px 14px', cursor: 'pointer', fontFamily: 'inherit',
|
||||
}}>{t('common.cancel')}</button>
|
||||
<button onClick={confirmTimeRemoval} className="bg-[#ef4444] text-white" style={{
|
||||
<button type="button" onClick={confirmTimeRemoval} className="bg-[#ef4444] text-white" style={{
|
||||
fontSize: 'calc(12px * var(--fs-scale-body, 1))',
|
||||
border: 'none', borderRadius: 8, padding: '6px 16px', cursor: 'pointer', fontWeight: 600, fontFamily: 'inherit',
|
||||
}}>{t('common.confirm')}</button>
|
||||
|
||||
@@ -94,7 +94,7 @@ export function DayPlanSidebarToolbar({
|
||||
const label = allExpanded ? t('dayplan.collapseAll') : t('dayplan.expandAll')
|
||||
return (
|
||||
<Tooltip label={label} placement="bottom">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => {
|
||||
const next = allExpanded ? new Set<number>() : new Set(days.map(d => d.id))
|
||||
setExpandedDays(next)
|
||||
@@ -138,7 +138,7 @@ export function DayPlanSidebarToolbar({
|
||||
})()}
|
||||
{onUndo && (
|
||||
<div style={{ position: 'relative', flexShrink: 0 }}>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onUndo}
|
||||
disabled={!canUndo}
|
||||
aria-label={t('undo.button')}
|
||||
@@ -172,7 +172,7 @@ export function DayPlanSidebarToolbar({
|
||||
{canEditDays && onReorderDays && onAddDay && days.length > 0 && (
|
||||
<div style={{ position: 'relative', flexShrink: 0 }}>
|
||||
<Tooltip label={t('dayplan.reorderDays')} placement="bottom">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setReorderOpen(v => !v)}
|
||||
aria-label={t('dayplan.reorderDays')}
|
||||
aria-pressed={reorderOpen}
|
||||
|
||||
@@ -241,7 +241,7 @@ export function DayPlanSidebarTransportDetailModal({
|
||||
{/* Aktionen */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
{onEdit && (
|
||||
<button onClick={() => onEdit(res)} className="bg-surface-tertiary text-content" style={{
|
||||
<button type="button" onClick={() => onEdit(res)} className="bg-surface-tertiary text-content" style={{
|
||||
fontSize: 'calc(12px * var(--fs-scale-body, 1))',
|
||||
border: 'none', borderRadius: 8, padding: '6px 14px', cursor: 'pointer', fontWeight: 600, fontFamily: 'inherit',
|
||||
display: 'inline-flex', alignItems: 'center', gap: 5,
|
||||
@@ -249,7 +249,7 @@ export function DayPlanSidebarTransportDetailModal({
|
||||
<Pencil size={12} /> {t('common.edit')}
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => setTransportDetail(null)} className="bg-accent text-accent-text" style={{
|
||||
<button type="button" onClick={() => setTransportDetail(null)} className="bg-accent text-accent-text" style={{
|
||||
fontSize: 'calc(12px * var(--fs-scale-body, 1))',
|
||||
border: 'none', borderRadius: 8, padding: '6px 16px', cursor: 'pointer', fontWeight: 600, fontFamily: 'inherit',
|
||||
}}>
|
||||
|
||||
@@ -56,7 +56,7 @@ export function DayReorderPopup({ isOpen, days, t, locale, onReorder, onAddDay,
|
||||
size="md"
|
||||
footer={
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
padding: '8px 16px', borderRadius: 8, fontSize: 13, fontWeight: 500,
|
||||
@@ -66,7 +66,7 @@ export function DayReorderPopup({ isOpen, days, t, locale, onReorder, onAddDay,
|
||||
>
|
||||
{t('common.close')}
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onAddDay}
|
||||
className="bg-accent text-accent-text"
|
||||
style={{
|
||||
|
||||
@@ -360,7 +360,7 @@ export default function FileImportModal({ isOpen, onClose, tripId, pushUndo, ini
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleClose}
|
||||
style={{
|
||||
padding: '8px 16px', borderRadius: 10, border: '1px solid var(--border-primary)',
|
||||
@@ -370,7 +370,7 @@ export default function FileImportModal({ isOpen, onClose, tripId, pushUndo, ini
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleImport}
|
||||
disabled={!canImport}
|
||||
className={canImport ? 'bg-accent text-accent-text' : 'bg-surface-tertiary text-content-faint'}
|
||||
|
||||
@@ -89,7 +89,7 @@ export function IcsSubscribeModal({ endpoint, title, description, onClose }: Ics
|
||||
<Calendar size={16} strokeWidth={2} style={{ color: 'var(--accent, #6366f1)' }} />
|
||||
<span style={{ fontWeight: 600, fontSize: 14 }}>{title}</span>
|
||||
</div>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none', border: 'none', cursor: 'pointer', padding: 4,
|
||||
@@ -110,7 +110,7 @@ export function IcsSubscribeModal({ endpoint, title, description, onClose }: Ics
|
||||
</div>
|
||||
) : !feedUrl ? (
|
||||
<>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => mutate('POST')}
|
||||
disabled={busy}
|
||||
style={{
|
||||
@@ -133,7 +133,7 @@ export function IcsSubscribeModal({ endpoint, title, description, onClose }: Ics
|
||||
<SubscribeLinks httpsUrl={httpsUrl} webcalUrl={webcalUrl} />
|
||||
|
||||
<div style={{ marginTop: 16, paddingTop: 12, borderTop: '1px solid var(--border-faint)', display: 'flex', gap: 8 }}>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => mutate('PUT')}
|
||||
disabled={busy}
|
||||
style={{
|
||||
@@ -148,7 +148,7 @@ export function IcsSubscribeModal({ endpoint, title, description, onClose }: Ics
|
||||
<RefreshCw size={11} strokeWidth={2} style={{ animation: busy ? 'spin 0.8s linear infinite' : 'none' }} />
|
||||
Regenerate
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => mutate('DELETE')}
|
||||
disabled={busy}
|
||||
style={{
|
||||
|
||||
@@ -66,8 +66,8 @@ function findDuplicatePlace(
|
||||
): { name?: string | null } | null {
|
||||
const name = (form.name || '').trim().toLowerCase()
|
||||
const gid = (form.google_place_id || '').trim()
|
||||
const lat = form.lat ? parseFloat(form.lat) : null
|
||||
const lng = form.lng ? parseFloat(form.lng) : null
|
||||
const lat = form.lat ? Number.parseFloat(form.lat) : null
|
||||
const lng = form.lng ? Number.parseFloat(form.lng) : null
|
||||
for (const p of places || []) {
|
||||
if (gid && p.google_place_id && p.google_place_id === gid) return p
|
||||
if (name && p.name && p.name.trim().toLowerCase() === name) return p
|
||||
@@ -473,8 +473,8 @@ function usePlaceFormModal(props: PlaceFormModalProps) {
|
||||
try {
|
||||
const saved = await onSave({
|
||||
...form,
|
||||
lat: form.lat ? parseFloat(form.lat) : null,
|
||||
lng: form.lng ? parseFloat(form.lng) : null,
|
||||
lat: form.lat ? Number.parseFloat(form.lat) : null,
|
||||
lng: form.lng ? Number.parseFloat(form.lng) : null,
|
||||
category_id: form.category_id || null,
|
||||
_pendingFiles: pendingFiles.length > 0 ? pendingFiles : undefined,
|
||||
})
|
||||
|
||||
@@ -580,7 +580,7 @@ export function ActionButton({ onClick, variant, icon, label, ref }: ActionButto
|
||||
}
|
||||
const s = base[variant] || base.ghost
|
||||
return (
|
||||
<button
|
||||
<button type="button"
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
@@ -676,7 +676,7 @@ function ParticipantsBox({ tripMembers, participantIds, allJoined, onSetParticip
|
||||
{/* Add button */}
|
||||
{availableToAdd.length > 0 && (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<button onClick={() => setShowAdd(!showAdd)} className="text-content-faint" style={{
|
||||
<button type="button" onClick={() => setShowAdd(!showAdd)} className="text-content-faint" style={{
|
||||
width: 22, height: 22, borderRadius: '50%', border: '1.5px dashed var(--border-primary)',
|
||||
background: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 'calc(12px * var(--fs-scale-body, 1))', transition: 'all 0.12s',
|
||||
@@ -692,7 +692,7 @@ function ParticipantsBox({ tripMembers, participantIds, allJoined, onSetParticip
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.12)', padding: 4, minWidth: 140,
|
||||
}}>
|
||||
{availableToAdd.map(member => (
|
||||
<button key={member.id} onClick={() => handleAdd(member.id)} className="text-content" style={{
|
||||
<button type="button" key={member.id} onClick={() => handleAdd(member.id)} className="text-content" style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6, width: '100%', padding: '5px 8px',
|
||||
borderRadius: 6, border: 'none', background: 'none', cursor: 'pointer',
|
||||
fontFamily: 'inherit', fontSize: 'calc(11px * var(--fs-scale-caption, 1))', textAlign: 'left',
|
||||
@@ -807,7 +807,7 @@ function PlaceInspectorHeader({ openNow, place, category, t, editingName, nameIn
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onClose}
|
||||
className="bg-surface-hover"
|
||||
style={{ width: 28, height: 28, borderRadius: '50%', border: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0, alignSelf: 'flex-start', transition: 'background 0.15s' }}
|
||||
@@ -940,7 +940,7 @@ function TrackColorRow({ place, trackColor, onUpdatePlace, t }: any) {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<div className="bg-surface-hover" style={{ borderRadius: 10, padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setOpen(o => !o)}
|
||||
aria-expanded={open}
|
||||
style={{
|
||||
@@ -980,7 +980,7 @@ function PlaceExtras({ openingHours, weekdayIndex, hoursExpanded, setHoursExpand
|
||||
<div className={`grid grid-cols-1 ${openingHours?.length > 0 ? 'sm:grid-cols-2' : ''} gap-2`}>
|
||||
{openingHours && openingHours.length > 0 && (
|
||||
<div className="bg-surface-hover" style={{ borderRadius: 10, overflow: 'hidden' }}>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setHoursExpanded(h => !h)}
|
||||
style={{
|
||||
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
@@ -1111,7 +1111,7 @@ function PlaceExtras({ openingHours, weekdayIndex, hoursExpanded, setHoursExpand
|
||||
{(placeFiles.length > 0 || onFileUpload) && (
|
||||
<div className="bg-surface-hover" style={{ borderRadius: 10, overflow: 'hidden' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', padding: '8px 12px', gap: 6 }}>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setFilesExpanded(f => !f)}
|
||||
style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 6, background: 'none', border: 'none', cursor: 'pointer', padding: 0, fontFamily: 'inherit', textAlign: 'left' }}
|
||||
>
|
||||
@@ -1135,7 +1135,7 @@ function PlaceExtras({ openingHours, weekdayIndex, hoursExpanded, setHoursExpand
|
||||
{filesExpanded && placeFiles.length > 0 && (
|
||||
<div style={{ padding: '0 12px 10px', display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{placeFiles.map(f => (
|
||||
<button key={f.id} onClick={() => openFile(f.url).catch(() => {})} style={{ display: 'flex', alignItems: 'center', gap: 8, textDecoration: 'none', cursor: 'pointer', background: 'none', border: 'none', width: '100%', textAlign: 'left' }}>
|
||||
<button type="button" key={f.id} onClick={() => openFile(f.url).catch(() => {})} style={{ display: 'flex', alignItems: 'center', gap: 8, textDecoration: 'none', cursor: 'pointer', background: 'none', border: 'none', width: '100%', textAlign: 'left' }}>
|
||||
{(f.mime_type || '').startsWith('image/') ? <FileImage size={12} color="#6b7280" /> : <File size={12} color="#6b7280" />}
|
||||
<span className="text-content-secondary" style={{ fontSize: 'calc(12px * var(--fs-scale-body, 1))', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</span>
|
||||
{f.file_size && <span className="text-content-faint" style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', flexShrink: 0 }}>{formatFileSize(f.file_size)}</span>}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function PlacesBulkCategoryModal({ count, categories, onPick, onClose }:
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<span style={{ fontWeight: 600, fontSize: 14 }}>{t('places.changeCategory')}</span>
|
||||
<button onClick={onClose} aria-label={t('common.close')} className="text-content-muted" style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4, display: 'flex' }}>
|
||||
<button type="button" onClick={onClose} aria-label={t('common.close')} className="text-content-muted" style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 4, display: 'flex' }}>
|
||||
<X size={15} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -47,7 +47,7 @@ export function PlacesBulkCategoryModal({ count, categories, onPick, onClose }:
|
||||
{categories.map(c => {
|
||||
const CatIcon = getCategoryIcon(c.icon)
|
||||
return (
|
||||
<button key={c.id} onClick={() => onPick(c.id)} className="text-content bg-transparent" style={rowStyle} onMouseEnter={hoverOn} onMouseLeave={hoverOff}>
|
||||
<button type="button" key={c.id} onClick={() => onPick(c.id)} className="text-content bg-transparent" style={rowStyle} onMouseEnter={hoverOn} onMouseLeave={hoverOff}>
|
||||
<CatIcon size={14} strokeWidth={2} color={c.color || 'var(--text-muted)'} />
|
||||
<span style={{ flex: 1 }}>{c.name}</span>
|
||||
</button>
|
||||
|
||||
@@ -30,7 +30,7 @@ export function PlacesHeader(S: SidebarState) {
|
||||
} = S
|
||||
return (
|
||||
<div className="border-b border-edge-faint" style={{ padding: '14px 16px 10px', flexShrink: 0 }}>
|
||||
{canEditPlaces && <button
|
||||
{canEditPlaces && <button type="button"
|
||||
onClick={onAddPlace}
|
||||
className="bg-accent text-accent-text"
|
||||
style={{
|
||||
@@ -44,7 +44,7 @@ export function PlacesHeader(S: SidebarState) {
|
||||
</button>}
|
||||
{canEditPlaces && <>
|
||||
<div style={{ display: 'flex', gap: 6, marginBottom: 10 }}>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setFileImportOpen(true)}
|
||||
className="border border-dashed border-edge text-content-faint"
|
||||
style={{
|
||||
@@ -56,7 +56,7 @@ export function PlacesHeader(S: SidebarState) {
|
||||
>
|
||||
<FileDown size={11} strokeWidth={2} /> {t('places.importFile')}
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setListImportOpen(true)}
|
||||
className="border border-dashed border-edge text-content-faint"
|
||||
style={{
|
||||
@@ -101,7 +101,7 @@ export function PlacesHeader(S: SidebarState) {
|
||||
{tabs.map(f => {
|
||||
const active = filter === f.id
|
||||
return (
|
||||
<button
|
||||
<button type="button"
|
||||
key={f.id}
|
||||
onClick={() => { setFilter(f.id); setSelectedIds(new Set()) }}
|
||||
className={active ? 'bg-accent text-accent-text' : 'bg-surface-card text-content'}
|
||||
@@ -145,7 +145,7 @@ export function PlacesHeader(S: SidebarState) {
|
||||
}}
|
||||
/>
|
||||
{search && (
|
||||
<button onClick={() => setSearch('')} style={{ position: 'absolute', right: 8, top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', cursor: 'pointer', padding: 0, display: 'flex' }}>
|
||||
<button type="button" onClick={() => setSearch('')} style={{ position: 'absolute', right: 8, top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', cursor: 'pointer', padding: 0, display: 'flex' }}>
|
||||
<X size={12} strokeWidth={2} color="var(--text-faint)" />
|
||||
</button>
|
||||
)}
|
||||
@@ -162,7 +162,7 @@ export function PlacesHeader(S: SidebarState) {
|
||||
: `${categoryFilters.size} ${t('places.categoriesSelected')}`
|
||||
return (
|
||||
<div style={{ marginTop: 6, position: 'relative', display: 'flex', gap: 6, alignItems: 'stretch' }}>
|
||||
{categories.length > 0 && <button onClick={() => setCatDropOpen(v => !v)} className="bg-surface-card text-content" style={{
|
||||
{categories.length > 0 && <button type="button" onClick={() => setCatDropOpen(v => !v)} className="bg-surface-card text-content" style={{
|
||||
flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '6px 10px', borderRadius: 8, border: '1px solid var(--border-primary)',
|
||||
fontSize: 'calc(12px * var(--fs-scale-body, 1))',
|
||||
@@ -173,7 +173,7 @@ export function PlacesHeader(S: SidebarState) {
|
||||
</button>}
|
||||
{/* Minimum-stars filter (#1435), same floors as the collections bar. */}
|
||||
<Tooltip label={t('places.filterByRating')} placement="bottom">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => { setStarDropOpen(v => !v); setCatDropOpen(false) }}
|
||||
aria-label={t('places.filterByRating')}
|
||||
aria-expanded={starDropOpen}
|
||||
@@ -203,7 +203,7 @@ export function PlacesHeader(S: SidebarState) {
|
||||
{(['all', 5, 4, 3, 2, 1] as const).map(opt => {
|
||||
const active = ratingFilter === opt
|
||||
return (
|
||||
<button
|
||||
<button type="button"
|
||||
key={String(opt)}
|
||||
onClick={() => { setRatingFilter(opt as number | 'all'); setStarDropOpen(false) }}
|
||||
className={`text-content ${active ? 'bg-surface-hover' : 'bg-transparent'}`}
|
||||
@@ -230,7 +230,7 @@ export function PlacesHeader(S: SidebarState) {
|
||||
)}
|
||||
{canEditPlaces && (
|
||||
<Tooltip label={t('common.select')} placement="bottom">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => { setSelectMode(v => !v); setSelectedIds(new Set()) }}
|
||||
aria-label={t('common.select')}
|
||||
aria-pressed={selectMode}
|
||||
@@ -273,7 +273,7 @@ export function PlacesHeader(S: SidebarState) {
|
||||
const active = categoryFilters.has(String(c.id))
|
||||
const CatIcon = getCategoryIcon(c.icon)
|
||||
return (
|
||||
<button key={c.id} onClick={() => toggleCategoryFilter(String(c.id))} className={`text-content ${active ? 'bg-surface-hover' : 'bg-transparent'}`} style={{
|
||||
<button type="button" key={c.id} onClick={() => toggleCategoryFilter(String(c.id))} className={`text-content ${active ? 'bg-surface-hover' : 'bg-transparent'}`} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, width: '100%',
|
||||
padding: '6px 10px', borderRadius: 6, border: 'none', cursor: 'pointer',
|
||||
fontFamily: 'inherit', fontSize: 'calc(12px * var(--fs-scale-body, 1))',
|
||||
@@ -295,7 +295,7 @@ export function PlacesHeader(S: SidebarState) {
|
||||
{places.some(p => p.category_id == null) && (() => {
|
||||
const active = categoryFilters.has('uncategorized')
|
||||
return (
|
||||
<button onClick={() => toggleCategoryFilter('uncategorized')} className={`text-content-muted ${active ? 'bg-surface-hover' : 'bg-transparent'}`} style={{
|
||||
<button type="button" onClick={() => toggleCategoryFilter('uncategorized')} className={`text-content-muted ${active ? 'bg-surface-hover' : 'bg-transparent'}`} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, width: '100%',
|
||||
padding: '6px 10px', borderRadius: 6, border: 'none', cursor: 'pointer',
|
||||
fontFamily: 'inherit', fontSize: 'calc(12px * var(--fs-scale-body, 1))',
|
||||
@@ -314,7 +314,7 @@ export function PlacesHeader(S: SidebarState) {
|
||||
)
|
||||
})()}
|
||||
{categoryFilters.size > 0 && (
|
||||
<button onClick={() => setCategoryFilters(new Set())} className="bg-transparent text-content-faint" style={{
|
||||
<button type="button" onClick={() => setCategoryFilters(new Set())} className="bg-transparent text-content-faint" style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4,
|
||||
width: '100%', padding: '6px 10px', borderRadius: 6, border: 'none', cursor: 'pointer',
|
||||
fontFamily: 'inherit', fontSize: 'calc(11px * var(--fs-scale-caption, 1))',
|
||||
|
||||
@@ -18,7 +18,7 @@ export function PlacesList(S: SidebarState) {
|
||||
<span className="text-content-faint" style={{ fontSize: 'calc(13px * var(--fs-scale-body, 1))' }}>
|
||||
{filter === 'unplanned' ? t('places.allPlanned') : t('places.noneFound')}
|
||||
</span>
|
||||
{canEditPlaces && <button onClick={onAddPlace} className="text-content" style={{ fontSize: 'calc(12px * var(--fs-scale-body, 1))', background: 'none', border: 'none', cursor: 'pointer', textDecoration: 'underline', fontFamily: 'inherit' }}>
|
||||
{canEditPlaces && <button type="button" onClick={onAddPlace} className="text-content" style={{ fontSize: 'calc(12px * var(--fs-scale-body, 1))', background: 'none', border: 'none', cursor: 'pointer', textDecoration: 'underline', fontFamily: 'inherit' }}>
|
||||
{t('places.addPlace')}
|
||||
</button>}
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ export function ListImportModal(S: SidebarState) {
|
||||
{hasMultipleListImportProviders && (
|
||||
<div style={{ display: 'flex', gap: 6, marginBottom: 10 }}>
|
||||
{availableListImportProviders.map(provider => (
|
||||
<button
|
||||
<button type="button"
|
||||
key={provider}
|
||||
onClick={() => setListImportProvider(provider)}
|
||||
className={listImportProvider === provider ? 'bg-accent text-accent-text' : 'bg-surface-tertiary text-content-muted'}
|
||||
@@ -67,7 +67,7 @@ export function ListImportModal(S: SidebarState) {
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 16, justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => { setListImportOpen(false); setListImportUrl('') }}
|
||||
className="text-content"
|
||||
style={{
|
||||
@@ -78,7 +78,7 @@ export function ListImportModal(S: SidebarState) {
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleListImport}
|
||||
disabled={!listImportUrl.trim() || listImportLoading}
|
||||
className={!listImportUrl.trim() || listImportLoading ? 'bg-surface-tertiary text-content-faint' : 'bg-accent text-accent-text'}
|
||||
|
||||
@@ -23,7 +23,7 @@ export function MobileDayPickerSheet(S: SidebarState) {
|
||||
</div>
|
||||
<div style={{ overflowY: 'auto', padding: '8px 12px' }}>
|
||||
{/* View details */}
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => { onPlaceClick(dayPickerPlace.id); setDayPickerPlace(null); setMobileShowDays(false) }}
|
||||
className="bg-transparent text-content"
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '12px 14px', borderRadius: 12, border: 'none', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left', fontSize: 'calc(14px * var(--fs-scale-body, 1))' }}
|
||||
@@ -32,7 +32,7 @@ export function MobileDayPickerSheet(S: SidebarState) {
|
||||
</button>
|
||||
{/* Edit */}
|
||||
{canEditPlaces && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => { onEditPlace(dayPickerPlace); setDayPickerPlace(null); setMobileShowDays(false) }}
|
||||
className="bg-transparent text-content"
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '12px 14px', borderRadius: 12, border: 'none', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left', fontSize: 'calc(14px * var(--fs-scale-body, 1))' }}
|
||||
@@ -43,7 +43,7 @@ export function MobileDayPickerSheet(S: SidebarState) {
|
||||
{/* Assign to day */}
|
||||
{days?.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setMobileShowDays(v => !v)}
|
||||
className="bg-transparent text-content"
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '12px 14px', borderRadius: 12, border: 'none', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left', fontSize: 'calc(14px * var(--fs-scale-body, 1))' }}
|
||||
@@ -54,7 +54,7 @@ export function MobileDayPickerSheet(S: SidebarState) {
|
||||
{mobileShowDays && (
|
||||
<div style={{ paddingLeft: 20 }}>
|
||||
{days.map((day, i) => (
|
||||
<button
|
||||
<button type="button"
|
||||
key={day.id}
|
||||
onClick={() => { onAssignToDay(dayPickerPlace.id, day.id); setDayPickerPlace(null); setMobileShowDays(false) }}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 10, width: '100%', padding: '10px 14px', borderRadius: 10, border: 'none', cursor: 'pointer', background: 'transparent', fontFamily: 'inherit', textAlign: 'left' }}
|
||||
@@ -73,7 +73,7 @@ export function MobileDayPickerSheet(S: SidebarState) {
|
||||
)}
|
||||
{/* Delete */}
|
||||
{canEditPlaces && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => { onDeletePlace(dayPickerPlace.id); setDayPickerPlace(null); setMobileShowDays(false) }}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%', padding: '12px 14px', borderRadius: 12, border: 'none', cursor: 'pointer', background: 'transparent', fontFamily: 'inherit', textAlign: 'left', fontSize: 'calc(14px * var(--fs-scale-body, 1))', color: '#ef4444' }}
|
||||
>
|
||||
|
||||
@@ -121,7 +121,7 @@ export const MemoPlaceRow = React.memo(function MemoPlaceRow({
|
||||
</div>
|
||||
<div style={{ flexShrink: 0, display: 'flex', alignItems: 'center' }}>
|
||||
{!selectMode && !inDay && selectedDayId && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={e => { e.stopPropagation(); onAssignToDay(place.id) }}
|
||||
className="bg-surface-hover text-content-faint"
|
||||
style={{
|
||||
|
||||
@@ -14,7 +14,7 @@ export function PlacesSelectionBar(S: SidebarState) {
|
||||
{t('places.selectionCount', { count: selectedIds.size })}
|
||||
</span>
|
||||
<Tooltip label={selectedIds.size === filtered.length && filtered.length > 0 ? t('common.deselectAll') : t('common.selectAll')} placement="bottom">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => {
|
||||
if (selectedIds.size === filtered.length) setSelectedIds(new Set())
|
||||
else setSelectedIds(new Set(filtered.map(p => p.id)))
|
||||
@@ -33,7 +33,7 @@ export function PlacesSelectionBar(S: SidebarState) {
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('places.changeCategory')} placement="bottom">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => { if (selectedIds.size === 0) return; setCategoryPickerOpen(true) }}
|
||||
disabled={selectedIds.size === 0}
|
||||
aria-label={t('places.changeCategory')}
|
||||
@@ -51,7 +51,7 @@ export function PlacesSelectionBar(S: SidebarState) {
|
||||
</Tooltip>
|
||||
{collectionsEnabled && (
|
||||
<Tooltip label={t('inspector.saveToCollection')} placement="bottom">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => { if (selectedIds.size === 0) return; setSaveToListOpen(true) }}
|
||||
disabled={selectedIds.size === 0}
|
||||
aria-label={t('inspector.saveToCollection')}
|
||||
@@ -70,7 +70,7 @@ export function PlacesSelectionBar(S: SidebarState) {
|
||||
)}
|
||||
{collectionsEnabled && (
|
||||
<Tooltip label={t('collections.markVisitedSelection')} placement="bottom">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => { if (selectedIds.size === 0) return; void markSelectionVisited() }}
|
||||
disabled={selectedIds.size === 0 || markVisitedBusy}
|
||||
aria-label={t('collections.markVisitedSelection')}
|
||||
@@ -88,7 +88,7 @@ export function PlacesSelectionBar(S: SidebarState) {
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip label={t('places.deleteSelected')} placement="bottom">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => {
|
||||
if (selectedIds.size === 0) return
|
||||
if (isMobile) setPendingDeleteIds(Array.from(selectedIds))
|
||||
|
||||
@@ -229,7 +229,7 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
|
||||
maxWidth: 140, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
}}>{r.title}</span>
|
||||
{canEdit && (
|
||||
<button onClick={() => onEdit(r)} title={t('common.edit')} className="bg-transparent text-content-faint" style={{
|
||||
<button type="button" onClick={() => onEdit(r)} title={t('common.edit')} className="bg-transparent text-content-faint" style={{
|
||||
appearance: 'none', border: 'none',
|
||||
width: 26, height: 26, borderRadius: 6, display: 'grid', placeItems: 'center',
|
||||
cursor: 'pointer', flexShrink: 0,
|
||||
@@ -240,7 +240,7 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
|
||||
</button>
|
||||
)}
|
||||
{canEdit && (
|
||||
<button onClick={() => setShowDeleteConfirm(true)} title={t('common.delete')} className="bg-transparent text-content-faint" style={{
|
||||
<button type="button" onClick={() => setShowDeleteConfirm(true)} title={t('common.delete')} className="bg-transparent text-content-faint" style={{
|
||||
appearance: 'none', border: 'none',
|
||||
width: 26, height: 26, borderRadius: 6, display: 'grid', placeItems: 'center',
|
||||
cursor: 'pointer', flexShrink: 0,
|
||||
@@ -522,11 +522,11 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
|
||||
{t('reservations.confirm.deleteBody', { name: r.title })}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 4 }}>
|
||||
<button onClick={() => setShowDeleteConfirm(false)} className="text-content-muted" style={{
|
||||
<button type="button" onClick={() => setShowDeleteConfirm(false)} className="text-content-muted" style={{
|
||||
fontSize: 'calc(12px * var(--fs-scale-body, 1))', background: 'none', border: '1px solid var(--border-primary)',
|
||||
borderRadius: 8, padding: '6px 14px', cursor: 'pointer', fontFamily: 'inherit',
|
||||
}}>{t('common.cancel')}</button>
|
||||
<button onClick={handleDelete} className="bg-[#ef4444] text-white" style={{
|
||||
<button type="button" onClick={handleDelete} className="bg-[#ef4444] text-white" style={{
|
||||
fontSize: 'calc(12px * var(--fs-scale-body, 1))',
|
||||
border: 'none', borderRadius: 8, padding: '6px 16px', cursor: 'pointer', fontWeight: 600, fontFamily: 'inherit',
|
||||
}}>{t('common.confirm')}</button>
|
||||
@@ -561,7 +561,7 @@ function Section({ title, count, children, defaultOpen = true, accent, storageKe
|
||||
}, [open, storageKey])
|
||||
return (
|
||||
<div style={{ marginBottom: 28 }}>
|
||||
<button onClick={() => setOpen(o => !o)} style={{
|
||||
<button type="button" onClick={() => setOpen(o => !o)} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, width: '100%',
|
||||
background: 'none', border: 'none', cursor: 'pointer', padding: '4px 0', marginBottom: 12, fontFamily: 'inherit',
|
||||
userSelect: 'none',
|
||||
@@ -632,7 +632,7 @@ function TransitJourneyCard({ r, days, onOpen, onDelete, canEdit, tripId, contri
|
||||
</div>
|
||||
</div>
|
||||
{canEdit && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={e => { e.stopPropagation(); setConfirmOpen(true) }}
|
||||
title={t('common.delete')}
|
||||
className="bg-transparent text-content-faint"
|
||||
@@ -680,8 +680,8 @@ function TransitJourneyCard({ r, days, onOpen, onDelete, canEdit, tripId, contri
|
||||
<div className="text-content" style={{ fontWeight: 600, fontSize: 'calc(14px * var(--fs-scale-body, 1))', marginBottom: 6 }}>{t('reservations.confirm.deleteTitle')}</div>
|
||||
<div className="text-content-muted" style={{ fontSize: 'calc(12.5px * var(--fs-scale-body, 1))', marginBottom: 14 }}>{t('reservations.confirm.deleteBody', { name: r.title })}</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
<button onClick={e => { e.stopPropagation(); setConfirmOpen(false) }} className="text-content-muted" style={{ padding: '7px 14px', borderRadius: 9, border: '1px solid var(--border-primary)', background: 'none', fontSize: 'calc(12px * var(--fs-scale-body, 1))', cursor: 'pointer', fontFamily: 'inherit' }}>{t('common.cancel')}</button>
|
||||
<button onClick={e => { e.stopPropagation(); setConfirmOpen(false); onDelete(r.id) }} style={{ padding: '7px 14px', borderRadius: 9, border: 'none', background: '#ef4444', color: '#fff', fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>{t('common.delete')}</button>
|
||||
<button type="button" onClick={e => { e.stopPropagation(); setConfirmOpen(false) }} className="text-content-muted" style={{ padding: '7px 14px', borderRadius: 9, border: '1px solid var(--border-primary)', background: 'none', fontSize: 'calc(12px * var(--fs-scale-body, 1))', cursor: 'pointer', fontFamily: 'inherit' }}>{t('common.cancel')}</button>
|
||||
<button type="button" onClick={e => { e.stopPropagation(); setConfirmOpen(false); onDelete(r.id) }} style={{ padding: '7px 14px', borderRadius: 9, border: 'none', background: '#ef4444', color: '#fff', fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>{t('common.delete')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
@@ -837,7 +837,7 @@ export default function ReservationsPanel({ tripId, reservations, days, assignme
|
||||
<>
|
||||
<div className="hidden md:block" style={{ width: 1, height: 22, background: 'var(--border-faint)', flexShrink: 0 }} />
|
||||
<div className="hidden md:inline-flex" style={{ gap: 4, flexWrap: 'wrap', flex: 1, minWidth: 0 }}>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => { setTypeFilters(new Set()); sessionStorage.removeItem(storageKey) }}
|
||||
className={typeFilters.size === 0 ? 'bg-surface-card text-content' : 'bg-transparent text-content-muted'}
|
||||
style={{
|
||||
@@ -859,7 +859,7 @@ export default function ReservationsPanel({ tripId, reservations, days, assignme
|
||||
const active = typeFilters.has(opt.value)
|
||||
const Icon = opt.Icon
|
||||
return (
|
||||
<button
|
||||
<button type="button"
|
||||
key={opt.value}
|
||||
onClick={() => toggleTypeFilter(opt.value)}
|
||||
className={active ? 'bg-surface-card text-content' : 'bg-transparent text-content-muted'}
|
||||
@@ -896,7 +896,7 @@ export default function ReservationsPanel({ tripId, reservations, days, assignme
|
||||
{canEdit && (
|
||||
<div style={{ display: 'flex', gap: 6, marginLeft: 'auto', flexShrink: 0 }}>
|
||||
{onImport && bookingImportAvailable && (
|
||||
<button onClick={onImport} className="bg-surface-card text-content" style={{
|
||||
<button type="button" onClick={onImport} className="bg-surface-card text-content" style={{
|
||||
appearance: 'none', border: '1px solid var(--border-primary)', cursor: 'pointer', fontFamily: 'inherit',
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 13px', borderRadius: 10, fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 500,
|
||||
@@ -911,7 +911,7 @@ export default function ReservationsPanel({ tripId, reservations, days, assignme
|
||||
</button>
|
||||
)}
|
||||
{onAirTrailImport && airTrailAvailable && (
|
||||
<button onClick={onAirTrailImport} className="bg-surface-secondary text-content" style={{
|
||||
<button type="button" onClick={onAirTrailImport} className="bg-surface-secondary text-content" style={{
|
||||
appearance: 'none', border: '1px solid var(--border-primary)', cursor: 'pointer', fontFamily: 'inherit',
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 14px', borderRadius: 10, fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 500, boxSizing: 'border-box',
|
||||
@@ -925,7 +925,7 @@ export default function ReservationsPanel({ tripId, reservations, days, assignme
|
||||
<span className="hidden sm:inline">{t('reservations.airtrail.cta')}</span>
|
||||
</button>
|
||||
)}
|
||||
<button onClick={onAdd} className="bg-accent text-accent-text" style={{
|
||||
<button type="button" onClick={onAdd} className="bg-accent text-accent-text" style={{
|
||||
appearance: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: '9px 14px', borderRadius: 10, fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontWeight: 500,
|
||||
@@ -952,7 +952,7 @@ export default function ReservationsPanel({ tripId, reservations, days, assignme
|
||||
// page, so the import in particular went unnoticed (#2007).
|
||||
action={canEdit ? (
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||
<button onClick={onAdd} className="bg-accent text-accent-text" style={CTA_STYLE}>
|
||||
<button type="button" onClick={onAdd} className="bg-accent text-accent-text" style={CTA_STYLE}>
|
||||
<Plus size={14} strokeWidth={2} />
|
||||
{t(addManualKey)}
|
||||
</button>
|
||||
|
||||
@@ -114,7 +114,7 @@ function UrlRow({ label, hint, url, copied, onCopy }: {
|
||||
}}>
|
||||
{url}
|
||||
</div>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onCopy}
|
||||
title="Copy"
|
||||
style={{
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function TransitJourneyModal({ reservation, onClose, onSave, onDe
|
||||
footer={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
{canEdit && (
|
||||
<button onClick={() => setConfirmDelete(true)} aria-label={t('common.delete')} title={t('common.delete')} style={{
|
||||
<button type="button" onClick={() => setConfirmDelete(true)} aria-label={t('common.delete')} title={t('common.delete')} style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 5, padding: isMobile ? '9px 11px' : '8px 14px', borderRadius: 10,
|
||||
border: '1px solid rgba(239,68,68,0.3)', background: 'rgba(239,68,68,0.06)', color: '#ef4444',
|
||||
fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 500, cursor: 'pointer', fontFamily: 'inherit',
|
||||
@@ -152,7 +152,7 @@ export default function TransitJourneyModal({ reservation, onClose, onSave, onDe
|
||||
)}
|
||||
<div style={{ flex: 1 }} />
|
||||
{canEdit && (
|
||||
<button onClick={onChangeRoute} className="text-content-muted" style={{
|
||||
<button type="button" onClick={onChangeRoute} className="text-content-muted" style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6, padding: '8px 16px', borderRadius: 10,
|
||||
border: '1px solid var(--border-primary)', background: 'none',
|
||||
fontSize: 'calc(12px * var(--fs-scale-body, 1))', cursor: 'pointer', fontFamily: 'inherit',
|
||||
@@ -161,7 +161,7 @@ export default function TransitJourneyModal({ reservation, onClose, onSave, onDe
|
||||
</button>
|
||||
)}
|
||||
{canEdit ? (
|
||||
<button onClick={save} disabled={saving || !title.trim() || !dirty} className="bg-[var(--text-primary)] text-[var(--bg-primary)]" style={{
|
||||
<button type="button" onClick={save} disabled={saving || !title.trim() || !dirty} className="bg-[var(--text-primary)] text-[var(--bg-primary)]" style={{
|
||||
padding: '8px 20px', borderRadius: 10, border: 'none',
|
||||
fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit',
|
||||
opacity: saving || !title.trim() || !dirty ? 0.5 : 1,
|
||||
@@ -169,7 +169,7 @@ export default function TransitJourneyModal({ reservation, onClose, onSave, onDe
|
||||
{saving ? t('common.saving') : t('common.save')}
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={onClose} className="bg-accent text-accent-text" style={{ padding: '8px 20px', borderRadius: 10, border: 'none', fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>
|
||||
<button type="button" onClick={onClose} className="bg-accent text-accent-text" style={{ padding: '8px 20px', borderRadius: 10, border: 'none', fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit' }}>
|
||||
{t('common.close')}
|
||||
</button>
|
||||
)}
|
||||
@@ -198,7 +198,7 @@ export default function TransitJourneyModal({ reservation, onClose, onSave, onDe
|
||||
<div className="text-content" style={{ display: 'flex', alignItems: 'center', gap: 7, fontSize: 'calc(17px * var(--fs-scale-subtitle, 1))', fontWeight: 700, letterSpacing: '-0.015em', minWidth: 0 }}>
|
||||
<span style={{ minWidth: 0, overflow: 'hidden' }}><TransitTitle title={title} iconSize={15} /></span>
|
||||
{canEdit && (
|
||||
<button onClick={() => setEditingTitle(true)} aria-label={t('common.edit')} title={t('common.edit')} className="text-content-faint" style={{ border: 'none', background: 'none', padding: 3, cursor: 'pointer', display: 'flex', flexShrink: 0 }}>
|
||||
<button type="button" onClick={() => setEditingTitle(true)} aria-label={t('common.edit')} title={t('common.edit')} className="text-content-faint" style={{ border: 'none', background: 'none', padding: 3, cursor: 'pointer', display: 'flex', flexShrink: 0 }}>
|
||||
<Pencil size={13} strokeWidth={1.8} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -149,7 +149,7 @@ function StopPicker({ label, value, onPick, quickPicks, near, placeholder }: {
|
||||
<div className="bg-surface-card border border-edge" style={{ position: 'absolute', top: '100%', left: 0, right: 0, marginTop: 4, borderRadius: 10, boxShadow: '0 8px 32px rgba(0,0,0,0.14)', zIndex: 30, overflow: 'hidden', maxHeight: 240, overflowY: 'auto' }}>
|
||||
{results.length > 0
|
||||
? results.map((r, i) => (
|
||||
<button key={i} onClick={() => { onPick({ name: r.name, lat: r.lat, lng: r.lng }); setText(''); setResults([]); setOpen(false) }}
|
||||
<button type="button" key={i} onClick={() => { onPick({ name: r.name, lat: r.lat, lng: r.lng }); setText(''); setResults([]); setOpen(false) }}
|
||||
className="text-content"
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', textAlign: 'left', padding: '8px 10px', border: 'none', background: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 'calc(13px * var(--fs-scale-body, 1))' }}
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
|
||||
@@ -162,7 +162,7 @@ function StopPicker({ label, value, onPick, quickPicks, near, placeholder }: {
|
||||
</button>
|
||||
))
|
||||
: quickPicks.map((p, i) => (
|
||||
<button key={i} onClick={() => { onPick(p); setText(''); setOpen(false) }}
|
||||
<button type="button" key={i} onClick={() => { onPick(p); setText(''); setOpen(false) }}
|
||||
className="text-content"
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', textAlign: 'left', padding: '8px 10px', border: 'none', background: 'none', cursor: 'pointer', fontFamily: 'inherit', fontSize: 'calc(13px * var(--fs-scale-body, 1))' }}
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
|
||||
@@ -206,7 +206,7 @@ function ItineraryCard({ it, tzFrom, tzTo, is12h, expanded, onToggle, onAdd, add
|
||||
const walkMins = Math.round(it.walkSeconds / 60)
|
||||
return (
|
||||
<div className="bg-surface-card border border-edge" style={{ borderRadius: 14, overflow: 'hidden' }}>
|
||||
<button onClick={onToggle} style={{ display: 'block', width: '100%', textAlign: 'left', padding: '12px 14px', border: 'none', background: 'none', cursor: 'pointer', fontFamily: 'inherit' }}>
|
||||
<button type="button" onClick={onToggle} style={{ display: 'block', width: '100%', textAlign: 'left', padding: '12px 14px', border: 'none', background: 'none', cursor: 'pointer', fontFamily: 'inherit' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
|
||||
<span className="text-content" style={{ fontSize: 'calc(15px * var(--fs-scale-subtitle, 1))', fontWeight: 700, letterSpacing: '-0.01em' }}>
|
||||
{fmtTimeInTz(it.startTime, tzFrom, is12h)} – {fmtTimeInTz(it.endTime, tzTo, is12h)}
|
||||
@@ -289,7 +289,7 @@ function ItineraryCard({ it, tzFrom, tzTo, is12h, expanded, onToggle, onAdd, add
|
||||
{[...new Set(transitLegs.map(l => l.agency).filter(Boolean))].join(' · ')}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onAdd}
|
||||
disabled={adding}
|
||||
className="bg-accent text-accent-text"
|
||||
@@ -507,7 +507,7 @@ export default function TransitSearchPanel({ day, days, places, accommodations =
|
||||
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 8, alignItems: isMobile ? 'stretch' : 'flex-end' }}>
|
||||
<StopPicker label={t('transit.from')} value={from} onPick={setFrom} quickPicks={quickPicks} near={near} placeholder={t('transit.searchStop')} />
|
||||
{!isMobile && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => { const f = from; setFrom(to); setTo(f) }}
|
||||
aria-label={t('transit.swap')}
|
||||
title={t('transit.swap')}
|
||||
@@ -525,8 +525,8 @@ export default function TransitSearchPanel({ day, days, places, accommodations =
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<div className="bg-surface-secondary" style={{ display: 'flex', borderRadius: 9, padding: 3 }}>
|
||||
<button onClick={() => setArriveBy(false)} style={segBtn(!arriveBy)}>{t('transit.depart')}</button>
|
||||
<button onClick={() => setArriveBy(true)} style={segBtn(arriveBy)}>{t('transit.arrive')}</button>
|
||||
<button type="button" onClick={() => setArriveBy(false)} style={segBtn(!arriveBy)}>{t('transit.depart')}</button>
|
||||
<button type="button" onClick={() => setArriveBy(true)} style={segBtn(arriveBy)}>{t('transit.arrive')}</button>
|
||||
</div>
|
||||
<div style={{ width: 110 }}>
|
||||
<CustomTimePicker value={time} onChange={setTime} />
|
||||
@@ -552,7 +552,7 @@ export default function TransitSearchPanel({ day, days, places, accommodations =
|
||||
{MODE_GROUPS.map(m => {
|
||||
const active = activeModes.has(m.key)
|
||||
return (
|
||||
<button key={m.key} onClick={() => toggleMode(m.key)}
|
||||
<button type="button" key={m.key} onClick={() => toggleMode(m.key)}
|
||||
className={active ? 'bg-surface-card text-content' : 'text-content-faint'}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 5, padding: '5px 11px', borderRadius: 99,
|
||||
@@ -569,7 +569,7 @@ export default function TransitSearchPanel({ day, days, places, accommodations =
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={search}
|
||||
disabled={!from || !to || loading}
|
||||
className="bg-accent text-accent-text"
|
||||
|
||||
@@ -17,7 +17,7 @@ export function convertHoursLine(line: string, timeFormat: string): string {
|
||||
if (timeFormat === '12h' && !hasAmPm) {
|
||||
// 24h → 12h: "10:00" → "10:00 AM", "21:00" → "9:00 PM", "Uhr" entfernen
|
||||
return line.replace(/\s*Uhr/g, '').replace(/(\d{1,2}):(\d{2})/g, (match, h, m) => {
|
||||
const hour = parseInt(h)
|
||||
const hour = Number.parseInt(h)
|
||||
if (isNaN(hour)) return match
|
||||
const period = hour >= 12 ? 'PM' : 'AM'
|
||||
const h12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour
|
||||
@@ -27,7 +27,7 @@ export function convertHoursLine(line: string, timeFormat: string): string {
|
||||
if (timeFormat !== '12h' && hasAmPm) {
|
||||
// 12h → 24h: "10:00 AM" → "10:00", "9:00 PM" → "21:00"
|
||||
return line.replace(/(\d{1,2}):(\d{2})\s*(AM|PM)/gi, (_, h, m, p) => {
|
||||
let hour = parseInt(h)
|
||||
let hour = Number.parseInt(h)
|
||||
if (p.toUpperCase() === 'PM' && hour !== 12) hour += 12
|
||||
if (p.toUpperCase() === 'AM' && hour === 12) hour = 0
|
||||
return `${String(hour).padStart(2, '0')}:${m}`
|
||||
|
||||
@@ -67,8 +67,8 @@ function localParts(now: Date, timeZone: string | null): { day: number; minutes:
|
||||
}).formatToParts(now)
|
||||
const value = (type: Intl.DateTimeFormatPartTypes) => parts.find(p => p.type === type)?.value ?? ''
|
||||
const day = WEEKDAY_NUMBER[value('weekday')]
|
||||
const hour = parseInt(value('hour'), 10)
|
||||
const minute = parseInt(value('minute'), 10)
|
||||
const hour = Number.parseInt(value('hour'), 10)
|
||||
const minute = Number.parseInt(value('minute'), 10)
|
||||
if (day === undefined || Number.isNaN(hour) || Number.isNaN(minute)) return null
|
||||
return {
|
||||
day,
|
||||
|
||||
Reference in New Issue
Block a user