From ebd3beac5bd34bdceaf5ab83717eb5ead7653ef5 Mon Sep 17 00:00:00 2001 From: Vitaliy Bravikov Date: Mon, 28 Sep 2026 13:24:04 +0200 Subject: [PATCH 1/5] Current first version of README file with specification of the project, needs to be adjusted later --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index 748634b..c35e09c 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,36 @@ Before a pull request can be merged: 3. **Pushing new commits dismisses the approval**, so if you change something after being approved, ask for another review. This is deliberate: what was approved is what gets merged. + +## Project Description +The project is about observed UFO sightings. +Our group found an interesting dataset about observed UFO sightings which is about 80k observations. +This infromation is used to create an application which allows to perform search, and view the infromation about any UFO sighting. +Described further in planned functionality. + +## Planned Functionality +The planned functionality is following: + +Landing page with one 'Daily' chosen UFO sighting. +The page does display information about the UFO sighting with a map of the location of observation. +Performed search to find UFO sightings. +The user can create a UFO sighting. +For future: +Maybe create an option for comments or up-votes on a UFO sighting. + + +## Stored User-Generated Data +Created UFO sightings. +TODO + + +## Outline Technology Choices +TODO + + +## AI Usage +TODO + + + + From 12eb8c06bccd35ffb554dc44f42009da226a68b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20M=C3=BCller-Grud?= Date: Thu, 1 Oct 2026 10:31:05 +0200 Subject: [PATCH 2/5] Feat: add sighting detail page with routing ADd react-router with hash URLs so each sighting has its own page at /#/sightings/:id. The detail page shows a bigger map then the listview, but as of now contains all the same content. Store the search query and sort order in the URL so they survive navigation and refresh. Also move the map and formatters out of SightingList into shared files so both view can use them. Refs #9 --- frontend/package.json | 3 +- frontend/src/App.tsx | 19 ++--- frontend/src/components/SightingList.css | 36 +++++---- frontend/src/components/SightingList.tsx | 94 +++++---------------- frontend/src/components/SightingMap.css | 9 +++ frontend/src/components/SightingMap.tsx | 72 +++++++++++++++++ frontend/src/hooks/use-sighting-search.ts | 38 +++++++-- frontend/src/hooks/use-sighting.ts | 10 +++ frontend/src/main.tsx | 19 +++-- frontend/src/pages/SearchPage.tsx | 21 +++++ frontend/src/pages/SightingDetailPage.css | 69 ++++++++++++++++ frontend/src/pages/SightingDetailPage.tsx | 99 +++++++++++++++++++++++ frontend/src/types/ufo-sighting.ts | 17 +++- frontend/src/utils/format-sighting.ts | 26 ++++++ package-lock.json | 37 ++++++++- 15 files changed, 454 insertions(+), 115 deletions(-) create mode 100644 frontend/src/components/SightingMap.css create mode 100644 frontend/src/components/SightingMap.tsx create mode 100644 frontend/src/hooks/use-sighting.ts create mode 100644 frontend/src/pages/SearchPage.tsx create mode 100644 frontend/src/pages/SightingDetailPage.css create mode 100644 frontend/src/pages/SightingDetailPage.tsx create mode 100644 frontend/src/utils/format-sighting.ts diff --git a/frontend/package.json b/frontend/package.json index de92545..26173b5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,7 +14,8 @@ "leaflet": "^1.9.4", "react": "^19.2.8", "react-dom": "^19.2.8", - "react-leaflet": "^5.0.0" + "react-leaflet": "^5.0.0", + "react-router": "^8.4.0" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9eff901..67530db 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,20 +1,15 @@ -import SearchBar from './components/SearchBar' -import SightingList from './components/SightingList' -import { useSightingSearch } from './hooks/use-sighting-search' +import { Route, Routes } from 'react-router' +import SearchPage from './pages/SearchPage' +import SightingDetailPage from './pages/SightingDetailPage' import './App.css' function App() { - const { query, setQuery, sort, setSort, results } = useSightingSearch() - return (
- - + + } /> + } /> +
) } diff --git a/frontend/src/components/SightingList.css b/frontend/src/components/SightingList.css index d65a68b..8a00dca 100644 --- a/frontend/src/components/SightingList.css +++ b/frontend/src/components/SightingList.css @@ -37,16 +37,9 @@ padding: 24px 0; } -.sighting-map { +.sighting-list__item .sighting-map, +.sighting-list__item .sighting-map .leaflet-container { min-height: 180px; - overflow: hidden; - position: relative; -} - -.sighting-map .leaflet-container { - height: 100%; - min-height: 180px; - width: 100%; } .sighting-list__content { @@ -64,6 +57,21 @@ text-transform: capitalize; } +.sighting-list__topline a { + color: inherit; + text-decoration-color: var(--border); + text-underline-offset: 4px; + + &:hover { + text-decoration-color: currentColor; + } + + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + .sighting-list__topline time { color: var(--text); flex: 0 0 auto; @@ -119,8 +127,8 @@ grid-template-columns: 140px minmax(0, 1fr); } - .sighting-map, - .sighting-map .leaflet-container { + .sighting-list__item .sighting-map, + .sighting-list__item .sighting-map .leaflet-container { min-height: 140px; } } @@ -130,8 +138,8 @@ grid-template-columns: 1fr; } - .sighting-map, - .sighting-map .leaflet-container { + .sighting-list__item .sighting-map, + .sighting-list__item .sighting-map .leaflet-container { min-height: 180px; } -} \ No newline at end of file +} diff --git a/frontend/src/components/SightingList.tsx b/frontend/src/components/SightingList.tsx index 748cebf..341112c 100644 --- a/frontend/src/components/SightingList.tsx +++ b/frontend/src/components/SightingList.tsx @@ -1,80 +1,20 @@ -import L from 'leaflet' -import { - MapContainer, - Marker, - Popup, - TileLayer, -} from 'react-leaflet' +import { Link, useLocation } from 'react-router' import type { UfoSighting } from '../types/ufo-sighting' +import { + formatDate, + formatDuration, + formatLocation, +} from '../utils/format-sighting' +import SightingMap from './SightingMap' import './SightingList.css' -import 'leaflet/dist/leaflet.css' - -import markerIcon from 'leaflet/dist/images/marker-icon.png' -import markerIconRetina from 'leaflet/dist/images/marker-icon-2x.png' -import markerShadow from 'leaflet/dist/images/marker-shadow.png' - -const marker = L.icon({ - iconUrl: markerIcon, - iconRetinaUrl: markerIconRetina, - shadowUrl: markerShadow, - iconSize: [25, 41], - iconAnchor: [12, 41], - popupAnchor: [1, -34], - shadowSize: [41, 41], -}) interface SightingListProps { sightings: UfoSighting[] } -function formatDate(date: string) { - return new Intl.DateTimeFormat('en', { - dateStyle: 'medium', - }).format(new Date(date)) -} - -function formatDuration(seconds: number) { - if (seconds < 60) return `${seconds} sec` - - const minutes = Math.floor(seconds / 60) - const remainingSeconds = seconds % 60 - if (remainingSeconds === 0) return `${minutes} min` - - return `${minutes} min ${remainingSeconds} sec` -} - -function formatLocation(sighting: UfoSighting) { - const region = [sighting.state, sighting.country] - .filter(Boolean) - .join(', ') - .toUpperCase() - - return region ? `${sighting.city}, ${region}` : sighting.city -} - -function SightingMap({ sighting }: { sighting: UfoSighting }) { - return ( -
- - - - {formatLocation(sighting)} - - -
- ) -} - function SightingList({ sightings }: SightingListProps) { + const location = useLocation() + return (
@@ -86,7 +26,8 @@ function SightingList({ sightings }: SightingListProps) { {sightings.length === 0 ? (

- No sightings match your search. Try another city, shape or description. + No sightings match your search. Try another city, shape or + description.

) : (
    @@ -95,7 +36,16 @@ function SightingList({ sightings }: SightingListProps) {
    -

    {formatLocation(sighting)}

    +

    + {/* Carries the current search so the detail page can + link back to the same results. */} + + {formatLocation(sighting)} + +

    @@ -120,4 +70,4 @@ function SightingList({ sightings }: SightingListProps) { ) } -export default SightingList \ No newline at end of file +export default SightingList diff --git a/frontend/src/components/SightingMap.css b/frontend/src/components/SightingMap.css new file mode 100644 index 0000000..8a3eb6f --- /dev/null +++ b/frontend/src/components/SightingMap.css @@ -0,0 +1,9 @@ +.sighting-map { + overflow: hidden; + position: relative; +} + +.sighting-map .leaflet-container { + height: 100%; + width: 100%; +} diff --git a/frontend/src/components/SightingMap.tsx b/frontend/src/components/SightingMap.tsx new file mode 100644 index 0000000..9af6281 --- /dev/null +++ b/frontend/src/components/SightingMap.tsx @@ -0,0 +1,72 @@ +import L from 'leaflet' +import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet' +import type { UfoSighting } from '../types/ufo-sighting' +import { formatLocation } from '../utils/format-sighting' +import 'leaflet/dist/leaflet.css' +import './SightingMap.css' + +import markerIcon from 'leaflet/dist/images/marker-icon.png' +import markerIconRetina from 'leaflet/dist/images/marker-icon-2x.png' +import markerShadow from 'leaflet/dist/images/marker-shadow.png' + +// Leaflet's default icon resolves image paths at runtime, which breaks +// under Vite's bundling, so the icon is built from imported assets. +const marker = L.icon({ + iconUrl: markerIcon, + iconRetinaUrl: markerIconRetina, + shadowUrl: markerShadow, + iconSize: [25, 41], + iconAnchor: [12, 41], + popupAnchor: [1, -34], + shadowSize: [41, 41], +}) + +interface SightingMapProps { + sighting: UfoSighting + /** Static thumbnails in lists; pannable and zoomable on the detail page. */ + interactive?: boolean + zoom?: number + className?: string +} + +function SightingMap({ + sighting, + interactive = false, + zoom = 10, + className, +}: SightingMapProps) { + const position: [number, number] = [sighting.latitude, sighting.longitude] + + return ( +
    + + + + {formatLocation(sighting)} + + +
    + ) +} + +export default SightingMap diff --git a/frontend/src/hooks/use-sighting-search.ts b/frontend/src/hooks/use-sighting-search.ts index 3762a36..5b62139 100644 --- a/frontend/src/hooks/use-sighting-search.ts +++ b/frontend/src/hooks/use-sighting-search.ts @@ -1,19 +1,45 @@ -import { useMemo, useState } from 'react' +import { useMemo } from 'react' +import { useSearchParams } from 'react-router' import { mockSightings } from '../data/mock-sightings' -import type { SightingSortOption } from '../types/ufo-sighting' +import { + isSightingSortOption, + type SightingSortOption, +} from '../types/ufo-sighting' import { searchSightings } from '../utils/filter-sightings' import { useDebouncedValue } from './use-debounced-value' const SEARCH_DEBOUNCE_MS = 300 +const DEFAULT_SORT: SightingSortOption = 'date-desc' /** * Owns the search query and sort order and derives the matching sightings. - * Kept out of the page components so the search bar and the result view - * can share it, and so the data source can later be swapped for GraphQL. + * Both live in the URL (?q=...&sort=...) so they survive navigating to a + * detail page and back, a page refresh, and can be shared as a link. */ export function useSightingSearch() { - const [query, setQuery] = useState('') - const [sort, setSort] = useState('date-desc') + const [searchParams, setSearchParams] = useSearchParams() + + const query = searchParams.get('q') ?? '' + const sortParam = searchParams.get('sort') + const sort = isSightingSortOption(sortParam) ? sortParam : DEFAULT_SORT + + // Replace instead of push so each keystroke doesn't add a history entry. + function updateParam(key: string, value: string, defaultValue: string) { + setSearchParams( + (previous) => { + const next = new URLSearchParams(previous) + if (value === defaultValue) next.delete(key) + else next.set(key, value) + return next + }, + { replace: true }, + ) + } + + const setQuery = (nextQuery: string) => updateParam('q', nextQuery, '') + const setSort = (nextSort: SightingSortOption) => + updateParam('sort', nextSort, DEFAULT_SORT) + const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS) const results = useMemo( diff --git a/frontend/src/hooks/use-sighting.ts b/frontend/src/hooks/use-sighting.ts new file mode 100644 index 0000000..491a7ed --- /dev/null +++ b/frontend/src/hooks/use-sighting.ts @@ -0,0 +1,10 @@ +import { mockSightings } from '../data/mock-sightings' +import type { UfoSighting } from '../types/ufo-sighting' + +/** + * Looks up a single sighting by id. Wrapped in a hook so the detail page + * won't need to change when this becomes a GraphQL query. + */ +export function useSighting(id: string | undefined): UfoSighting | undefined { + return mockSightings.find((sighting) => sighting.id === id) +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index eff7ccc..b10eb49 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,15 @@ -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import "./index.css"; -import App from "./App.tsx"; +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { HashRouter } from 'react-router' +import './index.css' +import App from './App.tsx' -createRoot(document.getElementById("root")!).render( +// Hash URLs (/project2/#/sightings/1) need no server rewrite rules, so +// refreshing or sharing a deep link works on the course VM as-is. +createRoot(document.getElementById('root')!).render( - + + + , -); +) diff --git a/frontend/src/pages/SearchPage.tsx b/frontend/src/pages/SearchPage.tsx new file mode 100644 index 0000000..450ca9a --- /dev/null +++ b/frontend/src/pages/SearchPage.tsx @@ -0,0 +1,21 @@ +import SearchBar from '../components/SearchBar' +import SightingList from '../components/SightingList' +import { useSightingSearch } from '../hooks/use-sighting-search' + +function SearchPage() { + const { query, setQuery, sort, setSort, results } = useSightingSearch() + + return ( + <> + + + + ) +} + +export default SearchPage diff --git a/frontend/src/pages/SightingDetailPage.css b/frontend/src/pages/SightingDetailPage.css new file mode 100644 index 0000000..3e52210 --- /dev/null +++ b/frontend/src/pages/SightingDetailPage.css @@ -0,0 +1,69 @@ +.sighting-detail { + display: grid; + gap: 24px; +} + +.sighting-detail__back { + justify-self: start; + color: var(--accent); + text-underline-offset: 4px; + + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.sighting-detail h1 { + font-size: 40px; + margin: 0 0 4px; + text-transform: capitalize; +} + +.sighting-detail__map, +.sighting-detail__map .leaflet-container { + height: 420px; + border-radius: 8px; +} + +.sighting-detail__facts { + display: flex; + flex-wrap: wrap; + gap: 12px 32px; + margin: 0; + + div { + display: flex; + gap: 6px; + } + + dt { + color: var(--text); + } + + dd { + margin: 0; + color: var(--text-h); + text-transform: capitalize; + } +} + +.sighting-detail__comments { + max-width: 76ch; + color: var(--text-h); +} + +@media (max-width: 640px) { + .sighting-detail { + gap: 16px; + } + + .sighting-detail h1 { + font-size: 28px; + } + + .sighting-detail__map, + .sighting-detail__map .leaflet-container { + height: 280px; + } +} diff --git a/frontend/src/pages/SightingDetailPage.tsx b/frontend/src/pages/SightingDetailPage.tsx new file mode 100644 index 0000000..d77c0a1 --- /dev/null +++ b/frontend/src/pages/SightingDetailPage.tsx @@ -0,0 +1,99 @@ +import { useEffect } from 'react' +import { Link, useLocation, useParams } from 'react-router' +import SightingMap from '../components/SightingMap' +import { useSighting } from '../hooks/use-sighting' +import { + formatDate, + formatDuration, + formatLocation, +} from '../utils/format-sighting' +import './SightingDetailPage.css' + +/** + * The list passes its search string in navigation state so "Back to + * results" restores the same search. Opened directly (e.g. a shared link), + * there is no state and the link goes to the unfiltered list. + */ +function getFromSearch(state: unknown): string { + if ( + typeof state === 'object' && + state !== null && + 'fromSearch' in state && + typeof state.fromSearch === 'string' + ) { + return state.fromSearch + } + return '' +} + +function SightingDetailPage() { + const { id } = useParams() + const sighting = useSighting(id) + const location = useLocation() + const backTo = { pathname: '/', search: getFromSearch(location.state) } + const title = sighting ? formatLocation(sighting) : 'Sighting not found' + + // Route changes keep the list's scroll position, which would open the + // detail page scrolled past its heading. + useEffect(() => { + window.scrollTo(0, 0) + }, [id]) + + useEffect(() => { + const previousTitle = document.title + document.title = `${title} ยท UFOund it` + return () => { + document.title = previousTitle + } + }, [title]) + + const backLink = ( + + Back to results + + ) + + if (!sighting) { + return ( +
    + {backLink} +

    {title}

    +

    There is no sighting with id โ€œ{id}โ€.

    +
    + ) + } + + return ( +
    + {backLink} +
    +

    {title}

    + +
    + + + +
    +
    +
    Shape
    +
    {sighting.shape}
    +
    +
    +
    Duration
    +
    {formatDuration(sighting.durationSeconds)}
    +
    +
    + +

    {sighting.comments}

    +
    + ) +} + +export default SightingDetailPage diff --git a/frontend/src/types/ufo-sighting.ts b/frontend/src/types/ufo-sighting.ts index cd3e2b1..3689dc3 100644 --- a/frontend/src/types/ufo-sighting.ts +++ b/frontend/src/types/ufo-sighting.ts @@ -22,5 +22,18 @@ export interface UfoSighting { longitude: number } -export type SightingSortOption = - 'date-desc' | 'date-asc' | 'city-asc' | 'shape-asc' +// Kept as a runtime list so values read from the URL can be validated. +export const sightingSortOptions = [ + 'date-desc', + 'date-asc', + 'city-asc', + 'shape-asc', +] as const + +export type SightingSortOption = (typeof sightingSortOptions)[number] + +export function isSightingSortOption( + value: string | null, +): value is SightingSortOption { + return sightingSortOptions.some((option) => option === value) +} diff --git a/frontend/src/utils/format-sighting.ts b/frontend/src/utils/format-sighting.ts new file mode 100644 index 0000000..db18eec --- /dev/null +++ b/frontend/src/utils/format-sighting.ts @@ -0,0 +1,26 @@ +import type { UfoSighting } from '../types/ufo-sighting' + +export function formatDate(date: string) { + return new Intl.DateTimeFormat('en', { + dateStyle: 'medium', + }).format(new Date(date)) +} + +export function formatDuration(seconds: number) { + if (seconds < 60) return `${seconds} sec` + + const minutes = Math.floor(seconds / 60) + const remainingSeconds = seconds % 60 + if (remainingSeconds === 0) return `${minutes} min` + + return `${minutes} min ${remainingSeconds} sec` +} + +export function formatLocation(sighting: UfoSighting) { + const region = [sighting.state, sighting.country] + .filter(Boolean) + .join(', ') + .toUpperCase() + + return region ? `${sighting.city}, ${region}` : sighting.city +} diff --git a/package-lock.json b/package-lock.json index 6788c23..4b44ffc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "leaflet": "^1.9.4", "react": "^19.2.8", "react-dom": "^19.2.8", - "react-leaflet": "^5.0.0" + "react-leaflet": "^5.0.0", + "react-router": "^8.4.0" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -836,6 +837,12 @@ "react-dom": "^19.0.0" } }, + "node_modules/@remix-run/route-pattern": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/@remix-run/route-pattern/-/route-pattern-0.22.1.tgz", + "integrity": "sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==", + "license": "MIT" + }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.11", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", @@ -1837,6 +1844,12 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3281,6 +3294,28 @@ "react-dom": "^19.0.0" } }, + "node_modules/react-router": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.4.0.tgz", + "integrity": "sha512-JtydAkBvU9UTEe5qNC+POhu+ZBNM7rlKY2IegwXc7Idj7xfRG16x5RZrw3mju+XlqBxfvb2ky9P3jUp9WyWC1g==", + "license": "MIT", + "dependencies": { + "@remix-run/route-pattern": "^0.22.1", + "cookie-es": "^3.1.1" + }, + "engines": { + "node": ">=22.22.0" + }, + "peerDependencies": { + "react": ">=19.2.7", + "react-dom": ">=19.2.7" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", From c389ea723c37d0660b4b80efb78208c56d4bdccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20M=C3=BCller-Grud?= Date: Thu, 1 Oct 2026 10:36:16 +0200 Subject: [PATCH 3/5] Feat: make everything in a sighting from the listview clickable Clicking the map, title or anything in the sighting listview now navigates to the detailed view for that sighting. Refs #9 --- frontend/src/components/SightingList.css | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frontend/src/components/SightingList.css b/frontend/src/components/SightingList.css index 8a00dca..a5d59c1 100644 --- a/frontend/src/components/SightingList.css +++ b/frontend/src/components/SightingList.css @@ -35,6 +35,10 @@ gap: 24px; grid-template-columns: 240px minmax(0, 1fr); padding: 24px 0; + /* Anchors the heading link's overlay and keeps its z-index (needed to + sit above Leaflet's panes) from leaking outside the item. */ + position: relative; + isolation: isolate; } .sighting-list__item .sighting-map, @@ -62,6 +66,16 @@ text-decoration-color: var(--border); text-underline-offset: 4px; + /* Stretches the link over the whole item so clicking the map or text + opens the sighting, while it stays a single link for keyboard and + screen reader users. Leaflet's controls use z-index 1000. */ + &::after { + content: ''; + position: absolute; + inset: 0; + z-index: 1001; + } + &:hover { text-decoration-color: currentColor; } From fbbaee49954be352524f513ddb5803eb3efd8016 Mon Sep 17 00:00:00 2001 From: Thor Tveito Date: Thu, 1 Oct 2026 11:03:00 +0200 Subject: [PATCH 4/5] Update App.tsx --- frontend/src/App.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c7f74c7..111efb5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,7 +13,6 @@ function App() { } /> - <>
    Date: Thu, 1 Oct 2026 11:06:54 +0200 Subject: [PATCH 5/5] Fix: Repair App routing layout --- frontend/src/App.tsx | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 111efb5..a07ce72 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,27 +1,18 @@ import { Route, Routes } from 'react-router' import Banner from './components/Banner' -import SearchBar from './components/SearchBar' -import SightingList from './components/SightingList' -import { useSightingSearch } from './hooks/use-sighting-search' +import SearchPage from './pages/SearchPage' +import SightingDetailPage from './pages/SightingDetailPage' import './App.css' function App() { return ( -
    - - } /> - } /> - -
    + <>
    - - + + } /> + } /> +
    )