diff --git a/src/components/BottomNav.tsx b/src/components/BottomNav.tsx index feccf3d..f7b96d8 100644 --- a/src/components/BottomNav.tsx +++ b/src/components/BottomNav.tsx @@ -1,5 +1,5 @@ -import { Home, List, MapPin, Settings } from 'lucide-react'; -import { clsx } from 'clsx'; +import { Home, List, MapPin, Settings } from "lucide-react"; +import { clsx } from "clsx"; interface BottomNavProps { currentTab: string; @@ -8,10 +8,10 @@ interface BottomNavProps { export function BottomNav({ currentTab, onChange }: BottomNavProps) { const tabs = [ - { id: 'record', icon: Home, label: 'Record' }, - { id: 'spottings', icon: List, label: 'Spottings' }, - { id: 'places', icon: MapPin, label: 'Places' }, - { id: 'settings', icon: Settings, label: 'Settings' }, + { id: "record", icon: Home, label: "Tallenna" }, + { id: "spottings", icon: List, label: "Havainnot" }, + { id: "places", icon: MapPin, label: "Paikat" }, + { id: "settings", icon: Settings, label: "Asetukset" }, ]; return ( @@ -25,8 +25,10 @@ export function BottomNav({ currentTab, onChange }: BottomNavProps) { key={tab.id} onClick={() => onChange(tab.id)} className={clsx( - 'flex flex-col items-center justify-center w-full h-full space-y-1 transition-colors', - isActive ? 'text-emerald-600 dark:text-emerald-500' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-300' + "flex flex-col items-center justify-center w-full h-full space-y-1 transition-colors", + isActive + ? "text-emerald-600 dark:text-emerald-500" + : "text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-300", )} > diff --git a/src/screens/RecordScreen.tsx b/src/screens/RecordScreen.tsx index a0873a2..d6de8c5 100644 --- a/src/screens/RecordScreen.tsx +++ b/src/screens/RecordScreen.tsx @@ -1,51 +1,60 @@ -import { useState, useEffect } from 'react'; -import { useLiveQuery } from 'dexie-react-hooks'; -import { Bird, db, CustomPlace } from '../database'; -import { haversineDistance, reverseGeocode } from '../utils'; -import { useAppContext } from '../AppContext'; -import { Search, MapPin, CheckCircle, Loader2 } from 'lucide-react'; -import { motion, AnimatePresence } from 'motion/react'; +import { useState, useEffect } from "react"; +import { useLiveQuery } from "dexie-react-hooks"; +import { Bird, db, CustomPlace } from "../database"; +import { haversineDistance, reverseGeocode } from "../utils"; +import { useAppContext } from "../AppContext"; +import { Search, MapPin, CheckCircle, Loader2 } from "lucide-react"; +import { motion, AnimatePresence } from "motion/react"; export function RecordScreen() { const { language } = useAppContext(); - const [query, setQuery] = useState(''); + const [query, setQuery] = useState(""); const [selectedBird, setSelectedBird] = useState(null); const [isRecording, setIsRecording] = useState(false); - const [successMsg, setSuccessMsg] = useState(''); - const [locationStatus, setLocationStatus] = useState<'idle' | 'fetching' | 'error'>('idle'); + const [successMsg, setSuccessMsg] = useState(""); + const [locationStatus, setLocationStatus] = useState< + "idle" | "fetching" | "error" + >("idle"); - const searchResults = useLiveQuery( - () => { - if (!query) return []; - const lowerQuery = query.toLowerCase(); - return db.birds.filter(b => b.searchIndex.includes(lowerQuery)).limit(10).toArray(); - }, - [query] - ); + const searchResults = useLiveQuery(() => { + if (!query) return []; + const lowerQuery = query.toLowerCase(); + return db.birds + .filter((b) => b.searchIndex.includes(lowerQuery)) + .limit(10) + .toArray(); + }, [query]); const handleRecord = async () => { if (!selectedBird) return; setIsRecording(true); - setLocationStatus('fetching'); + setLocationStatus("fetching"); try { - const position = await new Promise((resolve, reject) => { - navigator.geolocation.getCurrentPosition(resolve, reject, { - enableHighAccuracy: true, - timeout: 10000, - maximumAge: 0 - }); - }); + const position = await new Promise( + (resolve, reject) => { + navigator.geolocation.getCurrentPosition(resolve, reject, { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 0, + }); + }, + ); const { latitude, longitude } = position.coords; - setLocationStatus('idle'); + setLocationStatus("idle"); // Silent Place Match const places = await db.customPlaces.toArray(); let matchedPlaceId: number | undefined; for (const place of places) { - const dist = haversineDistance(latitude, longitude, place.latitude, place.longitude); + const dist = haversineDistance( + latitude, + longitude, + place.latitude, + place.longitude, + ); if (dist <= place.radiusMeters) { matchedPlaceId = place.id; break; @@ -57,14 +66,16 @@ export function RecordScreen() { timestamp: Date.now(), latitude, longitude, - customPlaceId: matchedPlaceId + customPlaceId: matchedPlaceId, }); - setSuccessMsg(`Recorded ${language === 'fi' ? selectedBird.finnishName : selectedBird.englishName}!`); + setSuccessMsg( + `Recorded ${language === "fi" ? selectedBird.finnishName : selectedBird.englishName}!`, + ); setSelectedBird(null); - setQuery(''); - - setTimeout(() => setSuccessMsg(''), 3000); + setQuery(""); + + setTimeout(() => setSuccessMsg(""), 3000); // Async Reverse Geocode reverseGeocode(latitude, longitude).then(async (name) => { @@ -72,11 +83,12 @@ export function RecordScreen() { await db.spottings.update(spottingId, { reverseGeocodedName: name }); } }); - } catch (error) { console.error(error); - setLocationStatus('error'); - alert('Failed to get location. Please ensure location services are enabled.'); + setLocationStatus("error"); + alert( + "Failed to get location. Please ensure location services are enabled.", + ); } finally { setIsRecording(false); } @@ -84,8 +96,10 @@ export function RecordScreen() { return (
-

Record Spotting

- +

+ Tallenna havainto +

+
@@ -97,29 +111,38 @@ export function RecordScreen() { setQuery(e.target.value); if (selectedBird) setSelectedBird(null); }} - placeholder={language === 'fi' ? 'Etsi lintua...' : 'Search for a bird...'} + placeholder={ + language === "fi" ? "Etsi lintua..." : "Search for a bird..." + } className="w-full pl-10 pr-4 py-3 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl shadow-sm focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 outline-none transition-all dark:text-white" /> - - {query && !selectedBird && searchResults && searchResults.length > 0 && ( -
- {searchResults.map(bird => ( - - ))} -
- )} + + {query && + !selectedBird && + searchResults && + searchResults.length > 0 && ( +
+ {searchResults.map((bird) => ( + + ))} +
+ )}
@@ -130,16 +153,20 @@ export function RecordScreen() { exit={{ opacity: 0, y: -10 }} className="flex flex-col items-center bg-white dark:bg-zinc-900 p-6 rounded-2xl shadow-sm border border-zinc-200 dark:border-zinc-800" > - {selectedBird.englishName}

- {language === 'fi' ? selectedBird.finnishName : selectedBird.englishName} + {language === "fi" + ? selectedBird.finnishName + : selectedBird.englishName}

-

{selectedBird.scientificName}

+

+ {selectedBird.scientificName} +

diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index fc6e219..860537e 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -1,39 +1,41 @@ -import { useState } from 'react'; -import { useAppContext } from '../AppContext'; -import { db } from '../database'; -import { Download, Upload, Globe, Moon, Sun, Monitor } from 'lucide-react'; +import { useState } from "react"; +import { useAppContext } from "../AppContext"; +import { db } from "../database"; +import { Download, Upload, Globe, Moon, Sun, Monitor } from "lucide-react"; export function SettingsScreen() { const { language, setLanguage, themeMode, setThemeMode } = useAppContext(); - const [backupStatus, setBackupStatus] = useState(''); + const [backupStatus, setBackupStatus] = useState(""); const handleExport = async () => { try { const spottings = await db.spottings.toArray(); const places = await db.customPlaces.toArray(); - + const backup = { version: 1, timestamp: new Date().toISOString(), spottings, - places + places, }; - const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' }); + const blob = new Blob([JSON.stringify(backup, null, 2)], { + type: "application/json", + }); const url = URL.createObjectURL(blob); - const a = document.createElement('a'); + const a = document.createElement("a"); a.href = url; - a.download = `birdspotter_backup_${new Date().toISOString().split('T')[0]}.json`; + a.download = `birdspotter_backup_${new Date().toISOString().split("T")[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - - setBackupStatus('Export successful!'); - setTimeout(() => setBackupStatus(''), 3000); + + setBackupStatus("Export successful!"); + setTimeout(() => setBackupStatus(""), 3000); } catch (error) { console.error(error); - setBackupStatus('Export failed.'); + setBackupStatus("Export failed."); } }; @@ -54,27 +56,31 @@ export function SettingsScreen() { await db.customPlaces.bulkAdd(backup.places); } - setBackupStatus('Import successful!'); - setTimeout(() => setBackupStatus(''), 3000); + setBackupStatus("Import successful!"); + setTimeout(() => setBackupStatus(""), 3000); } catch (error) { console.error(error); - setBackupStatus('Import failed. Invalid file format.'); + setBackupStatus("Import failed. Invalid file format."); } }; return (
-

Settings

+

+ Asetukset +

{/* Appearance */}
-

Appearance

+

+ Ulkoasu +

- Language + Kieli
- +
- {themeMode === 'dark' ? : - themeMode === 'light' ? : - } - Theme + {themeMode === "dark" ? ( + + ) : themeMode === "light" ? ( + + ) : ( + + )} + Teema