Skip to content

Commit

Permalink
feat: export GeolocationPicker, improve map (#60)
Browse files Browse the repository at this point in the history
  • Loading branch information
pyphilia authored Mar 11, 2024
1 parent c32d4cb commit ddc1080
Show file tree
Hide file tree
Showing 40 changed files with 2,802 additions and 813 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ node_modules
.husky
.nyc_output
.yarn
storybook-static
12 changes: 12 additions & 0 deletions .storybook/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const MOCK_USE_SUGGESTIONS = ({ address }: { address: string }) => {
if (address) {
return {
data: [
{ addressLabel: 'suggestion 1' },
{ addressLabel: 'suggestion 2' },
{ addressLabel: 'suggestion 3' },
],
};
}
return { data: undefined };
};
21 changes: 9 additions & 12 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { I18nextProvider, initReactI18next } from 'react-i18next';
import 'react-quill/dist/quill.snow.css';

import { CssBaseline } from '@mui/material';
import { ThemeProvider } from '@mui/system';

import buildI18n from '@graasp/translations';
import { theme } from '@graasp/ui';

import type { Preview } from '@storybook/react';

const i18n = buildI18n().use(initReactI18next);

const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
Expand All @@ -24,18 +28,11 @@ export default preview;
export const decorators = [
(Story, { globals }) => {
return (
<ThemeProvider
theme={{
...theme,
direction: globals.direction,
palette: {
...theme.palette,
mode: globals.theme,
},
}}
>
<CssBaseline />
<Story />
<ThemeProvider theme={theme}>
<I18nextProvider i18n={i18n}>
<CssBaseline />
<Story />
</I18nextProvider>
</ThemeProvider>
);
},
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,15 @@

[![Storybook deployment](https://img.shields.io/badge/storybook-ui-%23FF4785?logo=storybook)](https://graasp.github.io/graasp-map/)
<a href="https://gitlocalize.com/repo/9355?utm_source=badge"> <img src="https://gitlocalize.com/repo/9355/whole_project/badge.svg" /> </a>

# Behaviors

- On load:
- without item id:
- Without allowed geolocation: show the country form to choose a focus. Choosing a country will
- With allowed geolocation: show the current country (TBD)
- with item id:
- without any geolocation: show country form
- with geolocation (current or children): fit bounds of the map

If the user is logged out, geocoding and reverse geocoding are disabled.
4 changes: 2 additions & 2 deletions example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@ const viewItem = (item) => {

const App = (): JSX.Element => {
const { data: currentMember } = hooks.useCurrentMember();

return (
<ThemeProvider theme={theme}>
<I18nextProvider i18n={i18n}>
<Map
itemId="d5a1c73d-cd4d-4f20-8a91-3c689ee87ea4"
itemId="6c0c2f21-8787-4ff2-a426-9a9358e1423f"
viewItem={viewItem}
currentMember={currentMember}
useDeleteItemGeolocation={mutations.useDeleteItemGeolocation}
useItemsInMap={hooks.useItemsInMap}
useAddressFromGeolocation={hooks.useAddressFromGeolocation}
useSuggestionsForAddress={hooks.useSuggestionsForAddress}
usePostItem={mutations.usePostItem}
useRecycleItems={mutations.useRecycleItems}
/>
Expand Down
2 changes: 1 addition & 1 deletion example/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { QueryClientProvider, queryClient } from './queryClient';

ReactDOM.createRoot(document.getElementById('root')!).render(
<StrictMode>
<div style={{ width: '80vw', height: '80vh' }}>
<div style={{ width: '100vw', height: '100vh', margin: 0 }}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
Expand Down
11 changes: 4 additions & 7 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,19 @@
"prettier:write": "prettier --write {src,cypress}/**/*.{js,ts,tsx,json}",
"type-check": "tsc --noEmit",
"check": "yarn prettier:check && yarn lint && yarn type-check",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"test": "yarn build && yarn cypress:ci",
"hooks:uninstall": "husky uninstall",
"hooks:install": "husky install",
"prepack": "yarn build"
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.0",
"@graasp/query-client": "2.6.3",
"@graasp/sdk": "3.8.3",
"@graasp/query-client": "2.9.0",
"@graasp/sdk": "4.1.0",
"@graasp/translations": "1.23.0",
"@graasp/ui": "4.5.1",
"@mui/icons-material": "5.15.12",
"@mui/lab": "5.0.0-alpha.167",
"@mui/material": "5.15.12",
"date-fns": "3.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "14.0.8",
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@
"dependencies": {
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.0",
"@graasp/sdk": "3.8.3",
"@graasp/sdk": "4.1.0",
"@graasp/translations": "1.23.0",
"@graasp/ui": "4.5.1",
"@graasp/ui": "4.9.1",
"@mui/icons-material": "5.15.12",
"@mui/lab": "5.0.0-alpha.167",
"@mui/material": "5.15.12",
Expand All @@ -76,7 +76,7 @@
"devDependencies": {
"@commitlint/cli": "17.8.1",
"@commitlint/config-conventional": "17.8.1",
"@graasp/query-client": "2.6.3",
"@graasp/query-client": "2.9.0",
"@storybook/addon-coverage": "1.0.1",
"@storybook/addon-essentials": "7.6.17",
"@storybook/addon-interactions": "7.6.17",
Expand Down
99 changes: 99 additions & 0 deletions src/components/GeolocationPicker/GeolocationPicker.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { expect } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import type { BoundFunctions } from '@testing-library/dom';
// eslint-disable-next-line import/no-extraneous-dependencies
import { queries } from '@testing-library/dom';

import { MOCK_USE_SUGGESTIONS } from '../../../.storybook/fixtures';
import GeolocationPicker from './GeolocationPicker';

const meta = {
title: 'GeolocationPicker',
component: GeolocationPicker,

argTypes: {
onChangeOption: {
action: 'choose option',
table: {
category: 'events',
},
},
},
} satisfies Meta<typeof GeolocationPicker>;
export default meta;

type Story = StoryObj<typeof meta>;

const checkSuggestions = async (canvas: BoundFunctions<typeof queries>) => {
// suggestions are showing
await userEvent.type(canvas.getByLabelText('Location'), 'my address');
const suggestions = MOCK_USE_SUGGESTIONS({ address: 'query' }).data;
suggestions?.forEach((value) => {
expect(canvas.getByText(value.addressLabel)).toBeVisible();
});

// select a suggestion
const toSelect = suggestions![0].addressLabel;
await userEvent.click(canvas.getByText(toSelect));
expect(canvas.getByLabelText('Location')).toHaveTextContent(toSelect);
};

export const Default = {
args: {
useSuggestionsForAddress: MOCK_USE_SUGGESTIONS as any,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

await checkSuggestions(canvas);
},
} satisfies Story;

export const InitialValue = {
args: {
useSuggestionsForAddress: MOCK_USE_SUGGESTIONS as any,
initialValue: 'initial value',
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
await expect(canvas.getByLabelText('Location')).toHaveTextContent(
args.initialValue!,
);

await checkSuggestions(canvas);
},
} satisfies Story;

export const Background = {
args: {
useSuggestionsForAddress: MOCK_USE_SUGGESTIONS as any,
initialValue: 'initial value',
},
parameters: {
backgrounds: {
default: 'default',
values: [{ name: 'default', value: '#00aced' }],
},
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
await expect(canvas.getByLabelText('Location')).toHaveTextContent(
args.initialValue!,
);

await checkSuggestions(canvas);
},
} satisfies Story;

export const Invisible = {
args: {
useSuggestionsForAddress: MOCK_USE_SUGGESTIONS as any,
invisible: true,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

await checkSuggestions(canvas);
},
} satisfies Story;
137 changes: 137 additions & 0 deletions src/components/GeolocationPicker/GeolocationPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { ChangeEventHandler, useEffect, useState } from 'react';

import {
Box,
LinearProgress,
List,
ListItemButton,
TextField,
} from '@mui/material';

import { DEFAULT_LANG } from '@graasp/translations';

import i18n, { useMapTranslation } from '../../config/i18n';
import { MAP } from '../../langs/constants';
import { QueryClientContextInterface } from '../context/QueryClientContext';
import { useOutsideClick } from './hook';

export type GeolocationPickerProps = {
disabled?: boolean;
useSuggestionsForAddress: QueryClientContextInterface['useSuggestionsForAddress'];
onChangeOption?: (args: {
addressLabel: string;
lat: number;
lng: number;
country?: string;
}) => void;
invisible?: boolean;
initialValue?: string;
endAdornment?: JSX.Element;
};

const GeolocationPicker = ({
disabled = false,
onChangeOption,
useSuggestionsForAddress,
invisible = false,
initialValue = '',
}: GeolocationPickerProps): JSX.Element => {
const { t } = useMapTranslation();

const [showSuggestions, setShowSuggestions] = useState(false);
const [query, setQuery] = useState<string | undefined>(initialValue);
const [selectedAddress, setSelectedAddress] = useState<string | undefined>();
const { data: suggestions, isFetching } = useSuggestionsForAddress({
address: query !== initialValue ? query : undefined,
lang: i18n.language ?? DEFAULT_LANG,
});
const ref = useOutsideClick(() => {
setShowSuggestions(false);
});

useEffect(() => {
if (initialValue !== query) {
setQuery(initialValue);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialValue]);

const onChange: ChangeEventHandler<HTMLInputElement> = (e) => {
setQuery(e.target.value);
setSelectedAddress(undefined);
};

const handleChangeOption = (option: {
addressLabel: string;
lat: number;
lng: number;
country?: string;
}): void => {
onChangeOption?.(option);

setSelectedAddress(option.addressLabel);
setQuery(undefined);
};

return (
<Box sx={{ position: 'relative', width: '100%' }} ref={ref}>
<TextField
disabled={disabled}
fullWidth
label="Location"
multiline
placeholder={t(MAP.GEOLOCATION_PICKER_PLACEHOLDER)}
onChange={onChange}
onFocus={() => setShowSuggestions(true)}
value={selectedAddress ?? query}
sx={{ minWidth: 250 }}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(invisible
? {
variant: 'standard',
InputLabelProps: {
shrink: true,
},
InputProps: {
disableUnderline: true,
},
}
: {})}
/>
{isFetching && query && (
<Box sx={{ width: '100%' }}>
<LinearProgress />
</Box>
)}

{showSuggestions && suggestions && !selectedAddress && (
<List
sx={{
position: 'absolute',
background: 'white',
top: 70,
width: '100%',
}}
>
{suggestions.map((r) => (
<ListItemButton
key={r.id}
onMouseDown={() => {
handleChangeOption(r);
setShowSuggestions(false);
}}
>
{r.addressLabel}
</ListItemButton>
))}
{!suggestions.length &&
query &&
!isFetching &&
t(MAP.GEOLOCATION_PICKER_NO_ADDRESS)}
</List>
)}
</Box>
);
};

export default GeolocationPicker;
Loading

0 comments on commit ddc1080

Please sign in to comment.