import { useAI, useAuth, useProxy, share } from '@deplixo/sdk'; import { useEffect, useRef, useState } from 'react'; import { ObservationLog } from './components/ObservationLog.jsx'; import { AddObservation } from './components/AddObservation.jsx'; import { Stats } from './components/Stats.jsx'; /* @component-map * App — Main container, tab navigation [app.jsx] * ObservationLog — Timeline of stargazing observations [components/ObservationLog.jsx] * AddObservation — Form to log a new observation [components/AddObservation.jsx] * ObservationDetail — Detail view for a single observation [components/ObservationDetail.jsx] * Stats — Overview statistics and charts [components/Stats.jsx] * LocationCaptureFields — Adds a browser geolocation helper for the AddObservation form, letting users auto-fill latitude, longitude, and optional accuracy from their device. * ObservationLocationMap — Google Maps view centered on a single observation coordinate pair, with a fallback when coordinates are missing. * ObservationLogItemMap — Compact, non-interactive Google Maps preview for observation cards in the timeline. * null — Single-observation detail panel with a full Google Map centered on the saved coordinates. Note: if your existing ObservationDetail has different props/markup, merge the map section into that component. * TonightVisibleObjects — Shows tonight’s visible sky objects from the new proxy feed with refresh and fallback states. * AIObservationAssist — Lets the user describe what they saw and uses AI to suggest a better observation description and likely object matches. * @end-component-map */ // DEPLOY_CONFIG: {"server": {"proxy_endpoints": [{"name": "tonight_visible_objects", "method": "GET", "path": "/api/astronomy/tonight-visible-objects", "description": "Server-side proxy endpoint that fetches tonight's visible astronomical objects from an external astronomy API, using a server-stored API key and returning a normalized list for the client.", "env": {"ASTRONOMY_API_KEY": {"type": "secret", "required": true}, "ASTRONOMY_API_BASE_URL": {"type": "env", "required": false, "default": "https://api.astronomy.example.com"}}}]}} function App() { const { user, loading, login, logout } = useAuth(); const [activeTab, setActiveTab] = useState('log'); const [showAddModal, setShowAddModal] = useState(false); const [editItem, setEditItem] = useState(null); const [sharedObservationId, setSharedObservationId] = useState(null); const [sharedObservation, setSharedObservation] = useState(null); const [sharedLoading, setSharedLoading] = useState(false); const handleEdit = (item) => { setEditItem(item); setShowAddModal(true); }; const handleCloseModal = () => { setShowAddModal(false); setEditItem(null); }; const handleAddClick = () => { if (!user) { login(); return; } setShowAddModal(true); }; const handleOpenSharedObservation = (observation) => { if (!observation) return; setActiveTab('log'); setSharedObservationId(observation.id || null); setSharedObservation(observation); }; const clearSharedObservation = () => { setSharedObservationId(null); setSharedObservation(null); }; const syncShareRoute = () => { if (typeof window === 'undefined') return; const params = new URLSearchParams(window.location.search); const view = params.get('view'); const id = params.get('id'); if (view === 'observation' && id) { setSharedObservationId(id); } }; useEffect(() => { syncShareRoute(); if (typeof window === 'undefined') return undefined; const onPopState = () => syncShareRoute(); window.addEventListener('popstate', onPopState); return () => window.removeEventListener('popstate', onPopState); }, []); useEffect(() => { if (!sharedObservationId || !user) return; let cancelled = false; const loadSharedObservation = async () => { setSharedLoading(true); try { const response = await fetch(window.location.href, { method: 'GET' }); const text = await response.text(); const match = text && text.includes(sharedObservationId); if (!cancelled) { setSharedObservation(match ? sharedObservation : sharedObservation); } } catch (error) { if (!cancelled) setSharedObservation(sharedObservation); } finally { if (!cancelled) setSharedLoading(false); } }; loadSharedObservation(); return () => { cancelled = true; }; }, [sharedObservationId, user]); if (loading) return
Signing in...
; if (!user) { return (

Stargazer Journal

Sign in to view your observations

Use Google authentication to keep your stargazing notes private and tied to your account.

); } return (

Stargazer Journal

Welcome, {user.name}
{activeTab === 'log' && } {activeTab === 'stats' && }
{sharedObservationId && activeTab === 'log' && (
{sharedLoading ? (
Loading shared report...
) : sharedObservation ? ( ) : (
Shared observation not found.
)}
)} {showAddModal && ( )}
); } function LocationCaptureFields({ value, onChange }) { const [capturing, setCapturing] = useState(false); const [error, setError] = useState(''); const requestLocation = () => { setError(''); if (!navigator.geolocation) { setError('Geolocation is not supported by this browser.'); return; } setCapturing(true); navigator.geolocation.getCurrentPosition( (position) => { const latitude = position?.coords?.latitude; const longitude = position?.coords?.longitude; const accuracy = position?.coords?.accuracy; if (latitude === undefined || longitude === undefined || latitude === null || longitude === null) { setError('Could not determine your location.'); setCapturing(false); return; } onChange({ ...value, latitude, longitude, accuracy, }); setCapturing(false); }, () => { setError('Unable to access your location. Please allow location access and try again.'); setCapturing(false); }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } ); }; return (
onChange({ ...value, latitude: e.target.value })} placeholder="Latitude" />
onChange({ ...value, longitude: e.target.value })} placeholder="Longitude" />
onChange({ ...value, accuracy: e.target.value })} placeholder="Accuracy in meters" />
{error &&

{error}

}
); } function ObservationLocationMap({ latitude, longitude, title, description, zoom = 14, className = '' }) { const containerRef = useRef(null); useEffect(() => { const lat = Number(latitude); const lng = Number(longitude); if (!containerRef.current) return; if (Number.isNaN(lat) || Number.isNaN(lng)) return; if (typeof window === 'undefined' || !window.google || !window.google.maps) return; const map = new window.google.maps.Map(containerRef.current, { center: { lat, lng }, zoom, mapTypeControl: false, streetViewControl: false, fullscreenControl: false, zoomControl: true, }); new window.google.maps.Marker({ position: { lat, lng }, map, title: title || 'Observation site', }); if (description) { const infoWindow = new window.google.maps.InfoWindow({ content: `
${title || 'Observation site'}

${description}

`, }); map.addListener('click', () => infoWindow.close()); } }, [latitude, longitude, title, description, zoom]); const latNum = Number(latitude); const lngNum = Number(longitude); const hasCoords = !Number.isNaN(latNum) && !Number.isNaN(lngNum); return (
{hasCoords ? (
) : (

No coordinates available

)}
); } function ObservationLogItemMap({ latitude, longitude, title }) { const containerRef = useRef(null); useEffect(() => { const lat = Number(latitude); const lng = Number(longitude); if (!containerRef.current) return; if (Number.isNaN(lat) || Number.isNaN(lng)) return; if (typeof window === 'undefined' || !window.google || !window.google.maps) return; const map = new window.google.maps.Map(containerRef.current, { center: { lat, lng }, zoom: 11, mapTypeControl: false, streetViewControl: false, fullscreenControl: false, gestureHandling: 'none', draggable: false, zoomControl: false, }); new window.google.maps.Marker({ position: { lat, lng }, map, title: title || 'Observation site', }); }, [latitude, longitude, title]); const latNum = Number(latitude); const lngNum = Number(longitude); const hasCoords = !Number.isNaN(latNum) && !Number.isNaN(lngNum); return (
{hasCoords ? (
) : (

No map available

)}
); } function ObservationDetail({ observation, onEdit, onClose }) { const lat = observation?.latitude; const lng = observation?.longitude; const hasCoords = lat !== undefined && lat !== null && lng !== undefined && lng !== null && lat !== '' && lng !== ''; const images = Array.isArray(observation?.imageUrls) ? observation.imageUrls : (Array.isArray(observation?.images) ? observation.images : []); const buildShareSummary = () => { if (!observation) return 'Stargazer Journal observation report'; const parts = []; parts.push(`Stargazer Journal report: ${observation.title || 'Observation Details'}`); if (observation.date) parts.push(`Date: ${observation.date}`); if (observation.notes) parts.push(`Notes: ${observation.notes}`); if (hasCoords) { parts.push(`Location: ${observation.latitude}, ${observation.longitude}`); if (observation.accuracy !== undefined && observation.accuracy !== null && observation.accuracy !== '') { parts.push(`Accuracy: ${observation.accuracy} m`); } } if (images.length > 0) parts.push(`Photos: ${images.length}`); return parts.join('\n'); }; const getShareUrl = () => { if (typeof window === 'undefined' || !observation?.id) return ''; const base = window.location.origin + window.location.pathname; const url = new URL(base, window.location.origin); url.searchParams.set('view', 'observation'); url.searchParams.set('id', String(observation.id)); return url.toString(); }; const handleCopyShareSummary = async () => { const shareText = `${buildShareSummary()}\n${getShareUrl() ? `\nView shared report: ${getShareUrl()}` : ''}`.trim(); try { if (navigator?.clipboard?.writeText) { await navigator.clipboard.writeText(shareText); } } catch (error) { const textarea = document.createElement('textarea'); textarea.value = shareText; textarea.setAttribute('readonly', 'true'); textarea.className = 'share-copy-fallback'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); } }; const handleNativeShare = async () => { const shareUrl = getShareUrl(); const shareText = buildShareSummary(); if (navigator?.share && shareUrl) { try { await navigator.share({ title: observation?.title || 'Observation Details', text: shareText, url: shareUrl, }); return; } catch (error) { /* user canceled or share failed */ } } await handleCopyShareSummary(); }; return (

{observation?.title || 'Observation Details'}

{observation?.date &&

{observation.date}

}
{onEdit && ( )} {onClose && ( )}
{observation?.notes &&

{observation.notes}

} {images.length > 0 && (

Astrophotography

{images.map((src, index) => ( {`${observation?.title ))}
{images[0] && (
{`${observation?.title
)}
)}

Observation site

{hasCoords ? ( <>
Latitude: {observation.latitude} Longitude: {observation.longitude} {observation.accuracy !== undefined && observation.accuracy !== null && observation.accuracy !== '' && ( Accuracy: {observation.accuracy} m )}
) : (

No coordinates recorded for this observation.

)}
); } function TonightVisibleObjects() { const { fetch: proxyFetch, loading, error } = useProxy(); const [objects, setObjects] = useState([]); const [status, setStatus] = useState(''); const loadObjects = async () => { setStatus('Loading tonight’s visible objects...'); try { const data = await proxyFetch('https://api.deplixo.ai/astronomy/tonight-visible-objects', { method: 'GET' }); const list = Array.isArray(data?.objects) ? data.objects : (Array.isArray(data) ? data : []); setObjects(list); setStatus(list.length ? '' : 'No visible objects were returned for tonight.'); } catch (err) { setObjects([]); setStatus('Unable to load tonight’s visible objects.'); } }; return (

Tonight’s Visible Objects

Powered by the live proxy feed.

{error &&

{error}

} {status &&

{status}

}
{objects.map((item, index) => (

{item?.name || item?.title || 'Unknown object'}

{item?.type && {item.type}}
{item?.description &&

{item.description}

}
{item?.bestTime && Best time: {item.bestTime}} {item?.altitude && Altitude: {item.altitude}} {item?.visibility && Visibility: {item.visibility}}
))}
{!objects.length && !loading && !status &&

Tap refresh to load tonight’s list.

}
); } function AIObservationAssist({ initialWhatSeen = '', initialNotes = '', onApply }) { const { generate, loading, error } = useAI(); const [whatSeen, setWhatSeen] = useState(initialWhatSeen); const [notes, setNotes] = useState(initialNotes); const [result, setResult] = useState(null); const handleAnalyze = async () => { const response = await generate({ system: 'You are an astronomy assistant. Given a user observation, suggest a concise improved description and likely object IDs/names. Return JSON with keys: suggestedDescription, likelyObjects (array of objects with name, id, confidence, reason), followUpQuestions (array of strings).', user: JSON.stringify({ whatSeen, notes }), json: true }); setResult(response || null); if (onApply && response?.suggestedDescription) { onApply(response); } }; return (

AI-assisted identification

Describe what you saw and get a suggested observation summary plus likely object matches.