redux toolkit How to change the price when the variability of the product changes - react-redux

There is a json file
{
"items": [
{ "id": "0", "imageUrl": "https://dodopizza.azureedge.net/static/Img/Products/f035c7f46c0844069722f2bb3ee9f113_584x584.jpeg", "title": "Пепперони Фреш с перцем", "types": [0, 1], "sizes": [26, 30, 40], "price": 803, "category": 0, "rating": 4 },
]
}
Pizza is loaded from the file, and all data is output to react
http://joxi.ru/krDaNEVSGRlpJm
Tell me please, how in redux toolkit to make the price of the product increase depending on the selected parameter sizes. That is, if the value 26 is selected, then you need to increase the amount by 100 rubles, if the size is 30, then by 200 rubles.
I tried to do it with various crutches, but I don't have enough knowledge
Here is the code where I get the pizzas
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
pizzas: [],
itemsCount: 0,
isLoading: true,
};
export const getItemsSlice = createSlice({
name: "items",
initialState,
reducers: {
setItems(state, action) {
state.pizzas = action.payload;
},
setItemsCount(state, action) {
state.itemsCount = action.payload;
},
setIsLoading(state, action) {
state.isLoading = action.payload;
},
},
});
export const { setItems, setItemsCount, setIsLoading } = getItemsSlice.actions;
export default getItemsSlice.reducer;
React output code
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { addPizzaInCart } from "../../../redux/slices/CartSlice";
import { typeName } from "../../../redux/slices/GetItemsSlice";
import styles from "./PizzaBlock.module.scss";
function PizzaBlock({ id, imageUrl, title, price, types, sizes }) {
const [activeType, setActiveType] = React.useState(0);
const [activeSize, setActiveSize] = React.useState(0);
const dispatch = useDispatch();
const itemInCart = useSelector((state) => state.cart.pizzasInCart.find((obj) => obj.id === id && obj.type === typeName[activeType] && obj.size === sizes[activeSize]));
const addedCount = itemInCart ? itemInCart.count : 0;
const pizza = {
id,
imageUrl,
title,
price,
type: typeName[activeType],
size: sizes[activeSize],
};
const onClickAddPizza = () => {
dispatch(addPizzaInCart(pizza));
};
const onChangeSize = (i) => {
setActiveSize(i);
};
return (
<div className={styles.item}>
<img className={styles.item__image} src={imageUrl} alt="Pizza" />
<Link to={`/product/${pizza.id}`} className={styles.item__title}>
{title}
</Link>
<div className={styles.item__selector}>
<ul>
{types.map((type, i) => (
<li className={activeType === i ? styles.active : ""} onClick={() => setActiveType(type)} key={i}>
{typeName[type]}
</li>
))}
</ul>
<ul>
{sizes.map((size, i) => (
<li className={activeSize === i ? styles.active : ""} onClick={() => onChangeSize(i)} key={i}>
{size} см.
</li>
))}
</ul>
</div>
<div className={styles.item__bottom}>
<div className={styles.item__price}>от {Math.trunc(pizza.price * (pizza.size / 100 + 1))} ₽</div>
<div className={styles.item__buttons}>
<button className={`${styles.button} ${styles.button_outline} ${styles.button_add}`} onClick={onClickAddPizza}>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.8 4.8H7.2V1.2C7.2 0.5373 6.6627 0 6 0C5.3373 0 4.8 0.5373 4.8 1.2V4.8H1.2C0.5373 4.8 0 5.3373 0 6C0 6.6627 0.5373 7.2 1.2 7.2H4.8V10.8C4.8 11.4627 5.3373 12 6 12C6.6627 12 7.2 11.4627 7.2 10.8V7.2H10.8C11.4627 7.2 12 6.6627 12 6C12 5.3373 11.4627 4.8 10.8 4.8Z" fill="white" />
</svg>
<span>Добавить</span>
{addedCount > 0 && <i>{addedCount}</i>}
</button>
</div>
</div>
</div>
);
}
export default PizzaBlock;
There are items with sizes 26, 30 and 40. When I change the active size, that is, I press the button 26, 30 or 40, I need the price for pizza to automatically increase by 0, 100 and 200 rubles, depending on the size of the pizza. I tried using state to pass size parameters and already substitute the required amount in redux, but in this case problems appeared, the value of this state was applied to all pizzas at once, but only to the current one
Here Cart logic
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
totalPricePizzasInCart: 0,
totalCountPizzasInCart: 0,
pizzasInCart: [],
};
export const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
addPizzaInCart(state, action) {
const findItem = state.pizzasInCart.find((obj) => {
return obj.id === action.payload.id && obj.type === action.payload.type && obj.size === action.payload.size;
});
if (findItem) {
findItem.count++;
} else {
state.pizzasInCart.push({
...action.payload,
count: 1,
});
}
state.totalPricePizzasInCart = state.pizzasInCart.reduce((sum, obj) => {
return Math.trunc(obj.price * (obj.size / 100 + 1)) * obj.count + sum;
}, 0);
state.totalCountPizzasInCart = state.pizzasInCart.reduce((count, obj) => {
return obj.count + count;
}, 0);
},
minusPizzaInCart(state, action) {
const findItem = state.pizzasInCart.find((obj) => {
return obj.id === action.payload.id && obj.type === action.payload.type && obj.size === action.payload.size;
});
if (findItem && findItem.count > 0) {
findItem.count--;
state.totalPricePizzasInCart = state.totalPricePizzasInCart - Math.trunc(findItem.price * (findItem.size / 100 + 1));
}
state.totalCountPizzasInCart = state.pizzasInCart.reduce((count, obj) => {
return obj.count + count;
}, 0);
},
removePizzaInCart(state, action) {
state.pizzasInCart = state.pizzasInCart.filter((obj) => {
return obj.id !== action.payload.id || obj.type !== action.payload.type || obj.size !== action.payload.size;
});
state.totalPricePizzasInCart = state.pizzasInCart.reduce((sum, obj) => {
return Math.trunc(obj.price * (obj.size / 100 + 1)) * obj.count + sum;
}, 0);
state.totalCountPizzasInCart = state.pizzasInCart.reduce((count, obj) => {
return obj.count + count;
}, 0);
},
clearPizzasInCart(state) {
state.pizzasInCart = [];
state.totalPricePizzasInCart = 0;
state.totalCountPizzasInCart = 0;
},
},
});
export const { addPizzaInCart, removePizzaInCart, minusPizzaInCart, clearPizzasInCart } = cartSlice.actions;
export default cartSlice.reducer;
github github.com/antonboec1994/reactPizza.git

