diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 28c8a2890..3f4e5d873 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -961,6 +961,7 @@ export const healthApi = { export const weatherApi = { get: (lat: number, lng: number, date: string): Promise => apiClient.get('/weather', { params: { lat, lng, date } }).then(r => parseInDev(weatherResultSchema, r.data, 'weather.get')), + getCurrent: (lat: number, lng: number, lang?: string): Promise => apiClient.get('/weather', { params: { lat, lng, lang } }).then(r => parseInDev(weatherResultSchema, r.data, 'weather.getCurrent')), getDetailed: (lat: number, lng: number, date: string, lang?: string): Promise => apiClient.get('/weather/detailed', { params: { lat, lng, date, lang } }).then(r => parseInDev(weatherResultSchema, r.data, 'weather.getDetailed')), } diff --git a/client/src/mobile/screens/journey/MJourneyDetail.tsx b/client/src/mobile/screens/journey/MJourneyDetail.tsx index 8f4f6aea4..1998b289d 100644 --- a/client/src/mobile/screens/journey/MJourneyDetail.tsx +++ b/client/src/mobile/screens/journey/MJourneyDetail.tsx @@ -355,6 +355,7 @@ export default function MJourneyDetail() { setEditingEntry(null)} onSave={async data => { diff --git a/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx b/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx index 773a5e99e..64a78fe14 100644 --- a/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx +++ b/client/src/mobile/screens/journey/MJourneyEntrySheet.tsx @@ -1,17 +1,17 @@ -import { useRef, useState } from 'react' -import { Plus, Image, X, MapPin, Trash2, CheckCircle2, MinusCircle } from 'lucide-react' +import { useEffect, useRef, useState } from 'react' +import { Camera, Plus, Image, X, MapPin, Trash2, CheckCircle2, MinusCircle } from 'lucide-react' import MSheet from '../../components/MSheet' import MIconBtn from '../../components/MIconBtn' import { useTranslation } from '../../../i18n' import { useToast } from '../../../components/shared/Toast' -import { journeyApi, mapsApi } from '../../../api/client' +import { journeyApi, mapsApi, weatherApi } from '../../../api/client' import { getApiErrorMessage } from '../../../types' import { normalizeImageFiles } from '../../../utils/convertHeic' import type { ResilientResult, UploadProgress } from '../../../utils/uploadQueue' import type { JourneyEntry, JourneyPhoto, GalleryPhoto } from '../../../store/journeyStore' import { photoUrl } from '../../../pages/journeyDetail/JourneyDetailPage.helpers' import JournalBody from '../../../components/Journey/JournalBody' -import { MOBILE_MOODS, MOBILE_WEATHERS } from './mobileJourneyMeta' +import { journeyWeatherCategory, MOBILE_MOODS, MOBILE_WEATHERS } from './mobileJourneyMeta' const PRO_COLOR = '#2FA37A' const CON_COLOR = '#D6273B' @@ -26,6 +26,7 @@ interface LocationResult { interface MJourneyEntrySheetProps { entry: JourneyEntry galleryPhotos: GalleryPhoto[] + quickCapture?: boolean readOnly?: boolean onClose: () => void onSave: (data: Record) => Promise @@ -40,7 +41,7 @@ interface MJourneyEntrySheetProps { * weather (6) and tags. Read-only for viewer contributors. */ export default function MJourneyEntrySheet({ - entry, galleryPhotos, readOnly = false, onClose, onSave, onUploadPhotos, onDelete, onDone, + entry, galleryPhotos, quickCapture = false, readOnly = false, onClose, onSave, onUploadPhotos, onDelete, onDone, }: MJourneyEntrySheetProps) { const { t } = useTranslation() const toast = useToast() @@ -67,8 +68,49 @@ export default function MJourneyEntrySheet({ const [pendingLinkIds, setPendingLinkIds] = useState([]) const [showGalleryPick, setShowGalleryPick] = useState(false) const [saving, setSaving] = useState(false) + const [captureOnly, setCaptureOnly] = useState(quickCapture) + const [locating, setLocating] = useState(false) + const [locationError, setLocationError] = useState('') const [uploadProgress, setUploadProgress] = useState<{ done: number; total: number } | null>(null) const fileRef = useRef(null) + const cameraRef = useRef(null) + + useEffect(() => { + if (!quickCapture || readOnly || entry.location_lat != null || entry.location_lng != null) return + if (!navigator.geolocation) { + setLocationError(t('common.error')) + return + } + + let active = true + setLocating(true) + navigator.geolocation.getCurrentPosition(async position => { + const lat = position.coords.latitude + const lng = position.coords.longitude + if (!active) return + setLocationLat(lat) + setLocationLng(lng) + + const [placeResult, weatherResult] = await Promise.allSettled([ + mapsApi.reverse(lat, lng), + weatherApi.getCurrent(lat, lng, 'en'), + ]) + if (!active) return + if (placeResult.status === 'fulfilled') { + setLocationName(placeResult.value.name || placeResult.value.address || '') + } + if (weatherResult.status === 'fulfilled' && !weatherResult.value.error) { + setWeather(current => current || journeyWeatherCategory(weatherResult.value.main, weatherResult.value.description)) + } + setLocating(false) + }, error => { + if (!active) return + setLocationError(error.message || t('common.error')) + setLocating(false) + }, { enableHighAccuracy: true, maximumAge: 60_000, timeout: 10_000 }) + + return () => { active = false } + }, [quickCapture, readOnly, entry.location_lat, entry.location_lng, entry.entry_date, t]) const isDirty = title !== (entry.title || '') || @@ -85,7 +127,7 @@ export default function MJourneyEntrySheet({ pendingLinkIds.length > 0 const handleClose = () => { - if (!readOnly && isDirty && !window.confirm(t('journey.editor.discardChangesConfirm'))) return + if (!captureOnly && !readOnly && isDirty && !window.confirm(t('journey.editor.discardChangesConfirm'))) return onClose() } @@ -193,7 +235,7 @@ export default function MJourneyEntrySheet({
- {readOnly ? ( + {!captureOnly && (readOnly ? (
{title || t('journey.editor.titlePlaceholder')}
) : ( - )} + ))} {!readOnly && ( <> + { (e.target as HTMLInputElement).value = '' }} /> { (e.target as HTMLInputElement).value = '' }} />
- {showGalleryPick && ( + {!captureOnly && showGalleryPick && (
{availableGalleryPhotos.map(gp => ( @@ -333,24 +376,25 @@ export default function MJourneyEntrySheet({
)} - {readOnly ? ( - story && ( -
- -
- ) - ) : ( -