@memori.ai/memori-react 6.1.7 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/components/ChatBubble/ChatBubble.css +5 -0
  3. package/dist/components/ChatBubble/ChatBubble.js +9 -7
  4. package/dist/components/ChatBubble/ChatBubble.js.map +1 -1
  5. package/dist/components/MemoriWidget/MemoriWidget.js +58 -5
  6. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  7. package/dist/components/PositionDrawer/PositionDrawer.d.ts +8 -1
  8. package/dist/components/PositionDrawer/PositionDrawer.js +8 -1
  9. package/dist/components/PositionDrawer/PositionDrawer.js.map +1 -1
  10. package/dist/components/VenueWidget/VenueWidget.css +142 -0
  11. package/dist/components/VenueWidget/VenueWidget.d.ts +33 -0
  12. package/dist/components/VenueWidget/VenueWidget.js +195 -0
  13. package/dist/components/VenueWidget/VenueWidget.js.map +1 -0
  14. package/dist/components/ui/Select.d.ts +2 -1
  15. package/dist/components/ui/Select.js +2 -2
  16. package/dist/components/ui/Select.js.map +1 -1
  17. package/dist/helpers/utils.d.ts +1 -0
  18. package/dist/helpers/utils.js +25 -1
  19. package/dist/helpers/utils.js.map +1 -1
  20. package/dist/helpers/venue.d.ts +3 -0
  21. package/dist/helpers/venue.js +23 -0
  22. package/dist/helpers/venue.js.map +1 -0
  23. package/dist/locales/en.json +5 -0
  24. package/dist/locales/it.json +5 -0
  25. package/dist/styles.css +1 -0
  26. package/esm/components/ChatBubble/ChatBubble.css +5 -0
  27. package/esm/components/ChatBubble/ChatBubble.js +9 -7
  28. package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
  29. package/esm/components/MemoriWidget/MemoriWidget.js +58 -5
  30. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  31. package/esm/components/PositionDrawer/PositionDrawer.d.ts +8 -1
  32. package/esm/components/PositionDrawer/PositionDrawer.js +7 -1
  33. package/esm/components/PositionDrawer/PositionDrawer.js.map +1 -1
  34. package/esm/components/VenueWidget/VenueWidget.css +142 -0
  35. package/esm/components/VenueWidget/VenueWidget.d.ts +33 -0
  36. package/esm/components/VenueWidget/VenueWidget.js +192 -0
  37. package/esm/components/VenueWidget/VenueWidget.js.map +1 -0
  38. package/esm/components/ui/Select.d.ts +2 -1
  39. package/esm/components/ui/Select.js +2 -2
  40. package/esm/components/ui/Select.js.map +1 -1
  41. package/esm/helpers/utils.d.ts +1 -0
  42. package/esm/helpers/utils.js +24 -1
  43. package/esm/helpers/utils.js.map +1 -1
  44. package/esm/helpers/venue.d.ts +3 -0
  45. package/esm/helpers/venue.js +19 -0
  46. package/esm/helpers/venue.js.map +1 -0
  47. package/esm/locales/en.json +5 -0
  48. package/esm/locales/it.json +5 -0
  49. package/esm/styles.css +1 -0
  50. package/package.json +5 -2
  51. package/src/components/Chat/__snapshots__/Chat.test.tsx.snap +0 -154
  52. package/src/components/ChatBubble/ChatBubble.css +5 -0
  53. package/src/components/ChatBubble/ChatBubble.tsx +3 -1
  54. package/src/components/ChatBubble/__snapshots__/ChatBubble.test.tsx.snap +41 -100
  55. package/src/components/MemoriWidget/MemoriWidget.stories.tsx +9 -0
  56. package/src/components/MemoriWidget/MemoriWidget.tsx +86 -4
  57. package/src/components/PositionDrawer/PositionDrawer.stories.tsx +46 -0
  58. package/src/components/PositionDrawer/PositionDrawer.test.tsx +33 -0
  59. package/src/components/PositionDrawer/PositionDrawer.tsx +24 -5
  60. package/src/components/PositionDrawer/__snapshots__/PositionDrawer.test.tsx.snap +5 -0
  61. package/src/components/VenueWidget/VenueWidget.css +142 -0
  62. package/src/components/VenueWidget/VenueWidget.stories.tsx +66 -0
  63. package/src/components/VenueWidget/VenueWidget.test.tsx +49 -0
  64. package/src/components/VenueWidget/VenueWidget.tsx +430 -0
  65. package/src/components/VenueWidget/__snapshots__/VenueWidget.test.tsx.snap +335 -0
  66. package/src/components/ui/Select.tsx +3 -1
  67. package/src/components/ui/__snapshots__/Select.test.tsx.snap +0 -24
  68. package/src/helpers/utils.ts +36 -1
  69. package/src/helpers/venue.ts +36 -0
  70. package/src/locales/en.json +5 -0
  71. package/src/locales/it.json +5 -0
  72. package/src/mocks/data.ts +8 -0
  73. package/src/styles.css +1 -0