Store :
import { configureStore, createSlice } from "#reduxjs/toolkit";
const initialState = {
basePrice: 100,
total: 0,
itemsCount: 0
};
export const getItemsSlice = createSlice({
name: "items",
initialState,
reducers: {
addToCart(state, action) {
console.log(action.payload);
state.itemsCount = action.payload;
state.total = state.itemsCount * state.basePrice;
}
}
});
export const { addToCart } = getItemsSlice.actions;
export const store = configureStore({
reducer: {
pizza: getItemsSlice.reducer
}
});
export default getItemsSlice.reducer;
Component :
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { addToCart } from "../store/__counterStore__";
const PizzaComponent = () => {
const dispatch = useDispatch();
const storeData = useSelector((state) => state.pizza);
console.log(storeData);
const [itemCount, setItemCount] = React.useState(0);
const handleAdd = () => {
setItemCount(itemCount + 1);
dispatch(addToCart(itemCount));
};
return (
<div>
<button onClick={handleAdd}>Add 1 Pitzzza</button>
<h2>Cart Price</h2>
<div>{storeData.total}</div>
</div>
);
};
export default PizzaComponent;
I just updated price for one type of pitza, what you can do is you can updated price according to type of different pitzas, by setting dispatch different kind of payload of you can put locgic in your reducer.
I tried to make it simple, since purpose is to understand the folw and logic.
EDIT:
CODESANDBOX

Related

How to use React useContext with leaflet routing machine and react leaflet?

