diff --git a/package-lock.json b/package-lock.json
index a1062d8..9929ccd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6259,6 +6259,11 @@
"safer-buffer": ">= 2.1.2 < 3"
}
},
+ "idb": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/idb/-/idb-6.1.1.tgz",
+ "integrity": "sha512-5AnQiuTwELdLlriQ+UTVEzT3J7cC/4NSF0S0H5WC35FcOME4dbPHnfsbzkdfJWaFadiXvATHIV06bK/yilmjqQ=="
+ },
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -8465,9 +8470,9 @@
"dev": true
},
"nanoid": {
- "version": "3.1.22",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz",
- "integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ=="
+ "version": "3.1.23",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
+ "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw=="
},
"nanomatch": {
"version": "1.2.13",
diff --git a/package.json b/package.json
index 3b89f9f..7138fd3 100644
--- a/package.json
+++ b/package.json
@@ -34,8 +34,10 @@
"easymde": "^2.15.0",
"gpxparser": "^3.0.7",
"graphql": "15.3.0",
+ "idb": "^6.1.1",
"mapbox-gl": "^2.2.0",
"markdown-to-jsx": "^7.1.2",
+ "nanoid": "^3.1.23",
"next": "latest",
"postcss": "^8.1.10",
"react": "^16.13.1",
diff --git a/src/components/Event/HeroImg.tsx b/src/components/Event/HeroImg.tsx
index 0ac91f6..6b8594e 100644
--- a/src/components/Event/HeroImg.tsx
+++ b/src/components/Event/HeroImg.tsx
@@ -5,6 +5,7 @@ interface HeroImageProps {
name: string | null;
size: number | null;
src: string | null;
+ dataURL: string | null;
};
children: React.ReactNode;
}
@@ -30,12 +31,13 @@ export const HeroImg = ({ heroImg, children }: HeroImageProps) => {
const opacity = 0.7;
const rgb = 50;
const overlay = `rgba(${rgb}, ${rgb}, ${rgb}, ${opacity})`;
+ const src = heroImg.src ?? heroImg.dataURL;
return (
{children}
diff --git a/src/components/EventEditor/EditorForm.tsx b/src/components/EventEditor/EditorForm.tsx
index 5c05d88..2a73434 100644
--- a/src/components/EventEditor/EditorForm.tsx
+++ b/src/components/EventEditor/EditorForm.tsx
@@ -1,22 +1,26 @@
import React, { ChangeEventHandler } from 'react';
+import { useSelector } from 'react-redux';
+import { RootState } from '../../lib/redux/reducers';
+import { EventInterface } from '../../types';
+
import { EventDetailsInput } from './EventDetailsInput';
import { ImageInput } from './ImageInput';
import { Input } from './Input';
-import { EventInterface } from '../../types';
interface EditorFormProps {
- eventState: EventInterface;
handleChange: ChangeEventHandler
;
handleImageInput: any;
handleEventDetailsInput: any;
}
+type State = RootState & { event: EventInterface };
+
export const EditorForm = ({
- eventState,
handleChange,
handleEventDetailsInput,
handleImageInput,
}: EditorFormProps) => {
+ const eventState = useSelector((state: State) => state.event);
const { date, time, name, address, city, state, heroImg, eventDetails } =
eventState;
@@ -65,10 +69,7 @@ export const EditorForm = ({
handleChange={handleChange}
/>
Hero Image
-
+
Event Details
import('react-simplemde-editor'), {
ssr: false,
-})
-import 'easymde/dist/easymde.min.css'
-import { EventActionInterface } from '../../types'
+});
+import 'easymde/dist/easymde.min.css';
interface EventDetailsProps {
- value: string
- handleChange: any
+ value: string;
+ handleChange: any;
}
export const EventDetailsInput = ({
@@ -19,5 +18,5 @@ export const EventDetailsInput = ({
- )
-}
+ );
+};
diff --git a/src/components/EventEditor/ImageInput.tsx b/src/components/EventEditor/ImageInput.tsx
index bf5cd14..4340e59 100644
--- a/src/components/EventEditor/ImageInput.tsx
+++ b/src/components/EventEditor/ImageInput.tsx
@@ -8,7 +8,7 @@ interface ImageInputInterface {
}
export const ImageInput = ({ image, handleInput }: ImageInputInterface) => {
- const { error, src, name } = image;
+ const { error, src, name, dataURL } = image;
return (
<>
@@ -27,14 +27,15 @@ export const ImageInput = ({ image, handleInput }: ImageInputInterface) => {
- {src && name && (
-
-
{image.name}
-
-
+ {src ||
+ (dataURL && name && (
+
+
{image.name}
+
+
+
-
- )}
+ ))}
>
);
};
diff --git a/src/components/EventEditor/ToolBar.tsx b/src/components/EventEditor/ToolBar.tsx
index 38ee8eb..c87c7c6 100644
--- a/src/components/EventEditor/ToolBar.tsx
+++ b/src/components/EventEditor/ToolBar.tsx
@@ -25,7 +25,7 @@ export const ToolBar = ({ handleDiscard }: ToolBarProps) => {
-
+
diff --git a/src/components/EventEditor/index.tsx b/src/components/EventEditor/index.tsx
index 420dcfb..59b9d24 100644
--- a/src/components/EventEditor/index.tsx
+++ b/src/components/EventEditor/index.tsx
@@ -1,40 +1,44 @@
-import React, { useEffect, useReducer, useState } from 'react';
+import React, { useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { actions } from '../../lib/redux/reducers/eventEditor';
import { imageToDataURL, dataURLtoFile } from '../../lib/utils';
-import { reducer, init } from './reducer';
+import { initDB, putFile, getItem } from '../../lib/utils/indexDB';
import { EditorForm } from './EditorForm';
import { ToolBar } from './ToolBar';
import { DialogModal } from '../Common/DialogModal';
import { EventInterface } from '../../types';
+import { RootState } from '../../lib/redux/reducers';
+
+type State = RootState & { event: EventInterface };
const EventEditor = () => {
- const [eventState, dispatch] = useReducer(reducer, {}, init);
+ const dispatch = useDispatch();
+ const eventState = useSelector((state: State) => state.event);
+ const [isLoaded, setIsLoaded] = useState(false);
// const [createEvent] = useCreateEventMutation({
// onError: (error) => console.log(error),
// });
const [discardWarning, setDiscardWarning] = useState(false);
-
useEffect(() => {
- const initFromLocalStorage = () => {
+ const initFromLocalStorage = async () => {
+ await initDB();
const data = localStorage.getItem('eventState');
if (data) {
const localState: EventInterface = JSON.parse(data);
- const heroImgLocal = localState.heroImg;
- if (heroImgLocal.src && heroImgLocal.name) {
- const heroImgFile = dataURLtoFile(
- heroImgLocal.src,
- heroImgLocal.name
+
+ const heroImgLocalFile: File = await getItem('heroImg');
+ if (heroImgLocalFile) {
+ const heroImgDataURL = await imageToDataURL(
+ heroImgLocalFile
);
- if (heroImgFile) heroImgLocal.file = heroImgFile;
+ localState.heroImg = heroImgDataURL;
}
- const initState = {
- ...localState,
- heroImg: { ...heroImgLocal },
- };
- dispatch({ type: 'init', payload: initState });
+ dispatch(actions.updateEvent(localState));
}
};
initFromLocalStorage();
window.addEventListener('storage', initFromLocalStorage, true);
+ setIsLoaded(true);
return () => {
setDiscardWarning(false);
window.removeEventListener('storage', initFromLocalStorage, true);
@@ -42,7 +46,12 @@ const EventEditor = () => {
}, []);
useEffect(() => {
- localStorage.setItem('eventState', JSON.stringify(eventState));
+ if (isLoaded) {
+ const { heroImg } = eventState;
+ const { dataURL, ...rest } = heroImg;
+ const localState = { ...eventState, heroImg: rest };
+ localStorage.setItem('eventState', JSON.stringify(localState));
+ }
}, [eventState]);
const handleImageInput = async ({
@@ -53,37 +62,49 @@ const EventEditor = () => {
const { files } = target;
if (files) {
try {
- const image = await imageToDataURL(files);
- dispatch({
- type: 'updateHeroImg',
- payload: image,
- });
+ await putFile('heroImg', files[0]);
+ const image = await imageToDataURL(files[0]);
+ dispatch(actions.updateHeroImg(image));
} catch ({ error }) {
- dispatch({
- type: 'updateHeroImg',
- payload: { error },
- });
+ dispatch(actions.updateHeroImg(error));
}
}
};
const handleEventDetailsInput = (value: string) => {
- dispatch({
- type: 'updateEventDetails',
- payload: value,
- });
+ dispatch(actions.updateEventDetails(value));
};
const handleChange = ({ target }: { target: HTMLInputElement }) => {
const { name, value } = target;
- const actionType = `update${name[0].toUpperCase()}${name.substr(1)}`;
- dispatch({ type: actionType, payload: value });
+ switch (name) {
+ case 'name':
+ dispatch(actions.updateName(value));
+ break;
+ case 'address':
+ dispatch(actions.updateAddress(value));
+ break;
+ case 'city':
+ dispatch(actions.updateCity(value));
+ break;
+ case 'state':
+ dispatch(actions.updateState(value));
+ break;
+ case 'date':
+ dispatch(actions.updateDate(value));
+ break;
+ case 'time':
+ dispatch(actions.updateTime(value));
+ break;
+ default:
+ break;
+ }
};
const handleDiscard = (confirm?: boolean) => {
if (confirm === true) {
localStorage.removeItem('eventState');
- dispatch({ type: 'init' });
+ dispatch(actions.init());
setDiscardWarning(false);
} else {
setDiscardWarning(!discardWarning);
@@ -113,7 +134,6 @@ const EventEditor = () => {
handleDiscard()} />
{
- return {
- name,
- heroImg,
- date,
- address,
- city,
- state,
- time,
- eventDetails,
- };
-};
-
-export const reducer = (
- state: EventInterface,
- action: EventActionInterface
-) => {
- switch (action.type) {
- case 'updateName':
- return { ...state, name: action.payload };
- case 'updateDate':
- return { ...state, date: action.payload };
- case 'updateAddress':
- return { ...state, address: action.payload };
- case 'updateCity':
- return { ...state, city: action.payload };
- case 'updateState':
- return { ...state, state: action.payload };
- case 'updateTime':
- return { ...state, time: action.payload };
- case 'updateHeroImg':
- return { ...state, heroImg: action.payload };
- case 'updateEventDetails':
- return { ...state, eventDetails: action.payload };
- case 'init':
- return action.payload ? init(action.payload) : init({});
- default:
- return state;
- }
-};
diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx
index ff2fc76..4cc2ed5 100644
--- a/src/components/Hero.tsx
+++ b/src/components/Hero.tsx
@@ -25,7 +25,7 @@ export const Hero = () => {
className="text-xl absolute top-1/2"
style={{ transform: 'translate(20%, -50%)' }}
>
-
+
diff --git a/src/components/PreviewEvent/index.tsx b/src/components/PreviewEvent/index.tsx
index 689c550..f687526 100644
--- a/src/components/PreviewEvent/index.tsx
+++ b/src/components/PreviewEvent/index.tsx
@@ -1,37 +1,43 @@
-import React, { useEffect, useState } from 'react'
-import { EventInterface } from '../../types'
+import React, { useEffect, useState } from 'react';
+import { EventInterface } from '../../types';
-import { Hero } from '../Event/Hero'
-import { EventDetails } from '../Event/EventDetails'
+import { Hero } from '../Event/Hero';
+import { EventDetails } from '../Event/EventDetails';
+import { initDB, getItem } from '../../lib/utils/indexDB';
+import { imageToDataURL } from '../../lib/utils';
export const PreviewEvent = () => {
- const [event, setEvent]: [
- EventInterface | null,
- React.Dispatch>
- ] = useState(null)
+ const [event, setEvent] = useState();
useEffect(() => {
- const init = () => {
+ const init = async () => {
try {
- const data = localStorage.getItem('eventState')
+ let localState: EventInterface;
+ await initDB();
+ const data = localStorage.getItem('eventState');
if (data) {
- const localState = JSON.parse(data)
- setEvent(localState)
+ localState = JSON.parse(data);
+ const heroImgFile = await getItem('heroImg');
+ if (heroImgFile) {
+ const heroImg = await imageToDataURL(heroImgFile);
+ localState.heroImg = heroImg;
+ }
+ setEvent(localState);
}
} catch (error) {
- console.log(error.message)
+ console.log(error.message);
}
- }
- init()
- window.addEventListener('storage', init, true)
+ };
+ init();
+ window.addEventListener('storage', init, true);
return () => {
- window.removeEventListener('storage', init, true)
- }
- }, [])
- if (!event) return null
- const eventState = event as unknown as EventInterface
+ window.removeEventListener('storage', init, true);
+ };
+ }, []);
+ if (!event) return null;
+ const eventState = event as unknown as EventInterface;
const { name, date, heroImg, address, city, state, eventDetails } =
- eventState
+ eventState;
return (
@@ -39,5 +45,5 @@ export const PreviewEvent = () => {
- )
-}
+ );
+};
diff --git a/src/components/RaceEditor/MapEditor/Button.tsx b/src/components/RaceEditor/MapEditor/Button.tsx
new file mode 100644
index 0000000..ed695d8
--- /dev/null
+++ b/src/components/RaceEditor/MapEditor/Button.tsx
@@ -0,0 +1,19 @@
+import { MouseEventHandler } from 'react';
+
+interface ButtonInterface {
+ name: string;
+ onClick?: MouseEventHandler;
+ isActive?: boolean;
+}
+
+export const Button = ({ isActive, name, onClick }: ButtonInterface) => {
+ const bgColor = isActive ? 'bg-blue-400' : 'bg-blueGray-300';
+ return (
+
+ );
+};
diff --git a/src/components/RaceEditor/MapEditor/EditorTools.tsx b/src/components/RaceEditor/MapEditor/EditorTools.tsx
new file mode 100644
index 0000000..3ecaf54
--- /dev/null
+++ b/src/components/RaceEditor/MapEditor/EditorTools.tsx
@@ -0,0 +1,97 @@
+import React, { MouseEventHandler } from 'react';
+import { useDispatch } from 'react-redux';
+import { actions } from '../../../lib/redux/reducers/raceEditor';
+
+interface EditorToolsProps {
+ tools: { [k: string]: boolean };
+}
+
+export const EditorTools = ({ tools }: EditorToolsProps) => {
+ const dispatch = useDispatch();
+ return (
+
+
+ dispatch(actions.setSelectActive())}>
+
+
+ dispatch(actions.setAddMarkerActive())}
+ >
+
+ dispatch(actions.undoAddMarker())}
+ />
+
+ dispatch(actions.setCreateRouteActive())}
+ >
+
+ console.log('UNDO ROUTE')}
+ />
+
+
+
+ );
+};
+
+interface ToolGroupProps {
+ onClick?: MouseEventHandler;
+ children: React.ReactNode;
+}
+
+const ToolGroup = ({ children, onClick }: ToolGroupProps) => {
+ return (
+
+ {children}
+
+ );
+};
+
+interface ToolPrimaryInterface {
+ name: string;
+ onClick?: MouseEventHandler;
+ isActive?: boolean;
+}
+
+export const ToolPrimary = ({
+ isActive,
+ name,
+ onClick,
+}: ToolPrimaryInterface) => {
+ const bgColor = isActive ? 'bg-blue-400' : 'bg-blueGray-400';
+ return (
+
+ );
+};
+
+interface ToolSecondaryInterface {
+ name: string;
+ onClick?: MouseEventHandler;
+ isActive?: boolean;
+}
+
+const ToolSecondary = ({ name, onClick, isActive }: ToolSecondaryInterface) => {
+ const bgColor = isActive ? 'bg-blue-200' : 'bg-blueGray-300';
+ return (
+
+ );
+};
diff --git a/src/components/RaceEditor/MapEditor/Legend.tsx b/src/components/RaceEditor/MapEditor/Legend.tsx
new file mode 100644
index 0000000..9bf1c72
--- /dev/null
+++ b/src/components/RaceEditor/MapEditor/Legend.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+
+export const Legend = () => {
+ return (
+
+
Legend
+
+
+
+
Aid Station Level 1
+
+
+
+
Aid Station Level 2
+
+
+
+ );
+};
diff --git a/src/components/RaceEditor/MapEditor/Map.tsx b/src/components/RaceEditor/MapEditor/Map.tsx
new file mode 100644
index 0000000..19ce54a
--- /dev/null
+++ b/src/components/RaceEditor/MapEditor/Map.tsx
@@ -0,0 +1,53 @@
+import React, { useEffect, useRef } from 'react';
+import { useSelector } from 'react-redux';
+import mapboxgl, { EventData, MapMouseEvent } from 'mapbox-gl';
+
+import { coordsToRouteFeature } from '../../../lib/utils';
+import {
+ initMap,
+ setPoints,
+ setRoutePoints,
+ removeMap,
+} from '../../../lib/mapBox/';
+
+import { RaceEditorState } from '../../../types';
+
+mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_KEY || '';
+
+interface MapboxMapProps {
+ clickEventHandler: (event?: MapMouseEvent & EventData) => void;
+}
+
+export const MapboxMap = ({ clickEventHandler }: MapboxMapProps) => {
+ const state = useSelector((state: RaceEditorState) => state.race);
+ const { points, routePoints } = state;
+ const mapContainer = useRef(null);
+
+ useEffect(() => {
+ if (!mapContainer.current) return; // initialize map only once
+ initMap({
+ center: [-94.115251, 36.184605],
+ container: mapContainer.current,
+ zoom: 14,
+ canvasClickHandler: clickEventHandler,
+ });
+ return () => {
+ removeMap();
+ };
+ }, []);
+
+ useEffect(() => {
+ setPoints(points);
+ }, [points]);
+
+ useEffect(() => {
+ const newRoute = coordsToRouteFeature(routePoints);
+ setRoutePoints(newRoute);
+ }, [routePoints]);
+
+ return (
+
+ );
+};
diff --git a/src/components/RaceEditor/MapEditor/MarkerOptions.tsx b/src/components/RaceEditor/MapEditor/MarkerOptions.tsx
new file mode 100644
index 0000000..e43c8cb
--- /dev/null
+++ b/src/components/RaceEditor/MapEditor/MarkerOptions.tsx
@@ -0,0 +1,157 @@
+import React, {
+ ChangeEvent,
+ FormEvent,
+ FormEventHandler,
+ MouseEventHandler,
+ useEffect,
+ useState,
+} from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { actions } from '../../../lib/redux/reducers/raceEditor';
+import { RaceEditorState } from '../../../types';
+
+export const MarkerOptions = () => {
+ const [type, setType] = useState();
+ const [amenities, setAmenities] = useState([]);
+ const dispatch = useDispatch();
+ const state = useSelector(
+ (state: RaceEditorState) => state.race.modals.markerOptions
+ );
+
+ useEffect(() => {
+ if (!state.marker) return;
+ setType(state.marker.properties.type);
+ setAmenities(state.marker.properties.amenities.split(','));
+ }, []);
+
+ const handleAmenitiesSelect = (e: ChangeEvent) => {
+ const items = e.target.selectedOptions;
+ const selectedOptions = [];
+ for (let i = 0; i < items.length; i++) {
+ const item = items.item(i);
+ if (!item) continue;
+ selectedOptions.push(item.value);
+ }
+ setAmenities(selectedOptions);
+ };
+
+ const handleRemove = () => {
+ if (!state.marker) {
+ dispatch(actions.closeMarkerOptionsModal());
+ } else {
+ dispatch(actions.removeMarker(state.marker.properties.id));
+ dispatch(actions.closeMarkerOptionsModal());
+ }
+ };
+
+ const handleCancel = () => {
+ if (state.marker && !state.marker.properties.type) {
+ dispatch(actions.removeMarker(state.marker.properties.id));
+ }
+ dispatch(actions.closeMarkerOptionsModal());
+ };
+
+ const handleSave = (e: FormEvent) => {
+ e.preventDefault();
+ if (!state.marker || !type) return;
+ const amenitiesString = amenities.join(',');
+ console.log(type, amenitiesString);
+ dispatch(
+ actions.updateMarker({
+ amenities: amenitiesString,
+ type,
+ id: state.marker.properties.id,
+ })
+ );
+ dispatch(actions.closeMarkerOptionsModal());
+ };
+
+ const isSelected = (item: string): boolean => {
+ if (!state.marker) return false;
+ return amenities.includes(item);
+ };
+
+ return (
+
+ );
+};
+
+interface ButtonProps {
+ color?: string;
+ name: string;
+ clickHandler?: MouseEventHandler;
+ type?: 'button' | 'reset' | 'submit';
+}
+
+const Button = ({
+ color = 'bg-gray',
+ name,
+ clickHandler,
+ type = 'button',
+}: ButtonProps) => {
+ return (
+
+ );
+};
diff --git a/src/components/RaceEditor/MapEditor/index.tsx b/src/components/RaceEditor/MapEditor/index.tsx
new file mode 100644
index 0000000..85c0522
--- /dev/null
+++ b/src/components/RaceEditor/MapEditor/index.tsx
@@ -0,0 +1,162 @@
+import React, { useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { actions } from '../../../lib/redux/reducers/raceEditor';
+import mapboxgl, { EventData, MapMouseEvent } from 'mapbox-gl';
+import { Feature, LineString, Position } from 'geojson';
+import axios from 'axios';
+import { createMarker } from '../../../lib/utils';
+
+import { EditorTools } from './EditorTools';
+import { MapboxMap } from './Map';
+import { Legend } from './Legend';
+import { MarkerOptions } from './MarkerOptions';
+import { Marker, RaceEditorState } from '../../../types';
+
+export const MapEditor = () => {
+ const dispatch = useDispatch();
+ const state = useSelector((state: RaceEditorState) => state.race);
+ const [mapClick, setMapClick] = useState();
+
+ const handleAddMarkerEvent = (event: MapMouseEvent & EventData) => {
+ const features = event.target.queryRenderedFeatures(event.point, {
+ layers: ['points'],
+ });
+ const clickedMarker =
+ features.length !== 0 && features[0].source === 'points';
+ let marker: Marker | undefined;
+ if (!clickedMarker) {
+ const markerPosition = event.lngLat;
+ marker = createMarker(markerPosition);
+ dispatch(actions.addMarker(marker));
+ } else if (features[0].properties) {
+ const featureID = features[0].properties.id;
+ marker = state.points.find((m) => m.properties.id === featureID);
+ }
+ const position = { x: event.point.x, y: event.point.y };
+ if (!marker) return;
+ dispatch(
+ actions.openMarkerOptionsModal({
+ position,
+ marker,
+ })
+ );
+ };
+
+ const handleCreateRouteEvent = (event: MapMouseEvent & EventData) => {
+ console.log('CREATE ROUTE');
+ // if (addMarker) {
+ // console.log('EVENT: ', event);
+ // const feature = eventToFeature(event);
+ // console.log('FEATURE: ', feature);
+ // dispatch(actions.addMarker(feature));
+ // }
+ };
+
+ const handleSelectEvent = (event: MapMouseEvent & EventData) => {
+ console.log('SELECT');
+ };
+
+ useEffect(() => {
+ if (!mapClick) return;
+ if (state.tools.addMarker) {
+ handleAddMarkerEvent(mapClick);
+ } else if (state.tools.createRoute) {
+ handleCreateRouteEvent(mapClick);
+ } else if (state.tools.select) {
+ handleSelectEvent(mapClick);
+ }
+ setMapClick(undefined);
+ // if (state.tools.addMarker) {
+ // const feature = eventToFeature(clickEvent);
+ // dispatch(actions.addMarker(feature));
+ // } else if (createRoute) {
+ // const newPoint = [clickEvent.lngLat.lng, clickEvent.lngLat.lat];
+ // dispatch(actions.addRoutePoint(newPoint));
+ // const newPoints = [...routePoints, newPoint];
+ // if (newPoints.length === 1) {
+ // const startFeature = coordsToStartFeature(newPoint);
+ // const startSrc = map.getSource('points') as GeoJSONSource;
+ // startSrc.setData({
+ // type: 'FeatureCollection',
+ // features: [startFeature],
+ // });
+ // } else {
+ // const last = newPoints.length - 1;
+ // const addNextFeature = async () => {
+ // const feature = await getMatchRoute(
+ // newPoints[last - 1],
+ // newPoints[last]
+ // );
+ // setRoutePoints([...routePoints, ...feature.coordinates]);
+ // };
+ // addNextFeature();
+ // }
+ // } else {
+ // const features = map.queryRenderedFeatures(clickEvent.point, {
+ // layers: ['points'],
+ // });
+ // console.log(features);
+ // }
+ }, [mapClick]);
+
+ const { modals } = state;
+ return (
+ <>
+
+
+
+
+ {modals.markerOptions.active && }
+
+ >
+ );
+};
+
+const getMatchRoute = async (
+ lnglat1: number[],
+ lnglat2: number[]
+): Promise => {
+ const accessToken = mapboxgl.accessToken;
+ const coords1 = `${lnglat1.join(',')}`;
+ const coords2 = `${lnglat2.join(',')}`;
+ const base = 'https://api.mapbox.com/directions/v5/mapbox/walking/';
+ const url = `${base}${coords1};${coords2}?geometries=geojson&access_token=${accessToken}`;
+ const res = await axios.get(url);
+ if (res.data.code === 'Ok') {
+ return res.data.routes[0].geometry;
+ } else {
+ console.log(res.data);
+ throw new Error('Failed to match route');
+ }
+};
+
+function eventToFeature(e: MapMouseEvent & EventData): Feature {
+ const lat = e.lngLat.lat;
+ const lng = e.lngLat.lng;
+ return {
+ type: 'Feature',
+ geometry: {
+ type: 'Point',
+ coordinates: [lng, lat],
+ },
+ properties: {
+ // title: '',
+ // types: '',
+ // type: '',
+ },
+ };
+}
+
+const coordsToStartFeature = (coordinates: Position): Feature => {
+ return {
+ type: 'Feature',
+ geometry: {
+ type: 'Point',
+ coordinates,
+ },
+ properties: {
+ title: 'Start',
+ type: 'start',
+ },
+ };
+};
diff --git a/src/components/common/Map.tsx b/src/components/RaceEditor/MapEditor/index.tsx.bak
similarity index 92%
rename from src/components/common/Map.tsx
rename to src/components/RaceEditor/MapEditor/index.tsx.bak
index 0b52824..c9b86bf 100644
--- a/src/components/common/Map.tsx
+++ b/src/components/RaceEditor/MapEditor/index.tsx.bak
@@ -1,4 +1,6 @@
import React, { MouseEventHandler, useEffect, useRef, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { actions } from '../../../lib/redux/reducers/raceEditor';
import mapboxgl, {
EventData,
GeoJSONSource,
@@ -8,56 +10,55 @@ import mapboxgl, {
} from 'mapbox-gl';
import { Feature, LineString, Point, Position } from 'geojson';
import axios from 'axios';
-import { initMap } from './MapEditor/initMap';
+import { initMap, setPoints } from '../../../lib/mapBox/';
+
+import {} from './'
+import { RaceEditorState } from '../../../types';
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_KEY || '';
export const MapboxMap = () => {
+ const dispatch = useDispatch();
+ const state = useSelector((state: RaceEditorState) => state.race);
const mapContainer = useRef(null);
- const [map, setMap] = useState
);
diff --git a/src/components/RaceEditor/reducer.ts b/src/components/RaceEditor/reducer.ts
deleted file mode 100644
index 7e656d8..0000000
--- a/src/components/RaceEditor/reducer.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import dayjs from 'dayjs';
-import { EventActionInterface, EventInterface } from '../../types';
-
-export const init = ({
- name = '',
- heroImg = { name: null, size: null, src: null, file: null, error: null },
- date = dayjs().format('YYYY-MM-DD'),
- address = '',
- city = '',
- state = '',
- time = dayjs('01/01/2021 12:00').format('HH:mm'),
- eventDetails = '',
-}): EventInterface => {
- return {
- name,
- heroImg,
- date,
- address,
- city,
- state,
- time,
- eventDetails,
- };
-};
-
-export const reducer = (
- state: EventInterface,
- action: EventActionInterface
-) => {
- switch (action.type) {
- case 'updateName':
- return { ...state, name: action.payload };
- case 'updateDate':
- return { ...state, date: action.payload };
- case 'updateAddress':
- return { ...state, address: action.payload };
- case 'updateCity':
- return { ...state, city: action.payload };
- case 'updateState':
- return { ...state, state: action.payload };
- case 'updateTime':
- return { ...state, time: action.payload };
- case 'updateHeroImg':
- return { ...state, heroImg: action.payload };
- case 'updateEventDetails':
- return { ...state, eventDetails: action.payload };
- case 'init':
- return action.payload ? init(action.payload) : init({});
- default:
- return state;
- }
-};
diff --git a/src/lib/mapBox/index.ts b/src/lib/mapBox/index.ts
new file mode 100644
index 0000000..c6843f2
--- /dev/null
+++ b/src/lib/mapBox/index.ts
@@ -0,0 +1,57 @@
+import { Feature, FeatureCollection } from 'geojson';
+import mapboxgl, {
+ EventData,
+ EventedListener,
+ GeoJSONSource,
+ LngLatLike,
+ Map,
+ MapMouseEvent,
+} from 'mapbox-gl';
+import { initMap as _initMap } from './initMap';
+mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_KEY || '';
+
+let map: Map | undefined;
+interface InitMapInterface {
+ center?: LngLatLike;
+ container: HTMLElement;
+ zoom?: number;
+ canvasClickHandler: (event?: MapMouseEvent & EventData) => void;
+}
+
+export const initMap = (options: InitMapInterface) => {
+ if (map) return;
+ const {
+ center = [0, 0],
+ zoom = 10,
+ container,
+ canvasClickHandler,
+ } = options;
+
+ map = new mapboxgl.Map({
+ container,
+ style: 'mapbox://styles/mapbox/streets-v11',
+ center,
+ zoom,
+ });
+ map.on('click', canvasClickHandler);
+ _initMap(map);
+ return map;
+};
+
+export const removeMap = () => {
+ map = undefined;
+};
+
+export const setPoints = (points: Feature[]) => {
+ if (!map) return;
+ const pointsSrc = map.getSource('points') as GeoJSONSource;
+ if (!pointsSrc) return;
+ pointsSrc.setData({ type: 'FeatureCollection', features: points });
+};
+
+export const setRoutePoints = (routePoints: Feature) => {
+ if (!map) return;
+ const pointsSrc = map.getSource('points') as GeoJSONSource;
+ if (!pointsSrc) return;
+ pointsSrc.setData({ type: 'FeatureCollection', features: [routePoints] });
+};
diff --git a/src/components/common/MapEditor/initMap.ts b/src/lib/mapBox/initMap.ts
similarity index 79%
rename from src/components/common/MapEditor/initMap.ts
rename to src/lib/mapBox/initMap.ts
index 5122d25..b73d9fa 100644
--- a/src/components/common/MapEditor/initMap.ts
+++ b/src/lib/mapBox/initMap.ts
@@ -1,4 +1,6 @@
+import { FeatureCollection } from 'geojson';
import mapboxgl, { LngLatLike, Map } from 'mapbox-gl';
+import { Marker } from '../../types';
interface InitMapInterface {
center?: LngLatLike;
@@ -7,20 +9,7 @@ interface InitMapInterface {
canvasClickHandler: any;
}
-export const initMap = (options: InitMapInterface): Map => {
- const {
- center = [0, 0],
- zoom = 10,
- container,
- canvasClickHandler,
- } = options;
-
- const map = new mapboxgl.Map({
- container,
- style: 'mapbox://styles/mapbox/streets-v11',
- center,
- zoom,
- });
+export const initMap = (map: Map): Map => {
const popup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false,
@@ -71,7 +60,7 @@ export const initMap = (options: InitMapInterface): Map => {
['get', 'type'],
'start',
'#A3E635',
- 'Aid Station Level 1',
+ 'Aid Station',
'#BFDBFE',
'Aid Station Level 2',
'#C4B5FD',
@@ -84,7 +73,7 @@ export const initMap = (options: InitMapInterface): Map => {
['get', 'type'],
'start',
'#4D7C0F',
- 'Aid Station Level 1',
+ 'Aid Station',
'#1E3A8A',
'Aid Station Level 2',
'#5B21B6',
@@ -98,24 +87,25 @@ export const initMap = (options: InitMapInterface): Map => {
minzoom: 9,
});
});
- map.on('click', (e) => {
- canvasClickHandler(e);
- });
map.on('mouseenter', 'points', function (e) {
// Change the cursor style as a UI indicator.
map.getCanvas().style.cursor = 'pointer';
- console.log(e);
if (!e.features) return;
const coordinates = e.lngLat;
- // const { title, types } = e.features[0].properties;
- // const typesList = types
- // .split(', ')
- // .map((type) => {
- // return `${type}`;
- // })
- // .join('');
- const title = 'TITLE';
- const description = `${title}
`;
+ const feature = e.features[0].properties as Marker['properties'];
+ const { type, amenities } = feature;
+ let list = '';
+ if (amenities) {
+ const amenitiesList = amenities
+ .split(',')
+ .map((type) => {
+ return `${type}`;
+ })
+ .join('');
+ list = ``;
+ }
+
+ const description = `${type}
${list}`;
// Ensure that if the map is zoomed out such that multiple
// copies of the feature are visible, the popup appears
diff --git a/src/lib/mapboxgl/index.ts b/src/lib/mapboxgl/index.ts
deleted file mode 100644
index 41c85fc..0000000
--- a/src/lib/mapboxgl/index.ts
+++ /dev/null
@@ -1,270 +0,0 @@
-import { FeatureCollection } from 'geojson';
-import mapboxgl, { EventData, Map, MapMouseEvent } from 'mapbox-gl';
-
-export class mapboxMap {
- map!: Map;
- element: HTMLDivElement;
- isLoaded: boolean;
- points: FeatureCollection;
-
- constructor() {
- this.element = document.createElement('div');
- this.isLoaded = false;
- this.points = { type: 'FeatureCollection', features: [] };
- }
-
- loadMap(_data: { route: any; points: any }) {
- return new Promise((resolve) => {
- this.map = new mapboxgl.Map({
- container: this.element,
- style: 'mapbox://styles/mapbox/outdoors-v11',
- center: [-94.115251, 36.184605],
- maxZoom: 14,
- minZoom: 10,
- });
- // route = route || {
- // type: 'FeatureCollection',
- // features: [],
- // };
-
- // if (route.features.length > 0) {
- // this.bounds = getBounds(route);
- // this.fitBounds();
- // }
-
- this.map.on('load', () => {
- this.map.scrollZoom.disable();
- this.map.doubleClickZoom.disable();
- this.map.addControl(
- new mapboxgl.NavigationControl({ showCompass: false })
- );
- this.map.addSource('points', {
- type: 'geojson',
- data: this.points,
- });
- // this.map.addSource('route', {
- // type: 'geojson',
- // data: route,
- // });
-
- // this.map.addLayer({
- // id: 'route',
- // type: 'line',
- // source: 'route',
- // layout: {
- // 'line-join': 'round',
- // 'line-cap': 'round',
- // visibility: 'visible',
- // },
- // paint: {
- // 'line-color': '#c40000',
- // 'line-width': [
- // 'interpolate',
- // ['linear'],
- // ['zoom'],
- // 10,
- // 1,
- // 14,
- // 3,
- // ],
- // },
- // minzoom: 9,
- // });
-
- this.map.addLayer({
- id: 'points',
- type: 'circle',
- source: 'points',
- paint: {
- 'circle-color': [
- 'match',
- ['get', 'type'],
- 'Start / Finish',
- '#A3E635',
- 'Aid Station Level 1',
- '#BFDBFE',
- 'Aid Station Level 2',
- '#C4B5FD',
- 'Restroom',
- '#FDBA74',
- '#D4D4D8',
- ],
- 'circle-stroke-color': [
- 'match',
- ['get', 'type'],
- 'Start / Finish',
- '#4D7C0F',
- 'Aid Station Level 1',
- '#1E3A8A',
- 'Aid Station Level 2',
- '#5B21B6',
- 'Restroom',
- '#9A3412',
- '#000000',
- ],
- 'circle-stroke-width': 2,
- 'circle-radius': 10,
- },
- minzoom: 9,
- });
-
- this.isLoaded = true;
- resolve({ isLoaded: this.isLoaded });
- });
-
- const popup = new mapboxgl.Popup({
- closeButton: false,
- closeOnClick: false,
- });
-
- this.map.on('click', async function (e) {
- const coords = eventToFeature(e);
- console.log(coords);
- // Add starting point to the map
- // if (route.data.waypoints.length == 0) {
- // const index = 0;
- // const point = coordsToWaypointPoint(coords, index, false);
-
- // route.data.waypoints.push(point);
-
- // drawWaypoints();
- // } else {
- // const index = route.data.waypoints.length;
- // const point = coordsToWaypointPoint(
- // coords,
- // index,
- // goingDirect
- // );
-
- // route.data.waypoints.push(point);
-
- // const legIndex = index - 1;
- // const prevIndex = index - 1;
- // const start =
- // route.data.waypoints[prevIndex].geometry.coordinates;
- // const end =
- // route.data.waypoints[index].geometry.coordinates;
-
- // let [lineString, legSteps] = await processDirections(
- // start,
- // end,
- // legIndex,
- // goingDirect
- // );
-
- // route.data.legs.push(lineString);
- // route.data.directions.push(legSteps);
-
- // drawDirections();
- // drawWaypoints();
- // drawRoute();
- // }
- });
- // this.map.on('mouseenter', 'points', function (e) {
- // // Change the cursor style as a UI indicator.
- // this.getCanvas().style.cursor = 'pointer';
-
- // const coordinates = e.features[0].geometry.coordinates.slice();
- // const { title, types } = e.features[0].properties;
- // const typesList = types
- // .split(', ')
- // .map((type) => {
- // return `${type}`;
- // })
- // .join('');
- // const description = `${title}
`;
-
- // // Ensure that if the map is zoomed out such that multiple
- // // copies of the feature are visible, the popup appears
- // // over the copy being pointed to.
- // while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
- // coordinates[0] +=
- // e.lngLat.lng > coordinates[0] ? 360 : -360;
- // }
-
- // // Populate the popup and set its coordinates
- // // based on the feature found.
- // popup.setLngLat(coordinates).setHTML(description).addTo(this);
- // });
-
- // this.map.on('mouseleave', 'points', function () {
- // this.getCanvas().style.cursor = '';
- // popup.remove();
- // });
- });
- }
-
- // fitBounds() {
- // this.map.fitBounds(this.bounds, { padding: 100 });
- // }
-
- // loadSource({ route, points }) {
- // this.map.getSource('route').setData(route);
- // if (points) {
- // this.map.getSource('points').setData(pointsToGeojson(points));
- // }
- // this.bounds = getBounds(route);
- // this.fitBounds();
- // }
-
- // removeSources() {
- // if (this.map.getSource('route')) {
- // this.map.getSource('route').setData({
- // type: 'FeatureCollection',
- // features: [],
- // });
- // }
- // if (this.map.getSource('points')) {
- // this.map.getSource('points').setData(pointsToGeojson(null));
- // }
- // }
-}
-
-// const pointsToGeojson = (points: { lat: number; lng: number }) => {
-// let features;
-// if (points) {
-// features = points.map(({ lat, lng }) => {
-// return {
-// // feature for Mapbox DC
-// type: 'Feature',
-// geometry: {
-// type: 'Point',
-// coordinates: [lng, lat],
-// },
-// // properties: {
-// // title: name,
-// // types: aidTypes,
-// // type,
-// // },
-// };
-// });
-// } else {
-// features = [];
-// }
-// return { type: 'FeatureCollection', features };
-// };
-
-// export const getBounds = (geojson: FeatureCollection) => {
-// const coordinates = geojson.features[0].geometry.coordinates;
-// const bounds = coordinates.reduce(function (bounds, coord) {
-// return bounds.extend(coord);
-// }, new mapboxgl.LngLatBounds(coordinates[0], coordinates[0]));
-// return bounds;
-// };
-
-function eventToFeature(e: MapMouseEvent & EventData) {
- const lat = e.lngLat.lat;
- const lng = e.lngLat.lng;
- return {
- type: 'Feature',
- geometry: {
- type: 'Point',
- coordinates: [lng, lat],
- },
- // properties: {
- // title: name,
- // types: aidTypes,
- // type,
- // },
- };
-}
diff --git a/src/lib/redux/reducers/eventEditor.ts b/src/lib/redux/reducers/eventEditor.ts
new file mode 100644
index 0000000..d82a870
--- /dev/null
+++ b/src/lib/redux/reducers/eventEditor.ts
@@ -0,0 +1,73 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import dayjs from 'dayjs';
+import { EventInterface } from '../../../types';
+
+const initialState: EventInterface = {
+ name: '',
+ heroImg: {
+ name: null,
+ size: null,
+ src: null,
+ dataURL: null,
+ error: null,
+ },
+ date: dayjs().format('YYYY-MM-DD'),
+ address: '',
+ city: '',
+ state: '',
+ time: dayjs('01/01/2021 12:00').format('HH:mm'),
+ eventDetails: '',
+};
+
+const reducers = {
+ updateName: (state: EventInterface, action: PayloadAction) => {
+ state.name = action.payload;
+ },
+ updateDate: (state: EventInterface, action: PayloadAction) => {
+ state.date = action.payload;
+ },
+ updateTime: (state: EventInterface, action: PayloadAction) => {
+ state.time = action.payload;
+ },
+ updateAddress: (state: EventInterface, action: PayloadAction) => {
+ state.address = action.payload;
+ },
+ updateCity: (state: EventInterface, action: PayloadAction) => {
+ state.city = action.payload;
+ },
+ updateState: (state: EventInterface, action: PayloadAction) => {
+ state.state = action.payload;
+ },
+ updateHeroImg: (
+ state: EventInterface,
+ action: PayloadAction
+ ) => {
+ state.heroImg = action.payload;
+ },
+ updateEventDetails: (
+ state: EventInterface,
+ action: PayloadAction
+ ) => {
+ state.eventDetails = action.payload;
+ },
+ updateEvent: (
+ _state: EventInterface,
+ action: PayloadAction
+ ) => {
+ return action.payload;
+ },
+ init: () => {
+ return initialState;
+ },
+};
+
+export const eventSlice = createSlice({
+ name: 'event',
+ initialState,
+ reducers,
+});
+
+export const actions = eventSlice.actions;
+
+export default eventSlice.reducer;
+export const eventInitialState = initialState;
diff --git a/src/lib/redux/reducers/index.ts b/src/lib/redux/reducers/index.ts
index 42af8a8..de9b5ea 100644
--- a/src/lib/redux/reducers/index.ts
+++ b/src/lib/redux/reducers/index.ts
@@ -1,8 +1,11 @@
-import ui, { uiInitialState } from './ui'
-import user, { userInitialState } from './user'
+import ui, { uiInitialState } from './ui';
+import user, { userInitialState } from './user';
+import event, { eventInitialState } from './eventEditor';
+import race, { raceInitialState } from './raceEditor';
-const reducer = { ui, user }
-export default reducer
+export const reducers = { ui, user, event, race };
-const rootState = { ui: uiInitialState, user: userInitialState }
-export type RootState = typeof rootState
+export const rootState = { ui: uiInitialState, user: userInitialState };
+export const eventState = { event: eventInitialState };
+export const raceState = { race: raceInitialState };
+export type RootState = typeof rootState;
diff --git a/src/lib/redux/reducers/raceEditor.ts b/src/lib/redux/reducers/raceEditor.ts
new file mode 100644
index 0000000..cea5cd8
--- /dev/null
+++ b/src/lib/redux/reducers/raceEditor.ts
@@ -0,0 +1,123 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import dayjs from 'dayjs';
+import { stat } from 'fs';
+import { Feature } from 'geojson';
+import { RaceEditorInterface, Marker } from '../../../types';
+
+const initialState: RaceEditorInterface = {
+ name: '',
+ points: [],
+ routePoints: [],
+ activeTool: '',
+ tools: {
+ addMarker: false,
+ createRoute: false,
+ select: true,
+ },
+ modals: {
+ markerOptions: {
+ active: false,
+ position: { x: 0, y: 0 },
+ marker: null,
+ },
+ },
+};
+
+const reducers = {
+ updateName: (state: RaceState, action: PayloadAction) => {
+ state.name = action.payload;
+ },
+ addMarker: (state: RaceState, action: PayloadAction) => {
+ state.points = [...state.points, action.payload];
+ },
+ addRoutePoint: (state: RaceState, action: PayloadAction) => {
+ state.routePoints = [...state.routePoints, action.payload];
+ },
+ undoAddRoutePoint: (state: RaceState) => {
+ if (state.routePoints.length > 0) state.routePoints.pop();
+ },
+ undoAddMarker: (state: RaceState) => {
+ state.modals.markerOptions.active = false;
+ if (state.points.length > 0) state.points.pop();
+ },
+ init: () => {
+ return initialState;
+ },
+ updateState: (state: RaceState) => {
+ return state;
+ },
+ setAddMarkerActive: (state: RaceState) => {
+ state.tools.addMarker = true;
+ state.tools.createRoute = false;
+ state.tools.select = false;
+ },
+ setCreateRouteActive: (state: RaceState) => {
+ state.tools.addMarker = false;
+ state.tools.createRoute = true;
+ state.tools.select = false;
+ },
+ setSelectActive: (state: RaceState) => {
+ state.tools.addMarker = false;
+ state.tools.createRoute = false;
+ state.tools.select = true;
+ },
+ openMarkerOptionsModal: (
+ state: RaceState,
+ action: PayloadAction<{
+ position: { y: number; x: number };
+ marker: Marker;
+ }>
+ ) => {
+ state.modals.markerOptions.active = true;
+ state.modals.markerOptions.position = action.payload.position;
+ state.modals.markerOptions.marker = action.payload.marker;
+ },
+ closeMarkerOptionsModal: (state: RaceState) => {
+ state.modals.markerOptions.active = false;
+ state.modals.markerOptions.position = { y: 0, x: 0 };
+ },
+ removeMarker: (state: RaceState, action: PayloadAction) => {
+ const newPoints = state.points.filter((point) => {
+ return point.properties.id !== action.payload;
+ });
+ state.points = newPoints;
+ },
+ updateMarker: (
+ state: RaceState,
+ action: PayloadAction<{
+ id: string;
+ type?: string;
+ amenities?: string;
+ coordinates?: number[];
+ }>
+ ) => {
+ const newPoints = state.points.map((point) => {
+ if (point.properties.id === action.payload.id) {
+ console.log(action.payload.amenities);
+ if (action.payload.type) {
+ point.properties.type = action.payload.type;
+ }
+ if (action.payload.amenities !== undefined) {
+ point.properties.amenities = action.payload.amenities;
+ }
+ if (action.payload.coordinates) {
+ point.geometry.coordinates = action.payload.coordinates;
+ }
+ }
+ return point;
+ });
+ state.points = newPoints;
+ },
+};
+
+export const raceSlice = createSlice({
+ name: 'race',
+ initialState,
+ reducers,
+});
+
+export const actions = raceSlice.actions;
+
+export default raceSlice.reducer;
+export const raceInitialState = initialState;
+export type RaceState = typeof initialState;
diff --git a/src/lib/redux/store.ts b/src/lib/redux/store.ts
index 3d9b744..868821d 100644
--- a/src/lib/redux/store.ts
+++ b/src/lib/redux/store.ts
@@ -1,6 +1,76 @@
-import { configureStore } from '@reduxjs/toolkit';
-import reducer from './reducers';
+import {
+ configureStore,
+ DeepPartial,
+ Store,
+ ReducersMapObject,
+ Reducer,
+} from '@reduxjs/toolkit';
+import { rootState, RootState, reducers } from './reducers';
+import { useMemo } from 'react';
-export default configureStore({
- reducer,
-});
+let store: Store | undefined;
+const rootReducer = { ui: reducers.ui, user: reducers.user };
+
+function initStore(
+ preloadedState: DeepPartial = rootState,
+ reducer: ReducersMapObject = rootReducer
+) {
+ return configureStore({
+ reducer: reducer,
+ preloadedState,
+ });
+}
+
+export const initializeStore = (
+ preloadedState: DeepPartial,
+ reducer: ReducersMapObject
+) => {
+ let _store = store ?? initStore(preloadedState, reducer);
+
+ // After navigating to a page with an initial Redux state, merge that state
+ // with the current state in the store, and create a new store
+ if (preloadedState && store) {
+ const { ui, user } = store.getState();
+ _store = initStore(
+ {
+ ui,
+ user,
+ ...preloadedState,
+ },
+ reducer
+ );
+ // Reset the current store
+ store = undefined;
+ }
+
+ // For SSG and SSR always create a new store
+ if (typeof window === 'undefined') return _store;
+ // Create the store once in the client
+ if (!store) store = _store;
+
+ return _store;
+};
+
+export function useStore(
+ initialState: DeepPartial,
+ pageReducersRefs: string[]
+) {
+ const pageReducers = getPageReducers(pageReducersRefs);
+ const reducer = { ...rootReducer, ...pageReducers };
+ const store = useMemo(
+ () => initializeStore(initialState, reducer),
+ [initialState, reducer]
+ );
+ return store;
+}
+
+const getPageReducers = (refs: string[] = []) => {
+ const pageReducers: { [k: string]: Reducer } = {};
+ refs.forEach((ref) => {
+ const reducer = Object.getOwnPropertyDescriptor(reducers, ref);
+ if (reducer) {
+ pageReducers[ref] = reducer.value;
+ }
+ });
+ return pageReducers;
+};
diff --git a/src/lib/utils/coordsToRouteFeature.ts b/src/lib/utils/coordsToRouteFeature.ts
new file mode 100644
index 0000000..8e4c1e3
--- /dev/null
+++ b/src/lib/utils/coordsToRouteFeature.ts
@@ -0,0 +1,15 @@
+import { Feature, Position } from 'geojson';
+
+export const coordsToRouteFeature = (coordinates: Position[]): Feature => {
+ return {
+ type: 'Feature',
+ geometry: {
+ type: 'LineString',
+ coordinates,
+ },
+ properties: {
+ title: 'Start',
+ type: 'start',
+ },
+ };
+};
diff --git a/src/lib/utils/createMarker.ts b/src/lib/utils/createMarker.ts
new file mode 100644
index 0000000..bacd962
--- /dev/null
+++ b/src/lib/utils/createMarker.ts
@@ -0,0 +1,18 @@
+import { nanoid } from 'nanoid';
+import { LngLat } from 'mapbox-gl';
+import { Marker } from '../../types';
+
+export const createMarker = (position: LngLat): Marker => {
+ return {
+ type: 'Feature',
+ geometry: {
+ type: 'Point',
+ coordinates: [position.lng, position.lat],
+ },
+ properties: {
+ id: nanoid(),
+ type: '',
+ amenities: '',
+ },
+ };
+};
diff --git a/src/lib/utils/imageToDataURL.ts b/src/lib/utils/imageToDataURL.ts
index fa51637..5bf7587 100644
--- a/src/lib/utils/imageToDataURL.ts
+++ b/src/lib/utils/imageToDataURL.ts
@@ -1,19 +1,20 @@
interface ProcessImageResult {
- src: string;
name: string;
size: number;
- file: File;
+ dataURL: string | null;
+ src: string | null;
+ error: string | null;
}
export const imageToDataURL = (
- input: FileList | null
+ input: File | null
): Promise => {
return new Promise((resolve, reject) => {
if (!input) reject({ error: 'Invalid input' });
try {
- if (input && input[0]) {
- if (input[0].size > 4000000) {
- const fileSize = Math.round(input[0].size / 10000) / 100;
+ if (input) {
+ if (input.size > 4000000) {
+ const fileSize = Math.round(input.size / 10000) / 100;
reject({
error: `File must be smaller than 4MB, file size is ${fileSize}MB.`,
});
@@ -23,16 +24,17 @@ export const imageToDataURL = (
if (target) {
const result = target?.result as string;
resolve({
- src: result,
- name: input[0].name,
- size: input[0].size,
- file: input[0],
+ name: input.name,
+ size: input.size,
+ dataURL: result,
+ src: null,
+ error: null,
});
} else {
reject({ error: 'Could not read image file' });
}
};
- reader.readAsDataURL(input[0]);
+ reader.readAsDataURL(input);
}
} catch (error) {
reject({ error: error.message });
diff --git a/src/lib/utils/index.tsx b/src/lib/utils/index.tsx
index f949e9f..ea27f98 100644
--- a/src/lib/utils/index.tsx
+++ b/src/lib/utils/index.tsx
@@ -1,5 +1,7 @@
export { imageToDataURL } from './imageToDataURL';
export { gpxToGeoJSON } from './gpxToGeoJSON';
+export { coordsToRouteFeature } from './coordsToRouteFeature';
+export { createMarker } from './createMarker';
export function dataURLtoFile(dataurl: string, filename: string) {
if (dataurl.length === 0) return null;
diff --git a/src/lib/utils/indexDB.ts b/src/lib/utils/indexDB.ts
new file mode 100644
index 0000000..ababa27
--- /dev/null
+++ b/src/lib/utils/indexDB.ts
@@ -0,0 +1,31 @@
+import { openDB, deleteDB } from 'idb';
+import { IDBPDatabase } from 'idb';
+
+let localDB: IDBPDatabase;
+const dbName = 'runEventCreator_3diqkef79mdz552nw1mj8';
+const storeName = 'event';
+const version = 1; //versions start at 1
+
+export const initDB = async () => {
+ if (localDB) return;
+ localDB = await openDB(dbName, version, {
+ upgrade(db) {
+ db.createObjectStore(storeName);
+ },
+ });
+};
+
+export const putFile = async (name: string, file: File) => {
+ const tx = localDB.transaction(storeName, 'readwrite');
+ const store = tx.objectStore(storeName);
+ await store.put(file, name);
+ await tx.done;
+};
+
+export const getItem = async (key: string) => {
+ const tx = localDB.transaction(storeName, 'readwrite');
+ const store = tx.objectStore(storeName);
+ const item = await store.get(key);
+ await tx.done;
+ return item;
+};
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index e27597d..23c7985 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -2,14 +2,14 @@ import { AppProps } from 'next/app';
import { ApolloProvider } from '@apollo/client';
import { useApollo } from '../lib/graphql/apollo';
import { Provider } from 'react-redux';
-import store from '../lib/redux/store';
+import { useStore } from '../lib/redux/store';
import '../styles/globals.css';
import 'mapbox-gl/dist/mapbox-gl.css';
export default function App({ Component, pageProps }: AppProps) {
const apolloClient = useApollo(pageProps.initialApolloState);
- console.log(pageProps)
+ const store = useStore(pageProps.initialReduxState, pageProps.reduxReducer);
return (
diff --git a/src/pages/create_event.tsx b/src/pages/create_event.tsx
deleted file mode 100644
index ed4a913..0000000
--- a/src/pages/create_event.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react'
-import { Layout } from '../components/Common/Layout'
-import EventEditor from '../components/EventEditor'
-
-const Create_event = () => {
- return (
-
-
-
- )
-}
-
-export default Create_event
diff --git a/src/pages/editor/[editor].tsx b/src/pages/editor/[editor].tsx
new file mode 100644
index 0000000..6399d9f
--- /dev/null
+++ b/src/pages/editor/[editor].tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { eventState, raceState } from '../../lib/redux/reducers';
+import { Layout } from '../../components/Common/Layout';
+import EventEditor from '../../components/EventEditor';
+import RaceEditor from '../../components/RaceEditor';
+
+const Create_event = ({ editor }: { editor: string }) => {
+ return (
+
+ {editor === 'event' && }
+ {editor === 'race' && }
+
+ );
+};
+
+export function getStaticProps({ params }: any) {
+ return {
+ props: {
+ initialReduxState: { ...eventState, ...raceState },
+ reduxReducer: ['event', 'race'],
+ editor: params.editor,
+ },
+ };
+}
+
+export async function getStaticPaths() {
+ const paths = [
+ { params: { editor: 'event' } },
+ { params: { editor: 'race' } },
+ ];
+ return { paths, fallback: false };
+}
+
+export default Create_event;
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index 9387a55..dfb21e4 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,8 +1,9 @@
-import React from 'react'
-import { Layout } from '../components/Common/Layout'
-import { Hero } from '../components/Hero'
-import { FeatureSection } from '../components/FeatureSection'
-import { Feature } from '../components/Feature'
+import React from 'react';
+import { rootState } from '../lib/redux/reducers';
+import { Layout } from '../components/Common/Layout';
+import { Hero } from '../components/Hero';
+import { FeatureSection } from '../components/FeatureSection';
+import { Feature } from '../components/Feature';
const Index = () => {
return (
@@ -17,7 +18,19 @@ const Index = () => {
- )
+ );
+};
+
+// If you build and start the app, the date returned here will have the same
+// value for all requests, as this method gets executed at build time.
+export function getStaticProps() {
+ // Note that in this case we're returning the state directly, without creating
+ // the store first (like in /pages/ssr.js), this approach can be better and easier
+ return {
+ props: {
+ initialReduxState: rootState,
+ },
+ };
}
-export default Index
+export default Index;
diff --git a/src/pages/race-editor.tsx b/src/pages/race-editor.tsx
deleted file mode 100644
index af5aa9b..0000000
--- a/src/pages/race-editor.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-import mapboxgl from 'mapbox-gl';
-import { Layout } from '../components/Common/Layout';
-import RaceEditor from '../components/RaceEditor';
-
-const Race_Editor = () => {
- return (
-
-
-
- );
-};
-
-export default Race_Editor;
diff --git a/src/types/index.ts b/src/types/index.ts
index 8a94d22..267f8b5 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,3 +1,6 @@
+import { Feature, Point } from 'geojson';
+import { RootState } from '../lib/redux/reducers';
+
export interface EventInterface {
name: string;
heroImg: HeroImg;
@@ -9,11 +12,39 @@ export interface EventInterface {
eventDetails: string;
}
+export interface Marker extends Feature {
+ properties: {
+ type: string;
+ amenities: string;
+ id: string;
+ };
+}
+
+export type RaceEditorState = RootState & { race: RaceEditorInterface };
+export interface RaceEditorInterface {
+ name: string;
+ points: Marker[];
+ routePoints: number[][];
+ activeTool: string;
+ tools: {
+ addMarker: boolean;
+ createRoute: boolean;
+ select: boolean;
+ };
+ modals: {
+ markerOptions: {
+ active: boolean;
+ position: { x: number; y: number };
+ marker: Marker | null;
+ };
+ };
+}
+
export interface HeroImg {
name: string | null;
size: number | null;
src: string | null;
- file: File | null;
+ dataURL: string | null;
error: string | null;
}
export interface UserDataInterface {
diff --git a/tailwind.config.js b/tailwind.config.js
index 7bfefc4..40719ea 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,4 +1,4 @@
-const colors = require('tailwindcss/colors')
+const colors = require('tailwindcss/colors');
module.exports = {
purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
@@ -60,7 +60,9 @@ module.exports = {
extend: {
backgroundColor: ['active'],
boxShadow: ['active'],
+ borderRadius: ['last'],
+ borderRadius: ['first'],
},
},
plugins: [require('@tailwindcss/forms')],
-}
+};