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
Showing
with 910 additions and 0 deletions
import React from 'react';
import { PaperClipOutline as PaperClipIcon, UploadOutline as UploadIcon } from 'heroicons-react';
interface Props {
label: string;
value: File | null;
name: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
required: boolean;
}
const FileInput: React.FC<Props> = ({ name, label, value, onChange, ...inputProps }) => {
return (
<>
<span className="form-label">
{label}
{inputProps?.required && `*`}
</span>
<label
className={`mt-1 flex items-center rounded-lg border-2 border-black mb-6 w-full p-2 text-center focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 cursor-pointer${
value
? ' bg-white text-black border-opacity-20 hover:border-opacity-40 truncate text-sm text-opacity-40'
: ' bg-indigo-500 text-white border-opacity-0 hover:bg-indigo-600 text-md'
}`}
>
{value ? (
<>
<PaperClipIcon size={18} className="flex-shrink-0 mr-2" />
{value.name}
</>
) : (
<>
<UploadIcon size={18} className="flex-shrink-0 mr-2" />
{'Datei auswählen...'}
</>
)}
<input
className="hidden"
type="file"
name={name}
accept="image/png, image/jpeg"
onChange={onChange}
{...inputProps}
/>
</label>
</>
);
};
export default FileInput;
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;
import React, { useEffect, useMemo, useState } from 'react';
import type { Tag as TagType } from 'src/types/PointOfInterest';
import { generateRandomHslColor } from '../../util/color';
import Tag from '../Tag';
interface Props {
label?: string;
tags: TagType[];
options: TagType[];
onTagsChange: (tags: TagType[]) => void;
required?: boolean;
}
const TagInput: React.FC<Props> = ({ label, tags, options, onTagsChange, required }: Props) => {
// All options
const [tagOptions, setTagOptions] = useState<TagType[]>([]);
// The text input
const [draftEntry, setDraftEntry] = useState('');
const [showOptions, setShowOptions] = useState(false);
// Only show options that start with current draftEntry value
const filteredTagOptions = useMemo(
() => tagOptions.filter((tagOption) => tagOption.displayName.toLowerCase().startsWith(draftEntry.toLowerCase())),
[tagOptions, draftEntry],
);
const handleKeyboardInput = (e: React.KeyboardEvent) => {
if (draftEntry === '') {
if (e.nativeEvent.key === 'Backspace') {
const tagsClone = JSON.parse(JSON.stringify(tags));
const removedTag = tagsClone.pop();
if (removedTag) {
onTagsChange(tagsClone);
}
}
} else {
if (e.nativeEvent.key === 'Enter') {
e.preventDefault();
// Check if a Tag with that name already exists
const allTagNames = tags.map((tag) => tag.displayName.toLowerCase());
const isDuplicate = allTagNames.includes(draftEntry.toLowerCase());
if (!isDuplicate) {
onTagsChange([...tags, { displayName: draftEntry, color: generateRandomHslColor(60, 80), id: 'draft' }]);
}
setDraftEntry('');
}
}
};
const handleClickDeleteTag = (tag: TagType) => {
onTagsChange(tags.filter((filterTag) => tag.displayName.toLowerCase() !== filterTag.displayName.toLowerCase()));
};
const handleSelectTag = (tag: TagType) => {
onTagsChange([...tags, tag]);
};
useEffect(() => {
setTagOptions(
options.filter(
(tagOption) => !tags.find((tag) => tag.displayName.toLowerCase() === tagOption.displayName.toLowerCase()),
),
);
}, [tags, options]);
return (
<label className="relative block mb-4">
{!!label && (
<span className="form-label">
{label}
{required && `*`}
</span>
)}
<div
tabIndex={0}
className={`${
showOptions && filteredTagOptions.length ? 'rounded-t-lg hover:border-opacity-40 ' : 'rounded-lg '
}flex 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 input-chevron mt-1`}
>
<div className="flex flex-wrap items-center flex-1 p-2">
{tags.map((selectedTag) => (
<Tag
key={`selected${selectedTag.color}`}
onClickDelete={() => handleClickDeleteTag(selectedTag)}
color={selectedTag.color}
>
{selectedTag.displayName}
</Tag>
))}
<input
onKeyDown={handleKeyboardInput}
onChange={(e) => {
const newValue = e.target.value;
setDraftEntry(newValue);
}}
onFocus={() => setShowOptions(true)}
onBlur={() => {
setShowOptions(false);
}}
type={'text'}
value={draftEntry}
className={`form-input w-16 block p-0 flex-1 rounded-lg border-0 focus:outline-none focus:ring-0 ${
tags.length ? '' : 'px-3'
}`}
/>
</div>
</div>
{/* Options Dropdown */}
{filteredTagOptions.length > 0 && (
<ul
className={`${
showOptions && filteredTagOptions.length ? 'block ' : 'hidden '
}rounded-b-lg z-10 absolute w-full bg-white border-2 border-t-0 shadow-md`}
>
{filteredTagOptions.map((option) => {
return (
<li
key={`${option.id}`}
onMouseDown={() => {
handleSelectTag(option);
}}
className="w-full p-1 hover:bg-gray-100"
>
<Tag color={option.color}>{option.displayName}</Tag>
</li>
);
})}
</ul>
)}
</label>
);
};
export default TagInput;
import React from 'react';
interface Props {
label: string;
value: string;
name: string;
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
required?: boolean;
}
const TextAreaInput: React.FC<Props> = ({ name, label, value, onChange, ...textAreaProps }) => {
return (
<label className="block mb-4">
{!!label && (
<span className="form-label">
{label}
{textAreaProps?.required && `*`}
</span>
)}
<textarea
name={name}
value={value}
className="form-textarea form-input-custom"
rows={3}
onChange={onChange}
{...textAreaProps}
></textarea>
</label>
);
};
export default TextAreaInput;
import React from 'react';
interface Props {
label: string;
value: string;
name: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
required?: boolean;
type?: string;
}
const TextInput: React.FC<Props> = ({ name, label, value, onChange, type = 'text', ...inputProps }) => {
return (
<label className="block mb-4">
{!!label && (
<span className="form-label">
{label}
{inputProps?.required && `*`}
</span>
)}
<input
type={type}
name={name}
value={value}
className="form-input form-input-custom"
onChange={onChange}
{...inputProps}
/>
</label>
);
};
export default TextInput;
import type { LatLngTuple } from 'leaflet';
import { divIcon, DivIconOptions } from 'leaflet';
import React, { useEffect, useMemo, useState } from 'react';
import { MapContainer, Marker, TileLayer } from 'react-leaflet';
import { useStore } from '../../hooks';
import MapViewController from './MapViewController';
import { useFilteredPoiData } from '../../hooks/useFilteredPoiData';
import MapLayerControl from './MapLayerControl';
import { useHistory } from 'react-router-dom';
interface Props {
createMode?: boolean;
}
const DEFAULT_CENTER: LatLngTuple = [53.550359, 9.986701];
const iconProps: DivIconOptions = {
className: 'marker-red',
// Source: https://fontawesome.com/icons/map-marker-alt
html: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M172.268 501.67C26.97 291.031 0 269.413 0 192 0 85.961 85.961 0 192 0s192 85.961 192 192c0 77.413-26.97 99.031-172.268 309.67-9.535 13.774-29.93 13.773-39.464 0zM192 272c44.183 0 80-35.817 80-80s-35.817-80-80-80-80 35.817-80 80 35.817 80 80 80z"/></svg>`,
iconSize: [24, 32],
iconAnchor: [12, 32],
};
export const Map: React.FC<Props> = ({ createMode }) => {
const icon = useMemo(() => divIcon(iconProps), [iconProps]);
const largeIcon = useMemo(() => divIcon({ ...iconProps, iconSize: [30, 40], iconAnchor: [15, 40] }), [iconProps]);
const greenLargeIcon = useMemo(
() => divIcon({ ...iconProps, iconSize: [30, 40], iconAnchor: [15, 40], className: 'marker-green' }),
[iconProps],
);
const { data } = useFilteredPoiData();
const draftPoi = useStore((state) => state.draftPoi);
const [mapBounds, setMapBounds] = useState<Array<LatLngTuple>>([DEFAULT_CENTER]);
const hoveredPoi = useStore((state) => state.hoveredPoi);
const setHoveredPoi = useStore((state) => state.setHoveredPoi);
const selectedPoi = useStore((state) => state.selectedPoi);
const history = useHistory();
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 (
<div className="relative h-full w-full z-0">
{!createMode && !selectedPoi && <MapLayerControl />}
<MapContainer id={'mapid'} className={'h-full w-full z-0'} scrollWheelZoom={true}>
<TileLayer
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}"
id="mapbox/streets-v11"
tileSize={512}
accessToken={import.meta.env.SNOWPACK_PUBLIC_MAPBOX_TOKEN}
zoomOffset={-1}
maxZoom={18}
/>
<MapViewController bounds={mapBounds} createPoiMode={createMode} />
{/* Single marker when creating a new POI */}
{!!(createMode && draftPoi) && <Marker icon={greenLargeIcon} position={draftPoi} />}
{/* Single marker when POI is selected */}
{!!(selectedPoi && selectedLatlng && !createMode) && <Marker icon={largeIcon} position={selectedLatlng} />}
{/* Multiple markers, when no POI is selected */}
{!selectedPoi &&
!createMode &&
data?.map((poi) => {
const poiLatLng: LatLngTuple = [poi.lat, poi.lng];
return (
<Marker
icon={hoveredPoi?.id === poi.id ? largeIcon : icon}
opacity={hoveredPoi?.id === poi.id ? 1 : 0.7}
key={poi.id}
position={poiLatLng}
eventHandlers={{
click: () => {
history.push(`/poi/${String(poi.id)}`);
},
mouseover: () => {
setHoveredPoi(poi);
},
mouseout: () => {
setHoveredPoi(null);
},
}}
/>
);
})}
</MapContainer>
</div>
);
};
export default Map;
import React, { useState } from 'react';
import { useFilteredPoiData } from '../../hooks/useFilteredPoiData';
import { usePoiData } from '../../hooks';
import { ChevronDownOutline as DownIcon } from 'heroicons-react';
const MapLayerControl: React.FC = () => {
const { data } = usePoiData();
const { filterCategories, setFilterCategories } = useFilteredPoiData();
const layers = Array.from(new Set(data?.map((poi) => poi.category)));
const [isOpen, setIsOpen] = useState(false);
const onChangeCheckbox = (state: boolean, layer: string) => {
if (state) {
setFilterCategories([...filterCategories, layer]);
} else {
setFilterCategories(filterCategories.filter((cat) => cat !== layer));
}
};
return (
<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">
<div className={`flex justify-between cursor-pointer ${isOpen ? 'mb-2' : ''}`} onClick={() => setIsOpen(!isOpen)}>
<h3 className="text-base font-semibold text-gray-900">Kategorien:</h3>
<DownIcon
className={`w-6 h-6 text-gray-400 transform transition duration-300 ${isOpen ? 'rotate-180' : 'rotate-0'}`}
/>
</div>
{isOpen &&
layers.map((layer) => {
return (
<div key={layer} className="my-2">
<label className="inline-flex items-center">
<input
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>
);
};
export default MapLayerControl;
import type { LatLngTuple } from 'leaflet';
import React, { useEffect } from 'react';
import { useMap, useMapEvent } from 'react-leaflet';
import { useStore } from '../../hooks';
interface Props {
bounds: Array<LatLngTuple>;
createPoiMode?: boolean;
}
const MapViewController: React.FC<Props> = ({ bounds, createPoiMode }) => {
const setDraftPoi = useStore((state) => state.setDraftPoi);
const map = useMap();
useMapEvent('click', (event) => {
if (createPoiMode) {
setDraftPoi([event.latlng.lat, event.latlng.lng]);
}
});
useEffect(() => {
map.fitBounds(bounds, { maxZoom: 16 });
}, [bounds]);
return <></>;
};
export default MapViewController;
import React from 'react';
interface Props {
title: string;
text: string;
icon?: JSX.Element;
}
const Modal: React.FC<Props> = ({ title, text, icon }) => {
return (
<div role="dialog" aria-modal="true" className="fixed z-10 inset-0 flex flex-column items-center justify-center">
<div className="fixed inset-0 bg-gray-400 bg-opacity-50" aria-hidden="true"></div>
<div className="inline-block z-10 bg-white rounded-lg text-left overflow-hidden shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white p-4 sm:p-6">
<div className="sm:flex sm:items-start">
{icon && (
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
{icon}
</div>
)}
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-title">
{title}
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">{text}</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Modal;
import React from 'react';
import { CheckCircleOutline as CheckIcon, X as XIcon, ExclamationOutline as AlertIcon } from 'heroicons-react';
import { useStore } from '../hooks';
const Notification: React.FC = () => {
const notification = useStore((state) => state.notification);
const setNotification = useStore((state) => state.setNotification);
const icon: Record<string, JSX.Element> = {
alert: <AlertIcon className="h-6 w-6 text-red-600" />,
success: <CheckIcon className="h-6 w-6 text-green-400" />,
};
return (
notification && (
<div
aria-live="assertive"
className="z-10 fixed inset-0 flex items-end px-4 py-6 pointer-events-none sm:p-6 sm:items-start"
>
<div className="w-full flex flex-col items-center space-y-4 sm:items-end">
<div className="max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden">
<div className="p-4">
<div className="flex items-start">
<div className="flex-shrink-0">{icon[notification.type]}</div>
<div className="ml-3 w-0 flex-1 pt-0.5">
<p className="text-sm font-medium text-gray-900">{notification.title}</p>
<p className="mt-1 text-sm text-gray-500">{notification.text}</p>
</div>
<div className="ml-4 flex-shrink-0 flex">
<button
className="bg-white rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={() => setNotification(null)}
>
<span className="sr-only">Close</span>
<XIcon className="h-5 w-5" />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
)
);
};
export default Notification;
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 type { Theme } from 'react-select';
import ReactSelect, { NamedProps, StylesConfig, components } from 'react-select';
import Tag from './Tag';
const Option = ({ children, data, ...rest }: { children: any; data: any }) => {
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' }),
multiValue: (provided, state) => ({
...provided,
// @ts-expect-error: Not typed yet
backgroundColor: state?.data?.value?.color || 'grey',
borderRadius: '999px',
padding: '0 3px',
}),
multiValueRemove: (provided) => ({
...provided,
color: 'hsl(0, 0%, 50%)',
'&:hover': { backgroundColor: 'initial', color: 'black' },
}),
};
return (
<ReactSelect
components={{
Option,
}}
theme={(theme): Theme => ({
...theme,
// @ts-expect-error: ThemeConfig type from definitely-typed is not complete
borderRadius: '0.5em',
colors: {
...theme.colors,
primary25: '#C7D2FE',
},
})}
placeholder={'Tags auswählen...'}
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}
/>
);
};
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;
import React from 'react';
import React, { SyntheticEvent } from 'react';
import type { PointOfInterest } from 'src/types/PointOfInterest';
interface Props {
value: PointOfInterest;
hovered?: boolean;
onMouseEnter: (event: SyntheticEvent) => void;
onMouseLeave: (event: SyntheticEvent) => void;
onClick: (event: SyntheticEvent) => void;
}
const SidebarListElement: React.FC<Props> = ({ value, ...restProps }) => {
const ListElement: React.FC<Props> = ({ value, hovered, ...restProps }) => {
return (
value && (
<div
{...restProps}
className="border-2 border-black border-opacity-20 hover:border-opacity-40 cursor-pointer rounded-lg md:overflow-hidden mx-4 my-2"
className={`border-2 border-black border-opacity-20 ${
hovered ? 'border-opacity-40' : 'hover:border-opacity-40'
} cursor-pointer rounded-lg mx-4 my-2`}
>
<div className="p-3">
<h2 className="tracking-widest text-xs title-font font-medium text-gray-400 mb-1">OFFENE WERKSTATT</h2>
<h2 className="tracking-widest text-xs uppercase title-font font-medium text-gray-400 mb-1">
{value.category}
</h2>
<h1 className="title-font text-lg font-medium text-gray-900">{value.name}</h1>
</div>
</div>
......@@ -21,4 +29,4 @@ const SidebarListElement: React.FC<Props> = ({ value, ...restProps }) => {
);
};
export default SidebarListElement;
export default ListElement;
import React, { CSSProperties } from 'react';
import React from 'react';
interface Props {
style?: CSSProperties;
className?: string;
}
const SidebarContainer: React.FC<Props> = ({ style, className, children }) => {
const SidebarContainer: React.FC<Props> = ({ className, children }) => {
return (
<div
style={style}
className={`flex flex-col shadow-2xl border-t-2 md:border-r-2 md:border-t-0 border-black border-opacity-20 ${
<aside
className={`sidebar box-border flex flex-col shadow-2xl border-t-2 md:border-r-2 md:border-t-0 border-black border-opacity-20 ${
className ?? ''
}`}
>
{children}
</div>
</aside>
);
};
......
import React from 'react';
import { useHistory } from 'react-router-dom';
import AddPoiForm from '../Form/AddPoiForm';
import CloseButton from './CloseButton';
import SidebarContainer from './SidebarContainer';
const SidebarCreateView: React.FC = () => {
const history = useHistory();
return (
<SidebarContainer className="p-5">
<CloseButton onClick={() => history.push('/')} />
<h1 className="text-xl font-medium title-font text-gray-900 mt-2 mb-4">Neuen Ort anlegen:</h1>
<AddPoiForm />
</SidebarContainer>
);
};
export default SidebarCreateView;
import React, { useState } from 'react';
import { usePoiData, useStore } from '../../hooks';
import ListElement from './ListElement';
import SidebarContainer from './SidebarContainer';
import { removeDuplicateObjects } from '../../util/array';
import { useFilteredPoiData } from '../../hooks/useFilteredPoiData';
import type { Tag } from '../../types/PointOfInterest';
import { FilterOutline as FilterIcon } from 'heroicons-react';
import { useHistory } from 'react-router-dom';
import Select from '../Select';
const SidebarListView: React.FC = () => {
const tagsToSelectOptions = (tags?: Tag[]) => tags?.map((tag) => ({ label: tag.displayName, value: tag }));
const history = useHistory();
const { data } = usePoiData();
const { data: filteredData, filterTags, setFilterTags } = useFilteredPoiData();
const hoveredPoi = useStore((state) => state.hoveredPoi);
const setHoveredPoi = useStore((state) => state.setHoveredPoi);
const [filterInputIsOpen, setFilterInputIsOpen] = useState(false);
const tags =
data &&
removeDuplicateObjects(
data?.flatMap((poi) => poi.tags),
'id',
);
const options = tagsToSelectOptions(tags);
return (
<SidebarContainer>
<div className="flex-col m-4 mb-2 pb-2 border-black border-opacity-20 border-b-2">
<div className="flex justify-between items-center">
<h1 className="text-xl font-medium title-font text-gray-900">{filteredData?.length} Orte:</h1>
<FilterIcon
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>
{filteredData?.map((poi) => (
<ListElement
key={poi.id}
onMouseEnter={() => setHoveredPoi(poi)}
onMouseLeave={() => setHoveredPoi(null)}
onClick={() => {
history.push(`/poi/${String(poi.id)}`);
}}
value={poi}
hovered={hoveredPoi?.id === poi.id}
/>
))}
</SidebarContainer>
);
};
export default SidebarListView;
import {
HomeOutline as HomeIcon,
LocationMarkerOutline as AddressIcon,
UserGroupOutline as RealtionStatusIcon,
} from 'heroicons-react';
import React from 'react';
import { useStore } from '../../hooks';
import SidebarContainer from './SidebarContainer';
import Tag from '../Tag';
import CloseButton from './CloseButton';
import { useHistory } from 'react-router-dom';
const SidebarSingleView: React.FC = () => {
const selectedPoi = useStore((state) => state.selectedPoi);
const strippedUrl = selectedPoi?.website?.replace(/(^\w+:|^)\/\//, '');
const history = useHistory();
return (
<SidebarContainer className={`relative p-0`}>
<div className={`${selectedPoi?.image ? '' : 'pl-5 pt-5 '}`}>
<CloseButton
absolute
onClick={() => {
history.push('/');
}}
/>
</div>
{selectedPoi?.image && (
<img
className="lg:h-48 md:h-36 w-full object-cover object-center"
src={selectedPoi?.image}
alt={selectedPoi?.name}
/>
)}
<div className="p-6">
<h2 className="tracking-widest uppercase text-xs title-font font-medium text-gray-400 mb-1">
{selectedPoi?.category}
</h2>
<h1 className="title-font text-lg font-medium text-gray-900 mb-3">{selectedPoi?.name}</h1>
<p className="leading-relaxed mb-6">{selectedPoi?.description}</p>
{selectedPoi?.website && (
<div className={'flex items-center'}>
<HomeIcon size={18} className={'text-gray-500 mr-2'} />
<a
target="_blank"
rel="noopener noreferrer"
className={'text-sm text-gray-500 hover:underline'}
href={selectedPoi?.website}
>
{strippedUrl}
</a>
</div>
)}
{selectedPoi?.address && (
<div className={'flex items-center mt-3'}>
<AddressIcon size={18} className={'text-gray-500 mr-2'} />
<div className="text-sm text-gray-500">{selectedPoi?.address}</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 && (
<div className={'flex items-center mt-3 flex-wrap'}>
{selectedPoi?.tags.map((tag) => (
<Tag key={tag.id} color={tag.color}>
{tag.displayName}
</Tag>
))}
</div>
)}
</div>
</SidebarContainer>
);
};
export default SidebarSingleView;
import React from 'react';
const Spinner: React.FC = () => {
return (
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
);
};
export default Spinner;
import React from 'react';
import { SWRConfig } from 'swr';
import { useStore } from '../hooks';
import fetcher from '../util/fetcher';
const SwrWrapper: React.FC = ({ children }) => {
const setError = useStore((state) => state.setError);
const error = useStore((state) => state.error);
return (
<SWRConfig
value={{
fetcher,
onError: (error) => {
setError({
title: 'API nicht erreichbar',
message: 'Kann die API nicht erreichen. Bitte später erneut probieren.',
icon: 'alert',
});
console.error('Error while fetching', error);
},
onSuccess: () => {
// Reset error modal
if (error) setError(null);
},
}}
>
{children}
</SWRConfig>
);
};
export default SwrWrapper;