Skip to content

Instantly share code, notes, and snippets.

@GGrassiant
Created October 2, 2020 13:09
Show Gist options
  • Select an option

  • Save GGrassiant/777d47c58750c52cff73481a6bbe9035 to your computer and use it in GitHub Desktop.

Select an option

Save GGrassiant/777d47c58750c52cff73481a6bbe9035 to your computer and use it in GitHub Desktop.
Apollo Client Websocket (custom hook)

Apollo Client Websocket (Reed Barger)

// for subscriptions
import { ApolloProvider } from "react-apollo";
import { ApolloClient } from 'apollo-client';
import { WebSocketLink } from "apollo-link-ws";
import { InMemoryCache } from "apollo-cache-inmemory";
const wsLink = new WebSocketLink({
uri: process.env.NODE_ENV === 'production' ? 'wss://geopins-mtl.herokuapp.com/graphql' : 'ws://localhost:4000/graphql',
options: { reconnect: true }});
// for the Apollo Provider around the React app
const client = new ApolloClient({
link: wsLink,
cache: new InMemoryCache()
});
const Root = () => {
const initialState = useContext(Context);
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Router>
<ApolloProvider client={client}>
<Context.Provider value={ { state, dispatch } }> // see Gist on custom Global state hook
<Switch>
<ProtectedRoute exact path="/" component={App} />
<Route path="/login" component={Splash} />
</Switch>
</Context.Provider>
</ApolloProvider>
</Router>
);
};
ReactDOM.render(<Root />, document.getElementById("root"));
import React, { useState, useEffect, useContext } from "react";
import ReactMapGL, { NavigationControl, Marker, Popup } from 'react-map-gl';
import { withStyles } from "@material-ui/core/styles";
import Button from "@material-ui/core/Button";
import Typography from "@material-ui/core/Typography";
import DeleteIcon from "@material-ui/icons/DeleteTwoTone";
// for media-query
import { unstable_useMediaQuery as useMediaQuery } from "@material-ui/core/useMediaQuery";
// for subscriptions
import { Subscription } from 'react-apollo';
import { PIN_ADDED_SUBSCRIPTION, PIN_UPDATED_SUBSCRIPTION, PIN_DELETED_SUBSCRIPTION } from "../graphql/subscriptions";
// helper for time
import differenceInMinutes from "date-fns/difference_in_minutes";
// Get the custom client hook the query to populate the map with pins
import { useClient } from "../client";
import { GET_PINS_QUERY } from "../graphql/queries";
import { DELETE_PIN_MUTATION } from "../graphql/mutations";
import PinIcon from "./PinIcon";
import Blog from "./Blog";
// store and global state
import Context from "../context";
import {
CREATE_DRAFT,
UPDATE_DRAFT_LOCATION,
GET_PINS,
SET_PIN,
DELETE_PIN,
CREATE_PIN,
CREATE_COMMENT
} from '../actions/types';
// default value for the map positioning
const INITIAL_VIEWPORT = {
latitude: 45.5017,
longitude: -73.5673,
zoom: 13
};
const Map = ({ classes }) => {
// useMediaQuery for responsive
const mobileSize = useMediaQuery('(max-width: 650px )');
// custom hook to create the Apollo Server
const client = useClient();
// use global state draft for pins
const { state, dispatch } = useContext(Context);
// on Mount, get all the pins
useEffect(() => {
getPins();
}, []);
// track local state of map to move around by dragging it in the viewport
// no need for global state with useContext or useReducer for this feature
const[viewport, setViewport] = useState(INITIAL_VIEWPORT);
// same thing for the pins
const [userPosition, setUserPosition] = useState(null);
useEffect(() => {
getUserPosition()
}, []);
const getUserPosition = () => {
if ("geolocation" in navigator) { // function from the window object
navigator.geolocation.getCurrentPosition(position => {
const { latitude, longitude } = position.coords;
// spread the viewport object to keep the zoom value
// set the viewport on the current user location if they accepts the location detection
// we have the fallback INITIAL_VIEWPORT if they refuse
setViewport({ ...viewport, latitude, longitude });
// setting the value for user position as well, which initially is null
setUserPosition({ latitude, longitude });
});
}
};
const getPins = async() => {
// destructuring getPins from data
const { getPins } = await client.request(GET_PINS_QUERY);
dispatch({ type: GET_PINS, payload: getPins })
};
const [popup, setPopup] = useState(null);
// remove pop-up for all users if the pin is removed by its author
useEffect(() => {
const pinExists = popup && state.pins.findIndex(pin =>
pin._id === popup._id
) > -1;
// loop through the pin and check if the current one has an index in the array of pins
// if so compare to -1 to turn it into a boolean
if (!pinExists) {
setPopup(null);
}
}, [state.pins.length]); // run when the number of pins has changed
const handleMapClick = ({ lngLat, leftButton }) => { // props from the event object. left button is left click of the mouse
// if no click, return
if (!leftButton) return;
if (popup) return;
// if no draft yet i.e. if initial state of draft, create one with 0/0 as coordinates
if(!state.draft) {
dispatch({ type: CREATE_DRAFT })
}
const [longitude, latitude] = lngLat;
// update the draft location in the global state
dispatch({
type: UPDATE_DRAFT_LOCATION,
payload: { longitude, latitude }
})
};
const highlightNewPin = pin => {
const isNewPin = differenceInMinutes(Date.now(), Number(pin.createdAt)) <= 30;
return isNewPin ? "limegreen" : "darkblue";
};
const isAuthUser = () => state.currentUser._id === popup.author._id;
const handleDeletePin = async pin => {
const variables = { pinId: pin._id };
const { deletePin } = await client.request(DELETE_PIN_MUTATION, variables);
console.log('Pin deleted!', { deletePin });
// dispatch is moved to the subscriptions
// dispatch({ type: DELETE_PIN, payload: deletePin });
setPopup(null);
};
const handleSelectPin = pin => {
setPopup(pin);
dispatch({ type: SET_PIN, payload: pin })
};
return(
<div className={mobileSize ? classes.rootMobile : classes.root}>
<ReactMapGL
width={'100vw'}
height={'calc(100vh - 64px)'}
mapStyle={'mapbox://styles/mapbox/streets-v9'}
mapboxApiAccessToken={'MAPBOXTOKEN'}
scrollZoom={!mobileSize}
onViewportChange={newViewport => setViewport(newViewport)}
onClick={handleMapClick}
{...viewport}
>
{/*zoom controls*/}
<div className={classes.navigationControl}>
<NavigationControl
onViewportChange={newViewport => setViewport(newViewport)}
/>
</div>
{/*Current location*/}
{userPosition && (
<Marker
latitude={userPosition.latitude}
longitude={userPosition.longitude}
offsetLeft={-19}
offsetRight={-37}
>
<PinIcon
size={40}
color={'red'}
/>
</Marker>
)}
{/*draft pin*/}
{state.draft && (
<Marker
latitude={state.draft.latitude}
longitude={state.draft.longitude}
offsetLeft={-19}
offsetRight={-37}
>
<PinIcon
size={40}
color={'hotpink'}
/>
</Marker>
)}
{/*Display all pins*/}
{state.pins.map(pin =>
(<Marker
key={pin._id}
latitude={pin.latitude}
longitude={pin.longitude}
offsetLeft={-19}
offsetRight={-37}
>
<PinIcon
onClick={() => handleSelectPin(pin)}
size={40}
color={highlightNewPin(pin)}
/>
</Marker>)
)}
{/*Pop up info*/}
{popup && (
<Popup
anchor={'top'}
latitude={popup.latitude}
longitude={popup.longitude}
closeOnClick={false}
onClose={() => setPopup(null)}
>
<img
className={classes.popupImage}
src={popup.image}
alt={popup.title}
/>
<div className={classes.popupTab}>
<Typography>
{popup.latitude.toFixed(6)}, {popup.longitude.toFixed(6)}
</Typography>
{isAuthUser() && (
<Button onClick={() => handleDeletePin(popup)}>
<DeleteIcon className={classes.deleteIcon} />
</Button>
)}
</div>
</Popup>
)}
</ReactMapGL>
{/*Subscription for creating/updating/deleting pins*/}
<Subscription
subscription={PIN_ADDED_SUBSCRIPTION}
onSubscriptionData={({ subscriptionData }) => {
const { pinAdded } = subscriptionData.data;
dispatch({ type: CREATE_PIN, payload: pinAdded })
}}
/>
<Subscription
subscription={PIN_UPDATED_SUBSCRIPTION}
onSubscriptionData={({ subscriptionData }) => {
const { pinUpdated } = subscriptionData.data;
dispatch({ type: CREATE_COMMENT, payload: pinUpdated })
}}
/>
<Subscription
subscription={PIN_DELETED_SUBSCRIPTION}
onSubscriptionData={({ subscriptionData }) => {
const { pinDeleted } = subscriptionData.data;
dispatch({ type: DELETE_PIN, payload: pinDeleted })
}}
/>
{/*Blog*/}
<Blog popup={popup}/>
</div>
);
};
const styles = {
root: {
display: "flex"
},
rootMobile: {
display: "flex",
flexDirection: "column-reverse"
},
navigationControl: {
position: "absolute",
top: 0,
left: 0,
margin: "1em"
},
deleteIcon: {
color: "red"
},
popupImage: {
padding: "0.4em",
height: 200,
width: 200,
objectFit: "cover"
},
popupTab: {
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column"
}
};
export default withStyles(styles)(Map);
// Libs
import { useState, useEffect } from "react";
import { GraphQLClient } from "graphql-request";
export const BASE_URL =
process.env.NODE_ENV === "production"
? "https://geopins-mtl.herokuapp.com/graphql"
: "http://localhost:4000/graphql";
export const useClient = () => {
const [idToken, setIdToken] = useState("");
useEffect(() => {
const token = window.gapi.auth2
.getAuthInstance()
.currentUser.get()
.getAuthResponse().id_token;
setIdToken(token);
}, []);
return new GraphQLClient(BASE_URL, {
headers: { authorization: idToken }
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment