Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • fcos-suite/fcos-suite-map
1 result
Show changes
Commits on Source (24)
Showing
with 11613 additions and 1719 deletions
...@@ -28,4 +28,4 @@ lftp deploy: ...@@ -28,4 +28,4 @@ lftp deploy:
only: only:
- main - main
script: script:
- lftp -e "set net:timeout 5; set net:max-retries 3; set net:reconnect-interval-base 5; open sftp://$SFTP_HOST; user $SFTP_USER $SFTP_PASSWORD; mirror -X .* -X .*/ --reverse --verbose build/ live/; bye" - lftp -e "set net:timeout 5; set net:max-retries 3; set net:reconnect-interval-base 5; open sftp://$SFTP_HOST; user $SFTP_USER $SFTP_PASSWORD; mirror -X .* -X .*/ --reverse --verbose build/ /httpdocs/map; bye"
\ No newline at end of file
This diff is collapsed.
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
"react-dom": "^17.0.0", "react-dom": "^17.0.0",
"react-leaflet": "^3.2.0", "react-leaflet": "^3.2.0",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-select": "^4.3.1", "react-select": "4.0.2",
"swr": "^0.5.6", "swr": "^0.5.6",
"tailwindcss": "^2.0.3", "tailwindcss": "^2.0.3",
"zustand": "^3.5.1" "zustand": "^3.5.1"
......
...@@ -6,8 +6,8 @@ ...@@ -6,8 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Fab City Hamburg Map</title> <title>Fab City Hamburg Map</title>
<!-- TODO: This should be imported from the leaflet npm package, not manually from the public folder. See https://github.com/snowpackjs/snowpack/discussions/1716 --> <!-- TODO: This should be imported from the leaflet npm package, not manually from the public folder. See https://github.com/snowpackjs/snowpack/discussions/1716 -->
<link rel="stylesheet" href="leaflet.css" /> <link rel="stylesheet" href="/leaflet.css" />
<script src="leaflet.js"></script> <script src="/leaflet.js"></script>
</head> </head>
<body class="h-full w-full"> <body class="h-full w-full">
<div id="root" class="h-full w-full"></div> <div id="root" class="h-full w-full"></div>
......
...@@ -5,11 +5,10 @@ module.exports = { ...@@ -5,11 +5,10 @@ module.exports = {
src: { url: '/dist' }, src: { url: '/dist' },
}, },
plugins: [ plugins: [
'@snowpack/plugin-react-refresh',
'@snowpack/plugin-dotenv', '@snowpack/plugin-dotenv',
'@snowpack/plugin-react-refresh',
'@snowpack/plugin-typescript', '@snowpack/plugin-typescript',
'@snowpack/plugin-postcss', '@snowpack/plugin-postcss',
'@snowpack/plugin-dotenv',
], ],
routes: [ routes: [
/* Enable an SPA Fallback in development: */ /* Enable an SPA Fallback in development: */
......
...@@ -7,6 +7,7 @@ import Map from './Map/Map'; ...@@ -7,6 +7,7 @@ import Map from './Map/Map';
import SidebarCreateView from './Sidebar/SidebarCreateView'; import SidebarCreateView from './Sidebar/SidebarCreateView';
import SidebarListView from './Sidebar/SidebarListView'; import SidebarListView from './Sidebar/SidebarListView';
import SidebarSingleView from './Sidebar/SidebarSingleView'; import SidebarSingleView from './Sidebar/SidebarSingleView';
import PoiLoader from './PoiLoader';
const App: React.FC = () => { const App: React.FC = () => {
const selectedPoi = useStore((state) => state.selectedPoi); const selectedPoi = useStore((state) => state.selectedPoi);
...@@ -17,6 +18,10 @@ const App: React.FC = () => { ...@@ -17,6 +18,10 @@ const App: React.FC = () => {
<Notification /> <Notification />
<div className={'flex md:flex-row-reverse flex-col h-full'}> <div className={'flex md:flex-row-reverse flex-col h-full'}>
<Route path="/add">{({ match }) => <Map createMode={!!match} />}</Route> <Route path="/add">{({ match }) => <Map createMode={!!match} />}</Route>
<Route path="/poi/:poiId">{({ match }) => <PoiLoader poiId={match?.params?.poiId as string} />}</Route>
<Route exact path="/">
<PoiLoader poiId={null} />
</Route>
<Switch> <Switch>
<Route exact path="/add"> <Route exact path="/add">
<SidebarCreateView /> <SidebarCreateView />
......
import React from 'react';
import type { Theme } from 'react-select';
import type { NamedProps, StylesConfig } from 'react-select';
import Creatable from 'react-select/creatable';
const CreatableSelect = <OptionType, isMulti extends boolean>(props: NamedProps<OptionType, isMulti>): JSX.Element => {
const customStyles: StylesConfig<OptionType, isMulti> = {
control: (provided) => ({
...provided,
border: '0',
borderRadius: '0.5em',
boxShadow: 'none',
}),
multiValue: (provided) => ({ ...provided, borderRadius: '999px', padding: '0 3px' }),
multiValueRemove: (provided) => ({
...provided,
color: 'hsl(0, 0%, 50%)',
'&:hover': { backgroundColor: 'initial', color: 'black' },
}),
};
return (
<div>
<Creatable
theme={(theme): Theme => ({
...theme,
// @ts-expect-error: ThemeConfig type from definitely-typed is not complete
borderRadius: '0.5em',
colors: {
...theme.colors,
primary25: '#C7D2FE',
},
})}
styles={customStyles}
name="pois"
className="hover:border-opacity-40 rounded-lg w-full border-2 border-black border-opacity-20 focus-within:border-indigo-300 focus-within:ring focus-within:ring-indigo-200 focus-within:ring-opacity-50 mt-1"
{...props}
/>
</div>
);
};
export default CreatableSelect;
...@@ -5,7 +5,7 @@ import { validateFile } from '../../util/file'; ...@@ -5,7 +5,7 @@ import { validateFile } from '../../util/file';
import TextInput from './TextInput'; import TextInput from './TextInput';
import FileInput from './FileInput'; import FileInput from './FileInput';
import TextAreaInput from './TextAreaInput'; import TextAreaInput from './TextAreaInput';
import { useStore, usePoiData } from '../../hooks'; import { usePoiData, useStore } from '../../hooks';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import CoordinateInput from './CoordinateInput'; import CoordinateInput from './CoordinateInput';
import TagInput from './TagInput'; import TagInput from './TagInput';
...@@ -13,6 +13,9 @@ import { removeDuplicateObjects } from '../../util/array'; ...@@ -13,6 +13,9 @@ import { removeDuplicateObjects } from '../../util/array';
import { createPoi, createTags } from '../../graphql/mutations'; import { createPoi, createTags } from '../../graphql/mutations';
import Spinner from '../Spinner'; import Spinner from '../Spinner';
import type { CreatePoiMutationMutationVariables, Mutation } from '../../generated/graphql'; import type { CreatePoiMutationMutationVariables, Mutation } from '../../generated/graphql';
import SelectInput from './SelectInput';
type StringSelectOption = { label: string; value: string };
const AddPoiForm: React.FC = () => { const AddPoiForm: React.FC = () => {
const [formData, setFormData] = useState<PointOfInterestFormData>({ const [formData, setFormData] = useState<PointOfInterestFormData>({
...@@ -24,6 +27,7 @@ const AddPoiForm: React.FC = () => { ...@@ -24,6 +27,7 @@ const AddPoiForm: React.FC = () => {
description: '', description: '',
website: '', website: '',
category: '', category: '',
relationStatus: '',
image: null, image: null,
tags: [], tags: [],
}); });
...@@ -34,6 +38,8 @@ const AddPoiForm: React.FC = () => { ...@@ -34,6 +38,8 @@ const AddPoiForm: React.FC = () => {
const history = useHistory(); const history = useHistory();
const { data } = usePoiData(); const { data } = usePoiData();
const [tagOptions, setTagOptions] = useState<Tag[]>([]); const [tagOptions, setTagOptions] = useState<Tag[]>([]);
const [categoryOptions, setCatgeoryOptions] = useState<StringSelectOption[]>([]);
const [relationStatusOptions, setRelationStatusOptions] = useState<StringSelectOption[]>([]);
const [selectedTags, setSelectedTags] = useState<Tag[]>([]); const [selectedTags, setSelectedTags] = useState<Tag[]>([]);
const setNotification = useStore((state) => state.setNotification); const setNotification = useStore((state) => state.setNotification);
...@@ -113,6 +119,15 @@ const AddPoiForm: React.FC = () => { ...@@ -113,6 +119,15 @@ const AddPoiForm: React.FC = () => {
}); });
const tags = removeDuplicateObjects(tagsWithDuplicates, 'id'); const tags = removeDuplicateObjects(tagsWithDuplicates, 'id');
setTagOptions(tags); setTagOptions(tags);
const relationStatuses = removeDuplicateObjects(data, 'relationStatus')
.filter((poi) => !!poi.relationStatus)
.map((poi) => ({ label: poi.relationStatus, value: poi.relationStatus }));
setRelationStatusOptions(relationStatuses);
const categories = removeDuplicateObjects(data, 'category')
.filter((poi) => !!poi.category)
.map((poi) => ({ label: poi.category, value: poi.category }));
setCatgeoryOptions(categories);
} }
}, [data]); }, [data]);
...@@ -143,12 +158,31 @@ const AddPoiForm: React.FC = () => { ...@@ -143,12 +158,31 @@ const AddPoiForm: React.FC = () => {
/> />
<TagInput label={'Tags'} tags={selectedTags} options={tagOptions} onTagsChange={setSelectedTags} /> <TagInput label={'Tags'} tags={selectedTags} options={tagOptions} onTagsChange={setSelectedTags} />
<TextInput label={'Name des Orts'} name={'name'} value={formData.name} onChange={handleInputChange} required /> <TextInput label={'Name des Orts'} name={'name'} value={formData.name} onChange={handleInputChange} required />
<TextInput <SelectInput
label={'Kategorie'} label={'Kategorie'}
required
placeholder={'Auswählen...'}
name={'category'} name={'category'}
value={formData.category} options={categoryOptions}
onChange={handleInputChange} onChange={(selectedOption) =>
setFormData((prev) => ({
...prev,
category: selectedOption ? (selectedOption as StringSelectOption).value : '',
}))
}
/>
<SelectInput
label={'Verhältnis zum Fab City Hamburg e.V.'}
required required
name={'relationStatus'}
placeholder={'Auswählen...'}
options={relationStatusOptions}
onChange={(selectedOption) =>
setFormData((prev) => ({
...prev,
relationStatus: selectedOption ? (selectedOption as StringSelectOption).value : '',
}))
}
/> />
<TextInput <TextInput
label={'Anschrift'} label={'Anschrift'}
......
import React from 'react';
import type { NamedProps } from 'react-select';
import CreatableSelect from '../CreatableSelect';
interface Props<OptionType, isMulti extends boolean> extends NamedProps<OptionType, isMulti> {
label: string;
name: string;
required?: boolean;
}
const SelectInput = <OptionType, isMulti extends boolean>({
name,
label,
value,
onChange,
...inputProps
}: Props<OptionType, isMulti>): JSX.Element => {
return (
<label className="block mb-4">
{!!label && (
<span className="form-label">
{label}
{inputProps?.required && `*`}
</span>
)}
<CreatableSelect
name={name}
value={value}
className="p-0 form-input form-input-custom"
onChange={onChange}
{...inputProps}
/>
</label>
);
};
export default SelectInput;
...@@ -80,7 +80,7 @@ const TagInput: React.FC<Props> = ({ label, tags, options, onTagsChange, require ...@@ -80,7 +80,7 @@ const TagInput: React.FC<Props> = ({ label, tags, options, onTagsChange, require
<div className="flex flex-wrap items-center flex-1 p-2"> <div className="flex flex-wrap items-center flex-1 p-2">
{tags.map((selectedTag) => ( {tags.map((selectedTag) => (
<Tag <Tag
key={`selected${selectedTag.id}`} key={`selected${selectedTag.color}`}
onClickDelete={() => handleClickDeleteTag(selectedTag)} onClickDelete={() => handleClickDeleteTag(selectedTag)}
color={selectedTag.color} color={selectedTag.color}
> >
......
import type { LatLngTuple } from 'leaflet'; import type { LatLngTuple } from 'leaflet';
import { divIcon, DivIconOptions } from 'leaflet'; import { divIcon, DivIconOptions } from 'leaflet';
import React, { useMemo } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { MapContainer, Marker, TileLayer } from 'react-leaflet'; import { MapContainer, Marker, TileLayer } from 'react-leaflet';
import { useStore } from '../../hooks'; import { useStore } from '../../hooks';
import MapViewController from './MapViewController'; import MapViewController from './MapViewController';
import { useFilteredPoiData } from '../../hooks/useFilteredPoiData'; import { useFilteredPoiData } from '../../hooks/useFilteredPoiData';
import MapLayerControl from './MapLayerControl'; import MapLayerControl from './MapLayerControl';
import { useHistory } from 'react-router-dom';
interface Props { interface Props {
createMode?: boolean; createMode?: boolean;
...@@ -29,22 +30,27 @@ export const Map: React.FC<Props> = ({ createMode }) => { ...@@ -29,22 +30,27 @@ export const Map: React.FC<Props> = ({ createMode }) => {
); );
const { data } = useFilteredPoiData(); const { data } = useFilteredPoiData();
const draftPoi = useStore((state) => state.draftPoi); const draftPoi = useStore((state) => state.draftPoi);
const [mapBounds, setMapBounds] = useState<Array<LatLngTuple>>([DEFAULT_CENTER]);
const hoveredPoi = useStore((state) => state.hoveredPoi); const hoveredPoi = useStore((state) => state.hoveredPoi);
const setHoveredPoi = useStore((state) => state.setHoveredPoi); const setHoveredPoi = useStore((state) => state.setHoveredPoi);
const selectedPoi = useStore((state) => state.selectedPoi); const selectedPoi = useStore((state) => state.selectedPoi);
const setSelectedPoi = useStore((state) => state.setSelectedPoi); const history = useHistory();
const selectedLatlng: LatLngTuple | undefined = selectedPoi ? [selectedPoi?.lat, selectedPoi?.lng] : undefined; const selectedLatlng: LatLngTuple | undefined = selectedPoi ? [selectedPoi?.lat, selectedPoi?.lng] : undefined;
useEffect(() => {
let newBounds = [DEFAULT_CENTER];
if (selectedPoi) {
newBounds = [[selectedPoi.lat, selectedPoi.lng] as LatLngTuple];
} else if (data?.length) {
newBounds = data?.map((poi) => [poi.lat, poi.lng] as LatLngTuple) || [];
}
setMapBounds(newBounds);
}, [JSON.stringify(data), selectedPoi]);
return ( return (
<div className="relative h-full w-full z-0"> <div className="relative h-full w-full z-0">
<MapLayerControl /> {!createMode && !selectedPoi && <MapLayerControl />}
<MapContainer <MapContainer id={'mapid'} className={'h-full w-full z-0'} scrollWheelZoom={true}>
id={'mapid'}
className={'h-full w-full z-0'}
center={DEFAULT_CENTER}
zoom={13}
scrollWheelZoom={true}
>
<TileLayer <TileLayer
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}" url="https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}"
...@@ -54,7 +60,7 @@ export const Map: React.FC<Props> = ({ createMode }) => { ...@@ -54,7 +60,7 @@ export const Map: React.FC<Props> = ({ createMode }) => {
zoomOffset={-1} zoomOffset={-1}
maxZoom={18} maxZoom={18}
/> />
<MapViewController center={selectedLatlng ?? DEFAULT_CENTER} zoom={13} createPoiMode={createMode} /> <MapViewController bounds={mapBounds} createPoiMode={createMode} />
{/* Single marker when creating a new POI */} {/* Single marker when creating a new POI */}
{!!(createMode && draftPoi) && <Marker icon={greenLargeIcon} position={draftPoi} />} {!!(createMode && draftPoi) && <Marker icon={greenLargeIcon} position={draftPoi} />}
{/* Single marker when POI is selected */} {/* Single marker when POI is selected */}
...@@ -71,7 +77,9 @@ export const Map: React.FC<Props> = ({ createMode }) => { ...@@ -71,7 +77,9 @@ export const Map: React.FC<Props> = ({ createMode }) => {
key={poi.id} key={poi.id}
position={poiLatLng} position={poiLatLng}
eventHandlers={{ eventHandlers={{
click: () => setSelectedPoi(poi), click: () => {
history.push(`/poi/${String(poi.id)}`);
},
mouseover: () => { mouseover: () => {
setHoveredPoi(poi); setHoveredPoi(poi);
}, },
......
import React from 'react'; import React, { useState } from 'react';
import { useFilteredPoiData } from '../../hooks/useFilteredPoiData'; import { useFilteredPoiData } from '../../hooks/useFilteredPoiData';
import { usePoiData } from '../../hooks'; import { usePoiData } from '../../hooks';
import { ChevronDownOutline as DownIcon } from 'heroicons-react';
const MapLayerControl: React.FC = () => { const MapLayerControl: React.FC = () => {
const { data } = usePoiData(); const { data } = usePoiData();
const { filterCategories, setFilterCategories } = useFilteredPoiData(); const { filterCategories, setFilterCategories } = useFilteredPoiData();
const layers = Array.from(new Set(data?.map((poi) => poi.category))); const layers = Array.from(new Set(data?.map((poi) => poi.category)));
const [isOpen, setIsOpen] = useState(false);
const onChangeCheckbox = (state: boolean, layer: string) => { const onChangeCheckbox = (state: boolean, layer: string) => {
if (state) { if (state) {
...@@ -16,22 +18,29 @@ const MapLayerControl: React.FC = () => { ...@@ -16,22 +18,29 @@ const MapLayerControl: React.FC = () => {
}; };
return ( return (
<div className="border-2 border-black border-opacity-20 rounded-lg absolute right-0 bottom-0 mb-4 mr-4 bg-white p-4 z-10"> <div className="border-2 border-black w-52 border-opacity-20 rounded-lg absolute right-0 bottom-0 mb-4 mr-4 bg-white p-4 z-10">
{layers.map((layer) => { <div className={`flex justify-between cursor-pointer ${isOpen ? 'mb-2' : ''}`} onClick={() => setIsOpen(!isOpen)}>
return ( <h3 className="text-base font-semibold text-gray-900">Kategorien:</h3>
<div key={layer}> <DownIcon
<label className="inline-flex items-center"> className={`w-6 h-6 text-gray-400 transform transition duration-300 ${isOpen ? 'rotate-180' : 'rotate-0'}`}
<input />
type="checkbox" </div>
className="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-offset-0 focus:ring-indigo-200 focus:ring-opacity-50" {isOpen &&
checked={!!filterCategories?.find((cat) => cat === layer)} layers.map((layer) => {
onChange={(e) => onChangeCheckbox(e.target.checked, layer)} return (
/> <div key={layer} className="my-2">
<span className="ml-2">{layer}</span> <label className="inline-flex items-center">
</label> <input
</div> type="checkbox"
); className="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-offset-0 focus:ring-indigo-200 focus:ring-opacity-50"
})} checked={!!filterCategories?.find((cat) => cat === layer)}
onChange={(e) => onChangeCheckbox(e.target.checked, layer)}
/>
<span className="ml-2">{layer}</span>
</label>
</div>
);
})}
</div> </div>
); );
}; };
......
...@@ -4,12 +4,11 @@ import { useMap, useMapEvent } from 'react-leaflet'; ...@@ -4,12 +4,11 @@ import { useMap, useMapEvent } from 'react-leaflet';
import { useStore } from '../../hooks'; import { useStore } from '../../hooks';
interface Props { interface Props {
center: LatLngTuple; bounds: Array<LatLngTuple>;
zoom: number;
createPoiMode?: boolean; createPoiMode?: boolean;
} }
const MapViewController: React.FC<Props> = ({ center, zoom, createPoiMode }) => { const MapViewController: React.FC<Props> = ({ bounds, createPoiMode }) => {
const setDraftPoi = useStore((state) => state.setDraftPoi); const setDraftPoi = useStore((state) => state.setDraftPoi);
const map = useMap(); const map = useMap();
...@@ -20,8 +19,8 @@ const MapViewController: React.FC<Props> = ({ center, zoom, createPoiMode }) => ...@@ -20,8 +19,8 @@ const MapViewController: React.FC<Props> = ({ center, zoom, createPoiMode }) =>
}); });
useEffect(() => { useEffect(() => {
map.setView(center, zoom); map.fitBounds(bounds, { maxZoom: 16 });
}, [center, zoom]); }, [bounds]);
return <></>; return <></>;
}; };
......
import React, { useEffect } from 'react';
import { usePoiData, useStore } from '../hooks';
interface Props {
poiId: string | null;
}
const PoiLoader: React.FC<Props> = ({ poiId }) => {
const { data } = usePoiData();
const setSelectedPoi = useStore((state) => state.setSelectedPoi);
useEffect(() => {
if (data && poiId !== undefined) {
const poi = poiId ? data.find((poi) => String(poi.id) === poiId) : null;
if (poi !== undefined) setSelectedPoi(poi);
}
}, [data, poiId]);
return <></>;
};
export default PoiLoader;
import React from 'react'; import React from 'react';
import type { Theme } from 'react-select'; import type { Theme } from 'react-select';
import Select, { NamedProps, StylesConfig } from 'react-select'; import ReactSelect, { NamedProps, StylesConfig, components } from 'react-select';
import Tag from './Tag';
const MultiSelect = <OptionType,>(props: NamedProps<OptionType, true>): JSX.Element => { const Option = ({ children, data, ...rest }: { children: any; data: any }) => {
const customStyles: StylesConfig<OptionType, true> = { return (
// @ts-expect-error: Not typed yet
<components.Option {...rest}>
<Tag color={data.value.color}>{children}</Tag>
</components.Option>
);
};
const Select = <OptionType, isMulti extends boolean>(props: NamedProps<OptionType, isMulti>): JSX.Element => {
const customStyles: StylesConfig<OptionType, isMulti> = {
control: (provided) => ({ ...provided, border: '0', borderRadius: '0.5em' }), control: (provided) => ({ ...provided, border: '0', borderRadius: '0.5em' }),
multiValue: (provided) => ({ ...provided, borderRadius: '999px', padding: '0 3px' }), multiValue: (provided, state) => ({
...provided,
// @ts-expect-error: Not typed yet
backgroundColor: state?.data?.value?.color || 'grey',
borderRadius: '999px',
padding: '0 3px',
}),
multiValueRemove: (provided) => ({ multiValueRemove: (provided) => ({
...provided, ...provided,
color: 'hsl(0, 0%, 50%)', color: 'hsl(0, 0%, 50%)',
...@@ -14,7 +30,10 @@ const MultiSelect = <OptionType,>(props: NamedProps<OptionType, true>): JSX.Elem ...@@ -14,7 +30,10 @@ const MultiSelect = <OptionType,>(props: NamedProps<OptionType, true>): JSX.Elem
}; };
return ( return (
<Select <ReactSelect
components={{
Option,
}}
theme={(theme): Theme => ({ theme={(theme): Theme => ({
...theme, ...theme,
// @ts-expect-error: ThemeConfig type from definitely-typed is not complete // @ts-expect-error: ThemeConfig type from definitely-typed is not complete
...@@ -24,8 +43,8 @@ const MultiSelect = <OptionType,>(props: NamedProps<OptionType, true>): JSX.Elem ...@@ -24,8 +43,8 @@ const MultiSelect = <OptionType,>(props: NamedProps<OptionType, true>): JSX.Elem
primary25: '#C7D2FE', primary25: '#C7D2FE',
}, },
})} })}
placeholder={'Tags auswählen...'}
styles={customStyles} styles={customStyles}
isMulti={true}
name="pois" name="pois"
className="hover:border-opacity-40 rounded-lg w-full border-2 border-black border-opacity-20 focus-within:border-indigo-300 focus-within:ring focus-within:ring-indigo-200 focus-within:ring-opacity-50 mt-1" className="hover:border-opacity-40 rounded-lg w-full border-2 border-black border-opacity-20 focus-within:border-indigo-300 focus-within:ring focus-within:ring-indigo-200 focus-within:ring-opacity-50 mt-1"
{...props} {...props}
...@@ -33,4 +52,4 @@ const MultiSelect = <OptionType,>(props: NamedProps<OptionType, true>): JSX.Elem ...@@ -33,4 +52,4 @@ const MultiSelect = <OptionType,>(props: NamedProps<OptionType, true>): JSX.Elem
); );
}; };
export default MultiSelect; export default Select;
import React, { SyntheticEvent } from 'react';
import { X as CloseIcon } from 'heroicons-react';
interface Props {
onClick: (event: SyntheticEvent) => void;
absolute?: boolean;
}
const CloseButton: React.FC<Props> = ({ onClick, absolute = false }) => {
return (
<CloseIcon
size={32}
className={`${
absolute ? 'absolute left-5 top-5' : ''
} p-1 text-gray-600 inline-block cursor-pointer bg-gray-200 bg-opacity-30 hover:bg-opacity-80 rounded-full`}
onClick={onClick}
/>
);
};
export default CloseButton;
...@@ -16,7 +16,7 @@ const ListElement: React.FC<Props> = ({ value, hovered, ...restProps }) => { ...@@ -16,7 +16,7 @@ const ListElement: React.FC<Props> = ({ value, hovered, ...restProps }) => {
{...restProps} {...restProps}
className={`border-2 border-black border-opacity-20 ${ className={`border-2 border-black border-opacity-20 ${
hovered ? 'border-opacity-40' : 'hover:border-opacity-40' hovered ? 'border-opacity-40' : 'hover:border-opacity-40'
} cursor-pointer rounded-lg md:overflow-hidden mx-4 my-2`} } cursor-pointer rounded-lg mx-4 my-2`}
> >
<div className="p-3"> <div className="p-3">
<h2 className="tracking-widest text-xs uppercase title-font font-medium text-gray-400 mb-1"> <h2 className="tracking-widest text-xs uppercase title-font font-medium text-gray-400 mb-1">
......
import { X as CloseIcon } from 'heroicons-react';
import React from 'react'; import React from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import AddPoiForm from '../Form/AddPoiForm'; import AddPoiForm from '../Form/AddPoiForm';
import CloseButton from './CloseButton';
import SidebarContainer from './SidebarContainer'; import SidebarContainer from './SidebarContainer';
const SidebarCreateView: React.FC = () => { const SidebarCreateView: React.FC = () => {
const history = useHistory(); const history = useHistory();
return ( return (
<SidebarContainer className="p-5"> <SidebarContainer className="p-5">
<CloseIcon <CloseButton onClick={() => history.push('/')} />
size={32}
className={`flex-shrink-0 left-5 top-5 p-1 text-gray-500 inline-block cursor-pointer hover:bg-gray-300 hover:bg-opacity-50 rounded-full`}
onClick={() => history.push('/')}
/>
<h1 className="text-xl font-medium title-font text-gray-900 mt-2 mb-4">Neuen Ort anlegen:</h1> <h1 className="text-xl font-medium title-font text-gray-900 mt-2 mb-4">Neuen Ort anlegen:</h1>
<AddPoiForm /> <AddPoiForm />
</SidebarContainer> </SidebarContainer>
......
import React from 'react'; import React, { useState } from 'react';
import { usePoiData, useStore } from '../../hooks'; import { usePoiData, useStore } from '../../hooks';
import ListElement from './ListElement'; import ListElement from './ListElement';
import SidebarContainer from './SidebarContainer'; import SidebarContainer from './SidebarContainer';
import { removeDuplicateObjects } from '../../util/array'; import { removeDuplicateObjects } from '../../util/array';
import { useFilteredPoiData } from '../../hooks/useFilteredPoiData'; import { useFilteredPoiData } from '../../hooks/useFilteredPoiData';
import type { Tag } from '../../types/PointOfInterest'; import type { Tag } from '../../types/PointOfInterest';
import MultiSelect from '../MultiSelect'; import { FilterOutline as FilterIcon } from 'heroicons-react';
import { useHistory } from 'react-router-dom';
import Select from '../Select';
const SidebarListView: React.FC = () => { const SidebarListView: React.FC = () => {
const tagsToSelectOptions = (tags?: Tag[]) => tags?.map((tag) => ({ label: tag.displayName, value: tag })); const tagsToSelectOptions = (tags?: Tag[]) => tags?.map((tag) => ({ label: tag.displayName, value: tag }));
const history = useHistory();
const { data } = usePoiData(); const { data } = usePoiData();
const { data: filteredData, filterTags, setFilterTags } = useFilteredPoiData(); const { data: filteredData, filterTags, setFilterTags } = useFilteredPoiData();
const hoveredPoi = useStore((state) => state.hoveredPoi); const hoveredPoi = useStore((state) => state.hoveredPoi);
const setHoveredPoi = useStore((state) => state.setHoveredPoi); const setHoveredPoi = useStore((state) => state.setHoveredPoi);
const setSelectedPoi = useStore((state) => state.setSelectedPoi); const [filterInputIsOpen, setFilterInputIsOpen] = useState(false);
const tags = const tags =
data && data &&
...@@ -26,20 +29,35 @@ const SidebarListView: React.FC = () => { ...@@ -26,20 +29,35 @@ const SidebarListView: React.FC = () => {
return ( return (
<SidebarContainer> <SidebarContainer>
<div className="p-4"> <div className="flex-col m-4 mb-2 pb-2 border-black border-opacity-20 border-b-2">
<MultiSelect <div className="flex justify-between items-center">
options={options} <h1 className="text-xl font-medium title-font text-gray-900">{filteredData?.length} Orte:</h1>
value={filterTags && tagsToSelectOptions(filterTags)} <FilterIcon
onChange={(selectedOptions) => setFilterTags(selectedOptions.map((opt) => opt.value))} onClick={() => setFilterInputIsOpen(!filterInputIsOpen)}
/> className="text-black text-opacity-20 hover:text-opacity-60 w-5 h-5 cursor-pointer"
/>
</div>
{filterInputIsOpen && (
<div className="py-2">
<Select
options={options}
isMulti={true}
value={filterTags && tagsToSelectOptions(filterTags)}
onChange={(selectedOptions) => setFilterTags(selectedOptions.map((opt) => opt.value))}
/>
</div>
)}
</div> </div>
<h1 className="text-xl font-medium title-font m-4 text-gray-900 mb-2">{filteredData?.length} Orte:</h1>
{filteredData?.map((poi) => ( {filteredData?.map((poi) => (
<ListElement <ListElement
key={poi.id} key={poi.id}
onMouseEnter={() => setHoveredPoi(poi)} onMouseEnter={() => setHoveredPoi(poi)}
onMouseLeave={() => setHoveredPoi(null)} onMouseLeave={() => setHoveredPoi(null)}
onClick={() => setSelectedPoi(poi)} onClick={() => {
history.push(`/poi/${String(poi.id)}`);
}}
value={poi} value={poi}
hovered={hoveredPoi?.id === poi.id} hovered={hoveredPoi?.id === poi.id}
/> />
......
import { HomeOutline as HomeIcon, LocationMarkerOutline as AddressIcon, X as CloseIcon } from 'heroicons-react'; import {
HomeOutline as HomeIcon,
LocationMarkerOutline as AddressIcon,
UserGroupOutline as RealtionStatusIcon,
} from 'heroicons-react';
import React from 'react'; import React from 'react';
import { useStore } from '../../hooks'; import { useStore } from '../../hooks';
import SidebarContainer from './SidebarContainer'; import SidebarContainer from './SidebarContainer';
import Tag from '../Tag'; import Tag from '../Tag';
import CloseButton from './CloseButton';
import { useHistory } from 'react-router-dom';
const SidebarSingleView: React.FC = () => { const SidebarSingleView: React.FC = () => {
const selectedPoi = useStore((state) => state.selectedPoi); const selectedPoi = useStore((state) => state.selectedPoi);
const setSelectedPoi = useStore((state) => state.setSelectedPoi);
const strippedUrl = selectedPoi?.website?.replace(/(^\w+:|^)\/\//, ''); const strippedUrl = selectedPoi?.website?.replace(/(^\w+:|^)\/\//, '');
const history = useHistory();
return ( return (
<SidebarContainer className={`relative p-0`}> <SidebarContainer className={`relative p-0`}>
<div className={`${selectedPoi?.image ? '' : 'pl-5 pt-5 '}`}> <div className={`${selectedPoi?.image ? '' : 'pl-5 pt-5 '}`}>
<CloseIcon <CloseButton
size={32} absolute
className={`${ onClick={() => {
selectedPoi?.image ? 'absolute left-5 top-5 ' : '' history.push('/');
}p-1 text-gray-500 inline-block cursor-pointer hover:bg-gray-300 hover:bg-opacity-50 rounded-full`} }}
onClick={() => setSelectedPoi(null)}
/> />
</div> </div>
{selectedPoi?.image && ( {selectedPoi?.image && (
...@@ -36,7 +41,12 @@ const SidebarSingleView: React.FC = () => { ...@@ -36,7 +41,12 @@ const SidebarSingleView: React.FC = () => {
{selectedPoi?.website && ( {selectedPoi?.website && (
<div className={'flex items-center'}> <div className={'flex items-center'}>
<HomeIcon size={18} className={'text-gray-500 mr-2'} /> <HomeIcon size={18} className={'text-gray-500 mr-2'} />
<a className={'text-sm text-gray-500 hover:underline'} href={selectedPoi?.website}> <a
target="_blank"
rel="noopener noreferrer"
className={'text-sm text-gray-500 hover:underline'}
href={selectedPoi?.website}
>
{strippedUrl} {strippedUrl}
</a> </a>
</div> </div>
...@@ -47,6 +57,12 @@ const SidebarSingleView: React.FC = () => { ...@@ -47,6 +57,12 @@ const SidebarSingleView: React.FC = () => {
<div className="text-sm text-gray-500">{selectedPoi?.address}</div> <div className="text-sm text-gray-500">{selectedPoi?.address}</div>
</div> </div>
)} )}
{selectedPoi?.relationStatus && (
<div className={'flex items-center mt-3'}>
<RealtionStatusIcon size={18} className={'text-gray-500 mr-2'} />
<div className="text-sm text-gray-500">{selectedPoi?.relationStatus}</div>
</div>
)}
{!!selectedPoi?.tags?.length && ( {!!selectedPoi?.tags?.length && (
<div className={'flex items-center mt-3 flex-wrap'}> <div className={'flex items-center mt-3 flex-wrap'}>
{selectedPoi?.tags.map((tag) => ( {selectedPoi?.tags.map((tag) => (
......