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}
-
- {name} + {src || + (dataURL && name && ( +
+
{image.name}
+
+ {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/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

+
+
+
Start / Finish
+
+
+
+
Aid Station Level 1
+
+
+
+
Aid Station Level 2
+
+
+
+
Restroom
+
+
+ ); +}; 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 ( +
+
+

Marker Options

+
+ + +
+
+ ); +}; + +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(); + // const [map, setMap] = useState(); const [clickEvent, setClickEvent] = useState(); + const [points, setPoints] = useState([]); const [routePoints, setRoutePoints] = useState([]); const [addMarker, setAddMarker] = useState(false); const [createRoute, setCreateRoute] = useState(false); useEffect(() => { - if (map || !mapContainer.current) return; // initialize map only once - const newMap = initMap({ + if (!mapContainer.current) return; // initialize map only once + initMap({ center: [-94.115251, 36.184605], container: mapContainer.current, zoom: 14, canvasClickHandler: setClickEvent, }); - setMap(newMap); }, []); useEffect(() => { - if (!map) return; - const pointsSrc = map.getSource('points') as GeoJSONSource; - pointsSrc.setData({ type: 'FeatureCollection', features: points }); + setPoints(points); }, [points]); useEffect(() => { - console.log('CLICK'); - console.log('ADD MARKER: ', addMarker); - console.log('MAP: ', map); if (!map || !clickEvent) return; if (addMarker) { const feature = eventToFeature(clickEvent); - setPoints([...points, feature]); + 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); - console.log(startFeature); const startSrc = map.getSource('points') as GeoJSONSource; startSrc.setData({ type: 'FeatureCollection', features: [startFeature], }); - setRoutePoints(newPoints); } else { const last = newPoints.length - 1; const addNextFeature = async () => { diff --git a/src/components/RaceEditor/index.tsx b/src/components/RaceEditor/index.tsx index a25fffd..debd5b8 100644 --- a/src/components/RaceEditor/index.tsx +++ b/src/components/RaceEditor/index.tsx @@ -1,43 +1,31 @@ -import React, { useEffect, useReducer, useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { gpxToGeoJSON } from '../../lib/utils'; -import { reducer, init } from './reducer'; import { ToolBar } from './ToolBar'; import { DialogModal } from '../Common/DialogModal'; -import { MapboxMap } from '../Common/Map'; -import { EventInterface } from '../../types'; +import { MapEditor } from './MapEditor/'; +import { RaceEditorInterface } from '../../types'; import { FileInput } from './FileInput'; +import { RootState } from '../../lib/redux/reducers'; +import { actions } from '../../lib/redux/reducers/raceEditor'; + +type State = RootState & { race: RaceEditorInterface }; const RaceEditor = () => { - const [eventState, dispatch] = useReducer(reducer, {}, init); + const dispatch = useDispatch(); + const state = useSelector((state: State) => state.race); const [discardWarning, setDiscardWarning] = useState(false); - useEffect(() => { - const initFromLocalStorage = () => { - const data = localStorage.getItem('racesState'); - if (data) { - const localState: EventInterface = JSON.parse(data); - const initState = { - ...localState, - }; - dispatch({ type: 'init', payload: initState }); - } - }; - initFromLocalStorage(); - window.addEventListener('storage', initFromLocalStorage, true); - return () => { - setDiscardWarning(false); - window.removeEventListener('storage', initFromLocalStorage, true); - }; - }, []); + useEffect(() => {}, []); - useEffect(() => { - localStorage.setItem('racesState', JSON.stringify(eventState)); - }, [eventState]); + // useEffect(() => { + // localStorage.setItem('raceState', JSON.stringify(eventState)); + // }, [eventState]); const handleDiscard = (confirm?: boolean) => { if (confirm === true) { - localStorage.removeItem('racesState'); - dispatch({ type: 'init' }); + localStorage.removeItem('raceState'); + dispatch(actions.init()); setDiscardWarning(false); } else { setDiscardWarning(!discardWarning); @@ -69,7 +57,7 @@ const RaceEditor = () => { handleDiscard()} />
- +
); 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 = `
      ${amenitiesList}
    `; + } + + 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}

      ${typesList}
    `; - - // // 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')], -} +};