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 (20)
...@@ -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.
...@@ -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: */
......
...@@ -5,7 +5,12 @@ import Creatable from 'react-select/creatable'; ...@@ -5,7 +5,12 @@ import Creatable from 'react-select/creatable';
const CreatableSelect = <OptionType, isMulti extends boolean>(props: NamedProps<OptionType, isMulti>): JSX.Element => { const CreatableSelect = <OptionType, isMulti extends boolean>(props: NamedProps<OptionType, isMulti>): JSX.Element => {
const customStyles: StylesConfig<OptionType, isMulti> = { const customStyles: StylesConfig<OptionType, isMulti> = {
control: (provided) => ({ ...provided, border: '0', borderRadius: '0.5em' }), control: (provided) => ({
...provided,
border: '0',
borderRadius: '0.5em',
boxShadow: 'none',
}),
multiValue: (provided) => ({ ...provided, borderRadius: '999px', padding: '0 3px' }), multiValue: (provided) => ({ ...provided, borderRadius: '999px', padding: '0 3px' }),
multiValueRemove: (provided) => ({ multiValueRemove: (provided) => ({
...provided, ...provided,
......
...@@ -15,7 +15,7 @@ import Spinner from '../Spinner'; ...@@ -15,7 +15,7 @@ import Spinner from '../Spinner';
import type { CreatePoiMutationMutationVariables, Mutation } from '../../generated/graphql'; import type { CreatePoiMutationMutationVariables, Mutation } from '../../generated/graphql';
import SelectInput from './SelectInput'; import SelectInput from './SelectInput';
type RelationStatusOption = { label: string; value: string }; type StringSelectOption = { label: string; value: string };
const AddPoiForm: React.FC = () => { const AddPoiForm: React.FC = () => {
const [formData, setFormData] = useState<PointOfInterestFormData>({ const [formData, setFormData] = useState<PointOfInterestFormData>({
...@@ -38,7 +38,8 @@ const AddPoiForm: React.FC = () => { ...@@ -38,7 +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 [relationStatusOptions, setRelationStatusOptions] = useState<RelationStatusOption[]>([]); 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);
...@@ -123,6 +124,10 @@ const AddPoiForm: React.FC = () => { ...@@ -123,6 +124,10 @@ const AddPoiForm: React.FC = () => {
.filter((poi) => !!poi.relationStatus) .filter((poi) => !!poi.relationStatus)
.map((poi) => ({ label: poi.relationStatus, value: poi.relationStatus })); .map((poi) => ({ label: poi.relationStatus, value: poi.relationStatus }));
setRelationStatusOptions(relationStatuses); setRelationStatusOptions(relationStatuses);
const categories = removeDuplicateObjects(data, 'category')
.filter((poi) => !!poi.category)
.map((poi) => ({ label: poi.category, value: poi.category }));
setCatgeoryOptions(categories);
} }
}, [data]); }, [data]);
...@@ -153,21 +158,29 @@ const AddPoiForm: React.FC = () => { ...@@ -153,21 +158,29 @@ 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'}
name={'category'}
value={formData.category}
onChange={handleInputChange}
required required
placeholder={'Auswählen...'}
name={'category'}
options={categoryOptions}
onChange={(selectedOption) =>
setFormData((prev) => ({
...prev,
category: selectedOption ? (selectedOption as StringSelectOption).value : '',
}))
}
/> />
<SelectInput <SelectInput
label={'Verhältnis zum Fab City Hamburg e.V.'} label={'Verhältnis zum Fab City Hamburg e.V.'}
required
name={'relationStatus'} name={'relationStatus'}
placeholder={'Auswählen...'}
options={relationStatusOptions} options={relationStatusOptions}
onChange={(selectedOption) => onChange={(selectedOption) =>
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
relationStatus: selectedOption ? (selectedOption as RelationStatusOption).value : '', relationStatus: selectedOption ? (selectedOption as StringSelectOption).value : '',
})) }))
} }
/> />
......
...@@ -26,7 +26,7 @@ const SelectInput = <OptionType, isMulti extends boolean>({ ...@@ -26,7 +26,7 @@ const SelectInput = <OptionType, isMulti extends boolean>({
<CreatableSelect <CreatableSelect
name={name} name={name}
value={value} value={value}
className="form-input form-input-custom" className="p-0 form-input form-input-custom"
onChange={onChange} onChange={onChange}
{...inputProps} {...inputProps}
/> />
......
...@@ -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';
...@@ -30,22 +30,27 @@ export const Map: React.FC<Props> = ({ createMode }) => { ...@@ -30,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 history = useHistory(); 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}"
...@@ -55,7 +60,7 @@ export const Map: React.FC<Props> = ({ createMode }) => { ...@@ -55,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 */}
......
...@@ -18,7 +18,7 @@ const MapLayerControl: React.FC = () => { ...@@ -18,7 +18,7 @@ const MapLayerControl: React.FC = () => {
}; };
return ( return (
<div className="border-2 border-black w-48 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">
<div className={`flex justify-between cursor-pointer ${isOpen ? 'mb-2' : ''}`} onClick={() => setIsOpen(!isOpen)}> <div className={`flex justify-between cursor-pointer ${isOpen ? 'mb-2' : ''}`} onClick={() => setIsOpen(!isOpen)}>
<h3 className="text-base font-semibold text-gray-900">Kategorien:</h3> <h3 className="text-base font-semibold text-gray-900">Kategorien:</h3>
<DownIcon <DownIcon
...@@ -28,7 +28,7 @@ const MapLayerControl: React.FC = () => { ...@@ -28,7 +28,7 @@ const MapLayerControl: React.FC = () => {
{isOpen && {isOpen &&
layers.map((layer) => { layers.map((layer) => {
return ( return (
<div key={layer}> <div key={layer} className="my-2">
<label className="inline-flex items-center"> <label className="inline-flex items-center">
<input <input
type="checkbox" type="checkbox"
......
...@@ -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 from 'react'; import React from 'react';
import type { Theme } from 'react-select'; import type { Theme } from 'react-select';
import ReactSelect, { NamedProps, StylesConfig } 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 Select = <OptionType, isMulti extends boolean>(props: NamedProps<OptionType, isMulti>): JSX.Element => {
const customStyles: StylesConfig<OptionType, isMulti> = { 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%)',
...@@ -15,6 +31,9 @@ const Select = <OptionType, isMulti extends boolean>(props: NamedProps<OptionTyp ...@@ -15,6 +31,9 @@ const Select = <OptionType, isMulti extends boolean>(props: NamedProps<OptionTyp
return ( return (
<ReactSelect <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
......
...@@ -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 { HomeOutline as HomeIcon, LocationMarkerOutline as AddressIcon } 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';
...@@ -53,6 +57,12 @@ const SidebarSingleView: React.FC = () => { ...@@ -53,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) => (
......
export function generateRandomHslColor(saturation = 100, lightness = 50): string { export function generateRandomHslColor(saturation = 100, lightness = 50, distinctValues = 20): string {
const randomHue = getRandomIntInclusive(0, 360); const randomColorIndex = getRandomIntInclusive(0, distinctValues);
const color = `hsl(${randomHue},${saturation}%,${lightness}%)`; const color = `hsl(${Math.floor(randomColorIndex * (360 / distinctValues))},${saturation}%,${lightness}%)`;
return color; return color;
} }
......