mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
Merge branch 'dev' into dev
This commit is contained in:
@@ -107,6 +107,8 @@ export const placesApi = {
|
|||||||
const fd = new FormData(); fd.append('file', file)
|
const fd = new FormData(); fd.append('file', file)
|
||||||
return apiClient.post(`/trips/${tripId}/places/import/gpx`, fd, { headers: { 'Content-Type': 'multipart/form-data' } }).then(r => r.data)
|
return apiClient.post(`/trips/${tripId}/places/import/gpx`, fd, { headers: { 'Content-Type': 'multipart/form-data' } }).then(r => r.data)
|
||||||
},
|
},
|
||||||
|
importGoogleList: (tripId: number | string, url: string) =>
|
||||||
|
apiClient.post(`/trips/${tripId}/places/import/google-list`, { url }).then(r => r.data),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const assignmentsApi = {
|
export const assignmentsApi = {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { downloadTripPDF } from '../PDF/TripPDF'
|
|||||||
import { calculateRoute, generateGoogleMapsUrl, optimizeRoute } from '../Map/RouteCalculator'
|
import { calculateRoute, generateGoogleMapsUrl, optimizeRoute } from '../Map/RouteCalculator'
|
||||||
import PlaceAvatar from '../shared/PlaceAvatar'
|
import PlaceAvatar from '../shared/PlaceAvatar'
|
||||||
import { useContextMenu, ContextMenu } from '../shared/ContextMenu'
|
import { useContextMenu, ContextMenu } from '../shared/ContextMenu'
|
||||||
|
import Markdown from 'react-markdown'
|
||||||
|
import remarkGfm from 'remark-gfm'
|
||||||
import WeatherWidget from '../Weather/WeatherWidget'
|
import WeatherWidget from '../Weather/WeatherWidget'
|
||||||
import { useToast } from '../shared/Toast'
|
import { useToast } from '../shared/Toast'
|
||||||
import { getCategoryIcon } from '../shared/categoryIcons'
|
import { getCategoryIcon } from '../shared/categoryIcons'
|
||||||
@@ -1331,7 +1333,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
{note.text}
|
{note.text}
|
||||||
</span>
|
</span>
|
||||||
{note.time && (
|
{note.time && (
|
||||||
<div style={{ fontSize: 10.5, fontWeight: 400, color: 'var(--text-faint)', lineHeight: '1.3', marginTop: 2, wordBreak: 'break-word' }}>{note.time}</div>
|
<div className="collab-note-md" style={{ fontSize: 10.5, fontWeight: 400, color: 'var(--text-faint)', lineHeight: '1.3', marginTop: 2, wordBreak: 'break-word' }}><Markdown remarkPlugins={[remarkGfm]}>{note.time}</Markdown></div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{canEditDays && <div className="note-edit-buttons" style={{ display: 'flex', gap: 1, flexShrink: 0, opacity: isNoteHovered ? 1 : 0, transition: 'opacity 0.15s' }}>
|
{canEditDays && <div className="note-edit-buttons" style={{ display: 'flex', gap: 1, flexShrink: 0, opacity: isNoteHovered ? 1 : 0, transition: 'opacity 0.15s' }}>
|
||||||
@@ -1610,7 +1612,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
|
|||||||
{res.notes && (
|
{res.notes && (
|
||||||
<div style={{ padding: '8px 10px', background: 'var(--bg-tertiary)', borderRadius: 8 }}>
|
<div style={{ padding: '8px 10px', background: 'var(--bg-tertiary)', borderRadius: 8 }}>
|
||||||
<div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase', letterSpacing: '0.03em', marginBottom: 3 }}>{t('reservations.notes')}</div>
|
<div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase', letterSpacing: '0.03em', marginBottom: 3 }}>{t('reservations.notes')}</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-primary)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{res.notes}</div>
|
<div className="collab-note-md" style={{ fontSize: 12, color: 'var(--text-primary)', wordBreak: 'break-word' }}><Markdown remarkPlugins={[remarkGfm]}>{res.notes}</Markdown></div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
||||||
import { getAuthUrl } from '../../api/authUrl'
|
import { getAuthUrl } from '../../api/authUrl'
|
||||||
|
import Markdown from 'react-markdown'
|
||||||
|
import remarkGfm from 'remark-gfm'
|
||||||
import { X, Clock, MapPin, ExternalLink, Phone, Euro, Edit2, Trash2, Plus, Minus, ChevronDown, ChevronUp, FileText, Upload, File, FileImage, Star, Navigation, Users, Mountain, TrendingUp } from 'lucide-react'
|
import { X, Clock, MapPin, ExternalLink, Phone, Euro, Edit2, Trash2, Plus, Minus, ChevronDown, ChevronUp, FileText, Upload, File, FileImage, Star, Navigation, Users, Mountain, TrendingUp } from 'lucide-react'
|
||||||
import PlaceAvatar from '../shared/PlaceAvatar'
|
import PlaceAvatar from '../shared/PlaceAvatar'
|
||||||
import { mapsApi } from '../../api/client'
|
import { mapsApi } from '../../api/client'
|
||||||
@@ -340,10 +342,8 @@ export default function PlaceInspector({
|
|||||||
|
|
||||||
{/* Description / Summary */}
|
{/* Description / Summary */}
|
||||||
{(place.description || place.notes || googleDetails?.summary) && (
|
{(place.description || place.notes || googleDetails?.summary) && (
|
||||||
<div style={{ background: 'var(--bg-hover)', borderRadius: 10, overflow: 'hidden' }}>
|
<div className="collab-note-md" style={{ background: 'var(--bg-hover)', borderRadius: 10, overflow: 'hidden', fontSize: 12, color: 'var(--text-muted)', lineHeight: '1.5', padding: '8px 12px' }}>
|
||||||
<p style={{ fontSize: 12, color: 'var(--text-muted)', margin: 0, lineHeight: '1.5', padding: '8px 12px' }}>
|
<Markdown remarkPlugins={[remarkGfm]}>{place.description || place.notes || googleDetails?.summary || ''}</Markdown>
|
||||||
{place.description || place.notes || googleDetails?.summary}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -392,7 +392,7 @@ export default function PlaceInspector({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{res.notes && <div style={{ padding: '0 10px 6px', fontSize: 10, color: 'var(--text-faint)', lineHeight: 1.4 }}>{res.notes}</div>}
|
{res.notes && <div className="collab-note-md" style={{ padding: '0 10px 6px', fontSize: 10, color: 'var(--text-faint)', lineHeight: 1.4 }}><Markdown remarkPlugins={[remarkGfm]}>{res.notes}</Markdown></div>}
|
||||||
{(() => {
|
{(() => {
|
||||||
const meta = typeof res.metadata === 'string' ? JSON.parse(res.metadata || '{}') : (res.metadata || {})
|
const meta = typeof res.metadata === 'string' ? JSON.parse(res.metadata || '{}') : (res.metadata || {})
|
||||||
if (!meta || Object.keys(meta).length === 0) return null
|
if (!meta || Object.keys(meta).length === 0) return null
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import { useState, useRef, useMemo, useCallback } from 'react'
|
import { useState, useRef, useMemo, useCallback } from 'react'
|
||||||
import DOM from 'react-dom'
|
import DOM from 'react-dom'
|
||||||
import { Search, Plus, X, CalendarDays, Pencil, Trash2, ExternalLink, Navigation, Upload, ChevronDown, Check } from 'lucide-react'
|
import { Search, Plus, X, CalendarDays, Pencil, Trash2, ExternalLink, Navigation, Upload, ChevronDown, Check, MapPin } from 'lucide-react'
|
||||||
import PlaceAvatar from '../shared/PlaceAvatar'
|
import PlaceAvatar from '../shared/PlaceAvatar'
|
||||||
import { getCategoryIcon } from '../shared/categoryIcons'
|
import { getCategoryIcon } from '../shared/categoryIcons'
|
||||||
import { useTranslation } from '../../i18n'
|
import { useTranslation } from '../../i18n'
|
||||||
@@ -56,6 +56,27 @@ const PlacesSidebar = React.memo(function PlacesSidebar({
|
|||||||
toast.error(err?.response?.data?.error || t('places.gpxError'))
|
toast.error(err?.response?.data?.error || t('places.gpxError'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [googleListOpen, setGoogleListOpen] = useState(false)
|
||||||
|
const [googleListUrl, setGoogleListUrl] = useState('')
|
||||||
|
const [googleListLoading, setGoogleListLoading] = useState(false)
|
||||||
|
|
||||||
|
const handleGoogleListImport = async () => {
|
||||||
|
if (!googleListUrl.trim()) return
|
||||||
|
setGoogleListLoading(true)
|
||||||
|
try {
|
||||||
|
const result = await placesApi.importGoogleList(tripId, googleListUrl.trim())
|
||||||
|
await loadTrip(tripId)
|
||||||
|
toast.success(t('places.googleListImported', { count: result.count, list: result.listName }))
|
||||||
|
setGoogleListOpen(false)
|
||||||
|
setGoogleListUrl('')
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error(err?.response?.data?.error || t('places.googleListError'))
|
||||||
|
} finally {
|
||||||
|
setGoogleListLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [filter, setFilter] = useState('all')
|
const [filter, setFilter] = useState('all')
|
||||||
const [categoryFilters, setCategoryFiltersLocal] = useState<Set<string>>(new Set())
|
const [categoryFilters, setCategoryFiltersLocal] = useState<Set<string>>(new Set())
|
||||||
@@ -105,18 +126,32 @@ const PlacesSidebar = React.memo(function PlacesSidebar({
|
|||||||
</button>}
|
</button>}
|
||||||
{canEditPlaces && <>
|
{canEditPlaces && <>
|
||||||
<input ref={gpxInputRef} type="file" accept=".gpx" style={{ display: 'none' }} onChange={handleGpxImport} />
|
<input ref={gpxInputRef} type="file" accept=".gpx" style={{ display: 'none' }} onChange={handleGpxImport} />
|
||||||
<button
|
<div style={{ display: 'flex', gap: 6, marginBottom: 10 }}>
|
||||||
onClick={() => gpxInputRef.current?.click()}
|
<button
|
||||||
style={{
|
onClick={() => gpxInputRef.current?.click()}
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5,
|
style={{
|
||||||
width: '100%', padding: '5px 12px', borderRadius: 8, marginBottom: 10,
|
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5,
|
||||||
border: '1px dashed var(--border-primary)', background: 'none',
|
flex: 1, padding: '5px 12px', borderRadius: 8,
|
||||||
color: 'var(--text-faint)', fontSize: 11, fontWeight: 500,
|
border: '1px dashed var(--border-primary)', background: 'none',
|
||||||
cursor: 'pointer', fontFamily: 'inherit',
|
color: 'var(--text-faint)', fontSize: 11, fontWeight: 500,
|
||||||
}}
|
cursor: 'pointer', fontFamily: 'inherit',
|
||||||
>
|
}}
|
||||||
<Upload size={11} strokeWidth={2} /> {t('places.importGpx')}
|
>
|
||||||
</button>
|
<Upload size={11} strokeWidth={2} /> {t('places.importGpx')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setGoogleListOpen(true)}
|
||||||
|
style={{
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5,
|
||||||
|
flex: 1, padding: '5px 12px', borderRadius: 8,
|
||||||
|
border: '1px dashed var(--border-primary)', background: 'none',
|
||||||
|
color: 'var(--text-faint)', fontSize: 11, fontWeight: 500,
|
||||||
|
cursor: 'pointer', fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MapPin size={11} strokeWidth={2} /> {t('places.importGoogleList')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</>}
|
</>}
|
||||||
|
|
||||||
{/* Filter-Tabs */}
|
{/* Filter-Tabs */}
|
||||||
@@ -366,6 +401,64 @@ const PlacesSidebar = React.memo(function PlacesSidebar({
|
|||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
|
{googleListOpen && ReactDOM.createPortal(
|
||||||
|
<div
|
||||||
|
onClick={() => { setGoogleListOpen(false); setGoogleListUrl('') }}
|
||||||
|
style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 99999, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
style={{ background: 'var(--bg-card)', borderRadius: 16, width: '100%', maxWidth: 440, padding: 24, boxShadow: '0 8px 32px rgba(0,0,0,0.2)' }}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--text-primary)', marginBottom: 4 }}>
|
||||||
|
{t('places.importGoogleList')}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-faint)', marginBottom: 16 }}>
|
||||||
|
{t('places.googleListHint')}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={googleListUrl}
|
||||||
|
onChange={e => setGoogleListUrl(e.target.value)}
|
||||||
|
onKeyDown={e => { if (e.key === 'Enter' && !googleListLoading) handleGoogleListImport() }}
|
||||||
|
placeholder="https://maps.app.goo.gl/..."
|
||||||
|
autoFocus
|
||||||
|
style={{
|
||||||
|
width: '100%', padding: '10px 14px', borderRadius: 10,
|
||||||
|
border: '1px solid var(--border-primary)', background: 'var(--bg-tertiary)',
|
||||||
|
fontSize: 13, color: 'var(--text-primary)', outline: 'none',
|
||||||
|
fontFamily: 'inherit', boxSizing: 'border-box',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginTop: 16, justifyContent: 'flex-end' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => { setGoogleListOpen(false); setGoogleListUrl('') }}
|
||||||
|
style={{
|
||||||
|
padding: '8px 16px', borderRadius: 10, border: '1px solid var(--border-primary)',
|
||||||
|
background: 'none', color: 'var(--text-primary)', fontSize: 13, fontWeight: 500,
|
||||||
|
cursor: 'pointer', fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleGoogleListImport}
|
||||||
|
disabled={!googleListUrl.trim() || googleListLoading}
|
||||||
|
style={{
|
||||||
|
padding: '8px 16px', borderRadius: 10, border: 'none',
|
||||||
|
background: !googleListUrl.trim() || googleListLoading ? 'var(--bg-tertiary)' : 'var(--accent)',
|
||||||
|
color: !googleListUrl.trim() || googleListLoading ? 'var(--text-faint)' : 'var(--accent-text)',
|
||||||
|
fontSize: 13, fontWeight: 500, cursor: !googleListUrl.trim() || googleListLoading ? 'default' : 'pointer',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{googleListLoading ? t('common.loading') : t('common.import')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
<ContextMenu menu={ctxMenu.menu} onClose={ctxMenu.close} />
|
<ContextMenu menu={ctxMenu.menu} onClose={ctxMenu.close} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.edit': 'تعديل',
|
'common.edit': 'تعديل',
|
||||||
'common.add': 'إضافة',
|
'common.add': 'إضافة',
|
||||||
'common.loading': 'جارٍ التحميل...',
|
'common.loading': 'جارٍ التحميل...',
|
||||||
|
'common.import': 'استيراد',
|
||||||
'common.error': 'خطأ',
|
'common.error': 'خطأ',
|
||||||
'common.back': 'رجوع',
|
'common.back': 'رجوع',
|
||||||
'common.all': 'الكل',
|
'common.all': 'الكل',
|
||||||
@@ -783,9 +784,13 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
|
|
||||||
// Places Sidebar
|
// Places Sidebar
|
||||||
'places.addPlace': 'إضافة مكان/نشاط',
|
'places.addPlace': 'إضافة مكان/نشاط',
|
||||||
'places.importGpx': 'استيراد GPX',
|
'places.importGpx': 'GPX',
|
||||||
'places.gpxImported': 'تم استيراد {count} مكان من GPX',
|
'places.gpxImported': 'تم استيراد {count} مكان من GPX',
|
||||||
'places.gpxError': 'فشل استيراد GPX',
|
'places.gpxError': 'فشل استيراد GPX',
|
||||||
|
'places.importGoogleList': 'قائمة Google',
|
||||||
|
'places.googleListHint': 'الصق رابط قائمة Google Maps المشتركة لاستيراد جميع الأماكن.',
|
||||||
|
'places.googleListImported': 'تم استيراد {count} أماكن من "{list}"',
|
||||||
|
'places.googleListError': 'فشل استيراد قائمة Google Maps',
|
||||||
'places.urlResolved': 'تم استيراد المكان من الرابط',
|
'places.urlResolved': 'تم استيراد المكان من الرابط',
|
||||||
'places.assignToDay': 'إلى أي يوم تريد الإضافة؟',
|
'places.assignToDay': 'إلى أي يوم تريد الإضافة؟',
|
||||||
'places.all': 'الكل',
|
'places.all': 'الكل',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.edit': 'Editar',
|
'common.edit': 'Editar',
|
||||||
'common.add': 'Adicionar',
|
'common.add': 'Adicionar',
|
||||||
'common.loading': 'Carregando...',
|
'common.loading': 'Carregando...',
|
||||||
|
'common.import': 'Importar',
|
||||||
'common.error': 'Erro',
|
'common.error': 'Erro',
|
||||||
'common.back': 'Voltar',
|
'common.back': 'Voltar',
|
||||||
'common.all': 'Todos',
|
'common.all': 'Todos',
|
||||||
@@ -763,9 +764,13 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
|
|
||||||
// Places Sidebar
|
// Places Sidebar
|
||||||
'places.addPlace': 'Adicionar lugar/atividade',
|
'places.addPlace': 'Adicionar lugar/atividade',
|
||||||
'places.importGpx': 'Importar GPX',
|
'places.importGpx': 'GPX',
|
||||||
'places.gpxImported': '{count} lugares importados do GPX',
|
'places.gpxImported': '{count} lugares importados do GPX',
|
||||||
'places.gpxError': 'Falha ao importar GPX',
|
'places.gpxError': 'Falha ao importar GPX',
|
||||||
|
'places.importGoogleList': 'Lista Google',
|
||||||
|
'places.googleListHint': 'Cole um link compartilhado de uma lista do Google Maps para importar todos os lugares.',
|
||||||
|
'places.googleListImported': '{count} lugares importados de "{list}"',
|
||||||
|
'places.googleListError': 'Falha ao importar lista do Google Maps',
|
||||||
'places.urlResolved': 'Lugar importado da URL',
|
'places.urlResolved': 'Lugar importado da URL',
|
||||||
'places.assignToDay': 'Adicionar a qual dia?',
|
'places.assignToDay': 'Adicionar a qual dia?',
|
||||||
'places.all': 'Todos',
|
'places.all': 'Todos',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.edit': 'Upravit',
|
'common.edit': 'Upravit',
|
||||||
'common.add': 'Přidat',
|
'common.add': 'Přidat',
|
||||||
'common.loading': 'Načítání...',
|
'common.loading': 'Načítání...',
|
||||||
|
'common.import': 'Importovat',
|
||||||
'common.error': 'Chyba',
|
'common.error': 'Chyba',
|
||||||
'common.back': 'Zpět',
|
'common.back': 'Zpět',
|
||||||
'common.all': 'Vše',
|
'common.all': 'Vše',
|
||||||
@@ -783,10 +784,14 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
|
|
||||||
// Boční panel míst (Places Sidebar)
|
// Boční panel míst (Places Sidebar)
|
||||||
'places.addPlace': 'Přidat místo/aktivitu',
|
'places.addPlace': 'Přidat místo/aktivitu',
|
||||||
'places.importGpx': 'Importovat GPX',
|
'places.importGpx': 'GPX',
|
||||||
'places.gpxImported': '{count} míst importováno z GPX',
|
'places.gpxImported': '{count} míst importováno z GPX',
|
||||||
'places.urlResolved': 'Místo importováno z URL',
|
'places.urlResolved': 'Místo importováno z URL',
|
||||||
'places.gpxError': 'Import GPX se nezdařil',
|
'places.gpxError': 'Import GPX se nezdařil',
|
||||||
|
'places.importGoogleList': 'Google Seznam',
|
||||||
|
'places.googleListHint': 'Vložte sdílený odkaz na seznam Google Maps pro import všech míst.',
|
||||||
|
'places.googleListImported': '{count} míst importováno ze seznamu "{list}"',
|
||||||
|
'places.googleListError': 'Import seznamu Google Maps se nezdařil',
|
||||||
'places.assignToDay': 'Přidat do kterého dne?',
|
'places.assignToDay': 'Přidat do kterého dne?',
|
||||||
'places.all': 'Vše',
|
'places.all': 'Vše',
|
||||||
'places.unplanned': 'Nezařazené',
|
'places.unplanned': 'Nezařazené',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.edit': 'Bearbeiten',
|
'common.edit': 'Bearbeiten',
|
||||||
'common.add': 'Hinzufügen',
|
'common.add': 'Hinzufügen',
|
||||||
'common.loading': 'Laden...',
|
'common.loading': 'Laden...',
|
||||||
|
'common.import': 'Importieren',
|
||||||
'common.error': 'Fehler',
|
'common.error': 'Fehler',
|
||||||
'common.back': 'Zurück',
|
'common.back': 'Zurück',
|
||||||
'common.all': 'Alle',
|
'common.all': 'Alle',
|
||||||
@@ -781,10 +782,14 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
|
|
||||||
// Places Sidebar
|
// Places Sidebar
|
||||||
'places.addPlace': 'Ort/Aktivität hinzufügen',
|
'places.addPlace': 'Ort/Aktivität hinzufügen',
|
||||||
'places.importGpx': 'GPX importieren',
|
'places.importGpx': 'GPX',
|
||||||
'places.gpxImported': '{count} Orte aus GPX importiert',
|
'places.gpxImported': '{count} Orte aus GPX importiert',
|
||||||
'places.urlResolved': 'Ort aus URL importiert',
|
'places.urlResolved': 'Ort aus URL importiert',
|
||||||
'places.gpxError': 'GPX-Import fehlgeschlagen',
|
'places.gpxError': 'GPX-Import fehlgeschlagen',
|
||||||
|
'places.importGoogleList': 'Google Liste',
|
||||||
|
'places.googleListHint': 'Geteilten Google Maps Listen-Link einfügen, um alle Orte zu importieren.',
|
||||||
|
'places.googleListImported': '{count} Orte aus "{list}" importiert',
|
||||||
|
'places.googleListError': 'Google Maps Liste konnte nicht importiert werden',
|
||||||
'places.assignToDay': 'Zu welchem Tag hinzufügen?',
|
'places.assignToDay': 'Zu welchem Tag hinzufügen?',
|
||||||
'places.all': 'Alle',
|
'places.all': 'Alle',
|
||||||
'places.unplanned': 'Ungeplant',
|
'places.unplanned': 'Ungeplant',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.edit': 'Edit',
|
'common.edit': 'Edit',
|
||||||
'common.add': 'Add',
|
'common.add': 'Add',
|
||||||
'common.loading': 'Loading...',
|
'common.loading': 'Loading...',
|
||||||
|
'common.import': 'Import',
|
||||||
'common.error': 'Error',
|
'common.error': 'Error',
|
||||||
'common.back': 'Back',
|
'common.back': 'Back',
|
||||||
'common.all': 'All',
|
'common.all': 'All',
|
||||||
@@ -777,10 +778,14 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
|
|
||||||
// Places Sidebar
|
// Places Sidebar
|
||||||
'places.addPlace': 'Add Place/Activity',
|
'places.addPlace': 'Add Place/Activity',
|
||||||
'places.importGpx': 'Import GPX',
|
'places.importGpx': 'GPX',
|
||||||
'places.gpxImported': '{count} places imported from GPX',
|
'places.gpxImported': '{count} places imported from GPX',
|
||||||
'places.urlResolved': 'Place imported from URL',
|
'places.urlResolved': 'Place imported from URL',
|
||||||
'places.gpxError': 'GPX import failed',
|
'places.gpxError': 'GPX import failed',
|
||||||
|
'places.importGoogleList': 'Google List',
|
||||||
|
'places.googleListHint': 'Paste a shared Google Maps list link to import all places.',
|
||||||
|
'places.googleListImported': '{count} places imported from "{list}"',
|
||||||
|
'places.googleListError': 'Failed to import Google Maps list',
|
||||||
'places.assignToDay': 'Add to which day?',
|
'places.assignToDay': 'Add to which day?',
|
||||||
'places.all': 'All',
|
'places.all': 'All',
|
||||||
'places.unplanned': 'Unplanned',
|
'places.unplanned': 'Unplanned',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const es: Record<string, string> = {
|
|||||||
'common.edit': 'Editar',
|
'common.edit': 'Editar',
|
||||||
'common.add': 'Añadir',
|
'common.add': 'Añadir',
|
||||||
'common.loading': 'Cargando...',
|
'common.loading': 'Cargando...',
|
||||||
|
'common.import': 'Importar',
|
||||||
'common.error': 'Error',
|
'common.error': 'Error',
|
||||||
'common.back': 'Atrás',
|
'common.back': 'Atrás',
|
||||||
'common.all': 'Todo',
|
'common.all': 'Todo',
|
||||||
@@ -757,9 +758,13 @@ const es: Record<string, string> = {
|
|||||||
|
|
||||||
// Places Sidebar
|
// Places Sidebar
|
||||||
'places.addPlace': 'Añadir lugar/actividad',
|
'places.addPlace': 'Añadir lugar/actividad',
|
||||||
'places.importGpx': 'Importar GPX',
|
'places.importGpx': 'GPX',
|
||||||
'places.gpxImported': '{count} lugares importados desde GPX',
|
'places.gpxImported': '{count} lugares importados desde GPX',
|
||||||
'places.gpxError': 'Error al importar GPX',
|
'places.gpxError': 'Error al importar GPX',
|
||||||
|
'places.importGoogleList': 'Lista Google',
|
||||||
|
'places.googleListHint': 'Pega un enlace compartido de una lista de Google Maps para importar todos los lugares.',
|
||||||
|
'places.googleListImported': '{count} lugares importados de "{list}"',
|
||||||
|
'places.googleListError': 'Error al importar la lista de Google Maps',
|
||||||
'places.urlResolved': 'Lugar importado desde URL',
|
'places.urlResolved': 'Lugar importado desde URL',
|
||||||
'places.assignToDay': '¿A qué día añadirlo?',
|
'places.assignToDay': '¿A qué día añadirlo?',
|
||||||
'places.all': 'Todo',
|
'places.all': 'Todo',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const fr: Record<string, string> = {
|
|||||||
'common.edit': 'Modifier',
|
'common.edit': 'Modifier',
|
||||||
'common.add': 'Ajouter',
|
'common.add': 'Ajouter',
|
||||||
'common.loading': 'Chargement…',
|
'common.loading': 'Chargement…',
|
||||||
|
'common.import': 'Importer',
|
||||||
'common.error': 'Erreur',
|
'common.error': 'Erreur',
|
||||||
'common.back': 'Retour',
|
'common.back': 'Retour',
|
||||||
'common.all': 'Tout',
|
'common.all': 'Tout',
|
||||||
@@ -780,9 +781,13 @@ const fr: Record<string, string> = {
|
|||||||
|
|
||||||
// Places Sidebar
|
// Places Sidebar
|
||||||
'places.addPlace': 'Ajouter un lieu/activité',
|
'places.addPlace': 'Ajouter un lieu/activité',
|
||||||
'places.importGpx': 'Importer GPX',
|
'places.importGpx': 'GPX',
|
||||||
'places.gpxImported': '{count} lieux importés depuis GPX',
|
'places.gpxImported': '{count} lieux importés depuis GPX',
|
||||||
'places.gpxError': 'L\'import GPX a échoué',
|
'places.gpxError': 'L\'import GPX a échoué',
|
||||||
|
'places.importGoogleList': 'Liste Google',
|
||||||
|
'places.googleListHint': 'Collez un lien de liste Google Maps partagée pour importer tous les lieux.',
|
||||||
|
'places.googleListImported': '{count} lieux importés depuis "{list}"',
|
||||||
|
'places.googleListError': 'Impossible d\'importer la liste Google Maps',
|
||||||
'places.urlResolved': 'Lieu importé depuis l\'URL',
|
'places.urlResolved': 'Lieu importé depuis l\'URL',
|
||||||
'places.assignToDay': 'Ajouter à quel jour ?',
|
'places.assignToDay': 'Ajouter à quel jour ?',
|
||||||
'places.all': 'Tous',
|
'places.all': 'Tous',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.edit': 'Szerkesztés',
|
'common.edit': 'Szerkesztés',
|
||||||
'common.add': 'Hozzáadás',
|
'common.add': 'Hozzáadás',
|
||||||
'common.loading': 'Betöltés...',
|
'common.loading': 'Betöltés...',
|
||||||
|
'common.import': 'Importálás',
|
||||||
'common.error': 'Hiba',
|
'common.error': 'Hiba',
|
||||||
'common.back': 'Vissza',
|
'common.back': 'Vissza',
|
||||||
'common.all': 'Összes',
|
'common.all': 'Összes',
|
||||||
@@ -779,10 +780,14 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
|
|
||||||
// Helyek oldalsáv
|
// Helyek oldalsáv
|
||||||
'places.addPlace': 'Hely/Tevékenység hozzáadása',
|
'places.addPlace': 'Hely/Tevékenység hozzáadása',
|
||||||
'places.importGpx': 'GPX importálás',
|
'places.importGpx': 'GPX',
|
||||||
'places.gpxImported': '{count} hely importálva GPX-ből',
|
'places.gpxImported': '{count} hely importálva GPX-ből',
|
||||||
'places.urlResolved': 'Hely importálva URL-ből',
|
'places.urlResolved': 'Hely importálva URL-ből',
|
||||||
'places.gpxError': 'GPX importálás sikertelen',
|
'places.gpxError': 'GPX importálás sikertelen',
|
||||||
|
'places.importGoogleList': 'Google Lista',
|
||||||
|
'places.googleListHint': 'Illessz be egy megosztott Google Maps lista linket az osszes hely importalasahoz.',
|
||||||
|
'places.googleListImported': '{count} hely importalva a(z) "{list}" listabol',
|
||||||
|
'places.googleListError': 'Google Maps lista importalasa sikertelen',
|
||||||
'places.assignToDay': 'Melyik naphoz adod?',
|
'places.assignToDay': 'Melyik naphoz adod?',
|
||||||
'places.all': 'Összes',
|
'places.all': 'Összes',
|
||||||
'places.unplanned': 'Nem tervezett',
|
'places.unplanned': 'Nem tervezett',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'common.edit': 'Modifica',
|
'common.edit': 'Modifica',
|
||||||
'common.add': 'Aggiungi',
|
'common.add': 'Aggiungi',
|
||||||
'common.loading': 'Caricamento...',
|
'common.loading': 'Caricamento...',
|
||||||
|
'common.import': 'Importa',
|
||||||
'common.error': 'Errore',
|
'common.error': 'Errore',
|
||||||
'common.back': 'Indietro',
|
'common.back': 'Indietro',
|
||||||
'common.all': 'Tutti',
|
'common.all': 'Tutti',
|
||||||
@@ -779,10 +780,14 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
|
|
||||||
// Places Sidebar
|
// Places Sidebar
|
||||||
'places.addPlace': 'Aggiungi Luogo/Attività',
|
'places.addPlace': 'Aggiungi Luogo/Attività',
|
||||||
'places.importGpx': 'Importa GPX',
|
'places.importGpx': 'GPX',
|
||||||
'places.gpxImported': '{count} luoghi importati da GPX',
|
'places.gpxImported': '{count} luoghi importati da GPX',
|
||||||
'places.urlResolved': 'Luogo importato dall\'URL',
|
'places.urlResolved': 'Luogo importato dall\'URL',
|
||||||
'places.gpxError': 'Importazione GPX non riuscita',
|
'places.gpxError': 'Importazione GPX non riuscita',
|
||||||
|
'places.importGoogleList': 'Lista Google',
|
||||||
|
'places.googleListHint': 'Incolla un link condiviso di una lista Google Maps per importare tutti i luoghi.',
|
||||||
|
'places.googleListImported': '{count} luoghi importati da "{list}"',
|
||||||
|
'places.googleListError': 'Importazione lista Google Maps non riuscita',
|
||||||
'places.assignToDay': 'A quale giorno aggiungere?',
|
'places.assignToDay': 'A quale giorno aggiungere?',
|
||||||
'places.all': 'Tutti',
|
'places.all': 'Tutti',
|
||||||
'places.unplanned': 'Non pianificati',
|
'places.unplanned': 'Non pianificati',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const nl: Record<string, string> = {
|
|||||||
'common.edit': 'Bewerken',
|
'common.edit': 'Bewerken',
|
||||||
'common.add': 'Toevoegen',
|
'common.add': 'Toevoegen',
|
||||||
'common.loading': 'Laden...',
|
'common.loading': 'Laden...',
|
||||||
|
'common.import': 'Importeren',
|
||||||
'common.error': 'Fout',
|
'common.error': 'Fout',
|
||||||
'common.back': 'Terug',
|
'common.back': 'Terug',
|
||||||
'common.all': 'Alles',
|
'common.all': 'Alles',
|
||||||
@@ -780,9 +781,13 @@ const nl: Record<string, string> = {
|
|||||||
|
|
||||||
// Places Sidebar
|
// Places Sidebar
|
||||||
'places.addPlace': 'Plaats/activiteit toevoegen',
|
'places.addPlace': 'Plaats/activiteit toevoegen',
|
||||||
'places.importGpx': 'GPX importeren',
|
'places.importGpx': 'GPX',
|
||||||
'places.gpxImported': '{count} plaatsen geïmporteerd uit GPX',
|
'places.gpxImported': '{count} plaatsen geïmporteerd uit GPX',
|
||||||
'places.gpxError': 'GPX-import mislukt',
|
'places.gpxError': 'GPX-import mislukt',
|
||||||
|
'places.importGoogleList': 'Google Lijst',
|
||||||
|
'places.googleListHint': 'Plak een gedeelde Google Maps lijstlink om alle plaatsen te importeren.',
|
||||||
|
'places.googleListImported': '{count} plaatsen geimporteerd uit "{list}"',
|
||||||
|
'places.googleListError': 'Google Maps lijst importeren mislukt',
|
||||||
'places.urlResolved': 'Plaats geïmporteerd van URL',
|
'places.urlResolved': 'Plaats geïmporteerd van URL',
|
||||||
'places.assignToDay': 'Aan welke dag toevoegen?',
|
'places.assignToDay': 'Aan welke dag toevoegen?',
|
||||||
'places.all': 'Alle',
|
'places.all': 'Alle',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const ru: Record<string, string> = {
|
|||||||
'common.edit': 'Редактировать',
|
'common.edit': 'Редактировать',
|
||||||
'common.add': 'Добавить',
|
'common.add': 'Добавить',
|
||||||
'common.loading': 'Загрузка...',
|
'common.loading': 'Загрузка...',
|
||||||
|
'common.import': 'Импорт',
|
||||||
'common.error': 'Ошибка',
|
'common.error': 'Ошибка',
|
||||||
'common.back': 'Назад',
|
'common.back': 'Назад',
|
||||||
'common.all': 'Все',
|
'common.all': 'Все',
|
||||||
@@ -780,9 +781,13 @@ const ru: Record<string, string> = {
|
|||||||
|
|
||||||
// Places Sidebar
|
// Places Sidebar
|
||||||
'places.addPlace': 'Добавить место/активность',
|
'places.addPlace': 'Добавить место/активность',
|
||||||
'places.importGpx': 'Импорт GPX',
|
'places.importGpx': 'GPX',
|
||||||
'places.gpxImported': '{count} мест импортировано из GPX',
|
'places.gpxImported': '{count} мест импортировано из GPX',
|
||||||
'places.gpxError': 'Ошибка импорта GPX',
|
'places.gpxError': 'Ошибка импорта GPX',
|
||||||
|
'places.importGoogleList': 'Список Google',
|
||||||
|
'places.googleListHint': 'Вставьте ссылку на общий список Google Maps для импорта всех мест.',
|
||||||
|
'places.googleListImported': '{count} мест импортировано из "{list}"',
|
||||||
|
'places.googleListError': 'Не удалось импортировать список Google Maps',
|
||||||
'places.urlResolved': 'Место импортировано из URL',
|
'places.urlResolved': 'Место импортировано из URL',
|
||||||
'places.assignToDay': 'Добавить в какой день?',
|
'places.assignToDay': 'Добавить в какой день?',
|
||||||
'places.all': 'Все',
|
'places.all': 'Все',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const zh: Record<string, string> = {
|
|||||||
'common.edit': '编辑',
|
'common.edit': '编辑',
|
||||||
'common.add': '添加',
|
'common.add': '添加',
|
||||||
'common.loading': '加载中...',
|
'common.loading': '加载中...',
|
||||||
|
'common.import': '导入',
|
||||||
'common.error': '错误',
|
'common.error': '错误',
|
||||||
'common.back': '返回',
|
'common.back': '返回',
|
||||||
'common.all': '全部',
|
'common.all': '全部',
|
||||||
@@ -780,9 +781,13 @@ const zh: Record<string, string> = {
|
|||||||
|
|
||||||
// Places Sidebar
|
// Places Sidebar
|
||||||
'places.addPlace': '添加地点/活动',
|
'places.addPlace': '添加地点/活动',
|
||||||
'places.importGpx': '导入 GPX',
|
'places.importGpx': 'GPX',
|
||||||
'places.gpxImported': '已从 GPX 导入 {count} 个地点',
|
'places.gpxImported': '已从 GPX 导入 {count} 个地点',
|
||||||
'places.gpxError': 'GPX 导入失败',
|
'places.gpxError': 'GPX 导入失败',
|
||||||
|
'places.importGoogleList': 'Google 列表',
|
||||||
|
'places.googleListHint': '粘贴共享的 Google Maps 列表链接以导入所有地点。',
|
||||||
|
'places.googleListImported': '已从"{list}"导入 {count} 个地点',
|
||||||
|
'places.googleListError': 'Google Maps 列表导入失败',
|
||||||
'places.urlResolved': '已从 URL 导入地点',
|
'places.urlResolved': '已从 URL 导入地点',
|
||||||
'places.assignToDay': '添加到哪一天?',
|
'places.assignToDay': '添加到哪一天?',
|
||||||
'places.all': '全部',
|
'places.all': '全部',
|
||||||
|
|||||||
@@ -214,6 +214,111 @@ router.post('/import/gpx', authenticate, requireTripAccess, gpxUpload.single('fi
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Import places from a shared Google Maps list URL
|
||||||
|
router.post('/import/google-list', authenticate, requireTripAccess, async (req: Request, res: Response) => {
|
||||||
|
const authReq = req as AuthRequest;
|
||||||
|
if (!checkPermission('place_edit', authReq.user.role, authReq.trip!.user_id, authReq.user.id, authReq.trip!.user_id !== authReq.user.id))
|
||||||
|
return res.status(403).json({ error: 'No permission' });
|
||||||
|
|
||||||
|
const { tripId } = req.params;
|
||||||
|
const { url } = req.body;
|
||||||
|
if (!url || typeof url !== 'string') return res.status(400).json({ error: 'URL is required' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract list ID from various Google Maps list URL formats
|
||||||
|
let listId: string | null = null;
|
||||||
|
let resolvedUrl = url;
|
||||||
|
|
||||||
|
// Follow redirects for short URLs (maps.app.goo.gl, goo.gl)
|
||||||
|
if (url.includes('goo.gl') || url.includes('maps.app')) {
|
||||||
|
const redirectRes = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(10000) });
|
||||||
|
resolvedUrl = redirectRes.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern: /placelists/list/{ID}
|
||||||
|
const plMatch = resolvedUrl.match(/placelists\/list\/([A-Za-z0-9_-]+)/);
|
||||||
|
if (plMatch) listId = plMatch[1];
|
||||||
|
|
||||||
|
// Pattern: !2s{ID} in data URL params
|
||||||
|
if (!listId) {
|
||||||
|
const dataMatch = resolvedUrl.match(/!2s([A-Za-z0-9_-]{15,})/);
|
||||||
|
if (dataMatch) listId = dataMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!listId) {
|
||||||
|
return res.status(400).json({ error: 'Could not extract list ID from URL. Please use a shared Google Maps list link.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch list data from Google Maps internal API
|
||||||
|
const apiUrl = `https://www.google.com/maps/preview/entitylist/getlist?authuser=0&hl=en&gl=us&pb=!1m1!1s${encodeURIComponent(listId)}!2e2!3e2!4i500!16b1`;
|
||||||
|
const apiRes = await fetch(apiUrl, {
|
||||||
|
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },
|
||||||
|
signal: AbortSignal.timeout(15000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!apiRes.ok) {
|
||||||
|
return res.status(502).json({ error: 'Failed to fetch list from Google Maps' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawText = await apiRes.text();
|
||||||
|
const jsonStr = rawText.substring(rawText.indexOf('\n') + 1);
|
||||||
|
const listData = JSON.parse(jsonStr);
|
||||||
|
|
||||||
|
const meta = listData[0];
|
||||||
|
if (!meta) {
|
||||||
|
return res.status(400).json({ error: 'Invalid list data received from Google Maps' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const listName = meta[4] || 'Google Maps List';
|
||||||
|
const items = meta[8];
|
||||||
|
|
||||||
|
if (!Array.isArray(items) || items.length === 0) {
|
||||||
|
return res.status(400).json({ error: 'List is empty or could not be read' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse place data from items
|
||||||
|
const places: { name: string; lat: number; lng: number; notes: string | null }[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
const coords = item?.[1]?.[5];
|
||||||
|
const lat = coords?.[2];
|
||||||
|
const lng = coords?.[3];
|
||||||
|
const name = item?.[2];
|
||||||
|
const note = item?.[3] || null;
|
||||||
|
|
||||||
|
if (name && typeof lat === 'number' && typeof lng === 'number' && !isNaN(lat) && !isNaN(lng)) {
|
||||||
|
places.push({ name, lat, lng, notes: note || null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (places.length === 0) {
|
||||||
|
return res.status(400).json({ error: 'No places with coordinates found in list' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert places into trip
|
||||||
|
const insertStmt = db.prepare(`
|
||||||
|
INSERT INTO places (trip_id, name, lat, lng, notes, transport_mode)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 'walking')
|
||||||
|
`);
|
||||||
|
const created: any[] = [];
|
||||||
|
const insertAll = db.transaction(() => {
|
||||||
|
for (const p of places) {
|
||||||
|
const result = insertStmt.run(tripId, p.name, p.lat, p.lng, p.notes);
|
||||||
|
const place = getPlaceWithTags(Number(result.lastInsertRowid));
|
||||||
|
created.push(place);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
insertAll();
|
||||||
|
|
||||||
|
res.status(201).json({ places: created, count: created.length, listName });
|
||||||
|
for (const place of created) {
|
||||||
|
broadcast(tripId, 'place:created', { place }, req.headers['x-socket-id'] as string);
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error('[Places] Google list import error:', err instanceof Error ? err.message : err);
|
||||||
|
res.status(400).json({ error: 'Failed to import Google Maps list. Make sure the list is shared publicly.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => {
|
router.get('/:id', authenticate, requireTripAccess, (req: Request, res: Response) => {
|
||||||
const { tripId, id } = req.params
|
const { tripId, id } = req.params
|
||||||
|
|
||||||
|
|||||||
+11
-21
@@ -126,19 +126,12 @@ router.get('/', authenticate, (req: Request, res: Response) => {
|
|||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const archived = req.query.archived === '1' ? 1 : 0;
|
const archived = req.query.archived === '1' ? 1 : 0;
|
||||||
const userId = authReq.user.id;
|
const userId = authReq.user.id;
|
||||||
const isAdminUser = authReq.user.role === 'admin';
|
const trips = db.prepare(`
|
||||||
const trips = isAdminUser
|
${TRIP_SELECT}
|
||||||
? db.prepare(`
|
LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = :userId
|
||||||
${TRIP_SELECT}
|
WHERE (t.user_id = :userId OR m.user_id IS NOT NULL) AND t.is_archived = :archived
|
||||||
WHERE t.is_archived = :archived
|
ORDER BY t.created_at DESC
|
||||||
ORDER BY t.created_at DESC
|
`).all({ userId, archived });
|
||||||
`).all({ userId, archived })
|
|
||||||
: db.prepare(`
|
|
||||||
${TRIP_SELECT}
|
|
||||||
LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = :userId
|
|
||||||
WHERE (t.user_id = :userId OR m.user_id IS NOT NULL) AND t.is_archived = :archived
|
|
||||||
ORDER BY t.created_at DESC
|
|
||||||
`).all({ userId, archived });
|
|
||||||
res.json({ trips });
|
res.json({ trips });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -171,14 +164,11 @@ router.post('/', authenticate, (req: Request, res: Response) => {
|
|||||||
router.get('/:id', authenticate, (req: Request, res: Response) => {
|
router.get('/:id', authenticate, (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const userId = authReq.user.id;
|
const userId = authReq.user.id;
|
||||||
const isAdminUser = authReq.user.role === 'admin';
|
const trip = db.prepare(`
|
||||||
const trip = isAdminUser
|
${TRIP_SELECT}
|
||||||
? db.prepare(`${TRIP_SELECT} WHERE t.id = :tripId`).get({ userId, tripId: req.params.id })
|
LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = :userId
|
||||||
: db.prepare(`
|
WHERE t.id = :tripId AND (t.user_id = :userId OR m.user_id IS NOT NULL)
|
||||||
${TRIP_SELECT}
|
`).get({ userId, tripId: req.params.id });
|
||||||
LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = :userId
|
|
||||||
WHERE t.id = :tripId AND (t.user_id = :userId OR m.user_id IS NOT NULL)
|
|
||||||
`).get({ userId, tripId: req.params.id });
|
|
||||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||||
res.json({ trip });
|
res.json({ trip });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user