I'm trying to use a useContext hook inside a react-leaflet controlComponent but I have an error when my context fires the update function.
I use a react-leaflet controlComponent because of leaflet routing machine. I think the code + the error are better than word:
MainBoard.tsx
export const CartographyContext: React.Context<CartographyContextType> = React.createContext<CartographyContextType>({ positions: [] });
...
const routeSummaryValueContext = React.useMemo(
() => ({ routeSummary, setRouteSummary }),
[routeSummary]
);
const elevationProfileValueContext = React.useMemo(
() => ({ elevationProfile, setElevationProfile }),
[elevationProfile]
);
........
<CartographyContext.Provider value={{ positions, elevationProfileValueContext, routeSummaryValueContext, positionsValueContext, addPosition, changePosition }}>
.........
<RoutingMachine
orsOptions={{
....
}} />
..........
</CartographyContext.Provider>
RoutingMachine.tsx:
const CreateRoutineMachineLayer = (props: any) => {
const geoService = new GeoLocalisationService();
const cartographyContext: CartographyContextType = React.useContext<CartographyContextType>(CartographyContext);
const [routes, setRoutes] = React.useState<any[]>();
React.useEffect(() => {
if (routes) {
//The line which cause the error
cartographyContext.elevationProfileValueContext.setElevationProfile(geoService.getElevationProfile(decodePolyline(routes[0].geometry, true)));
const summary: RouteSummary = {
ascent: routes[0].routeSummary.ascent,
descent: routes[0].routeSummary.descent,
distance: routes[0].routeSummary.distance,
estimatedDuration: routes[0].routeSummary.duration
}
cartographyContext.routeSummaryValueContext.setRouteSummary(summary);
}
}, [routes]);
const { orsOptions } = props;
const instance = L.Routing.control({
router: new OpenRouteRouter(orsOptions),
lineOptions: {
styles: [{ color: "#3933ff", weight: 4 }],
extendToWaypoints: true,
missingRouteTolerance: 0
},
routeWhileDragging: true,
autoRoute: true,
geocoder: new geocoder.Geocoder(),
}).on('routesfound', (e) => {
setRoutes(e.routes);
});
useMapEvents({
click: (e: L.LeafletMouseEvent) => {
if (instance.getWaypoints().length === 2 && instance.getWaypoints()[0].latLng == null) {
instance.spliceWaypoints(0, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else if (instance.getWaypoints().length === 2 && instance.getWaypoints()[1].latLng == null) {
instance.spliceWaypoints(1, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else {
instance.spliceWaypoints(instance.getWaypoints().length, 0, new L.Routing.Waypoint(e.latlng, null, {}));
}
}
});
return instance;
};
const RoutingMachine = createControlComponent(CreateRoutineMachineLayer);
error :
g: React has detected a change in the order of Hooks called by ForwardRef(LeafComponent). This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
Previous render Next render
------------------------------------------------------
1. useContext useContext
2. useRef useRef
3. useContext useRef
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
..............
Uncaught Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
I clearly doing something wrong here but I haven't found yet.
Thank you
Kind regards
Ok I found the good implementation :
const RoutingMachine: React.FC<RoutingMachineProps> = (props) => {
//const RoutineMachine = (props: any) => {
const geoService = new GeoLocalisationService();
const cartographyContext: CartographyContextType = React.useContext<CartographyContextType>(CartographyContext);
const [instance, setInstance] = React.useState<any>();
const [alreadyDisplayed, setAlreadyDisplayed] = React.useState(false);
const { orsOptions } = props;
const map = useMap();
//const instance = L.Routing.control({
React.useEffect(() => {
const instance = L.Routing.control({
router: new OpenRouteRouter(orsOptions),
lineOptions: {
styles: [{ color: "#3933ff", weight: 4 }],
extendToWaypoints: true,
missingRouteTolerance: 0
},
routeWhileDragging: true,
autoRoute: true,
geocoder: (L.Control as any).Geocoder.google({
apiKey: GOOGLE.googleMapApiKey,
}),
}).on('routesfound', (e) => {
const routes = e.routes;
cartographyContext.setElevationProfile(geoService.getElevationProfile(decodePolyline(routes[0].geometry, true)));
const summary: RouteSummary = {
ascent: routes[0].routeSummary.ascent,
descent: routes[0].routeSummary.descent,
distance: routes[0].routeSummary.distance,
estimatedDuration: routes[0].routeSummary.duration
}
cartographyContext.setRouteSummary(summary);
})
setInstance(instance);
instance.addTo(map);
}, []);
useMapEvents({
click: (e: L.LeafletMouseEvent) => {
if (instance) {
if (instance.getWaypoints().length === 2 && instance.getWaypoints()[0].latLng == null) {
instance.spliceWaypoints(0, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else if (instance.getWaypoints().length === 2 && instance.getWaypoints()[1].latLng == null) {
instance.spliceWaypoints(1, 1, new L.Routing.Waypoint(e.latlng, null, {}));
} else {
instance.spliceWaypoints(instance.getWaypoints().length, 0, new L.Routing.Waypoint(e.latlng, null, {}));
}
}
}
});
return null;
};
export default RoutingMachine;

Delete session.storage on changing restaurant page

I'm using Vuejs 2 and vue router to create this website.
It has a cart saved in session.storage.
I would like to delete the cart (session.storage) when the restaurant page change.
I've tried to save the slug of the restaurant and compare it to the current slug:
setSlug() {
sessionStorage.setItem("slug", this.$route.params.slug);
},
checkSlug() {
if (sessionStorage.getItem("slug") != this.$route.params.slug) {
sessionStorage.clear();
}
},
But it doesn't work.
How can I achieve this?
Thank you.
TheRestaurant.vue
export default {
name: "TheRestaurant",
data() {
return {
restaurant: {},
cart: {},
quantity: 1,
partialTotal: 0,
total: 0,
};
},
methods: {
//get the restaurant and the dishes with axios call and set the data
getRestaurant() {
axios
.get("/api/restaurants/" + this.$route.params.slug)
.then((response) => {
this.restaurant = response.data;
})
.catch((error) => {
console.log(error);
});
},
showDetails(id) {
let modal = document.getElementById("modal-" + id);
modal.classList.replace("d-none", "d-flex");
},
hideDetails(id) {
let modal = document.getElementById("modal-" + id);
modal.classList.replace("d-flex", "d-none");
},
addToCart(dish) {
if (sessionStorage.getItem("cart") == null) {
sessionStorage.setItem("cart", JSON.stringify([]));
}
let cart = JSON.parse(sessionStorage.getItem("cart"));
let index = cart.findIndex((item) => item.id == dish.id);
if (index == -1) {
dish.quantity = 1;
cart.push(dish);
} else {
cart[index].quantity++;
}
sessionStorage.setItem("cart", JSON.stringify(cart));
this.cart = JSON.parse(sessionStorage.getItem("cart"));
this.partialTotal = round(
this.cart.reduce(
(acc, dish) => acc + dish.price * dish.quantity,
0
),
2
);
sessionStorage.setItem(
"partialTotal",
JSON.stringify(this.partialTotal)
);
this.total = this.partialTotal + this.restaurant.delivery_price;
sessionStorage.setItem("total", JSON.stringify(this.total));
},
removeOneFromCart(dish) {
let cart = JSON.parse(sessionStorage.getItem("cart"));
let index = cart.findIndex((item) => item.id == dish.id);
if (index !== -1) {
cart[index].quantity--;
if (cart[index].quantity == 0) {
cart.splice(index, 1);
}
}
sessionStorage.setItem("cart", JSON.stringify(cart));
this.cart = JSON.parse(sessionStorage.getItem("cart"));
this.partialTotal = round(
this.cart.reduce(
(acc, dish) => acc + dish.price * dish.quantity,
0
),
2
);
sessionStorage.setItem(
"partialTotal",
JSON.stringify(this.partialTotal)
);
this.total = this.partialTotal + this.restaurant.delivery_price;
sessionStorage.setItem("total", JSON.stringify(this.total));
},
removeAllFromCart(dish) {
let cart = JSON.parse(sessionStorage.getItem("cart"));
let index = cart.findIndex((item) => item.id == dish.id);
if (index !== -1) {
cart.splice(index, 1);
}
sessionStorage.setItem("cart", JSON.stringify(cart));
this.cart = JSON.parse(sessionStorage.getItem("cart"));
this.partialTotal = round(
this.cart.reduce(
(acc, dish) => acc + dish.price * dish.quantity,
0
),
2
);
sessionStorage.setItem(
"partialTotal",
JSON.stringify(this.partialTotal)
);
this.total = this.partialTotal + this.restaurant.delivery_price;
sessionStorage.setItem("total", JSON.stringify(this.total));
},
},
mounted() {
this.getRestaurant();
this.cart = JSON.parse(sessionStorage.getItem("cart"));
this.partialTotal = JSON.parse(sessionStorage.getItem("partialTotal"));
this.total = JSON.parse(sessionStorage.getItem("total"));
},
};
router.js
import Vue from "vue";
import VueRouter from "vue-router";
import Restaurant from "./pages/TheRestaurant.vue";
import Home from "./pages/TheMain.vue";
import Cart from "./pages/TheCart.vue";
import Search from "./pages/AdvancedSearch.vue";
//put all the different pages below
Vue.use(VueRouter);
/**
* #type {import("vue-router").RouteConfig[]}
*/
const routes = [
{
path: "/",
component: Home,
name: "home.index",
meta: {
title: "Deliveboo Homepage",
},
},
{
path: "/cart",
component: Cart,
name: "cart.index",
meta: {
title: "Deliveboo Cart",
},
},
{
path: "/search",
component: Search,
name: "search.index",
meta: {
title: "Deliveboo Search Restaurants",
},
},
{
path: "/:slug",
component: Restaurant,
name: "restaurant.index",
meta: {
title: "Deliveboo Restaurant",
},
},
];
const router = new VueRouter({
//it must contain an array of routes
routes,
mode: "history",
});
export default router;
If I understood your hierarchy and logic of components correctly,
in the mounted hook of Home and Search components you can reset your session.storage.

Implement force-directed graph in next js

I'm trying to create a force-directed graph for mapping the interactions between courses in an institution. Using Next JS + TypeScript for my frontend.
Have tried several attempts at charting this out using react-flow, dagre, vis-network but am getting either a window : undefined error or just the damn alignment of nodes not being force-directed inside the box I have defined.
Before I move on with implementing d3-force right out of the box, can someone please recommend any alternative solution to this ?
Here's what my nodes & edges look like :
Here's my attempt with reactflow & dagre :
import React, { useCallback, useEffect, useState } from 'react';
import ReactFlow, {
addEdge,
useNodesState,
useEdgesState,
Edge,
Node,
Position,
ConnectionLineType,
ReactFlowProvider,
MiniMap,
Controls,
Background,
} from 'react-flow-renderer';
import dagre from 'dagre';
import { NodeData, useCourseNodes } from 'src/hooks/useCourseNodes';
import { useDepartment } from '#contexts/ActiveDepartmentContext';
import {
useUpdateActiveCourse,
} from '#contexts/ActiveCourseContext';
import { useDrawerOpen, useUpdateDrawerOpen } from '#contexts/DrawerContext';
const dagreGraph = new dagre.graphlib.Graph({directed:true});
dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeWidth = 10.2;
const nodeHeight = 6.6;
const getLayoutedElements = (
nodes: Node[],
edges:Edge[],
) => {
// const isHorizontal = direction === 'LR';
dagreGraph.setGraph( {width:900, height:900, nodesep:20, ranker:'longest-path' });
nodes.forEach((node: Node) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
edges.forEach((edge: Edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
nodes.forEach((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
// node.targetPosition = isHorizontal ? Position.Left : Position.Top;
// node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
node.targetPosition = Position.Top;
node.sourcePosition = Position.Bottom;
// We are shifting the dagre node position (anchor=center center) to the top left
// so it matches the React Flow node anchor point (top left).
node.position = {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2,
};
console.log(nodeWithPosition)
return node;
})
return { layoutedNodes:nodes, layoutedEdges:edges };
};
const LayoutFlow = () => {
const activeDept = useDepartment();
const setActiveCourse = useUpdateActiveCourse();
const setDrawerOpen = useUpdateDrawerOpen()
const drawerOpen = useDrawerOpen();
const {courseList, edgeList} = useCourseNodes()
const { layoutedNodes, layoutedEdges } = getLayoutedElements(courseList, edgeList)
const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes);
const [edges, setEdges,onEdgesChange] = useEdgesState(layoutedEdges);
console.log(layoutedNodes)
const onConnect = useCallback(
(params) =>
setEdges((eds) =>
addEdge({ ...params, type: ConnectionLineType.SimpleBezier, animated: true }, eds),
),
[],
);
// ? For switching between layouts (horizontal & vertical) for phone & desktop
// const onLayout = useCallback(
// (direction) => {
// const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
// nodes,
// edges,
// direction
// );
// setNodes([...layoutedNodes]);
// setEdges([...layoutedEdges]);
// },
// [nodes, edges]
// );
// ? M1 - for force re-rendering react flow graph on state change - https://github.com/wbkd/react-flow/issues/1168
// ? M2 - (Applied currently in useEffect block below)for force re-rendering react flow graph on state change - https://github.com/wbkd/react-flow/issues/1168
useEffect(() => {
const {layoutedNodes, layoutedEdges} = getLayoutedElements(courseList, edgeList)
setNodes([...layoutedNodes]);
setEdges([...layoutedEdges]);
}, [activeDept, drawerOpen]);
return (
<div style={{ width: '100%', height: '100%' }} className="layoutflow">
<ReactFlowProvider>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={(e: React.MouseEvent, node: Node<NodeData>) => {
e.preventDefault();
// created a copy of the node since we're only deleting the "label" property from the node object to conveniently map the rest of the data to the "data" property of the active course
const nodeCopy = JSON.parse(JSON.stringify(node))
const { data } = nodeCopy;
const { label } = data
delete data.label
setActiveCourse({
courseId: label,
data
});
setDrawerOpen(true);
}}
connectionLineType={ConnectionLineType.SimpleBezier}
fitView
>
<MiniMap />
<Controls />
{/* <Background /> */}
</ReactFlow>
</ReactFlowProvider>
<div className="controls">
{/* <button onClick={() => onLayout('TB')}>vertical layout</button>
<button onClick={() => onLayout('LR')}>horizontal layout</button> */}
</div>
</div>
);
};
export default LayoutFlow;
Here's my attempt with vis-network : (note : I did slightly modify edges to have from-to instead of source-target when working with this)
import { useCourseNodes } from "#hooks/useCourseNodes";
import React, { useEffect, useRef } from "react";
import { Network } from "vis-network";
const GraphLayoutFour: React.FC = () => {
const {courseList:nodes, edgeList:edges} = useCourseNodes()
// Create a ref to provide DOM access
const visJsRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const network =
visJsRef.current &&
new Network(visJsRef.current, { nodes, edges } );
// Use `network` here to configure events, etc
}, [visJsRef, nodes, edges]);
return typeof window !== "undefined" ? <div ref={visJsRef} /> : <p>NOT AVAILABLE</p>;
};
export default GraphLayoutFour;
Here's my attempt with react-sigma
import React, { ReactNode, useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { UndirectedGraph } from "graphology";
import erdosRenyi from "graphology-generators/random/erdos-renyi";
import randomLayout from "graphology-layout/random";
import chroma from "chroma-js";
import { Attributes } from "graphology-types";
import { ControlsContainer, ForceAtlasControl, SearchControl, SigmaContainer, useLoadGraph, useRegisterEvents, useSetSettings, useSigma, ZoomControl } from "react-sigma-v2/lib/esm";
interface MyCustomGraphProps {
children?: ReactNode;
}
export const MyCustomGraph: React.FC<MyCustomGraphProps> = ({ children }) => {
const sigma = useSigma();
const registerEvents = useRegisterEvents();
const loadGraph = useLoadGraph();
const setSettings = useSetSettings();
const [hoveredNode, setHoveredNode] = useState<any>(null);
useEffect(() => {
// Create the graph
const graph = erdosRenyi(UndirectedGraph, { order: 100, probability: 0.2 });
randomLayout.assign(graph);
graph.nodes().forEach(node => {
graph.mergeNodeAttributes(node, {
label: "label",
size: Math.max(4, Math.random() * 10),
color: chroma.random().hex(),
});
});
loadGraph(graph);
// Register the events
registerEvents({
enterNode: event => setHoveredNode(event.node),
leaveNode: () => setHoveredNode(null),
});
}, []);
useEffect(() => {
setSettings({
nodeReducer: (node, data) => {
const graph = sigma.getGraph();
const newData: Attributes = { ...data, highlighted: data.highlighted || false };
if (hoveredNode) {
//TODO : add type safety
if (node === hoveredNode || (graph as any).neighbors(hoveredNode).includes(node)) {
newData.highlighted = true;
} else {
newData.color = "#E2E2E2";
newData.highlighted = false;
}
}
return newData;
},
edgeReducer: (edge, data) => {
const graph = sigma.getGraph();
const newData = { ...data, hidden: false };
//TODO : add type safety
if (hoveredNode && !(graph as any).extremities(edge).includes(hoveredNode)) {
newData.hidden = true;
}
return newData;
},
});
}, [hoveredNode]);
return <>{children}</>;
};
ReactDOM.render(
<React.StrictMode>
<SigmaContainer>
<MyCustomGraph />
<ControlsContainer position={"bottom-right"}>
<ZoomControl />
<ForceAtlasControl autoRunFor={2000} />
</ControlsContainer>
<ControlsContainer position={"top-right"}>
<SearchControl />
</ControlsContainer>
</SigmaContainer>
</React.StrictMode>,
document.getElementById("root"),
);
import { useCourseNodes } from '#hooks/useCourseNodes'
import dynamic from 'next/dynamic';
import React from 'react'
import { useSigma } from 'react-sigma-v2/lib/esm';
const GraphLayoutThree = () => {
const isBrowser = () => typeof window !== "undefined"
const { courseList, edgeList } = useCourseNodes()
const sigma = useSigma();
if(isBrowser) {
const SigmaContainer = dynamic(import("react-sigma-v2").then(mod => mod.SigmaContainer), {ssr: false});
const MyGraph = dynamic(import("./CustomGraph").then(mod => mod.MyCustomGraph), {ssr: false});
return (
<SigmaContainer style={{ height: "500px", width: "500px" }} >
<MyGraph/>
</SigmaContainer>
)
}
else return (<p>NOT AVAILABLE</p>)
}
export default GraphLayoutThree
Here's my attempt with react-force-graph (note : I did slightly modify edges to have from-to instead of source-target when working with this)
import dynamic from "next/dynamic";
const GraphLayoutTwo = () => {
const isBrowser = () => typeof window !== "undefined"
if(isBrowser) {
const MyGraph = dynamic(import("./CustomGraphTwo").then(mod => mod.default), {ssr: false});
return (
<MyGraph/>
)
}
else return (<p>NOT AVAILABLE</p>)
}
export default GraphLayoutTwo
import dynamic from "next/dynamic";
const GraphLayoutTwo = () => {
const isBrowser = () => typeof window !== "undefined"
if(isBrowser) {
const MyGraph = dynamic(import("./CustomGraphTwo").then(mod => mod.default), {ssr: false});
return (
<MyGraph/>
)
}
else return (<p>NOT AVAILABLE</p>)
}
export default GraphLayoutTwo
To implement something similar we use react-graph-vis inside a nextjs application.
If you have the window is not defined error, just wrap the component and import it with dynamic
// components/graph.tsx
export const Graph = ({data, options, events, ...props}) => {
return (
<GraphVis
graph={transformData(data)}
options={options}
events={events}
/>
)
}
then in your page
// pages/index.ts
const Graph = dynamic(() => (import("../components/graph").then(cmp => cmp.Graph)), { ssr: false })
const Index = () => {
return (
<>
<Graph data={...} .... />
</>
)
}
export default Index;

How Maximum days restriction in DateRangePicker for the React?

How Maximum days restriction in DateRangePicker for the React can be achieved?
I have tried to solve the requirement as below. Sharing if any finds this useful to use,
This restricts selection before and after from the range startDate by a maxDays.
import React, {useState, useEffect} from "react"
import {makeStyles} from "#material-ui/core/styles";
import { DateRangePicker } from 'react-date-range';
import 'react-date-range/dist/styles.css'; // main style file
import 'react-date-range/dist/theme/default.css';
import { addDays, subDays } from "date-fns"; // theme css file
const useStyles = makeStyles(theme => ({
root:{
// padding: 16
}
}))
const selectionRange = {
startDate: new Date(),
endDate: new Date(),
key: 'dateRange',
}
const DateRange = (props) => {
const classes = useStyles();
const {
dateRange,
onSelectPeriod,
hides,
minDate,
maxDate,
maxDays
} = props
const [period, setPeriod] = useState(selectionRange)
const [miDate, setMiDate] = useState(new Date(1970, 1, 1))
const [maDate, setMaDate] = useState(new Date(2100, 1, 1))
useEffect(()=>{
if(dateRange) {
setPeriod(dateRange)
}
if(hides)
hides.map((num, index) => {
(document.getElementsByClassName('rdrStaticRanges')[0]).childNodes[num-index].remove();
})
if(minDate != null) setMiDate(minDate)
if(maxDate != null) setMaDate(maxDate)
},[])
useEffect(()=>{
onSelectPeriod(period)
},[period])
const handleSelect = (ranges) => {
if(ranges === undefined) return
// console.log("DateRangePicker: ", ranges)
setPeriod(ranges.dateRange)
// set to restrict only maxDays range selection; post selection it reset to as initial for further selection
if(maxDays != null) {
if(ranges.dateRange.startDate.getTime() === ranges.dateRange.endDate.getTime()){
// REstrict maxDays before or after for selection
setMiDate(subDays(ranges.dateRange.startDate, maxDays-1))
const mDate = addDays(ranges.dateRange.startDate, maxDays-1)
if(mDate.getTime() <= maxDate.getTime()) setMaDate(mDate)
} else {
// RESET as INITIAL
if(minDate != null) {
setMiDate(minDate)
} else {
setMiDate(new Date(1970, 1, 1))
}
if(maxDate != null) {
setMaDate(maxDate)
} else {
setMaDate(new Date(2100, 1, 1))
}
}
}
}
return (
<div className={classes.root}>
<DateRangePicker
ranges={[period]}
onChange={handleSelect}
minDate={miDate}
maxDate={maDate}
/>
</div>
)
}
export default DateRange

Can I use useSWR with apollo-client?

I'm very new to this all next.js graphQL world.
I just found useSWR and I was wondering if I can use this with Apollo-client,
not with the graphql-request.
Yes, you can, and I do. I have two headless CMSs coexisting. One is headless wordpress bundled with google through OneGraph; the other is Headless Booksy, a closed source CMS with no publicly accessible endpoints -- dissected the network tab during user-driven events to determine any required headers/params and ultimately reverse engineered their auth flow to automate it in my repo using asynch partitioning.
That said, yes, I use apollo Client and SWR in tandem. Here is the _app.tsx config
_app.tsx
import '#/styles/index.css';
import '#/styles/chrome-bug.css';
import 'keen-slider/keen-slider.min.css';
import { AppProps, NextWebVitalsMetric } from 'next/app';
import { useRouter } from 'next/router';
import { ApolloProvider } from '#apollo/client';
import { useEffect, FC } from 'react';
import { useApollo } from '#/lib/apollo';
import * as gtag from '#/lib/analytics';
import { MediaContextProvider } from '#/lib/artsy-fresnel';
import { Head } from '#/components/Head';
import { GTagPageview } from '#/types/analytics';
import { ManagedGlobalContext } from '#/components/Context';
import { SWRConfig } from 'swr';
import { Provider as NextAuthProvider } from 'next-auth/client';
import fetch from 'isomorphic-unfetch';
import { fetcher, fetcherGallery } from '#/lib/swr-fetcher';
import { Configuration, Fetcher } from 'swr/dist/types';
type T = typeof fetcher | typeof fetcherGallery;
interface Combined extends Fetcher<T> {}
const Noop: FC = ({ children }) => <>{children}</>;
export default function NextApp({
Component,
pageProps
}: AppProps) {
const apolloClient = useApollo(pageProps);
const LayoutNoop = (Component as any).LayoutNoop || Noop;
const router = useRouter();
useEffect(() => {
document.body.classList?.remove('loading');
}, []);
useEffect(() => {
const handleRouteChange = (url: GTagPageview) => {
gtag.pageview(url);
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router.events]);
return (
<>
<SWRConfig
value={{
errorRetryCount: 5,
refreshInterval: 43200 * 10,
onLoadingSlow: (
key: string,
config: Readonly<
Required<Configuration<any, any, Combined>>
>
) => [key, { ...config }]
}}
>
<ApolloProvider client={apolloClient}>
<NextAuthProvider session={pageProps.session}>
<ManagedGlobalContext>
<MediaContextProvider>
<Head />
<LayoutNoop pageProps={pageProps}>
<Component {...pageProps} />
</LayoutNoop>
</MediaContextProvider>
</ManagedGlobalContext>
</NextAuthProvider>
</ApolloProvider>
</SWRConfig>
</>
);
}
Then, I have this api-route handling reviews + pagination of those reviews via SWR in index.tsx
pages/api/booksy-fetch.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { BooksyReviewFetchResponse } from '#/types/booksy';
import fetch from 'isomorphic-unfetch';
import { getAccessToken } from '#/lib/booksy';
const API_KEY = process.env.NEXT_PUBLIC_BOOKSY_BIZ_API_KEY ?? '';
const FINGERPRINT =
process.env.NEXT_PUBLIC_BOOKSY_BIZ_X_FINGERPRINT ?? '';
export default async function (
req: NextApiRequest,
res: NextApiResponse<BooksyReviewFetchResponse>
) {
const {
query: { reviews_page, reviews_per_page }
} = req;
const { access_token } = await getAccessToken();
const rev_page_number = reviews_page ? reviews_page : 1;
const reviews_pp = reviews_per_page ? reviews_per_page : 10;
const response = await fetch(
`https://us.booksy.com/api/us/2/business_api/me/businesses/481001/reviews/?reviews_page=${rev_page_number}&reviews_per_page=${reviews_pp}`,
{
headers: {
'X-Api-key': API_KEY,
'X-Access-Token': `${access_token}`,
'X-fingerprint': FINGERPRINT,
Authorization: `s-G1-cvdAC4PrQ ${access_token}`,
'Cache-Control':
's-maxage=86400, stale-while-revalidate=43200',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.152 Safari/537.36',
Connection: 'keep-alive',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate, br'
},
method: 'GET',
keepalive: true
}
);
const booksyReviews: BooksyReviewFetchResponse =
await response.json();
res.setHeader(
'Cache-Control',
'public, s-maxage=86400, stale-while-revalidate=43200'
);
return res.status(200).json(booksyReviews);
}
and the following api route handling image data for a custom marquee also in index.tsx
pages/api/booksy-images.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { Gallery } from '#/types/index';
import { getLatestBooksyPhotos } from '#/lib/booksy';
export default async function (
_req: NextApiRequest,
res: NextApiResponse<Gallery>
) {
const response: Response = await getLatestBooksyPhotos();
const booksyImages: Gallery = await response.json();
res.setHeader(
'Cache-Control',
'public, s-maxage=86400, stale-while-revalidate=43200'
);
return res.status(200).json(booksyImages);
}
Now, to examine index.tsx. I'll break the code into serverside vs clientside for clarity
index.tsx (getStaticProps -- server)
export async function getStaticProps(
ctx: GetStaticPropsContext
): Promise<
GetStaticPropsResult<{
other: LandingDataQuery['other'];
popular: LandingDataQuery['popular'];
Places: LandingDataQuery['Places'];
merchandise: LandingDataQuery['merchandise'];
businessHours: LandingDataQuery['businessHours'];
Header: DynamicNavQuery['Header'];
Footer: DynamicNavQuery['Footer'];
initDataGallery: Partial<
Configuration<Gallery, any, Fetcher<Gallery>>
>;
initialData: Partial<
Configuration<
BooksyReviewFetchResponse,
any,
Fetcher<BooksyReviewFetchResponse>
>
>;
}>
> {
console.log(ctx.params ?? '');
const apolloClient = initializeApollo();
const { data: DynamicSlugs } = await apolloClient.query<
DynamicNavQuery,
DynamicNavQueryVariables
>({
query: DynamicNavDocument,
variables: {
idHead: 'Header',
idTypeHead: WordpressMenuNodeIdTypeEnum.NAME,
idTypeFoot: WordpressMenuNodeIdTypeEnum.NAME,
idFoot: 'Footer'
}
});
const { data: LandingData } = await apolloClient.query<
LandingDataQuery,
LandingDataQueryVariables
>({
query: LandingDataDocument,
variables: {
other: WordPress.Services.Other,
popular: WordPress.Services.Popular,
path: Google.PlacesPath,
googleMapsKey: Google.MapsKey
}
});
const { other, popular, Places, businessHours, merchandise } =
LandingData;
const { Header, Footer } = DynamicSlugs;
const dataGallery = await getLatestBooksyPhotos();
const initDataGallery: Gallery = await dataGallery.json();
const dataInit = await getLatestBooksyReviews({
reviewsPerPage: 10,
pageIndex: 1
});
const initialData: BooksyReviewFetchResponse =
await dataInit.json();
return addApolloState
? addApolloState(apolloClient, {
props: {
Header,
Footer,
other,
popular,
Places,
businessHours,
merchandise
},
revalidate: 600
})
: {
props: {
initialData,
initDataGallery
},
revalidate: 600
};
}
notice how returned props can be handled as a function of whether it is SWR vs Apollo Client data? Next is pretty awesome
Note the functions being called here in getStaticProps
const dataGallery = await getLatestBooksyPhotos();
const initDataGallery: Gallery = await dataGallery.json();
const dataInit = await getLatestBooksyReviews({
reviewsPerPage: 10,
pageIndex: 1
});
const initialData: BooksyReviewFetchResponse =
await dataInit.json();
-- they come from the lib directory. They are meant to be consumed on the server of page files for injecting SWR with initial data. Essentially, it achieves the same means as the api route files but since those are only able to be consumed on the client this is a necessary workaround.
Now for the client
index.tsx (default export -- client)
export default function Index<T extends typeof getStaticProps>({
other,
popular,
Header,
Footer,
merchandise,
Places,
businessHours,
initialData,
initDataGallery
}: InferGetStaticPropsType<T>) {
const GalleryImageLoader = ({
src,
width,
quality
}: ImageLoaderProps) => {
return `${src}?w=${width}&q=${quality || 75}`;
};
const reviews_per_page = 10;
const [reviews_page, set_reviews_page] = useState<number>(1);
const page = useRef<number>(reviews_page);
const { data } = useSWR<BooksyReviewFetchResponse>(
() =>
`/api/booksy-fetch?reviews_page=${reviews_page}&reviews_per_page=${reviews_per_page}`,
fetcher,
initialData
);
const { data: galleryData } = useSWR<Gallery>(
'/api/booksy-images',
fetcherGallery,
initDataGallery
);
// total items
const reviewCount = data?.reviews_count ?? reviews_per_page;
// total pages
const totalPages =
(reviewCount / reviews_per_page) % reviews_per_page === 0
? reviewCount / reviews_per_page
: Math.ceil(reviewCount / reviews_per_page);
// correcting for array indeces starting at 0, not 1
const currentRangeCorrection =
reviews_per_page * page.current - (reviews_per_page - 1);
// current page range end item
const currentRangeEnd =
currentRangeCorrection + reviews_per_page - 1 <= reviewCount
? currentRangeCorrection + reviews_per_page - 1
: currentRangeCorrection +
reviews_per_page -
(reviewCount % reviews_per_page);
// current page range start item
const currentRangeStart =
page.current === 1
? page.current
: reviews_per_page * page.current - (reviews_per_page - 1);
const pages = [];
for (let i = 0; i <= reviews_page; i++) {
pages.push(
data?.reviews ? (
<BooksyReviews pageIndex={i} key={i} reviews={data.reviews}>
<nav aria-label='Pagination'>
<div className='hidden sm:block'>
<p className='text-sm text-gray-50'>
Showing{' '}
<span className='font-medium'>{`${currentRangeStart}`}</span>{' '}
to{' '}
<span className='font-medium'>{`${currentRangeEnd}`}</span>{' '}
of <span className='font-medium'>{reviewCount}</span>{' '}
reviews (page:{' '}
<span className='font-medium'>{page.current}</span> of{' '}
<span className='font-medium'>{totalPages}</span>)
</p>
</div>
<div className='flex-1 inline-flex justify-between sm:justify-center my-auto'>
<button
disabled={page.current - 1 === 0 ? true : false}
onClick={() => set_reviews_page(page.current - 1)}
className={cn('landing-page-pagination-btn', {
' cursor-not-allowed bg-redditSearch':
reviews_page - 1 === 0,
' cursor-pointer': reviews_page - 1 !== 0
})}
>
Previous
</button>
<button
disabled={page.current === totalPages ? true : false}
onClick={() => set_reviews_page(page.current + 1)}
className={cn('landing-page-pagination-btn', {
' cursor-not-allowed bg-redditSearch':
reviews_page === totalPages,
' cursor-pointer': reviews_page < totalPages
})}
>
Next
</button>
</div>
</nav>
</BooksyReviews>
) : (
<ReviewsSkeleton />
)
);
}
useEffect(() => {
(async function Update() {
return (await page.current) === reviews_page
? true
: set_reviews_page((page.current = reviews_page));
})();
}, [page.current, reviews_page]);
return (
<>
<AppLayout
title={'The Fade Room Inc.'}
Header={Header}
Footer={Footer}
>
{galleryData?.images ? (
<Grid>
{galleryData.images
.slice(6, 9)
.map((img, i) => {
<GalleryCard
key={img.image_id}
media={galleryData}
imgProps={{
loader: GalleryImageLoader,
width: i === 0 ? 1080 : 540,
height: i === 0 ? 1080 : 540
}}
/>;
})
.reverse()}
</Grid>
) : (
<LoadingSpinner />
)}
{galleryData?.images ? (
<Marquee variant='secondary'>
{galleryData.images
.slice(3, 6)
.map((img, j) => (
<GalleryCard
key={img.image_id}
media={galleryData}
variant='slim'
imgProps={{
loader: GalleryImageLoader,
width: j === 0 ? 320 : 320,
height: j === 0 ? 320 : 320
}}
/>
))
.reverse()}
</Marquee>
) : (
<LoadingSpinner />
)}
<LandingCoalesced
other={other}
popular={popular}
places={Places}
businessHours={businessHours}
merchandise={merchandise}
>
{data?.reviews ? (
<>
<>{pages[page.current]}</>
<span className='hidden'>
{
pages[
page.current < totalPages
? page.current + 1
: page.current - 1
]
}
</span>
</>
) : (
<ReviewsSkeleton />
)}
</LandingCoalesced>
</AppLayout>
</>
);
}
There are two useSWR hooks on the landing page:
const { data } = useSWR<BooksyReviewFetchResponse>(
() =>
`/api/booksy-fetch?reviews_page=${reviews_page}&reviews_per_page=${reviews_per_page}`,
fetcher,
initialData
);
const { data: galleryData } = useSWR<Gallery>(
'/api/booksy-images',
fetcherGallery,
initDataGallery
);
the initialData and initDataGallery values listed after their respective fetchers is the initial data shuttled from the server to the client and inferred via InferGetStaticPropsType<T>. This provides a solution to the first load data problem when it comes to client side data fetching.
An additional configuration you can make to expedite fetching of data on the client when using SWR is to specify which api routes should be preloaded in _document.tsx and the name of their corresponding fetcher(s)
_document.tsx
<link
rel='preload'
href={`/api/booksy-fetch?reviews_page=1&reviews_per_page=10`}
as='fetcher'
crossOrigin='anonymous'
/>
<link
rel='preload'
href='/api/booksy-images'
as='fetcherGallery'
crossOrigin='anonymous'
/>
I have had 0 problems with using the two in tandem, which I have been for about a month now, and actually incorporating SWR into the mix enhanced DX/UX and next analytics reflected that (it cut FCP time by over 50% to 0.4 seconds, LCP to around 0.8 seconds).
There is a comparison: Comparison | React Query vs SWR vs Apollo vs RTK Query

Resources