@@ -0,0 +1,142 @@
1
+ .memori--venue-widget {
2
+ padding: 0;
3
+ border: 0;
4
+ margin: 0;
5
+ }
6
+
7
+ .memori--venue-widget__uncertainty {
8
+ margin-inline: 0.5em;
9
+ }
10
+
11
+ .memori--venue-widget__map {
12
+ width: 100%;
13
+ height: 250px;
14
+ }
15
+
16
+ .memori--venue-widget__form-item {
17
+ margin-bottom: 1rem;
18
+ }
19
+
20
+ .memori--venue-widget__select-label {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ }
24
+
25
+ .memori--venue-widget__geosuggest {
26
+ position: relative;
27
+ display: flex;
28
+ align-items: center;
29
+ margin-bottom: 1rem;
30
+ }
31
+
32
+ .memori--venue-widget-search {
33
+ z-index: 1001;
34
+ }
35
+
36
+ .memori--venue-widget-search--label {
37
+ display: inline-block;
38
+ margin-bottom: 0.25rem;
39
+ }
40
+
41
+ .memori--venue-widget-search input,
42
+ .memori--venue-widget-search .memori--venue-widget-search--input {
43
+ flex: 1 1 auto;
44
+ padding: 10px;
45
+ border: 1px solid var(--memori-button-border-color, #ccc);
46
+ border-radius: 5px;
47
+ margin: 0;
48
+ font-size: 16px;
49
+ line-height: 1.5;
50
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
51
+ }
52
+
53
+ .memori--venue-widget-search .memori--venue-widget-search--input.memori--venue-widget-search--input-disabled {
54
+ background-color: #f7fafc;
55
+ color: #a0aec0;
56
+ cursor: not-allowed;
57
+ }
58
+
59
+ .memori--venue-widget-search .memori--venue-widget-search--input:focus,
60
+ .memori--venue-widget-search button:focus,
61
+ .memori--venue-widget-search input:focus {
62
+ border-color: var(--memori-primary);
63
+ box-shadow: 0 2px 0 rgba(0, 0, 0, 0.2);
64
+ outline: none;
65
+ }
66
+
67
+ .memori--venue-widget-search a {
68
+ color: var(--memori-primary);
69
+ text-decoration: underline;
70
+ }
71
+
72
+ ul.memori--venue-widget-search--options {
73
+ position: absolute;
74
+ z-index: 1;
75
+ overflow: auto;
76
+ width: 100%;
77
+ max-width: min(18rem, 30%);
78
+ max-height: 15rem;
79
+ padding-top: 0.25rem;
80
+ padding-right: 0;
81
+ padding-bottom: 0.25rem;
82
+ padding-left: 0;
83
+ border-radius: 0.375rem;
84
+ margin-top: 0.25rem;
85
+ margin-right: 0;
86
+ margin-bottom: 0;
87
+ margin-left: 0;
88
+ background: #fff;
89
+ box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px,
90
+ rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.1) 0px 4px 6px -4px;
91
+ list-style: none;
92
+ }
93
+
94
+ @media (min-width: 640px) {
95
+ .memori--venue-widget-search--options {
96
+ font-size: 0.875rem;
97
+ line-height: 1.25rem;
98
+ }
99
+ }
100
+
101
+ .memori--venue-widget-search--option {
102
+ position: relative;
103
+ padding-top: 0.5rem;
104
+ padding-right: 1rem;
105
+ padding-bottom: 0.5rem;
106
+ padding-left: 1rem;
107
+ color: rgb(17 24 39/1);
108
+ touch-action: none;
109
+ user-select: none;
110
+ }
111
+
112
+ li.memori--venue-widget-search--option {
113
+ cursor: pointer;
114
+ touch-action: initial;
115
+ }
116
+
117
+ li.memori--venue-widget-search--option:hover,
118
+ li.memori--venue-widget-search--option:focus {
119
+ background-color: #f7fafc;
120
+ }
121
+
122
+ li.memori--venue-widget-search--option:focus {
123
+ outline: 2px solid #63b3ed;
124
+ outline-offset: 2px;
125
+ }
126
+
127
+ li.memori--venue-widget-search--option--selected {
128
+ background-color: #f7fafc;
129
+ }
130
+
131
+ li.memori--venue-widget-search--option--active {
132
+ background-color: #f7fafc;
133
+ }
134
+
135
+ .memori--venue--widget__place-name {
136
+ margin: 0 0 1rem;
137
+ }
138
+
139
+ .memori--venue-widget__gps-button {
140
+ padding: 0.5rem 1rem;
141
+ margin-left: 1rem;
142
+ }
@@ -0,0 +1,66 @@
1
+ import React from 'react';
2
+ import { Meta, Story } from '@storybook/react';
3
+ import I18nWrapper from '../../I18nWrapper';
4
+ import VenueWidget, { Props } from './VenueWidget';
5
+ import { venue } from '../../mocks/data';
6
+ import './VenueWidget.css';
7
+
8
+ const meta: Meta = {
9
+ title: 'Widget/VenueWidget',
10
+ component: VenueWidget,
11
+ parameters: {
12
+ controls: { expanded: true },
13
+ },
14
+ };
15
+
16
+ export default meta;
17
+
18
+ const Template: Story<Props> = args => {
19
+ const [venue, setVenue] = React.useState(args.venue);
20
+ return (
21
+ <I18nWrapper>
22
+ <VenueWidget {...args} venue={venue} setVenue={setVenue} />
23
+ </I18nWrapper>
24
+ );
25
+ };
26
+
27
+ // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
28
+ // https://storybook.js.org/docs/react/workflows/unit-testing
29
+ export const Default = Template.bind({});
30
+ Default.args = {};
31
+
32
+ export const WithVenueExactLocartion = Template.bind({});
33
+ WithVenueExactLocartion.args = {
34
+ venue: {
35
+ ...venue,
36
+ uncertainty: 0,
37
+ },
38
+ };
39
+
40
+ export const WithVenueUncertainty = Template.bind({});
41
+ WithVenueUncertainty.args = {
42
+ venue: {
43
+ ...venue,
44
+ uncertainty: 2,
45
+ },
46
+ };
47
+
48
+ export const WithVenueLargeUncertainty = Template.bind({});
49
+ WithVenueLargeUncertainty.args = {
50
+ venue: {
51
+ ...venue,
52
+ uncertainty: 20,
53
+ },
54
+ };
55
+
56
+ export const WithUncertaintyUI = Template.bind({});
57
+ WithUncertaintyUI.args = {
58
+ venue,
59
+ showUncertainty: true,
60
+ };
61
+
62
+ export const WithoutGpsButton = Template.bind({});
63
+ WithoutGpsButton.args = {
64
+ venue,
65
+ showGpsButton: false,
66
+ };
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import VenueWidget from './VenueWidget';
4
+ import { venue } from '../../mocks/data';
5
+
6
+ it('renders empty VenueWidget unchanged', () => {
7
+ const { container } = render(<VenueWidget setVenue={jest.fn()} />);
8
+ expect(container).toMatchSnapshot();
9
+ });
10
+
11
+ it('renders VenueWidget with venue set, exact location', () => {
12
+ const { container } = render(
13
+ <VenueWidget
14
+ venue={{
15
+ ...venue,
16
+ uncertainty: 0,
17
+ }}
18
+ setVenue={jest.fn()}
19
+ />
20
+ );
21
+ expect(container).toMatchSnapshot();
22
+ });
23
+
24
+ it('renders VenueWidget with venue set, with uncertainty radius', () => {
25
+ const { container } = render(
26
+ <VenueWidget
27
+ venue={{
28
+ ...venue,
29
+ uncertainty: 10,
30
+ }}
31
+ setVenue={jest.fn()}
32
+ />
33
+ );
34
+ expect(container).toMatchSnapshot();
35
+ });
36
+
37
+ it('renders VenueWidget without uncertainty unchanged', () => {
38
+ const { container } = render(
39
+ <VenueWidget venue={venue} setVenue={jest.fn()} showUncertainty={false} />
40
+ );
41
+ expect(container).toMatchSnapshot();
42
+ });
43
+
44
+ it('renders VenueWidget without gps button unchanged', () => {
45
+ const { container } = render(
46
+ <VenueWidget venue={venue} setVenue={jest.fn()} showGpsButton={false} />
47
+ );
48
+ expect(container).toMatchSnapshot();
49
+ });
@@ -0,0 +1,430 @@
1
+ import { Venue } from '@memori.ai/memori-api-client/dist/types';
2
+ import { useEffect, useState, useCallback, Fragment } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { getUncertaintyByViewport } from '../../helpers/venue';
5
+ import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
6
+ import { useLeafletContext } from '@react-leaflet/core';
7
+ import L from 'leaflet';
8
+ import toast from 'react-hot-toast';
9
+ import Select from '../ui/Select';
10
+ import Button from '../ui/Button';
11
+ import { useDebounceFn } from '../../helpers/utils';
12
+ import { Combobox, Transition } from '@headlessui/react';
13
+ import cx from 'classnames';
14
+ import Spin from '../ui/Spin';
15
+
16
+ export type NominatimItem = {
17
+ place_id: number;
18
+ lat: number;
19
+ lon: number;
20
+ display_name: string;
21
+ type: string;
22
+ category: string;
23
+ importance: number;
24
+ place_rank: number;
25
+ address?: {
26
+ house_number?: string;
27
+ road?: string;
28
+ hamlet?: string;
29
+ village?: string;
30
+ suburb?: string;
31
+ town?: string;
32
+ city?: string;
33
+ municipality?: string;
34
+ county?: string;
35
+ state?: string;
36
+ country: string;
37
+ };
38
+ boundingbox: [number, number, number, number];
39
+ };
40
+
41
+ export interface Props {
42
+ venue?: Venue;
43
+ setVenue: (venue?: Venue) => void;
44
+ showUncertainty?: boolean;
45
+ showGpsButton?: boolean;
46
+ }
47
+
48
+ const Circle = ({
49
+ center,
50
+ size,
51
+ }: {
52
+ center: [number, number];
53
+ size: number;
54
+ }) => {
55
+ const context = useLeafletContext();
56
+
57
+ useEffect(() => {
58
+ const square = new L.Circle(center, size);
59
+ const container = context.layerContainer || context.map;
60
+ container.addLayer(square);
61
+
62
+ return () => {
63
+ container.removeLayer(square);
64
+ };
65
+ });
66
+
67
+ return null;
68
+ };
69
+
70
+ const CenterAndZoomUpdater = ({
71
+ center,
72
+ uncertainty,
73
+ }: {
74
+ center: [number, number];
75
+ uncertainty?: number;
76
+ }) => {
77
+ const [init, setInit] = useState(false);
78
+ const map = useMap();
79
+
80
+ const updateView = useCallback(() => {
81
+ let zoom =
82
+ uncertainty !== undefined
83
+ ? Math.round(Math.log2((10000 * 1000) / uncertainty))
84
+ : map.getZoom();
85
+ map.setView(center, zoom);
86
+ }, [center, uncertainty, map]);
87
+
88
+ useEffect(() => {
89
+ if (!init) {
90
+ updateView();
91
+ setInit(true);
92
+ }
93
+ // eslint-disable-next-line react-hooks/exhaustive-deps
94
+ }, []);
95
+
96
+ useEffect(() => {
97
+ updateView();
98
+ }, [center, uncertainty, updateView]);
99
+
100
+ return null;
101
+ };
102
+
103
+ let DefaultIcon = L.icon({
104
+ iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
105
+ shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
106
+ iconSize: [25, 41],
107
+ iconAnchor: [12.5, 20.5],
108
+ shadowAnchor: [12.5, 20.5],
109
+ });
110
+
111
+ L.Marker.prototype.options.icon = DefaultIcon;
112
+
113
+ const getPlaceName = (venue?: NominatimItem) => {
114
+ let placeName = 'Position';
115
+
116
+ if (venue?.address) {
117
+ placeName = [
118
+ venue.address.village || venue.address.suburb,
119
+ venue.address.town ||
120
+ venue.address.city ||
121
+ venue.address.county ||
122
+ venue.address.state,
123
+ venue.address.country,
124
+ ]
125
+ .filter(Boolean)
126
+ .join(', ');
127
+ } else if (venue?.display_name) {
128
+ placeName = venue.display_name;
129
+ }
130
+
131
+ return placeName;
132
+ };
133
+
134
+ const VenueWidget = ({
135
+ venue,
136
+ setVenue,
137
+ showUncertainty = false,
138
+ showGpsButton = true,
139
+ }: Props) => {
140
+ const { t } = useTranslation();
141
+ const [isClient, setIsClient] = useState(false);
142
+ const [updatingPosition, setUpdatingPosition] = useState(false);
143
+
144
+ const [fetching, setFetching] = useState(false);
145
+ const [query, setQuery] = useState('');
146
+ const [suggestions, setSuggestions] = useState<NominatimItem[]>([]);
147
+
148
+ const getGpsPosition = async () => {
149
+ setUpdatingPosition(true);
150
+
151
+ navigator.geolocation.getCurrentPosition(
152
+ async coords => {
153
+ try {
154
+ const result = await fetch(
155
+ `https://nominatim.openstreetmap.org/reverse?lat=${coords.coords.latitude}&lon=${coords.coords.longitude}&format=jsonv2&addressdetails=1`
156
+ );
157
+ const response = (await result.json()) as NominatimItem;
158
+
159
+ const placeName = getPlaceName(response);
160
+
161
+ setVenue({
162
+ latitude: coords.coords.latitude,
163
+ longitude: coords.coords.longitude,
164
+ placeName: placeName,
165
+ uncertainty: coords.coords.accuracy / 1000,
166
+ });
167
+ } catch (e) {
168
+ let err = e as Error;
169
+ console.error('[POSITION ERROR]', err);
170
+ if (err?.message) toast.error(err.message);
171
+
172
+ setVenue({
173
+ latitude: coords.coords.latitude,
174
+ longitude: coords.coords.longitude,
175
+ placeName: 'Position',
176
+ uncertainty: coords.coords.accuracy / 1000,
177
+ });
178
+ }
179
+
180
+ setUpdatingPosition(false);
181
+ },
182
+ err => {
183
+ console.error('[POSITION ERROR]', err);
184
+ toast.error(err.message);
185
+ setUpdatingPosition(false);
186
+ }
187
+ );
188
+ };
189
+
190
+ const handleSearch = useDebounceFn(async (value: string) => {
191
+ setFetching(true);
192
+
193
+ try {
194
+ let response = await fetch(
195
+ `https://nominatim.openstreetmap.org/search?q=${value}&format=jsonv2&limit=5&addressdetails=1`
196
+ );
197
+ let data = await response.json();
198
+ setSuggestions(data);
199
+ } catch (error) {
200
+ console.error(error);
201
+ } finally {
202
+ setFetching(false);
203
+ }
204
+ }, 1000);
205
+
206
+ const handleChange = (value: NominatimItem) => {
207
+ console.log(value);
208
+ const placeName = getPlaceName(value);
209
+
210
+ setVenue({
211
+ latitude: value.lat,
212
+ longitude: value.lon,
213
+ placeName: placeName,
214
+ uncertainty: value?.boundingbox
215
+ ? getUncertaintyByViewport(value.boundingbox)
216
+ : 2,
217
+ } as Venue);
218
+ };
219
+
220
+ const onQueryChange = (value: string) => {
221
+ setQuery(value);
222
+ handleSearch(value);
223
+ };
224
+
225
+ useEffect(() => {
226
+ setIsClient(true);
227
+ }, []);
228
+
229
+ useEffect(() => {
230
+ const leafletCSS = document.createElement('link');
231
+ leafletCSS.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
232
+ leafletCSS.rel = 'stylesheet';
233
+ leafletCSS.integrity =
234
+ 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=';
235
+ leafletCSS.crossOrigin = '';
236
+
237
+ document.head.appendChild(leafletCSS);
238
+
239
+ return () => {
240
+ document.head.removeChild(leafletCSS);
241
+ };
242
+ }, []);
243
+
244
+ return (
245
+ <fieldset className="memori--venue-widget">
246
+ <legend className="sr-only">Venue</legend>
247
+ <div className="memori--venue-widget__form-item">
248
+ <div className="memori--venue-widget__geosuggest">
249
+ {updatingPosition ? (
250
+ <p>{t('write_and_speak.updatingPosition')}</p>
251
+ ) : (
252
+ <>
253
+ <div className="memori--venue-widget-search">
254
+ <Combobox
255
+ value={
256
+ (venue
257
+ ? {
258
+ place_id: 0,
259
+ lat: venue?.latitude,
260
+ lon: venue?.longitude,
261
+ display_name: venue?.placeName,
262
+ }
263
+ : undefined) as NominatimItem | undefined
264
+ }
265
+ onChange={handleChange}
266
+ >
267
+ <Combobox.Input
268
+ className="memori--venue-widget-search--input"
269
+ displayValue={(i: NominatimItem) => getPlaceName(i)}
270
+ placeholder={t('searchVenue')}
271
+ onChange={e => onQueryChange(e.target.value)}
272
+ />
273
+ {(fetching ||
274
+ suggestions.length > 0 ||
275
+ (suggestions.length === 0 && query !== '')) && (
276
+ <Transition
277
+ as={Fragment}
278
+ leave="transition ease-in duration-100"
279
+ leaveFrom="opacity-100"
280
+ leaveTo="opacity-0"
281
+ >
282
+ <Combobox.Options className="memori--venue-widget-search--options">
283
+ {fetching ? (
284
+ <Spin spinning>
285
+ <center className="memori--venue-widget-search--option">
286
+ {t('loading')}...
287
+ </center>
288
+ </Spin>
289
+ ) : suggestions.length === 0 && query !== '' ? (
290
+ <center className="memori--venue-widget-search--option">
291
+ {t('nothingFound')}
292
+ </center>
293
+ ) : (
294
+ suggestions?.map(s => (
295
+ <Combobox.Option
296
+ as={Fragment}
297
+ key={s.place_id}
298
+ value={s}
299
+ >
300
+ {({ active, selected }) => (
301
+ <li
302
+ className={cx(
303
+ 'memori--venue-widget-search--option',
304
+ {
305
+ 'memori--venue-widget-search--option-active':
306
+ active,
307
+ 'memori--venue-widget-search--option-selected':
308
+ selected,
309
+ }
310
+ )}
311
+ >
312
+ {s.display_name}
313
+ </li>
314
+ )}
315
+ </Combobox.Option>
316
+ ))
317
+ )}
318
+ </Combobox.Options>
319
+ </Transition>
320
+ )}
321
+ </Combobox>
322
+ </div>
323
+ {showGpsButton && (
324
+ <Button
325
+ className="memori--venue-widget__gps-button"
326
+ outlined
327
+ loading={updatingPosition}
328
+ onClick={() => {
329
+ setUpdatingPosition(true);
330
+ getGpsPosition();
331
+ }}
332
+ >
333
+ {t('write_and_speak.useMyPosition')}
334
+ </Button>
335
+ )}
336
+ </>
337
+ )}
338
+ </div>
339
+ {showUncertainty && (
340
+ <label className="memori--venue-widget__select-label">
341
+ <span>{t('uncertain')}: </span>
342
+ <select
343
+ className="memori-select--button memori--venue-widget__uncertainty"
344
+ value={parseFloat((venue?.uncertainty ?? 0).toFixed(2))}
345
+ disabled={
346
+ !venue ||
347
+ !venue.placeName ||
348
+ !venue.latitude ||
349
+ !venue.longitude
350
+ }
351
+ onChange={e => {
352
+ setVenue({
353
+ ...venue,
354
+ uncertainty: parseFloat(e.target.value),
355
+ } as Venue);
356
+ }}
357
+ >
358
+ {venue?.uncertainty &&
359
+ ![0, 1, 2, 5, 10, 20, 50, 100].includes(venue.uncertainty) && (
360
+ <option value={venue.uncertainty}>
361
+ {venue.uncertainty} Km
362
+ </option>
363
+ )}
364
+ <option value={0}>{t('exactPosition')}</option>
365
+ <option value={1}>1 km</option>
366
+ <option value={2}>2 km</option>
367
+ <option value={5}>5 km</option>
368
+ <option value={10}>10 km</option>
369
+ <option value={20}>20 km</option>
370
+ <option value={50}>50 km</option>
371
+ <option value={100}>100 km</option>
372
+ </select>
373
+ </label>
374
+ )}
375
+ </div>
376
+ <div className="memori--venue-widget__form-item">
377
+ {venue?.placeName && (
378
+ <p className="memori--venue--widget__place-name">
379
+ <strong>{t('venue')}</strong>: {venue.placeName}
380
+ </p>
381
+ )}
382
+ <div className="memori--venue-widget__map">
383
+ {isClient && (
384
+ <MapContainer
385
+ className="memori--venue-widget__map"
386
+ center={
387
+ venue?.latitude && venue?.longitude
388
+ ? [venue.latitude, venue.longitude]
389
+ : [44.66579, 11.48823]
390
+ }
391
+ zoom={13}
392
+ scrollWheelZoom
393
+ >
394
+ <TileLayer
395
+ attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
396
+ url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
397
+ />
398
+ <CenterAndZoomUpdater
399
+ center={
400
+ venue?.latitude && venue?.longitude
401
+ ? [venue.latitude, venue.longitude]
402
+ : [44.66579, 11.48823]
403
+ }
404
+ uncertainty={(venue?.uncertainty ?? 0) * 1000}
405
+ />
406
+ {venue?.latitude && venue?.longitude && (
407
+ <Marker
408
+ position={[venue.latitude, venue.longitude]}
409
+ icon={DefaultIcon}
410
+ >
411
+ <Popup>{venue.placeName ?? ''}</Popup>
412
+ </Marker>
413
+ )}
414
+ {venue?.latitude &&
415
+ venue?.longitude &&
416
+ venue?.uncertainty !== undefined && (
417
+ <Circle
418
+ center={[venue.latitude, venue.longitude]}
419
+ size={venue.uncertainty * 1000}
420
+ />
421
+ )}
422
+ </MapContainer>
423
+ )}
424
+ </div>
425
+ </div>
426
+ </fieldset>
427
+ );
428
+ };
429
+
430
+ export default VenueWidget;