Skip to content
Snippets Groups Projects
Commit 45978603 authored by Moritz Stückler's avatar Moritz Stückler :cowboy:
Browse files

Revert "Merge branch 'feat/add-poi-form' into 'main'"

This reverts commit e52c5807, reversing
changes made to 804bcc17.
parent e52c5807
No related branches found
Tags v0.0.3
No related merge requests found
Showing
with 261 additions and 794 deletions
......@@ -9,7 +9,7 @@ before_script:
- apk add --no-cache lftp openssh
- mkdir -p ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
- echo "$FRONTEND_ENV" > .env
- echo "$FRONTEND_ENV" >> .env
npm build:
stage: build
......
This diff is collapsed.
......@@ -6,7 +6,6 @@
"lint": "prettier --check \"src/**/*.{js,jsx,ts,tsx}\""
},
"dependencies": {
"@tailwindcss/forms": "^0.3.2",
"graphql": "^15.5.0",
"graphql-request": "^3.4.0",
"heroicons-react": "1.3.0",
......@@ -14,13 +13,10 @@
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react-leaflet": "^3.1.0",
"react-router-dom": "^5.2.0",
"swr": "^0.5.5",
"tailwindcss": "^2.0.3",
"zustand": "^3.4.1"
"tailwindcss": "^2.0.3"
},
"devDependencies": {
"@jadex/snowpack-plugin-tailwindcss-jit": "^0.2.0",
"@snowpack/plugin-dotenv": "^2.1.0",
"@snowpack/plugin-postcss": "^1.1.0",
"@snowpack/plugin-react-refresh": "^2.4.0",
......@@ -28,7 +24,6 @@
"@types/leaflet": "^1.7.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-router-dom": "^5.1.7",
"@types/snowpack-env": "^2.3.2",
"autoprefixer": "^10.2.4",
"postcss": "^8.2.6",
......
......@@ -10,11 +10,10 @@ module.exports = {
'@snowpack/plugin-typescript',
'@snowpack/plugin-postcss',
'@snowpack/plugin-dotenv',
'@jadex/snowpack-plugin-tailwindcss-jit',
],
routes: [
/* Enable an SPA Fallback in development: */
{ match: 'routes', src: '.*', dest: '/index.html' },
// {"match": "routes", "src": ".*", "dest": "/index.html"},
],
optimize: {
/* Example: Bundle your final build: */
......
import React, { useEffect, useState } from 'react';
import useSWR from 'swr';
import SidebarListView from './Sidebar/SidebarListView';
import type { PointOfInterest } from './types/PointOfInterest';
import SidebarSingleView from './Sidebar/SidebarSingleView';
import Map from './Map/Map';
import Modal from './Modal';
import { ExclamationOutline as AlertIcon } from 'heroicons-react';
function App() {
const [poiData, setPoiData] = useState<PointOfInterest[]>([]);
const [selectedPoi, setSelectedPoi] = useState<null | PointOfInterest>(null);
const [hoveredPoiId, setHoveredPoiId] = useState<null | number>(null);
const [showErrorModal, setShowErrorModal] = useState<boolean>(false);
const handlePoiClick = (id: number) => {
const newPoi = poiData.find((poi) => poi.id === id);
newPoi && setSelectedPoi(newPoi);
};
const handlePoiClose = () => {
setSelectedPoi(null);
};
const handlePoiHoverOn = (poiId: number) => {
setHoveredPoiId(poiId);
};
const handlePoiHoverOff = () => {
setHoveredPoiId(null);
};
const { data, error } = useSWR(
`{
pois {
id
name
description
website
address
lat
lng
image
category
}
}
`,
);
useEffect(() => {
data && console.log('Fetched new data', data);
data?.pois && setPoiData(data.pois);
}, [data]);
useEffect(() => {
if (error) {
console.error('Error while fetching', error);
setShowErrorModal(true);
}
}, [error]);
return (
<>
{showErrorModal && (
<Modal
title="API nicht erreichbar"
text="Kann die API nicht erreichen. Bitte später erneut probieren."
icon={<AlertIcon className="h-6 w-6 text-red-600" />}
/>
)}
<div className={'flex md:flex-row-reverse flex-col h-full'}>
<Map
onMouseEnter={handlePoiHoverOn}
onMouseLeave={handlePoiHoverOff}
hoveredPoiId={hoveredPoiId}
values={poiData}
onSelect={handlePoiClick}
selectedEntry={selectedPoi}
/>
{selectedPoi ? (
<SidebarSingleView className="sidebar" value={selectedPoi} onClose={handlePoiClose} />
) : (
<SidebarListView
hoveredPoiId={hoveredPoiId}
onMouseEnter={handlePoiHoverOn}
onMouseLeave={handlePoiHoverOff}
className="sidebar"
values={poiData}
onClick={handlePoiClick}
/>
)}
</div>
</>
);
}
export default App;
import type { LatLngExpression } from 'leaflet';
import { divIcon, DivIconOptions } from 'leaflet';
import React, { useMemo } from 'react';
import { MapContainer, Marker, TileLayer } from 'react-leaflet';
import { usePoiData, useStore } from '../../hooks';
import React, { useMemo } from 'react';
import { divIcon, DivIconOptions } from 'leaflet';
import type { PointOfInterest } from '../types/PointOfInterest';
import type { LatLngExpression } from 'leaflet';
import MapViewController from './MapViewController';
interface Props {
hideAllPois?: boolean;
values: PointOfInterest[];
onSelect: (id: number) => void;
selectedEntry?: PointOfInterest | null;
hoveredPoiId?: number | null;
onMouseEnter?: (id: number) => void;
onMouseLeave?: () => void;
}
const DEFAULT_CENTER: LatLngExpression = [53.550359, 9.986701];
const iconProps: DivIconOptions = {
className: 'marker',
// 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> = ({ hideAllPois = false }) => {
export const Map: React.FC<Props> = (props) => {
const iconProps: DivIconOptions = {
className: 'marker',
// 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],
};
const icon = useMemo(() => divIcon(iconProps), [iconProps]);
const largeIcon = useMemo(() => divIcon({ ...iconProps, iconSize: [30, 40], iconAnchor: [15, 40] }), [iconProps]);
const { data } = usePoiData();
const hoveredPoi = useStore((state) => state.hoveredPoi);
const setHoveredPoi = useStore((state) => state.setHoveredPoi);
const selectedPoi = useStore((state) => state.selectedPoi);
const setSelectedPoi = useStore((state) => state.setSelectedPoi);
const selectedLatlng: LatLngExpression | undefined = selectedPoi ? [selectedPoi?.lat, selectedPoi?.lng] : undefined;
const selectedLatlng: LatLngExpression | undefined = props.selectedEntry
? [props.selectedEntry?.lat, props.selectedEntry?.lng]
: undefined;
return (
<MapContainer
......@@ -49,25 +50,25 @@ export const Map: React.FC<Props> = ({ hideAllPois = false }) => {
/>
<MapViewController center={selectedLatlng ?? DEFAULT_CENTER} zoom={13} />
{/* Single marker when POI is selected */}
{!!(selectedPoi && selectedLatlng && !hideAllPois) && <Marker icon={largeIcon} position={selectedLatlng} />}
{!!(props.selectedEntry && selectedLatlng) && <Marker icon={largeIcon} position={selectedLatlng} />}
{/* Multiple markers, when no POI is selected */}
{!selectedPoi &&
!hideAllPois &&
data?.map((poi) => {
{!props.selectedEntry &&
props.values &&
props.values.map((poi) => {
const poiLatLng: LatLngExpression = [poi.lat, poi.lng];
return (
<Marker
icon={hoveredPoi?.id === poi.id ? largeIcon : icon}
opacity={hoveredPoi?.id === poi.id ? 1 : 0.7}
icon={props.hoveredPoiId === poi.id ? largeIcon : icon}
opacity={props.hoveredPoiId === poi.id ? 1 : 0.7}
key={poi.id}
position={poiLatLng}
eventHandlers={{
click: () => setSelectedPoi(poi),
click: () => props.onSelect(poi.id),
mouseover: () => {
setHoveredPoi(poi);
props.onMouseEnter && props.onMouseEnter(poi.id);
},
mouseout: () => {
setHoveredPoi(null);
props.onMouseLeave && props.onMouseLeave();
},
}}
/>
......
File moved
import React from 'react';
import React, { CSSProperties } from 'react';
interface Props {
style?: CSSProperties;
className?: string;
}
const SidebarContainer: React.FC<Props> = ({ className, children }) => {
const SidebarContainer: React.FC<Props> = ({ style, className, children }) => {
return (
<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 ${
<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 ${
className ?? ''
}`}
>
{children}
</aside>
</div>
);
};
......
import React, { SyntheticEvent } from 'react';
import React 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 ListElement: React.FC<Props> = ({ value, hovered, ...restProps }) => {
const SidebarListElement: React.FC<Props> = ({ value, hovered, ...restProps }) => {
return (
value && (
<div
......@@ -29,4 +26,4 @@ const ListElement: React.FC<Props> = ({ value, hovered, ...restProps }) => {
);
};
export default ListElement;
export default SidebarListElement;
import React, { CSSProperties } from 'react';
import SidebarContainer from './SidebarContainer';
import SidebarListElement from './SidebarListElement';
import type { PointOfInterest } from '../types/PointOfInterest';
interface Props {
style?: CSSProperties;
values: PointOfInterest[];
onClick?: (id: number) => void;
onMouseEnter?: (id: number) => void;
onMouseLeave?: () => void;
className?: string;
hoveredPoiId?: number | null;
}
const SidebarListView: React.FC<Props> = ({
values,
onMouseEnter,
onMouseLeave,
onClick,
hoveredPoiId,
...restProps
}) => {
return (
values && (
<SidebarContainer {...restProps}>
<h1 className="text-xl font-medium title-font m-4 text-gray-900 mb-2">{values.length} Orte:</h1>
{values &&
values.map((poi) => (
<SidebarListElement
key={poi.id}
{...(onMouseLeave ? { onMouseLeave: () => onMouseLeave() } : {})}
{...(onMouseEnter ? { onMouseEnter: () => onMouseEnter(poi.id) } : {})}
{...(onClick ? { onClick: () => onClick(poi.id) } : {})}
value={poi}
hovered={hoveredPoiId === poi.id}
/>
))}
</SidebarContainer>
)
);
};
export default SidebarListView;
import { HomeOutline as HomeIcon, LocationMarkerOutline as AddressIcon, X as CloseIcon } from 'heroicons-react';
import React from 'react';
import { useStore } from '../../hooks';
import React, { CSSProperties } from 'react';
import SidebarContainer from './SidebarContainer';
import Tag from './Tag';
import type { PointOfInterest } from '../types/PointOfInterest';
import { X as CloseIcon, HomeOutline as HomeIcon, LocationMarkerOutline as AddressIcon } from 'heroicons-react';
interface Props {}
const SidebarSingleView: React.FC<Props> = () => {
const selectedPoi = useStore((state) => state.selectedPoi);
const setSelectedPoi = useStore((state) => state.setSelectedPoi);
const strippedUrl = selectedPoi?.website?.replace(/(^\w+:|^)\/\//, '');
interface Props {
style?: CSSProperties;
value: PointOfInterest;
onClose?: () => void;
className?: string;
}
const SidebarSingleView: React.FC<Props> = ({ value, onClose, className, ...restProps }) => {
const strippedUrl = value?.website?.replace(/(^\w+:|^)\/\//, '');
return (
<SidebarContainer className={`relative p-0`}>
<div className={`${selectedPoi?.image ? '' : 'pl-5 pt-5 '}`}>
<SidebarContainer className={`relative p-0 ${className || ''}`} {...restProps}>
<div className={`${value.image ? '' : 'pl-5 pt-5'}`}>
<CloseIcon
size={32}
className={`${
selectedPoi?.image ? 'absolute left-5 top-5 ' : ''
value.image ? 'absolute left-5 top-5 ' : ''
}p-1 text-gray-500 inline-block cursor-pointer hover:bg-gray-300 hover:bg-opacity-50 rounded-full`}
onClick={() => setSelectedPoi(null)}
onClick={onClose}
/>
</div>
{selectedPoi?.image && (
<img className="lg:h-48 md:h-36 w-full object-cover object-center" src={selectedPoi?.image} alt="blog" />
{value.image && (
<img className="lg:h-48 md:h-36 w-full object-cover object-center" src={value.image} alt="blog" />
)}
<div className="p-6">
<h2 className="tracking-widest uppercase text-xs title-font font-medium text-gray-400 mb-1">
{selectedPoi?.category}
{value.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 && (
<h1 className="title-font text-lg font-medium text-gray-900 mb-3">{value.name}</h1>
<p className="leading-relaxed mb-6">{value.description}</p>
{value.website && (
<div className={'flex items-center'}>
<HomeIcon size={18} className={'text-gray-500 mr-2'} />
<a className={'text-sm text-gray-500 hover:underline'} href={selectedPoi?.website}>
<a className={'text-sm text-gray-500 hover:underline'} href={value.website}>
{strippedUrl}
</a>
</div>
)}
{selectedPoi?.address && (
{value.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?.tags?.length && (
<div className={'flex items-center mt-3'}>
{selectedPoi?.tags.map((tag) => (
<Tag key={tag.id} color={tag.color}>
{tag.displayName}
</Tag>
))}
<div className="text-sm text-gray-500">{value.address}</div>
</div>
)}
</div>
......
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { useStore } from '../hooks';
import ErrorModal from './ErrorModal';
import Map from './Map/Map';
import SidebarCreateView from './Sidebar/SidebarCreateView';
import SidebarListView from './Sidebar/SidebarListView';
import SidebarSingleView from './Sidebar/SidebarSingleView';
const App = () => {
const selectedPoi = useStore((state) => state.selectedPoi);
return (
<>
<ErrorModal />
<div className={'flex md:flex-row-reverse flex-col h-full'}>
<Route path="/add" children={({ match }) => <Map hideAllPois={!!match} />} />
<Switch>
<Route exact path="/add">
<SidebarCreateView />
</Route>
<Route>{selectedPoi ? <SidebarSingleView /> : <SidebarListView />}</Route>
</Switch>
</div>
</>
);
};
export default App;
import { ExclamationOutline as AlertIcon } from 'heroicons-react';
import React from 'react';
import { useStore } from '../hooks';
import Modal from './Modal';
interface Props {}
const ErrorModal: React.FC<Props> = () => {
const error = useStore((state) => state.error);
const icons: { [index: string]: JSX.Element } = {
alert: <AlertIcon className="h-6 w-6 text-red-600" />,
};
return error && <Modal title={error.title} text={error.message} icon={icons[error.icon]} />;
};
export default ErrorModal;
import { GraphQLClient, request, gql } from 'graphql-request';
import React, { useEffect, useState } from 'react';
interface Props {}
const AddPoiForm: React.FC<Props> = () => {
const [formData, setFormData] = useState({
lat: 123,
lng: 123,
name: '',
address: '',
description: '',
website: '',
category: '',
image: '',
tags: '',
});
useEffect(() => {
console.log('Form data', formData);
}, [formData]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
// const mutation = gql`
// mutation createPoiMutation(
// $name: String!
// $email: String!
// $lat: Float!
// $lng: Float!
// $website: String
// $description: String
// $address: String!
// $category: String!
// ) {
// createPoi(
// poi: {
// name: $name
// email: $email
// lat: $lat
// lng: $lng
// website: $website
// description: $description
// address: $address
// category: $category
// image: "ababababa"
// }
// )
// }
// `;
// const variables = {
// name: 'Inception',
// email: 2010,
// file: document.querySelector('input#avatar').files[0]
// };
// const data = await request(import.meta.env.SNOWPACK_PUBLIC_API_URL, mutation, formData);
return (
<form className="flex flex-col">
<p className="leading-relaxed mb-5 text-gray-600">Bitte auf der Karte einen Pin setzen.</p>
<label className="block mb-4">
<span className="form-label">Name</span>
<input
type="text"
name="name"
value={formData.name}
className="form-input"
placeholder="Musterspace"
onChange={handleInputChange}
/>
</label>
<label className="block mb-4">
<span className="form-label">Adresse</span>
<input
type="text"
name="address"
value={formData.address}
className="form-input"
placeholder=""
onChange={handleInputChange}
/>
</label>
<label className="block mb-4">
<span className="form-label">Webseite</span>
<input
type="text"
name="website"
value={formData.website}
className="form-input"
placeholder=""
onChange={handleInputChange}
/>
</label>
<label className="block mb-4">
<span className="form-label">Beschreibung</span>
<textarea
name="description"
value={formData.description}
className="form-input"
rows={3}
onChange={handleInputChange}
></textarea>
</label>
<label className="block mb-6">
<span className="form-label">Bild</span>
<input
className="block w-full focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
type="file"
id="avatar"
name="avatar"
accept="image/png, image/jpeg"
></input>
</label>
<button
type="submit"
className="text-white bg-indigo-500 border-0 py-2 px-6 focus:outline-none hover:bg-indigo-600 rounded-lg text-lg"
>
Hinzufügen
</button>
</form>
);
};
export default AddPoiForm;
import { X as CloseIcon } from 'heroicons-react';
import React from 'react';
import { useHistory } from 'react-router-dom';
import AddPoiForm from './AddPoiForm';
import SidebarContainer from './SidebarContainer';
interface Props {}
const SidebarCreateView: React.FC<Props> = () => {
const history = useHistory();
return (
<SidebarContainer className="p-5">
<CloseIcon
size={32}
className={`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 my-2">Ort anlegen:</h1>
<AddPoiForm />
</SidebarContainer>
);
};
export default SidebarCreateView;
import React from 'react';
import { usePoiData, useStore } from '../../hooks';
import ListElement from './ListElement';
import SidebarContainer from './SidebarContainer';
interface Props {}
const SidebarListView: React.FC<Props> = () => {
const { data } = usePoiData();
const hoveredPoi = useStore((state) => state.hoveredPoi);
const setHoveredPoi = useStore((state) => state.setHoveredPoi);
const setSelectedPoi = useStore((state) => state.setSelectedPoi);
return (
<SidebarContainer>
<h1 className="text-xl font-medium title-font m-4 text-gray-900 mb-2">{data?.length} Orte:</h1>
{data?.map((poi) => (
<ListElement
key={poi.id}
onMouseEnter={() => setHoveredPoi(poi)}
onMouseLeave={() => setHoveredPoi(null)}
onClick={() => setSelectedPoi(poi)}
value={poi}
hovered={hoveredPoi?.id === poi.id}
/>
))}
</SidebarContainer>
);
};
export default SidebarListView;
import React from 'react';
interface Props {
children: string | JSX.Element;
color?: string;
}
const Tag: React.FC<Props> = ({ children, color }) => {
return (
<span
className="inline-flex items-center px-3 py-0.5 rounded-full text-sm font-medium bg-indigo-100 text-indigo-800 mr-2"
style={color ? { backgroundColor: color } : {}}
>
{children}
</span>
);
};
export default Tag;
import { request } from 'graphql-request';
import React from 'react';
import { SWRConfig } from 'swr';
import { useStore } from '../hooks';
interface Props {}
const SwrWrapper: React.FC<Props> = ({ children }) => {
const setError = useStore((state) => state.setError);
return (
<SWRConfig
value={{
fetcher: (query: string) => request(import.meta.env.SNOWPACK_PUBLIC_API_URL, query),
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);
},
}}
>
{children}
</SWRConfig>
);
};
export default SwrWrapper;
export * from './usePoiData';
export * from './useStore';
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment