mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
refactor(client): finish the React 19 migration (#1826)
Mechanical, and it ends the migration visibly.
45 files imported the whole react-dom default export to reach one function.
They now import createPortal by name, across 54 call sites; six files already
did it this way. main.tsx keeps its react-dom/client import, which is a
different module and already correct.
forwardRef is gone from every component. In React 19 ref is an ordinary prop,
so the wrapper only obscured the signature:
NumericInput — a plain DOM ref, no imperative handle involved
JourneyMap — useImperativeHandle stays, only the wrapping goes
JourneyMapGL — same
JourneyMapAuto — same
Each takes `ref?: Ref<Handle>` in its own Props interface now, so the ref is
part of the declared surface instead of arriving through a generic parameter.
The two journey bindings in glLazy shrink to one line each. They existed as
forwardRef wrappers purely to carry the ref past the lazy boundary; with ref an
ordinary prop it rides through the spread like everything else, which makes all
six bindings in that file identical again.
The five React.FC annotations go. Three were `Record<string, React.FC<{size?:
number}>>` for provider icons in files that never import React at all — those
only typechecked through the UMD global — and are now ComponentType. The two
pages become function declarations.
TranslationContext uses the 19 context shorthand, which leaves its React default
import unused, so that goes too.
Also drops two dead vi.mocks of react-dom/server. Their comment still said "used
in createMarkerIcon", which stopped being true when marker icons moved to the
in-house serializer — nothing under src/ imports react-dom/server any more
except the parity test that exists to compare against it.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useState, type ComponentType } from 'react'
|
||||
import { adminApi } from '../../api/client'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
@@ -27,7 +27,7 @@ function SynologyIcon({ size = 14 }: { size?: number }) {
|
||||
)
|
||||
}
|
||||
|
||||
const PROVIDER_ICONS: Record<string, React.FC<{ size?: number }>> = {
|
||||
const PROVIDER_ICONS: Record<string, ComponentType<{ size?: number }>> = {
|
||||
immich: ImmichIcon,
|
||||
synologyphotos: SynologyIcon,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { Loader2, CheckCircle2, AlertCircle, X } from 'lucide-react'
|
||||
@@ -124,7 +124,7 @@ export default function BackgroundTasksWidget() {
|
||||
navigate(`/trips/${task.tripId}`)
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div
|
||||
style={{ position: 'fixed', right: 16, bottom: 16, zIndex: 50000, display: 'flex', flexDirection: 'column', gap: 8, width: 380, maxWidth: 'calc(100vw - 32px)', fontFamily: 'var(--font-system)' }}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { Pencil, Users, Check } from 'lucide-react'
|
||||
import type { BudgetItemMember } from '../../types'
|
||||
@@ -51,7 +51,7 @@ export function ChipWithTooltip({ label, avatarUrl, size = 20, paid, onClick }:
|
||||
: label?.[0]?.toUpperCase()
|
||||
}
|
||||
</div>
|
||||
{hover && ReactDOM.createPortal(
|
||||
{hover && createPortal(
|
||||
<div style={{
|
||||
position: 'fixed', top: pos.top, left: pos.left, transform: 'translate(-50%, -100%)',
|
||||
pointerEvents: 'none', zIndex: 10000, whiteSpace: 'nowrap',
|
||||
@@ -140,7 +140,7 @@ export default function BudgetMemberChips({ members = [], tripMembers = [], onSe
|
||||
{members.length > 0 ? <Pencil size={iconSize} /> : <Users size={iconSize} />}
|
||||
</button>
|
||||
)}
|
||||
{showDropdown && ReactDOM.createPortal(
|
||||
{showDropdown && createPortal(
|
||||
<div ref={dropRef} style={{
|
||||
position: 'fixed', top: dropPos.top, left: dropPos.left, transform: 'translateX(-50%)', zIndex: 10000,
|
||||
background: 'var(--bg-card)', border: '1px solid var(--border-primary)', borderRadius: 10,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { ArrowUp, Reply, Smile, X } from 'lucide-react'
|
||||
import type { User } from '../../types'
|
||||
import { useCollabChat } from './useCollabChat'
|
||||
@@ -97,7 +97,7 @@ export default function CollabChat({ tripId, currentUser }: CollabChatProps) {
|
||||
{showEmoji && <EmojiPicker onSelect={handleEmojiSelect} onClose={() => setShowEmoji(false)} anchorRef={emojiBtnRef} containerRef={containerRef} />}
|
||||
|
||||
{/* Reaction quick menu (right-click) */}
|
||||
{reactMenu && ReactDOM.createPortal(
|
||||
{reactMenu && createPortal(
|
||||
<ReactionMenu x={reactMenu.x} y={reactMenu.y} onReact={(emoji) => handleReact(reactMenu.msgId, emoji)} onClose={() => setReactMenu(null)} />,
|
||||
document.body
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { EMOJI_CATEGORIES } from './CollabChat.constants'
|
||||
import { TwemojiImg } from './CollabChatTwemojiImg'
|
||||
|
||||
@@ -37,7 +37,7 @@ export function EmojiPicker({ onSelect, onClose, anchorRef, containerRef }: Emoj
|
||||
return () => document.removeEventListener('mousedown', close)
|
||||
}, [onClose, anchorRef])
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div ref={ref} style={{
|
||||
position: 'fixed', bottom: pos.bottom, left: pos.left, zIndex: 10000,
|
||||
background: 'var(--bg-card)', border: '1px solid var(--border-faint)', borderRadius: 16,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { TwemojiImg } from './CollabChatTwemojiImg'
|
||||
import type { ChatReaction } from './CollabChat.types'
|
||||
|
||||
@@ -36,7 +36,7 @@ export function ReactionBadge({ reaction, currentUserId, onReact }: ReactionBadg
|
||||
<TwemojiImg emoji={reaction.emoji} size={16} />
|
||||
{reaction.count > 1 && <span style={{ fontSize: 'calc(10px * var(--fs-scale-caption, 1))', fontWeight: 700, color: 'var(--text-muted)', minWidth: 8 }}>{reaction.count}</span>}
|
||||
</button>
|
||||
{hover && names && ReactDOM.createPortal(
|
||||
{hover && names && createPortal(
|
||||
<div style={{
|
||||
position: 'fixed', top: pos.top, left: pos.left, transform: 'translate(-50%, -100%)',
|
||||
pointerEvents: 'none', zIndex: 10000, whiteSpace: 'nowrap',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import Markdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkBreaks from 'remark-breaks'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Plus, Pencil, X, StickyNote, Settings } from 'lucide-react'
|
||||
import { collabApi } from '../../api/client'
|
||||
import { useCanDo } from '../../store/permissionsStore'
|
||||
@@ -363,7 +363,7 @@ function CollabNotesGrid(S: NotesState) {
|
||||
function ViewNoteModal(S: NotesState) {
|
||||
const { viewingNote, setViewingNote, canEdit, setEditingNote, getCategoryColor, t, setPreviewFile } = S
|
||||
if (!viewingNote) return null
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState } from 'react'
|
||||
import { Plus, Trash2, X } from 'lucide-react'
|
||||
import { FONT, NOTE_COLORS } from './CollabNotes.constants'
|
||||
@@ -59,7 +59,7 @@ export function CategorySettingsModal({ onClose, categories, categoryColors, onS
|
||||
// Merge existing categories from notes with saved colors
|
||||
const allCats = [...new Set([...categories, ...Object.keys(localColors)])]
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div style={{
|
||||
position: 'fixed', inset: 0, background: 'var(--overlay-bg, rgba(0,0,0,0.35))',
|
||||
backdropFilter: 'blur(6px)', WebkitBackdropFilter: 'blur(6px)',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { X, ExternalLink, Loader2 } from 'lucide-react'
|
||||
import { getAuthUrl } from '../../api/authUrl'
|
||||
@@ -27,7 +27,7 @@ export function FilePreviewPortal({ file, onClose }: FilePreviewPortalProps) {
|
||||
|
||||
const openInNewTab = () => openFile(rawUrl).catch(() => {})
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.88)', zIndex: 10000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }} onClick={onClose}>
|
||||
{isImage ? (
|
||||
/* Image lightbox — floating controls */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useRef } from 'react'
|
||||
import { Plus, X } from 'lucide-react'
|
||||
import { useCanDo } from '../../store/permissionsStore'
|
||||
@@ -67,7 +67,7 @@ export function NoteFormModal({ onClose, onSubmit, onDeleteFile, existingCategor
|
||||
|
||||
const canSubmit = title.trim() && !submitting
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useToast } from '../shared/Toast'
|
||||
import { useCanDo } from '../../store/permissionsStore'
|
||||
import { useTripStore } from '../../store/tripStore'
|
||||
import EmptyState from '../shared/EmptyState'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import type { User } from '../../types'
|
||||
|
||||
interface PollVoter {
|
||||
@@ -87,7 +87,7 @@ function CreatePollModal({ onClose, onCreate, t }: CreatePollModalProps) {
|
||||
} catch {} finally { setSubmitting(false) }
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div style={{ position: 'fixed', inset: 0, background: 'var(--overlay-bg, rgba(0,0,0,0.35))', backdropFilter: 'blur(6px)', WebkitBackdropFilter: 'blur(6px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999, padding: 16, fontFamily: FONT }} onClick={onClose}>
|
||||
<form style={{ background: 'var(--bg-card)', borderRadius: 16, width: '100%', maxWidth: 400, maxHeight: '90vh', overflow: 'auto', border: '1px solid var(--border-faint)' }} onClick={e => e.stopPropagation()} onSubmit={handleSubmit}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 16px 12px', borderBottom: '1px solid var(--border-faint)' }}>
|
||||
@@ -176,7 +176,7 @@ function VoterChip({ voter, offset }: VoterChipProps) {
|
||||
}}>
|
||||
{voter.avatar_url ? <img src={voter.avatar_url} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : (voter.username || '?')[0].toUpperCase()}
|
||||
</div>
|
||||
{hover && ReactDOM.createPortal(
|
||||
{hover && createPortal(
|
||||
<div style={{
|
||||
position: 'fixed', top: pos.top, left: pos.left, transform: 'translate(-50%, -100%)',
|
||||
pointerEvents: 'none', zIndex: 10000, whiteSpace: 'nowrap',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { X, MapPin, Ticket, Check } from 'lucide-react'
|
||||
import { filesApi } from '../../api/client'
|
||||
import type { Place, Reservation, Day } from '../../types'
|
||||
@@ -8,7 +8,7 @@ import { transportIcon } from './FileManager.helpers'
|
||||
|
||||
export function AssignModal(S: FileManagerState) {
|
||||
const { files, assignFileId, setAssignFileId, t, days, assignments, places, reservations, tripId, handleAssign, refreshFiles } = S
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 5000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
onClick={() => setAssignFileId(null)}>
|
||||
<div style={{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useRef } from 'react'
|
||||
|
||||
export function AvatarChip({ name, avatarUrl, size = 20 }: { name: string; avatarUrl?: string | null; size?: number }) {
|
||||
@@ -28,7 +28,7 @@ export function AvatarChip({ name, avatarUrl, size = 20 }: { name: string; avata
|
||||
: name?.[0]?.toUpperCase()
|
||||
}
|
||||
</div>
|
||||
{hover && ReactDOM.createPortal(
|
||||
{hover && createPortal(
|
||||
<div style={{
|
||||
position: 'fixed', top: pos.top, left: pos.left, transform: 'translate(-50%, -100%)',
|
||||
background: 'var(--bg-elevated)', color: 'var(--text-primary)',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { ExternalLink, Download, X } from 'lucide-react'
|
||||
import Markdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
@@ -32,7 +32,7 @@ export function MarkdownPreviewModal(S: FileManagerState) {
|
||||
return () => { cancelled = true }
|
||||
}, [previewFileUrl])
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div
|
||||
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.85)', zIndex: 10000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}
|
||||
onClick={() => setPreviewFile(null)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { ExternalLink, Download, X } from 'lucide-react'
|
||||
import { openFile as openFileUrl } from '../../utils/fileDownload'
|
||||
import type { FileManagerState } from './useFileManager'
|
||||
@@ -6,7 +6,7 @@ import { triggerDownload } from './FileManager.helpers'
|
||||
|
||||
export function PdfPreviewModal(S: FileManagerState) {
|
||||
const { previewFile, setPreviewFile, previewFileUrl, toast, t } = S
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div
|
||||
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.85)', zIndex: 10000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}
|
||||
onClick={() => setPreviewFile(null)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useImperativeHandle, forwardRef, useCallback } from 'react'
|
||||
import { useEffect, useRef, useImperativeHandle, useCallback, type Ref } from 'react'
|
||||
import L from 'leaflet'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
|
||||
@@ -31,6 +31,7 @@ interface MapEntry {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
ref?: Ref<JourneyMapHandle>
|
||||
checkins: any[]
|
||||
entries: MapEntry[]
|
||||
trail?: { lat: number; lng: number }[]
|
||||
@@ -84,9 +85,8 @@ function markerSvg(dayColor: string, dayLabel: number, highlighted: boolean): st
|
||||
|
||||
const EMPTY_TRAIL: { lat: number; lng: number }[] = []
|
||||
|
||||
const JourneyMap = forwardRef<JourneyMapHandle, Props>(function JourneyMap(
|
||||
{ entries, trail, height = 220, dark, activeMarkerId, onMarkerClick, fullScreen, paddingBottom },
|
||||
ref
|
||||
function JourneyMap(
|
||||
{ entries, trail, height = 220, dark, activeMarkerId, onMarkerClick, fullScreen, paddingBottom, ref }: Props,
|
||||
) {
|
||||
const stableTrail = trail || EMPTY_TRAIL
|
||||
const mapTileUrl = useSettingsStore(s => s.settings.map_tile_url)
|
||||
@@ -314,6 +314,6 @@ const JourneyMap = forwardRef<JourneyMapHandle, Props>(function JourneyMap(
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default JourneyMap
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef, Suspense, useImperativeHandle, useRef } from 'react'
|
||||
import { Suspense, useImperativeHandle, useRef, type Ref } from 'react'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import JourneyMap, { type JourneyMapHandle } from './JourneyMap'
|
||||
import ErrorBoundary from '../shared/ErrorBoundary'
|
||||
@@ -22,6 +22,7 @@ interface MapEntry {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
ref?: Ref<JourneyMapAutoHandle>
|
||||
checkins: unknown[]
|
||||
entries: MapEntry[]
|
||||
trail?: { lat: number; lng: number }[]
|
||||
@@ -33,7 +34,7 @@ interface Props {
|
||||
paddingBottom?: number
|
||||
}
|
||||
|
||||
const JourneyMapAuto = forwardRef<JourneyMapAutoHandle, Props>(function JourneyMapAuto(props, ref) {
|
||||
function JourneyMapAuto({ ref, ...props }: Props) {
|
||||
const provider = useSettingsStore(s => s.settings.map_provider)
|
||||
const token = useSettingsStore(s => s.settings.mapbox_access_token)
|
||||
const leafletRef = useRef<JourneyMapHandle>(null)
|
||||
@@ -67,6 +68,6 @@ const JourneyMapAuto = forwardRef<JourneyMapAutoHandle, Props>(function JourneyM
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return <JourneyMap ref={leafletRef} {...(props as any)} />
|
||||
})
|
||||
}
|
||||
|
||||
export default JourneyMapAuto
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useImperativeHandle, forwardRef, useCallback } from 'react'
|
||||
import { useEffect, useRef, useImperativeHandle, useCallback, type Ref } from 'react'
|
||||
import type mapboxgl from 'mapbox-gl'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { isStandardFamily, supportsCustom3d, wantsTerrain, addCustom3dBuildings, addTerrainAndSky } from '../Map/mapboxSetup'
|
||||
@@ -23,6 +23,7 @@ interface MapEntry {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
ref?: Ref<JourneyMapGLHandle>
|
||||
checkins: unknown[]
|
||||
entries: MapEntry[]
|
||||
trail?: { lat: number; lng: number }[]
|
||||
@@ -209,9 +210,8 @@ function markerHtml(dayColor: string, dayLabel: number, highlighted: boolean): H
|
||||
|
||||
const EMPTY_TRAIL: { lat: number; lng: number }[] = []
|
||||
|
||||
const JourneyMapGL = forwardRef<JourneyMapGLHandle, Props>(function JourneyMapGL(
|
||||
{ entries, trail, height = 220, dark, activeMarkerId, onMarkerClick, fullScreen, paddingBottom, glProvider = 'mapbox-gl', gl },
|
||||
ref
|
||||
function JourneyMapGL(
|
||||
{ entries, trail, height = 220, dark, activeMarkerId, onMarkerClick, fullScreen, paddingBottom, glProvider = 'mapbox-gl', gl, ref }: Props,
|
||||
) {
|
||||
const stableTrail = trail || EMPTY_TRAIL
|
||||
const rawMapboxStyle = useSettingsStore(s => s.settings.mapbox_style || MAPBOX_DEFAULT_STYLE)
|
||||
@@ -502,6 +502,6 @@ const JourneyMapGL = forwardRef<JourneyMapGLHandle, Props>(function JourneyMapGL
|
||||
<div ref={containerRef} style={{ width: '100%', height: '100%' }} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default JourneyMapGL
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { Bell, Trash2, CheckCheck } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
@@ -67,7 +67,7 @@ export default function InAppNotificationBell(): React.ReactElement {
|
||||
)}
|
||||
</button>
|
||||
|
||||
{open && ReactDOM.createPortal(
|
||||
{open && createPortal(
|
||||
<>
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 9998 }} onClick={() => setOpen(false)} />
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Link, useNavigate, useLocation } from 'react-router'
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
@@ -249,7 +249,7 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }:
|
||||
<ChevronDown className="w-4 h-4 text-content-faint" />
|
||||
</button>
|
||||
|
||||
{userMenuOpen && ReactDOM.createPortal(
|
||||
{userMenuOpen && createPortal(
|
||||
<>
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 9998 }} onClick={() => setUserMenuOpen(false)} />
|
||||
<div className="trek-menu-enter w-52 rounded-xl shadow-xl border overflow-hidden bg-surface-card border-edge" style={{ position: 'fixed', top: 'var(--nav-h)', right: 8, zIndex: 9999 }}>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { forwardRef } from 'react'
|
||||
import { lazyWithRetry } from '../../utils/lazyWithRetry'
|
||||
|
||||
/**
|
||||
@@ -39,15 +38,11 @@ export const JourneyMapGLMapbox = lazyWithRetry(async () => {
|
||||
import('./engines/mapbox'),
|
||||
])
|
||||
const JourneyMapGL = component.default
|
||||
return {
|
||||
// forwardRef because the journey map exposes an imperative handle
|
||||
// (highlightMarker / focusMarker / invalidateSize) that has to survive the wrapper.
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
default: forwardRef(function JourneyMapGLMapboxBinding(props: any, ref: any) {
|
||||
return <JourneyMapGL ref={ref} {...props} gl={engine.default} />
|
||||
}),
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
}
|
||||
// The journey map exposes an imperative handle (highlightMarker / focusMarker /
|
||||
// invalidateSize). Since React 19 its ref is an ordinary prop, so it rides
|
||||
// through the spread and the binding needs no forwardRef wrapper.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return { default: (props: any) => <JourneyMapGL {...props} gl={engine.default} /> }
|
||||
})
|
||||
|
||||
export const JourneyMapGLMaplibre = lazyWithRetry(async () => {
|
||||
@@ -56,13 +51,8 @@ export const JourneyMapGLMaplibre = lazyWithRetry(async () => {
|
||||
import('./engines/maplibre'),
|
||||
])
|
||||
const JourneyMapGL = component.default
|
||||
return {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
default: forwardRef(function JourneyMapGLMaplibreBinding(props: any, ref: any) {
|
||||
return <JourneyMapGL ref={ref} {...props} gl={engine.default} />
|
||||
}),
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return { default: (props: any) => <JourneyMapGL {...props} gl={engine.default} /> }
|
||||
})
|
||||
|
||||
export const GlMapPreviewMapbox = lazyWithRetry(async () => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Upload } from 'lucide-react'
|
||||
import type { PackingState } from './usePackingListPanel'
|
||||
|
||||
export function BulkImportModal(S: PackingState) {
|
||||
const { setShowImportModal, t, importText, setImportText, csvInputRef, handleCsvFile, handleBulkImport, parseImportLines } = S
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div style={{
|
||||
position: 'fixed', inset: 0, zIndex: 1000,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Users, UserRound, Share2, Check, Copy, HandHelping } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import type { PackingItem } from '../../types'
|
||||
@@ -78,7 +78,7 @@ export default function PackingShareControl({ item, tripMembers, currentUserId,
|
||||
onMouseLeave={e => { if (visibility === 'common') e.currentTarget.style.color = 'var(--text-faint)' }}>
|
||||
<Share2 size={14} />
|
||||
</button>
|
||||
{open && pos && ReactDOM.createPortal(
|
||||
{open && pos && createPortal(
|
||||
<>
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 1099 }} onClick={() => setOpen(false)} />
|
||||
<div style={{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useRef, useEffect, useMemo } from 'react'
|
||||
import { Plane, X, Check } from 'lucide-react'
|
||||
import type { AirtrailFlight, AirtrailImportResult } from '@trek/shared'
|
||||
@@ -307,7 +307,7 @@ export default function AirTrailImportModal({ isOpen, onClose, tripId, pushUndo
|
||||
const renderItem = (item: { chain?: AirtrailFlight[]; flight?: AirtrailFlight }) =>
|
||||
item.chain ? renderChain(item.chain) : renderFlight(item.flight!)
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div
|
||||
className="bg-[rgba(0,0,0,0.4)]"
|
||||
style={{ position: 'fixed', inset: 0, zIndex: 99999, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Upload, X } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
@@ -110,7 +110,7 @@ export default function BookingImportModal({ isOpen, onClose, tripId }: BookingI
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div
|
||||
className="bg-[rgba(0,0,0,0.4)]"
|
||||
style={{ position: 'fixed', inset: 0, zIndex: 99999, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { X, Sun, Cloud, CloudRain, CloudSnow, CloudDrizzle, CloudLightning, Wind, Droplets, Sunrise, Sunset, Hotel, Calendar, MapPin, LogIn, LogOut, Hash, Pencil, Plane, Utensils, Train, Car, Ship, Ticket, FileText, Users, ChevronsDown, ChevronsUp, TramFront, ParkingSquare } from 'lucide-react'
|
||||
|
||||
const RES_TYPE_ICONS = { flight: Plane, hotel: Hotel, restaurant: Utensils, train: Train, car: Car, cruise: Ship, transit: TramFront, event: Ticket, tour: Users, parking: ParkingSquare, other: FileText }
|
||||
@@ -495,7 +495,7 @@ function HotelPickerModal({ showHotelPicker, setShowHotelPicker, font, t, hotelD
|
||||
return (
|
||||
<>
|
||||
{/* Hotel Picker Popup — portal to body to escape transform stacking context */}
|
||||
{showHotelPicker && ReactDOM.createPortal(
|
||||
{showHotelPicker && createPortal(
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 99999, background: 'rgba(0,0,0,0.4)', backdropFilter: 'blur(4px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}
|
||||
onClick={() => setShowHotelPicker(false)}>
|
||||
<div onClick={e => e.stopPropagation()} style={{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { NOTE_ICONS } from './DayPlanSidebar.constants'
|
||||
|
||||
interface NoteModalUi {
|
||||
@@ -20,7 +20,7 @@ interface DayPlanSidebarNoteModalProps {
|
||||
export function DayPlanSidebarNoteModal({ noteUi, setNoteUi, noteInputRef, cancelNote, saveNote, t }: DayPlanSidebarNoteModalProps) {
|
||||
return (
|
||||
<>
|
||||
{Object.entries(noteUi).map(([dayId, ui]) => ui && ReactDOM.createPortal(
|
||||
{Object.entries(noteUi).map(([dayId, ui]) => ui && createPortal(
|
||||
<div key={dayId} className="bg-[rgba(0,0,0,0.3)]" style={{
|
||||
position: 'fixed', inset: 0, zIndex: 10000,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Clock } from 'lucide-react'
|
||||
|
||||
interface TimeConfirmState {
|
||||
@@ -21,7 +21,7 @@ interface DayPlanSidebarTimeConfirmModalProps {
|
||||
|
||||
export function DayPlanSidebarTimeConfirmModal({ timeConfirm, setTimeConfirm, confirmTimeRemoval, t }: DayPlanSidebarTimeConfirmModalProps) {
|
||||
if (!timeConfirm) return null
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div className="bg-[rgba(0,0,0,0.3)]" style={{
|
||||
position: 'fixed', inset: 0, zIndex: 1000,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Ticket, FileText, ExternalLink, Footprints, ArrowRight, Pencil } from 'lucide-react'
|
||||
import Markdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
@@ -24,7 +24,7 @@ export function DayPlanSidebarTransportDetailModal({
|
||||
transportDetail, setTransportDetail, onNavigateToFiles, onEdit, t, locale, timeFormat,
|
||||
}: DayPlanSidebarTransportDetailModalProps) {
|
||||
if (!transportDetail) return null
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div className="bg-[rgba(0,0,0,0.3)]" style={{
|
||||
position: 'fixed', inset: 0, zIndex: 1000,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Upload } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
@@ -215,7 +215,7 @@ export default function FileImportModal({ isOpen, onClose, tripId, pushUndo, ini
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div
|
||||
onClick={handleClose}
|
||||
className="bg-[rgba(0,0,0,0.4)]"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import ToggleSwitch from '../Settings/ToggleSwitch'
|
||||
import type { SidebarState } from './usePlacesSidebar'
|
||||
|
||||
@@ -8,7 +8,7 @@ export function ListImportModal(S: SidebarState) {
|
||||
listImportProvider, setListImportProvider, listImportUrl, listImportLoading, handleListImport,
|
||||
listImportEnrich, setListImportEnrich, canEnrichImport,
|
||||
} = S
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div
|
||||
onClick={() => { setListImportOpen(false); setListImportUrl('') }}
|
||||
className="bg-[rgba(0,0,0,0.4)]"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { CalendarDays, Pencil, Trash2, ChevronDown, Check, Eye } from 'lucide-react'
|
||||
import type { SidebarState } from './usePlacesSidebar'
|
||||
|
||||
@@ -7,7 +7,7 @@ export function MobileDayPickerSheet(S: SidebarState) {
|
||||
dayPickerPlace, setDayPickerPlace, setMobileShowDays, onPlaceClick, canEditPlaces, onEditPlace,
|
||||
t, days, mobileShowDays, onAssignToDay, assignments, onDeletePlace,
|
||||
} = S
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div
|
||||
onClick={() => { setDayPickerPlace(null); setMobileShowDays(false) }}
|
||||
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 99999, display: 'flex', alignItems: 'flex-end', justifyContent: 'center' }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTripStore } from '../../store/tripStore'
|
||||
import { useCanDo } from '../../store/permissionsStore'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
@@ -450,7 +450,7 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
|
||||
)}
|
||||
|
||||
{/* Delete confirmation */}
|
||||
{showDeleteConfirm && ReactDOM.createPortal(
|
||||
{showDeleteConfirm && createPortal(
|
||||
<div className="bg-[rgba(0,0,0,0.3)]" style={{
|
||||
position: 'fixed', inset: 0, zIndex: 1000,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
@@ -628,7 +628,7 @@ function TransitJourneyCard({ r, days, onOpen, onDelete, canEdit, tripId, contri
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{confirmOpen && ReactDOM.createPortal(
|
||||
{confirmOpen && createPortal(
|
||||
<div className="bg-[rgba(0,0,0,0.35)]" style={{ position: 'fixed', inset: 0, zIndex: 3000, display: 'flex', alignItems: 'center', justifyContent: 'center' }} onClick={e => { e.stopPropagation(); setConfirmOpen(false) }}>
|
||||
<div className="bg-surface-card" style={{ borderRadius: 14, padding: 20, width: 340, boxShadow: '0 16px 48px rgba(0,0,0,0.22)' }} onClick={e => e.stopPropagation()}>
|
||||
<div className="text-content" style={{ fontWeight: 600, fontSize: 'calc(14px * var(--fs-scale-body, 1))', marginBottom: 6 }}>{t('reservations.confirm.deleteTitle')}</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Fragment, useState, useMemo, useEffect, useRef } from 'react'
|
||||
import { avatarSrc } from '../../utils/avatarSrc'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTripStore } from '../../store/tripStore'
|
||||
import { useCanDo } from '../../store/permissionsStore'
|
||||
import { useToast } from '../shared/Toast'
|
||||
@@ -281,7 +281,7 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isAddingNew && !selectedItem && !isMobile && ReactDOM.createPortal(
|
||||
{isAddingNew && !selectedItem && !isMobile && createPortal(
|
||||
<div onClick={e => { if (e.target === e.currentTarget) setIsAddingNew(false) }}
|
||||
className="trek-modal-backdrop"
|
||||
style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(15,23,42,0.5)', display: 'flex', justifyContent: 'center', alignItems: 'flex-start', paddingTop: 'calc(var(--nav-h) + 60px)', paddingBottom: 40 }}>
|
||||
@@ -299,7 +299,7 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
{isAddingNew && !selectedItem && isMobile && ReactDOM.createPortal(
|
||||
{isAddingNew && !selectedItem && isMobile && createPortal(
|
||||
<div onClick={e => { if (e.target === e.currentTarget) setIsAddingNew(false) }}
|
||||
className="trek-modal-backdrop"
|
||||
style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(0,0,0,0.4)', display: 'flex', justifyContent: 'center', alignItems: 'flex-end', paddingBottom: 'var(--bottom-nav-h)' }}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { UserPlus, Check, Loader2, Clock, X } from 'lucide-react'
|
||||
import { useVacayStore } from '../../store/vacayStore'
|
||||
@@ -124,7 +124,7 @@ export default function VacayPersons() {
|
||||
</div>
|
||||
|
||||
{/* Invite Modal — Portal to body to avoid z-index issues */}
|
||||
{showInvite && ReactDOM.createPortal(
|
||||
{showInvite && createPortal(
|
||||
<div className="fixed inset-0 flex items-center justify-center px-4 trek-backdrop-enter bg-[rgba(15,23,42,0.5)]" style={{ zIndex: 99990, paddingTop: 70 }}
|
||||
onClick={() => setShowInvite(false)}>
|
||||
<div className="trek-modal-enter rounded-2xl shadow-2xl w-full max-w-sm bg-surface-card"
|
||||
@@ -165,7 +165,7 @@ export default function VacayPersons() {
|
||||
)}
|
||||
|
||||
{/* Color Picker Modal — Portal to body */}
|
||||
{showColorPicker && ReactDOM.createPortal(
|
||||
{showColorPicker && createPortal(
|
||||
<div className="fixed inset-0 flex items-center justify-center px-4 trek-backdrop-enter bg-[rgba(15,23,42,0.5)]" style={{ zIndex: 99990, paddingTop: 70 }}
|
||||
onClick={() => { setShowColorPicker(false); setColorEditUserId(null) }}>
|
||||
<div className="trek-modal-enter rounded-2xl shadow-2xl w-full max-w-xs bg-surface-card"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useState } from 'react'
|
||||
import { Share2, Eye, EyeOff, Loader2, X } from 'lucide-react'
|
||||
import { useVacayStore } from '../../store/vacayStore'
|
||||
@@ -126,7 +126,7 @@ export default function VacaySharedCalendars() {
|
||||
)}
|
||||
|
||||
{/* Share Modal — Portal to body to avoid z-index issues */}
|
||||
{showShare && ReactDOM.createPortal(
|
||||
{showShare && createPortal(
|
||||
<div className="fixed inset-0 flex items-center justify-center px-4 trek-backdrop-enter bg-[rgba(15,23,42,0.5)]" style={{ zIndex: 99990, paddingTop: 70 }}
|
||||
onClick={() => setShowShare(false)}>
|
||||
<div className="trek-modal-enter rounded-2xl shadow-2xl w-full max-w-sm bg-surface-card"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useCallback } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function ConfirmDialog({
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div
|
||||
className="fixed inset-0 z-[10000] flex items-center justify-center px-4 trek-backdrop-enter bg-[rgba(15,23,42,0.5)]"
|
||||
style={{ paddingBottom: 'var(--bottom-nav-h)' }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { LucideIcon } from 'lucide-react'
|
||||
|
||||
interface MenuItem {
|
||||
@@ -64,7 +64,7 @@ export function ContextMenu({ menu, onClose }: ContextMenuProps) {
|
||||
|
||||
if (!menu) return null
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div ref={ref} className="trek-popover-enter" style={{
|
||||
position: 'fixed', left: menu.x, top: menu.y, zIndex: 999999,
|
||||
background: 'var(--bg-card)', borderRadius: 10, padding: '4px',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useCallback } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Check, X } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function CopyTripDialog({ isOpen, tripTitle, onClose, onConfirm }
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div
|
||||
className="fixed inset-0 z-[10000] flex items-center justify-center px-4 trek-backdrop-enter bg-[rgba(15,23,42,0.5)]"
|
||||
style={{ paddingBottom: 'var(--bottom-nav-h)' }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Calendar, ChevronLeft, ChevronRight, Keyboard } from 'lucide-react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from '../../i18n';
|
||||
|
||||
function daysInMonth(year: number, month: number): number {
|
||||
@@ -356,7 +356,7 @@ export function CustomDatePicker({
|
||||
)}
|
||||
|
||||
{open &&
|
||||
ReactDOM.createPortal(
|
||||
createPortal(
|
||||
<div
|
||||
ref={dropRef}
|
||||
role="dialog"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { ChevronDown, Check } from 'lucide-react'
|
||||
|
||||
interface SelectOption {
|
||||
@@ -118,7 +118,7 @@ export default function CustomSelect({
|
||||
</button>
|
||||
|
||||
{/* Dropdown */}
|
||||
{open && ReactDOM.createPortal(
|
||||
{open && createPortal(
|
||||
<div ref={dropRef} style={{
|
||||
position: 'fixed',
|
||||
...(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Clock, ChevronUp, ChevronDown } from 'lucide-react'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { formatClockTime, parseMeridiemTime } from '../../utils/formatters'
|
||||
@@ -148,7 +148,7 @@ export default function CustomTimePicker({ value, onChange, placeholder = '00:00
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{open && ReactDOM.createPortal(
|
||||
{open && createPortal(
|
||||
<div ref={dropRef} style={{
|
||||
position: 'fixed',
|
||||
top: (() => { const r = ref.current?.getBoundingClientRect(); return r ? r.bottom + 4 : 0 })(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useCallback, useRef } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
const sizeClasses: Record<string, string> = {
|
||||
@@ -49,7 +49,7 @@ export default function Modal({
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div
|
||||
className="fixed inset-0 z-[10000] flex items-start sm:items-center justify-center px-4 trek-modal-backdrop trek-backdrop-enter bg-[rgba(15,23,42,0.5)]"
|
||||
style={{ paddingTop: 70, paddingBottom: 'calc(20px + var(--bottom-nav-h))', overflow: 'hidden' }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef, useRef, type InputHTMLAttributes } from 'react'
|
||||
import { useRef, type InputHTMLAttributes, type Ref } from 'react'
|
||||
|
||||
export type NumericMode = 'integer' | 'decimal' | 'signed'
|
||||
|
||||
@@ -23,6 +23,7 @@ type Props = Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'onChange' | '
|
||||
mode?: NumericMode
|
||||
/** Escape hatch for a field that must not steal the caret (none today). */
|
||||
selectOnFocus?: boolean
|
||||
ref?: Ref<HTMLInputElement>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,10 +53,9 @@ type Props = Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'onChange' | '
|
||||
* Styling and commit semantics stay with the caller: some fields save on every keystroke,
|
||||
* others on blur. This owns only the part that was uniformly broken.
|
||||
*/
|
||||
export const NumericInput = forwardRef<HTMLInputElement, Props>(function NumericInput(
|
||||
{ value, onValueChange, mode = 'integer', selectOnFocus = true, onFocus, inputMode, ...rest },
|
||||
ref,
|
||||
) {
|
||||
export function NumericInput({
|
||||
value, onValueChange, mode = 'integer', selectOnFocus = true, onFocus, inputMode, ref, ...rest
|
||||
}: Props) {
|
||||
// Set while a deferred select() is queued; any input in that window cancels it.
|
||||
const selectPending = useRef(false)
|
||||
|
||||
@@ -84,4 +84,4 @@ export const NumericInput = forwardRef<HTMLInputElement, Props>(function Numeric
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Star } from 'lucide-react'
|
||||
import { avatarSrc } from '../../utils/avatarSrc'
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
@@ -126,7 +126,7 @@ export default function PlaceRating({ ratings, ratingAvg, onRate, size = 16, com
|
||||
)}
|
||||
|
||||
{/* Custom voter tooltip — fixed at the root so nothing clips it. */}
|
||||
{tip && ratings.length > 0 && ReactDOM.createPortal(
|
||||
{tip && ratings.length > 0 && createPortal(
|
||||
<div
|
||||
role="tooltip"
|
||||
className="bg-surface-card text-content border border-edge-faint"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
type Placement = 'top' | 'bottom' | 'left' | 'right'
|
||||
|
||||
@@ -74,7 +74,7 @@ export function Tooltip({ label, placement = 'bottom', delay = 250, disabled, ch
|
||||
return (
|
||||
<>
|
||||
{trigger}
|
||||
{open && ReactDOM.createPortal(
|
||||
{open && createPortal(
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
role="tooltip"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState, ReactNode } from 'react'
|
||||
import { createContext, useContext, useEffect, useMemo, useState, ReactNode } from 'react'
|
||||
import { useSettingsStore } from '../store/settingsStore'
|
||||
import en from '@trek/shared/i18n/en'
|
||||
import type { SupportedLanguageCode } from '@trek/shared'
|
||||
@@ -157,7 +157,7 @@ export function TranslationProvider({ children }: TranslationProviderProps) {
|
||||
return { t, tHtml, language, locale: getLocaleForLanguage(language) }
|
||||
}, [strings, language])
|
||||
|
||||
return <TranslationContext.Provider value={value}>{children}</TranslationContext.Provider>
|
||||
return <TranslationContext value={value}>{children}</TranslationContext>
|
||||
}
|
||||
|
||||
export function useTranslation(): TranslationContextValue {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
export type MSheetVariant = 'card' | 'bottom' | 'drawer'
|
||||
export type MSheetMaterial = 'glass' | 'bar-glass' | 'opaque'
|
||||
@@ -190,7 +190,7 @@ export default function MSheet({
|
||||
? { transform: `translateY(${dragY}px)`, transition: 'none' }
|
||||
: { transition: 'transform 280ms var(--ease-drawer)' }
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
// m-root on the overlay so the --m-* tokens resolve even when the portal
|
||||
// has to fall back to document.body (sheet mounted in the same commit as
|
||||
// the shell).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useState, type ComponentType } from 'react'
|
||||
import { adminApi } from '../../../api/client'
|
||||
import { useTranslation } from '../../../i18n'
|
||||
import { useSettingsStore } from '../../../store/settingsStore'
|
||||
@@ -31,7 +31,7 @@ function SynologyIcon({ size = 14 }: { size?: number }) {
|
||||
)
|
||||
}
|
||||
|
||||
const PROVIDER_ICONS: Record<string, React.FC<{ size?: number }>> = {
|
||||
const PROVIDER_ICONS: Record<string, ComponentType<{ size?: number }>> = {
|
||||
immich: ImmichIcon,
|
||||
synologyphotos: SynologyIcon,
|
||||
}
|
||||
@@ -320,7 +320,7 @@ function MAddonRow({ addon, onToggle, t, first }: MAddonRowProps) {
|
||||
|
||||
interface MSubRowProps {
|
||||
icon?: typeof Luggage
|
||||
providerIcon?: React.FC<{ size?: number }>
|
||||
providerIcon?: ComponentType<{ size?: number }>
|
||||
title: string
|
||||
subtitle: string
|
||||
enabled: boolean
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { ArrowUp, ChevronUp, Loader2, Reply, Trash2 } from 'lucide-react'
|
||||
import MDancingTrek from '../../../components/MDancingTrek'
|
||||
import { collabApi } from '../../../../api/client'
|
||||
@@ -498,7 +498,7 @@ function MessageActionsPopover({ x, y, canDeleteOwn, t, onReact, onReply, onDele
|
||||
const height = POPOVER_HEIGHT + (canDeleteOwn ? POPOVER_DELETE_ROW : 0)
|
||||
const top = Math.max(64, Math.min(y - 96, window.innerHeight - height - POPOVER_MARGIN))
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div className="m-root fixed inset-0 z-[55]">
|
||||
<div
|
||||
ref={ref}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { ChevronLeft, ChevronRight, Download, ExternalLink, X } from 'lucide-react'
|
||||
import { getAuthUrl } from '../../../../api/authUrl'
|
||||
import { downloadFile, openFile } from '../../../../utils/fileDownload'
|
||||
@@ -64,7 +64,7 @@ export default function MFileLightbox({ files, index, onIndexChange, onClose, t
|
||||
|
||||
if (!file) return null
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
return createPortal(
|
||||
<div
|
||||
className="m-root fixed inset-0 z-[65] flex flex-col bg-black/[.92]"
|
||||
onClick={onClose}
|
||||
|
||||
@@ -10,7 +10,7 @@ const inputBase: React.CSSProperties = {
|
||||
background: 'white', color: '#111827',
|
||||
}
|
||||
|
||||
const ForgotPasswordPage: React.FC = () => {
|
||||
function ForgotPasswordPage() {
|
||||
const { t } = useTranslation()
|
||||
// Page = wiring container: form state, the SMTP probe and submit live in the hook.
|
||||
const { navigate, email, setEmail, submitted, isLoading, smtpConfigured, handleSubmit } = useForgotPassword()
|
||||
|
||||
@@ -34,10 +34,6 @@ vi.mock('leaflet', () => {
|
||||
return { default: L, ...L };
|
||||
});
|
||||
|
||||
vi.mock('react-dom/server', () => ({
|
||||
renderToStaticMarkup: vi.fn(() => '<svg></svg>'),
|
||||
}));
|
||||
|
||||
// Mock JourneyMap since it uses vanilla Leaflet (L.map) which requires a real DOM
|
||||
vi.mock('../components/Journey/JourneyMap', () => ({
|
||||
default: ({ entries }: any) => <div data-testid="journey-map">Map with {entries?.length || 0} entries</div>,
|
||||
|
||||
@@ -10,7 +10,7 @@ const inputBase: React.CSSProperties = {
|
||||
background: 'white', color: '#111827',
|
||||
}
|
||||
|
||||
const ResetPasswordPage: React.FC = () => {
|
||||
function ResetPasswordPage() {
|
||||
const { t } = useTranslation()
|
||||
// Page = wiring container: token, form state, validation + submit live in the hook.
|
||||
const {
|
||||
|
||||
@@ -34,11 +34,6 @@ vi.mock('leaflet', () => {
|
||||
return { default: L, ...L };
|
||||
});
|
||||
|
||||
// Mock react-dom/server (used in createMarkerIcon)
|
||||
vi.mock('react-dom/server', () => ({
|
||||
renderToStaticMarkup: vi.fn(() => '<svg></svg>'),
|
||||
}));
|
||||
|
||||
// Helper: render SharedTripPage under the correct route so useParams works
|
||||
function renderSharedTrip(token: string) {
|
||||
return render(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, Suspense } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useParams, useNavigate, useSearchParams } from 'react-router'
|
||||
import { useTripStore } from '../store/tripStore'
|
||||
import { useCanDo } from '../store/permissionsStore'
|
||||
@@ -396,7 +396,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
|
||||
|
||||
{/* Mobile POI search controls live in a portal like the Plan/Places
|
||||
buttons so map touch handlers cannot swallow the tap targets. */}
|
||||
{poiPillEnabled && !mobileSidebarOpen && !showPlaceForm && !showMembersModal && !showReservationModal && ReactDOM.createPortal(
|
||||
{poiPillEnabled && !mobileSidebarOpen && !showPlaceForm && !showMembersModal && !showReservationModal && createPortal(
|
||||
<div data-testid="mobile-poi-category-pill" className="flex md:hidden" style={{ position: 'fixed', left: 12, right: 12, bottom: 'calc(var(--bottom-nav-h, 0px) + 12px)', justifyContent: 'center', zIndex: 100, pointerEvents: 'none' }}>
|
||||
<PoiCategoryPill active={poi.active} onToggle={poi.toggle} loadingKeys={poi.loadingKeys} errorKeys={poi.errorKeys} moved={poi.moved} onSearchArea={poi.searchArea} />
|
||||
</div>,
|
||||
@@ -548,7 +548,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
|
||||
</div>
|
||||
|
||||
{/* Mobile sidebar buttons — portal to body to escape Leaflet touch handling */}
|
||||
{activeTab === 'plan' && !mobileSidebarOpen && !showPlaceForm && !showMembersModal && !showReservationModal && ReactDOM.createPortal(
|
||||
{activeTab === 'plan' && !mobileSidebarOpen && !showPlaceForm && !showMembersModal && !showReservationModal && createPortal(
|
||||
<div className="flex md:hidden" style={{ position: 'fixed', top: 'calc(var(--nav-h) + 44px + 12px)', left: 12, right: 12, justifyContent: 'space-between', zIndex: 100, pointerEvents: 'none' }}>
|
||||
<button onClick={() => setMobileSidebarOpen('left')}
|
||||
className="bg-surface-card text-content border border-edge"
|
||||
@@ -629,7 +629,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedPlace && isMobile && ReactDOM.createPortal(
|
||||
{selectedPlace && isMobile && createPortal(
|
||||
<div className="bg-[rgba(0,0,0,0.3)]" style={{ position: 'fixed', inset: 0, zIndex: 9999, display: 'flex', alignItems: 'flex-end', justifyContent: 'center', paddingBottom: 'var(--bottom-nav-h)' }} onClick={() => setSelectedPlaceId(null)}>
|
||||
<div style={{ width: '100%', maxHeight: '85vh' }} onClick={e => e.stopPropagation()}>
|
||||
<PlaceInspector
|
||||
@@ -672,7 +672,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
|
||||
document.body
|
||||
)}
|
||||
|
||||
{mobileSidebarOpen && ReactDOM.createPortal(
|
||||
{mobileSidebarOpen && createPortal(
|
||||
<div className="bg-[rgba(0,0,0,0.3)]" style={{ position: 'fixed', inset: 0, zIndex: 9999 }} onClick={() => setMobileSidebarOpen(null)}>
|
||||
<div className="bg-surface-card" style={{ position: 'absolute', top: 'var(--nav-h)', left: 0, right: 0, bottom: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }} onClick={e => e.stopPropagation()}>
|
||||
<div className="border-b border-edge-secondary" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 16px' }}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTranslation } from '../i18n'
|
||||
import PageShell from '../components/Layout/PageShell'
|
||||
import VacayCalendar from '../components/Vacay/VacayCalendar'
|
||||
@@ -166,7 +166,7 @@ function VacayPageDesktop(): React.ReactElement {
|
||||
</div>
|
||||
|
||||
{/* Mobile Sidebar Drawer */}
|
||||
{showMobileSidebar && ReactDOM.createPortal(
|
||||
{showMobileSidebar && createPortal(
|
||||
<div className="fixed inset-0 lg:hidden" style={{ zIndex: 99980 }}>
|
||||
<div className="absolute inset-0 bg-[rgba(0,0,0,0.4)]" onClick={() => setShowMobileSidebar(false)} />
|
||||
<div className="absolute left-0 top-0 bottom-0 w-[280px] overflow-y-auto p-3 flex flex-col gap-3 bg-surface"
|
||||
@@ -208,7 +208,7 @@ function VacayPageDesktop(): React.ReactElement {
|
||||
</Modal>
|
||||
|
||||
{/* Incoming invite — forced fullscreen modal */}
|
||||
{incomingInvites.length > 0 && ReactDOM.createPortal(
|
||||
{incomingInvites.length > 0 && createPortal(
|
||||
<div className="fixed inset-0 flex items-center justify-center px-4 bg-[rgba(0,0,0,0.7)]"
|
||||
style={{ zIndex: 99995, backdropFilter: 'blur(8px)' }}>
|
||||
{incomingInvites.map(inv => (
|
||||
|
||||
Reference in New Issue
Block a user