Skip to content
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

♻️ Rewrite LocationSearch to TypeScript #287

Merged
merged 1 commit into from
Jul 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 0 additions & 75 deletions src/components/LocationSearch.js

This file was deleted.

157 changes: 157 additions & 0 deletions src/components/LocationSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import React, {
FC,
MouseEvent,
useCallback,
useEffect,
useReducer,
useRef,
} from 'react'
import { Search, SearchProps } from 'semantic-ui-react'

interface SearchResult {
place_id: number
lat: number
lon: number
display_name: string
}

export interface Result {
title: string
lat: number
lon: number
}

interface State {
loading: boolean
results: Result[]
value: string
}

const initialState: State = {
loading: false,
results: [],
value: '',
}

enum SearchActionType {
CLEAN_QUERY = 'CLEAN_QUERY',
START_SEARCH = 'START_SEARCH',
FINISH_SEARCH = 'FINISH_SEARCH',
UPDATE_SELECTION = 'UPDATE_SELECTION',
}

interface SearchAction {
type: SearchActionType
query?: string
results?: Result[]
selection?: string
}

async function api(value: string): Promise<SearchResult[]> {
const url =
'https://nominatim.openstreetmap.org/search?format=json&limit=5&q=' + value

const timeout = (time: number) => {
const controller = new AbortController()
setTimeout(() => controller.abort(), time * 1000)
return controller
}

return fetch(url, { signal: timeout(30).signal }).then(res => res.json())
}

function searchReducer(state: any, action: SearchAction) {
switch (action.type) {
case SearchActionType.CLEAN_QUERY:
return initialState
case SearchActionType.START_SEARCH:
return { ...state, loading: true, value: action.query, results: [] }
case SearchActionType.FINISH_SEARCH:
return { ...state, loading: false, results: action.results }
case SearchActionType.UPDATE_SELECTION:
return { ...state, value: action.selection }

default:
throw new Error()
}
}

interface Props {
callback: (result: Result) => void
}

const LocationSearch: FC<Props> = props => {
const [state, dispatch] = useReducer(searchReducer, initialState)
const { loading, results, value } = state
const timeoutRef: { current: NodeJS.Timeout | null } = useRef(null)

const handleResultSelect = useCallback(
(event: MouseEvent, data: any) => {
dispatch({
type: SearchActionType.UPDATE_SELECTION,
selection: data.result.title,
})

props.callback(data.result)
},
[props]
)

const handleSearchChange = useCallback(
(event: MouseEvent, data: SearchProps) => {
const value = data.value
if (value === undefined) {
return
}

clearTimeout(timeoutRef.current as NodeJS.Timeout)
dispatch({ type: SearchActionType.START_SEARCH, query: value })

timeoutRef.current = setTimeout(() => {
if (value.length === 0) {
dispatch({ type: SearchActionType.CLEAN_QUERY })
return
}

const results: Result[] = []
api(value)
.then(data => {
data.forEach(entry => {
results.push({
title: entry.display_name,
lat: entry.lat,
lon: entry.lon,
})
})
})
.finally(() => {
dispatch({
type: SearchActionType.FINISH_SEARCH,
results: results,
})
})
}, 300)
},
[]
)

useEffect(() => {
return () => {
clearTimeout(timeoutRef.current as NodeJS.Timeout)
}
}, [])

return (
<React.Fragment>
<Search
loading={loading}
onResultSelect={handleResultSelect}
onSearchChange={handleSearchChange}
results={results}
value={value}
/>
</React.Fragment>
)
}

export default LocationSearch
61 changes: 52 additions & 9 deletions src/components/TickerForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ import React, {
useEffect,
} from 'react'
import {
Button,
CheckboxProps,
Form,
Header,
Icon,
Input,
InputOnChangeData,
Message,
TextAreaProps,
} from 'semantic-ui-react'
import { Ticker, useTickerApi } from '../api/Ticker'
import { SubmitHandler, useForm } from 'react-hook-form'
import { useQueryClient } from 'react-query'
import useAuth from './useAuth'
import LocationSearch, { Result } from './LocationSearch'

interface Props {
ticker?: Ticker
Expand All @@ -36,6 +39,10 @@ interface FormValues {
twitter: string
facebook: string
}
location: {
lat: number
lon: number
}
}

const TickerForm: FC<Props> = props => {
Expand All @@ -53,12 +60,34 @@ const TickerForm: FC<Props> = props => {
twitter: ticker?.information.twitter,
facebook: ticker?.information.facebook,
},
location: {
lat: ticker?.location.lat,
lon: ticker?.location.lon,
},
},
})
const { token } = useAuth()
const { postTicker, putTicker } = useTickerApi(token)
const queryClient = useQueryClient()

const onLocationChange = useCallback(
(result: Result) => {
setValue('location.lat', result.lat)
setValue('location.lon', result.lon)
},
[setValue]
)

const onLoctionReset = useCallback(
(e: React.MouseEvent) => {
setValue('location.lat', 0)
setValue('location.lon', 0)

e.preventDefault()
},
[setValue]
)

const onChange = useCallback(
(
e: ChangeEvent | FormEvent,
Expand Down Expand Up @@ -92,15 +121,8 @@ const TickerForm: FC<Props> = props => {
}

useEffect(() => {
register('title')
register('domain')
register('active')
register('description')
register('information.author')
register('information.email')
register('information.url')
register('information.twitter')
register('information.facebook')
register('location.lat', { valueAsNumber: true })
register('location.lon', { valueAsNumber: true })
})

return (
Expand Down Expand Up @@ -198,6 +220,27 @@ const TickerForm: FC<Props> = props => {
</Input>
</Form.Input>
</Form.Group>
<Header dividing>Location</Header>
<Message info size="small">
You can add a default location to the ticker. This will help you to have
a pre-selected location when you add a map to a message. <br />
Current Location: {ticker?.location.lat.toPrecision(2)},
{ticker?.location.lon.toPrecision(2)}
</Message>
<Form.Group widths="equal">
<Form.Field width="15">
<LocationSearch callback={onLocationChange} />
</Form.Field>
<Form.Field width="1">
<Button
color="red"
disabled={ticker?.location.lat === 0 && ticker.location.lon === 0}
icon="delete"
onClick={onLoctionReset}
/>
</Form.Field>
</Form.Group>
{/* TODO: Add Map for the current selected location */}
</Form>
)
}
Expand Down