-
Notifications
You must be signed in to change notification settings - Fork 5
Admin address search #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,6 @@ | ||
| import { | ||
| Route, | ||
| Router, | ||
| useLocation, | ||
| useNavigate, | ||
| useParams | ||
| useNavigate | ||
| } from '@solidjs/router'; | ||
| import { onMount, Show, For } from 'solid-js'; | ||
| import { createStore } from 'solid-js/store'; | ||
|
|
@@ -96,8 +93,12 @@ export default function Admin() { | |
| }); | ||
|
|
||
| function Model() { | ||
| const params = useParams(); | ||
| const model = () => models.find(m => m.key === params.model); | ||
| const modelKey = () => { | ||
| const path = location.pathname; | ||
| const match = path.match(/\/admin\/model\/(.+)/); | ||
| return match ? match[1] : ''; | ||
| }; | ||
| const model = () => models.find(m => m.key === modelKey()); | ||
|
|
||
| return ( | ||
| <Container> | ||
|
|
@@ -135,42 +136,35 @@ export default function Admin() { | |
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <> | ||
| <Router> | ||
| <Route | ||
| path="/login" | ||
| element={ | ||
| <form | ||
| onSubmit={login} | ||
| style={{ | ||
| position: 'absolute', | ||
| top: '50%', | ||
| left: '50%', | ||
| transform: 'translateY(-50%) translateX(-50%)' | ||
| }} | ||
| > | ||
| <Input | ||
| type="password" | ||
| label="Password" | ||
| autoComplete="current-password" | ||
| /> | ||
| | ||
| <Button type="submit" color="primary"> | ||
| Log in | ||
| </Button> | ||
| </form> | ||
| } | ||
| const currentPath = location.pathname; | ||
|
|
||
| if (currentPath === '/admin/login' || currentPath === '/admin/') { | ||
| return ( | ||
| <form | ||
| onSubmit={login} | ||
| style={{ | ||
| position: 'absolute', | ||
| top: '50%', | ||
| left: '50%', | ||
| transform: 'translateY(-50%) translateX(-50%)' | ||
| }} | ||
| > | ||
| <Input | ||
| type="password" | ||
| label="Password" | ||
| autoComplete="current-password" | ||
| /> | ||
| <Route path="/model/:model" component={Model} /> | ||
| </Router> | ||
| {/* <Snackbar | ||
| autoHideDuration={4000} | ||
| anchorOrigin={{ vertical: 'top', horizontal: 'center' }} | ||
| onClose={this.clearMessage} | ||
| message={<span>{message}</span>} | ||
| open={messageVisible} | ||
| /> */} | ||
| </> | ||
| ); | ||
| | ||
| <Button type="submit" color="primary"> | ||
| Log in | ||
| </Button> | ||
| </form> | ||
| ); | ||
| } | ||
|
|
||
| if (currentPath.startsWith('/admin/model/')) { | ||
| return <Model />; | ||
|
Comment on lines
+139
to
+166
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Make admin view react to router path changes Caching Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| return <div>Admin route not found</div>; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { createResource, createSignal, For, Show } from 'solid-js'; | ||
| import LatLngInput from '../components/LatLngInput'; | ||
| import OpeningHoursInput from '../components/OpeningHoursInput'; | ||
| import AddressSearchInput from '../components/AddressSearchInput'; | ||
| import * as api from './api'; | ||
| import models from './models'; | ||
| import { | ||
|
|
@@ -28,6 +29,10 @@ interface GroupInputProps { | |
| setValue(path: string, value: any): any; | ||
| } | ||
|
|
||
| interface AddressInputProps extends InputProps { | ||
| onCoordinatesChange?: (lat: number, lng: number) => void; | ||
| } | ||
|
|
||
| const Row = styled.div` | ||
| display: flex; | ||
| gap: 1rem; | ||
|
|
@@ -83,13 +88,20 @@ const MenuUrlInput = (props: InputProps) => { | |
| ); | ||
| }; | ||
|
|
||
| function AddressInput(props: InputProps) { | ||
| function AddressInput(props: AddressInputProps) { | ||
| return ( | ||
| <Input | ||
| onChange={value => props.setValue(props.field.path, value)} | ||
| <AddressSearchInput | ||
| value={props.value || ''} | ||
| label={props.field.title} | ||
| type="text" | ||
| onChange={value => props.setValue(props.field.path, value)} | ||
| onCoordinatesChange={(lat, lng) => { | ||
| if (props.onCoordinatesChange) { | ||
| props.onCoordinatesChange(lat, lng); | ||
| } | ||
| // Update coordinates directly if available | ||
| props.setValue('latitude', lat); | ||
| props.setValue('longitude', lng); | ||
|
Comment on lines
+102
to
+103
|
||
| }} | ||
| /> | ||
| ); | ||
| } | ||
|
|
@@ -166,6 +178,9 @@ const LocationInput = (props: GroupInputProps) => { | |
| props.setValue('longitude', v[1]); | ||
| } | ||
| }} | ||
| onAddressChange={(address: string) => { | ||
| props.setValue('address', address); | ||
| }} | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| import { createEffect, createSignal, For, Show } from 'solid-js'; | ||
| import { styled } from 'solid-styled-components'; | ||
| import Input from './Input'; | ||
| import { formatAddress, type AddressData } from '../utils/addressFormatter'; | ||
|
|
||
| interface NominatimResult extends AddressData { | ||
| place_id: number; | ||
| licence: string; | ||
| osm_type: string; | ||
| osm_id: number; | ||
| boundingbox: string[]; | ||
| lat: string; | ||
| lon: string; | ||
| class: string; | ||
| type: string; | ||
| importance: number; | ||
| } | ||
|
|
||
| interface Props { | ||
| value: string; | ||
| label: string; | ||
| onChange: (address: string) => void; | ||
| onCoordinatesChange?: (lat: number, lng: number) => void; | ||
| placeholder?: string; | ||
| } | ||
|
|
||
| const Container = styled.div` | ||
| position: relative; | ||
| `; | ||
|
|
||
| const ResultsList = styled.ul` | ||
| position: absolute; | ||
| top: 100%; | ||
| left: 0; | ||
| right: 0; | ||
| background: white; | ||
| border: 1px solid #ccc; | ||
| border-top: none; | ||
| border-radius: 0 0 4px 4px; | ||
| max-height: 200px; | ||
| overflow-y: auto; | ||
| z-index: 1000; | ||
| margin: 0; | ||
| padding: 0; | ||
| list-style: none; | ||
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| `; | ||
|
|
||
| const ResultItem = styled.li` | ||
| padding: 8px 12px; | ||
| cursor: pointer; | ||
| border-bottom: 1px solid #eee; | ||
|
|
||
| &:hover { | ||
| background-color: #f5f5f5; | ||
| } | ||
|
|
||
| &:last-child { | ||
| border-bottom: none; | ||
| } | ||
| `; | ||
|
|
||
| const LoadingMessage = styled.div` | ||
| padding: 8px 12px; | ||
| color: #666; | ||
| font-style: italic; | ||
| `; | ||
|
|
||
| const AddressSearchInput = (props: Props) => { | ||
| const [searchQuery, setSearchQuery] = createSignal(props.value); | ||
| const [results, setResults] = createSignal<NominatimResult[]>([]); | ||
| const [loading, setLoading] = createSignal(false); | ||
| const [showResults, setShowResults] = createSignal(false); | ||
| let searchTimeout: ReturnType<typeof setTimeout>; | ||
|
|
||
|
|
||
| const searchAddresses = async (query: string) => { | ||
| if (query.length < 3) { | ||
| setResults([]); | ||
| setShowResults(false); | ||
| return; | ||
| } | ||
|
|
||
| setLoading(true); | ||
|
|
||
| try { | ||
| const response = await fetch( | ||
| `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=5&addressdetails=1&countrycodes=fi` | ||
|
||
| ); | ||
|
|
||
| if (response.ok) { | ||
| const data: NominatimResult[] = await response.json(); | ||
|
|
||
| // Deduplicate results based on formatted address | ||
| const uniqueResults: NominatimResult[] = []; | ||
| const seenAddresses = new Set<string>(); | ||
|
|
||
| for (const result of data) { | ||
| const formattedAddr = formatAddress(result); | ||
| if (!seenAddresses.has(formattedAddr)) { | ||
| seenAddresses.add(formattedAddr); | ||
| uniqueResults.push(result); | ||
| } | ||
| } | ||
|
|
||
| setResults(uniqueResults); | ||
| setShowResults(true); | ||
| } else { | ||
| console.error('Geocoding request failed:', response.statusText); | ||
| setResults([]); | ||
| setShowResults(false); | ||
| } | ||
| } catch (error) { | ||
| console.error('Geocoding error:', error); | ||
| setResults([]); | ||
| setShowResults(false); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| const debouncedSearch = (query: string) => { | ||
| clearTimeout(searchTimeout); | ||
| searchTimeout = setTimeout(() => { | ||
| searchAddresses(query); | ||
| }, 300); | ||
| }; | ||
|
|
||
| const handleInputChange = (value: string) => { | ||
| setSearchQuery(value); | ||
| props.onChange(value); | ||
| debouncedSearch(value); | ||
| }; | ||
|
|
||
| const selectResult = (result: NominatimResult) => { | ||
| const address = formatAddress(result); | ||
| setSearchQuery(address); | ||
| props.onChange(address); | ||
|
|
||
| if (props.onCoordinatesChange) { | ||
| props.onCoordinatesChange(parseFloat(result.lat), parseFloat(result.lon)); | ||
| } | ||
|
|
||
| setShowResults(false); | ||
| setResults([]); | ||
| }; | ||
|
|
||
| const handleBlur = () => { | ||
| // Delay hiding results to allow click events | ||
| setTimeout(() => { | ||
| setShowResults(false); | ||
| }, 200); | ||
| }; | ||
|
Comment on lines
+148
to
+153
|
||
|
|
||
| createEffect(() => { | ||
| if (props.value !== searchQuery()) { | ||
| setSearchQuery(props.value); | ||
| } | ||
| }); | ||
|
|
||
| return ( | ||
| <Container> | ||
| <div onBlur={handleBlur}> | ||
| <Input | ||
| label={props.label} | ||
| value={searchQuery()} | ||
| onChange={handleInputChange} | ||
| type="text" | ||
| /> | ||
| </div> | ||
|
|
||
| <Show when={showResults() && (results().length > 0 || loading())}> | ||
| <ResultsList> | ||
| <Show when={loading()}> | ||
| <LoadingMessage>Searching...</LoadingMessage> | ||
| </Show> | ||
|
|
||
| <For each={results()}> | ||
| {(result) => ( | ||
| <ResultItem onClick={() => selectResult(result)}> | ||
| {formatAddress(result)} | ||
| </ResultItem> | ||
| )} | ||
| </For> | ||
|
|
||
| <Show when={!loading() && results().length === 0}> | ||
| <LoadingMessage>No results found</LoadingMessage> | ||
| </Show> | ||
| </ResultsList> | ||
| </Show> | ||
| </Container> | ||
| ); | ||
| }; | ||
|
|
||
| export default AddressSearchInput; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The manual path parsing with regex replaces SolidJS Router functionality without clear justification. This reduces maintainability and removes built-in router features like parameter validation and navigation guards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
somehow without this it didn't work, and claude fixed it like this. i'll test again organically 😎