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 (30)
Showing
with 15373 additions and 839 deletions
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 12,
sourceType: 'module',
},
plugins: ['react', '@typescript-eslint'],
rules: {
'react/prop-types': 0,
},
};
...@@ -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
overwrite: true
schema: 'http://localhost:8000/graphql'
documents: 'src/graphql/**/*.ts'
generates:
src/generated/graphql.ts:
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-graphql-request'
config:
useTypeImports: true
[[headers]]
for = "/*"
[headers.values]
Access-Control-Allow-Origin = "*"
\ No newline at end of file
This diff is collapsed.
...@@ -3,11 +3,12 @@ ...@@ -3,11 +3,12 @@
"start": "snowpack dev", "start": "snowpack dev",
"build": "snowpack build && npm run copy:htaccess", "build": "snowpack build && npm run copy:htaccess",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"", "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"",
"lint": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\"", "prettier:lint": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\"",
"copy:htaccess": "cp public/.htaccess build/" "eslint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
"copy:htaccess": "cp public/.htaccess build/",
"generate": "graphql-codegen --config codegen.yml"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^1.1.1",
"@tailwindcss/forms": "^0.3.2", "@tailwindcss/forms": "^0.3.2",
"graphql": "^15.5.0", "graphql": "^15.5.0",
"graphql-request": "^3.4.0", "graphql-request": "^3.4.0",
...@@ -15,28 +16,38 @@ ...@@ -15,28 +16,38 @@
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"react": "^17.0.0", "react": "^17.0.0",
"react-dom": "^17.0.0", "react-dom": "^17.0.0",
"react-leaflet": "^3.1.0", "react-leaflet": "^3.2.0",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"swr": "^0.5.5", "react-select": "4.0.2",
"swr": "^0.5.6",
"tailwindcss": "^2.0.3", "tailwindcss": "^2.0.3",
"zustand": "^3.4.1" "zustand": "^3.5.1"
}, },
"devDependencies": { "devDependencies": {
"@jadex/snowpack-plugin-tailwindcss-jit": "^0.2.0", "@graphql-codegen/cli": "1.21.4",
"@graphql-codegen/typescript": "1.22.0",
"@graphql-codegen/typescript-graphql-request": "^3.2.0",
"@graphql-codegen/typescript-operations": "1.17.16",
"@snowpack/plugin-dotenv": "^2.1.0", "@snowpack/plugin-dotenv": "^2.1.0",
"@snowpack/plugin-postcss": "^1.1.0", "@snowpack/plugin-postcss": "^1.3.0",
"@snowpack/plugin-react-refresh": "^2.4.0", "@snowpack/plugin-react-refresh": "^2.4.0",
"@snowpack/plugin-typescript": "^1.2.0", "@snowpack/plugin-typescript": "^1.2.0",
"@types/leaflet": "^1.7.0", "@types/leaflet": "^1.7.0",
"@types/react": "^17.0.0", "@types/react": "^17.0.6",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.5",
"@types/react-router-dom": "^5.1.7", "@types/react-router-dom": "^5.1.7",
"@types/react-select": "^4.0.15",
"@types/snowpack-env": "^2.3.2", "@types/snowpack-env": "^2.3.2",
"@typescript-eslint/eslint-plugin": "^4.24.0",
"@typescript-eslint/parser": "^4.24.0",
"autoprefixer": "^10.2.4", "autoprefixer": "^10.2.4",
"postcss": "^8.2.6", "eslint": "^7.26.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-react": "^7.23.2",
"postcss": "^8.3.0",
"postcss-cli": "^8.3.1", "postcss-cli": "^8.3.1",
"prettier": "^2.0.5", "prettier": "^2.3.0",
"snowpack": "^3.0.1", "snowpack": "^3.5.0",
"typescript": "^4.0.0" "typescript": "^4.0.0"
} }
} }
...@@ -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,12 +5,10 @@ module.exports = { ...@@ -5,12 +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',
'@jadex/snowpack-plugin-tailwindcss-jit',
], ],
routes: [ routes: [
/* Enable an SPA Fallback in development: */ /* Enable an SPA Fallback in development: */
...@@ -24,7 +22,7 @@ module.exports = { ...@@ -24,7 +22,7 @@ module.exports = {
/* ... */ /* ... */
}, },
devOptions: { devOptions: {
/* ... */ tailwindConfig: './tailwind.config.js',
}, },
buildOptions: { buildOptions: {
/* ... */ /* ... */
......
...@@ -7,8 +7,9 @@ import Map from './Map/Map'; ...@@ -7,8 +7,9 @@ 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 = () => { const App: React.FC = () => {
const selectedPoi = useStore((state) => state.selectedPoi); const selectedPoi = useStore((state) => state.selectedPoi);
return ( return (
...@@ -16,7 +17,11 @@ const App = () => { ...@@ -16,7 +17,11 @@ const App = () => {
<ErrorModal /> <ErrorModal />
<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" children={({ match }) => <Map createMode={!!match} />} /> <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;
...@@ -3,9 +3,7 @@ import React from 'react'; ...@@ -3,9 +3,7 @@ import React from 'react';
import { useStore } from '../hooks'; import { useStore } from '../hooks';
import Modal from './Modal'; import Modal from './Modal';
interface Props {} const ErrorModal: React.FC = () => {
const ErrorModal: React.FC<Props> = () => {
const error = useStore((state) => state.error); const error = useStore((state) => state.error);
const icons: { [index: string]: JSX.Element } = { const icons: { [index: string]: JSX.Element } = {
alert: <AlertIcon className="h-6 w-6 text-red-600" />, alert: <AlertIcon className="h-6 w-6 text-red-600" />,
......
...@@ -5,17 +5,19 @@ import { validateFile } from '../../util/file'; ...@@ -5,17 +5,19 @@ 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';
import { removeDuplicateObjects } from '../../util/array'; 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 SelectInput from './SelectInput';
interface Props {} type StringSelectOption = { label: string; value: string };
const AddPoiForm: React.FC<Props> = () => { const AddPoiForm: React.FC = () => {
const [formData, setFormData] = useState<PointOfInterestFormData>({ const [formData, setFormData] = useState<PointOfInterestFormData>({
lat: 0, lat: 0,
lng: 0, lng: 0,
...@@ -25,6 +27,7 @@ const AddPoiForm: React.FC<Props> = () => { ...@@ -25,6 +27,7 @@ const AddPoiForm: React.FC<Props> = () => {
description: '', description: '',
website: '', website: '',
category: '', category: '',
relationStatus: '',
image: null, image: null,
tags: [], tags: [],
}); });
...@@ -35,6 +38,8 @@ const AddPoiForm: React.FC<Props> = () => { ...@@ -35,6 +38,8 @@ const AddPoiForm: React.FC<Props> = () => {
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);
...@@ -57,33 +62,37 @@ const AddPoiForm: React.FC<Props> = () => { ...@@ -57,33 +62,37 @@ const AddPoiForm: React.FC<Props> = () => {
setIsLoading(true); setIsLoading(true);
try { try {
let newTags: Tag[] = []; const newTags: Tag[] = [];
let newTagIdsResponse: number[] = []; let newTagIdsResponse: string[] = [];
let oldTags: Tag[] = []; const oldTags: Tag[] = [];
selectedTags.forEach((tag) => (tag.id === 'draft' ? newTags.push(tag) : oldTags.push(tag))); selectedTags.forEach((tag) => (tag.id === 'draft' ? newTags.push(tag) : oldTags.push(tag)));
if (newTags.length) { if (newTags.length) {
const tagsRes = await fetcher(createTags, { const tagsRes: Mutation = await fetcher(createTags, {
tags: newTags.map(({ displayName, color }) => ({ tags: newTags.map(({ displayName, color }) => ({
displayName, displayName,
color, color,
})), })),
}); });
if (tagsRes.createTags.length > 0) { if (tagsRes.createTags && tagsRes.createTags.length > 0) {
tagsRes.createTags.map((newTagResponse: { id: string }) => { newTagIdsResponse = tagsRes.createTags
newTagIdsResponse.push(Number(newTagResponse.id)); .filter((newTagResponse) => newTagResponse?.id)
}); .map((newTagResponse) => {
return (newTagResponse as Tag).id;
});
} }
} }
const finalData = { ...formData, tagIds: [...newTagIdsResponse, ...oldTags.map((oldTag) => Number(oldTag.id))] }; const finalData: CreatePoiMutationMutationVariables = {
...formData,
tagIds: [...newTagIdsResponse, ...oldTags.map((oldTag) => oldTag.id)],
};
const res = await fetcher(createPoi, finalData); const res: Mutation = await fetcher(createPoi, finalData);
if (res.createPoi) { if (res.createPoi) {
setNotification({ setNotification({
title: 'Ort hinzugefügt', title: 'Ort hinzugefügt',
text: text: 'Ort wurde erfolgreich hinzugefügt. Bitte überprüfe deine E-Mails und klicke dort auf den Link um deine Mail-Adresse zu verifizieren.',
'Ort wurde erfolgreich hinzugefügt. Bitte überprüfe deine E-Mails und klicke dort auf den Link um deine Mail-Adresse zu verifizieren.',
type: 'success', type: 'success',
}); });
history.push('/'); history.push('/');
...@@ -110,6 +119,15 @@ const AddPoiForm: React.FC<Props> = () => { ...@@ -110,6 +119,15 @@ const AddPoiForm: React.FC<Props> = () => {
}); });
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]);
...@@ -129,10 +147,6 @@ const AddPoiForm: React.FC<Props> = () => { ...@@ -129,10 +147,6 @@ const AddPoiForm: React.FC<Props> = () => {
if (draftPoi) setFormData({ ...formData, lat: draftPoi[0], lng: draftPoi[1] }); if (draftPoi) setFormData({ ...formData, lat: draftPoi[0], lng: draftPoi[1] });
}, [draftPoi]); }, [draftPoi]);
useEffect(() => {
console.log('Form Data', formData);
}, [formData]);
return ( return (
<form className="flex-1 flex flex-col justify-between" onSubmit={handleSubmit} ref={formRef}> <form className="flex-1 flex flex-col justify-between" onSubmit={handleSubmit} ref={formRef}>
<div className="flex flex-col"> <div className="flex flex-col">
...@@ -144,12 +158,31 @@ const AddPoiForm: React.FC<Props> = () => { ...@@ -144,12 +158,31 @@ const AddPoiForm: React.FC<Props> = () => {
/> />
<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'}
......
...@@ -18,7 +18,7 @@ const CoordinateInput: React.FC<Props> = ({ label, text, value, required }) => { ...@@ -18,7 +18,7 @@ const CoordinateInput: React.FC<Props> = ({ label, text, value, required }) => {
</span> </span>
)} )}
<label className="flex mt-1 mb-4 p-2 items-center w-full rounded-lg border-2 border-black border-opacity-20 hover:border-opacity-40 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"> <label className="flex mt-1 mb-4 p-2 items-center w-full rounded-lg border-2 border-black border-opacity-20 hover:border-opacity-40 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<CheckIcon className={`${Boolean(value.filter(Boolean).length) ? 'text-indigo-500' : 'text-gray-400'} mr-2`} /> <CheckIcon className={`${value.filter(Boolean).length ? 'text-indigo-500' : 'text-gray-400'} mr-2`} />
<input readOnly type="text" name={'lat'} value={value[0]} className="hidden" required={required}></input> <input readOnly type="text" name={'lat'} value={value[0]} className="hidden" required={required}></input>
<input readOnly type="text" name={'lng'} value={value[1]} className="hidden" required={required}></input> <input readOnly type="text" name={'lng'} value={value[1]} className="hidden" required={required}></input>
{!!text && <span className="form-label">{text}</span>} {!!text && <span className="form-label">{text}</span>}
......
...@@ -23,7 +23,7 @@ const FileInput: React.FC<Props> = ({ name, label, value, onChange, ...inputProp ...@@ -23,7 +23,7 @@ const FileInput: React.FC<Props> = ({ name, label, value, onChange, ...inputProp
: ' bg-indigo-500 text-white border-opacity-0 hover:bg-indigo-600 text-md' : ' bg-indigo-500 text-white border-opacity-0 hover:bg-indigo-600 text-md'
}`} }`}
> >
{!!value ? ( {value ? (
<> <>
<PaperClipIcon size={18} className="flex-shrink-0 mr-2" /> <PaperClipIcon size={18} className="flex-shrink-0 mr-2" />
{value.name} {value.name}
......
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;
...@@ -11,8 +11,7 @@ interface Props { ...@@ -11,8 +11,7 @@ interface Props {
required?: boolean; required?: boolean;
} }
const TagInput = ({ label, tags, options, onTagsChange, required }: Props) => { const TagInput: React.FC<Props> = ({ label, tags, options, onTagsChange, required }: Props) => {
// const [allTags, setAllTags] = useState<TagType[]>([]);
// All options // All options
const [tagOptions, setTagOptions] = useState<TagType[]>([]); const [tagOptions, setTagOptions] = useState<TagType[]>([]);
// The text input // The text input
...@@ -81,7 +80,7 @@ const TagInput = ({ label, tags, options, onTagsChange, required }: Props) => { ...@@ -81,7 +80,7 @@ const TagInput = ({ label, tags, options, onTagsChange, required }: Props) => {
<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}
> >
...@@ -100,7 +99,7 @@ const TagInput = ({ label, tags, options, onTagsChange, required }: Props) => { ...@@ -100,7 +99,7 @@ const TagInput = ({ label, tags, options, onTagsChange, required }: Props) => {
}} }}
type={'text'} type={'text'}
value={draftEntry} value={draftEntry}
className={`w-16 block p-0 flex-1 rounded-lg border-0 focus:outline-none focus:ring-0 ${ className={`form-input w-16 block p-0 flex-1 rounded-lg border-0 focus:outline-none focus:ring-0 ${
tags.length ? '' : 'px-3' tags.length ? '' : 'px-3'
}`} }`}
/> />
...@@ -117,7 +116,7 @@ const TagInput = ({ label, tags, options, onTagsChange, required }: Props) => { ...@@ -117,7 +116,7 @@ const TagInput = ({ label, tags, options, onTagsChange, required }: Props) => {
return ( return (
<li <li
key={`${option.id}`} key={`${option.id}`}
onMouseDown={(e) => { onMouseDown={() => {
handleSelectTag(option); handleSelectTag(option);
}} }}
className="w-full p-1 hover:bg-gray-100" className="w-full p-1 hover:bg-gray-100"
......
...@@ -20,7 +20,7 @@ const TextAreaInput: React.FC<Props> = ({ name, label, value, onChange, ...textA ...@@ -20,7 +20,7 @@ const TextAreaInput: React.FC<Props> = ({ name, label, value, onChange, ...textA
<textarea <textarea
name={name} name={name}
value={value} value={value}
className="form-input" className="form-textarea form-input-custom"
rows={3} rows={3}
onChange={onChange} onChange={onChange}
{...textAreaProps} {...textAreaProps}
......
import React, { MutableRefObject, useRef } from 'react'; import React from 'react';
interface Props { interface Props {
label: string; label: string;
...@@ -18,7 +18,14 @@ const TextInput: React.FC<Props> = ({ name, label, value, onChange, type = 'text ...@@ -18,7 +18,14 @@ const TextInput: React.FC<Props> = ({ name, label, value, onChange, type = 'text
{inputProps?.required && `*`} {inputProps?.required && `*`}
</span> </span>
)} )}
<input type={type} name={name} value={value} className="form-input" onChange={onChange} {...inputProps} /> <input
type={type}
name={name}
value={value}
className="form-input form-input-custom"
onChange={onChange}
{...inputProps}
/>
</label> </label>
); );
}; };
......
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 { usePoiData, useStore } from '../../hooks'; import { useStore } from '../../hooks';
import MapViewController from './MapViewController'; import MapViewController from './MapViewController';
import { useFilteredPoiData } from '../../hooks/useFilteredPoiData';
import MapLayerControl from './MapLayerControl';
import { useHistory } from 'react-router-dom';
interface Props { interface Props {
createMode?: boolean; createMode?: boolean;
...@@ -25,54 +28,70 @@ export const Map: React.FC<Props> = ({ createMode }) => { ...@@ -25,54 +28,70 @@ export const Map: React.FC<Props> = ({ createMode }) => {
() => divIcon({ ...iconProps, iconSize: [30, 40], iconAnchor: [15, 40], className: 'marker-green' }), () => divIcon({ ...iconProps, iconSize: [30, 40], iconAnchor: [15, 40], className: 'marker-green' }),
[iconProps], [iconProps],
); );
const { data } = usePoiData(); 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 (
<MapContainer id={'mapid'} className={'h-full w-full z-0'} center={DEFAULT_CENTER} zoom={13} scrollWheelZoom={true}> <div className="relative h-full w-full z-0">
<TileLayer {!createMode && !selectedPoi && <MapLayerControl />}
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' <MapContainer id={'mapid'} className={'h-full w-full z-0'} scrollWheelZoom={true}>
url="https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}" <TileLayer
id="mapbox/streets-v11" attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
tileSize={512} url="https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}"
accessToken={import.meta.env.SNOWPACK_PUBLIC_MAPBOX_TOKEN} id="mapbox/streets-v11"
zoomOffset={-1} tileSize={512}
maxZoom={18} accessToken={import.meta.env.SNOWPACK_PUBLIC_MAPBOX_TOKEN}
/> zoomOffset={-1}
<MapViewController center={selectedLatlng ?? DEFAULT_CENTER} zoom={13} createPoiMode={createMode} /> maxZoom={18}
{/* Single marker when creating a new POI */} />
{!!(createMode && draftPoi) && <Marker icon={greenLargeIcon} position={draftPoi} />} <MapViewController bounds={mapBounds} createPoiMode={createMode} />
{/* Single marker when POI is selected */} {/* Single marker when creating a new POI */}
{!!(selectedPoi && selectedLatlng && !createMode) && <Marker icon={largeIcon} position={selectedLatlng} />} {!!(createMode && draftPoi) && <Marker icon={greenLargeIcon} position={draftPoi} />}
{/* Multiple markers, when no POI is selected */} {/* Single marker when POI is selected */}
{!selectedPoi && {!!(selectedPoi && selectedLatlng && !createMode) && <Marker icon={largeIcon} position={selectedLatlng} />}
!createMode && {/* Multiple markers, when no POI is selected */}
data?.map((poi) => { {!selectedPoi &&
const poiLatLng: LatLngTuple = [poi.lat, poi.lng]; !createMode &&
return ( data?.map((poi) => {
<Marker const poiLatLng: LatLngTuple = [poi.lat, poi.lng];
icon={hoveredPoi?.id === poi.id ? largeIcon : icon} return (
opacity={hoveredPoi?.id === poi.id ? 1 : 0.7} <Marker
key={poi.id} icon={hoveredPoi?.id === poi.id ? largeIcon : icon}
position={poiLatLng} opacity={hoveredPoi?.id === poi.id ? 1 : 0.7}
eventHandlers={{ key={poi.id}
click: () => setSelectedPoi(poi), position={poiLatLng}
mouseover: () => { eventHandlers={{
setHoveredPoi(poi); click: () => {
}, history.push(`/poi/${String(poi.id)}`);
mouseout: () => { },
setHoveredPoi(null); mouseover: () => {
}, setHoveredPoi(poi);
}} },
/> mouseout: () => {
); setHoveredPoi(null);
})} },
</MapContainer> }}
/>
);
})}
</MapContainer>
</div>
); );
}; };
......
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;