MAJOR data flow change (events)

Made API_CLIENT stateless, used context to distribute data over the application
This commit is contained in:
mmacierzanka committed 2021-10-08 18:21:13 +02:00
1 parent 74fc2d184a
commit fe2e58850c
8 files changed
+223 -274

No files matched your search

+27 -74
View File
@@ -8,82 +8,35 @@ import { Navbar } from "./components/Navbar";
import { Footer } from "./components/Footer";
import { CreateEvent } from "./pages/CreateEvent";
import { Event } from "./pages/Event";
import { API_CLIENT } from "./constants";
import { useEvents } from "./context/Events";
export const App = () => {
const [routes, setRoutes] = useState(
API_CLIENT.events.map((e) => {
return (
<Route key={e._id} exact path={`/event/${e._id}`}>
<Event
data={{
name: e.name,
type: e.type,
description: e.description,
address: e.address,
time: e.time,
lat: e.lat,
lon: e.lon,
id: e._id,
createdTime: e.created?.time,
user: e.user,
}}
></Event>
</Route>
);
})
);
const events = useEvents();
const [routes, setRoutes] = useState([]);
useEffect(() => {
API_CLIENT.on("EVENT_RELOAD", (list) => {
setRoutes(
list.map((e) => {
return (
<Route key={e._id} exact path={`/event/${e._id}`}>
<Event
data={{
name: e.name,
type: e.type,
description: e.description,
address: e.address,
time: e.time,
lat: e.lat,
lon: e.lon,
id: e._id,
createdTime: e.created.time,
user: e.user,
}}
></Event>
</Route>
);
})
);
});
API_CLIENT.on("EVENT_CREATE", (list) => {
setRoutes(
list.map((e) => {
return (
<Route key={e._id} exact path={`/event/${e._id}`}>
<Event
data={{
name: e.name,
type: e.type,
description: e.description,
address: e.address,
time: e.time,
lat: e.lat,
lon: e.lon,
id: e._id,
createdTime: e.created.time,
user: e.user,
}}
></Event>
</Route>
);
})
);
});
}, []);
setRoutes(
events.map((e) => {
return (
<Route key={e._id} exact path={`/event/${e._id}`}>
<Event
data={{
name: e.name,
type: e.type,
description: e.description,
address: e.address,
time: e.time,
lat: e.lat,
lon: e.lon,
id: e._id,
createdTime: e.created?.time,
user: e.user,
}}
></Event>
</Route>
);
})
);
}, [events]);
return (
<Router>
@@ -98,7 +51,7 @@ export const App = () => {
{routes}
<Route component={NotFound} />
</Switch>
<Footer/>
<Footer />
</Router>
);
};
@@ -2,6 +2,7 @@ import { BIcon } from "../BIcon";
import styles from "./index.module.scss";
import { EventType } from "../EventType";
import { API_CLIENT } from "../../constants";
import { removeEvent } from "../../context/Events";
export const EventPreviewEdit = (props) => {
const getDate = (time) => {
@@ -15,8 +16,11 @@ export const EventPreviewEdit = (props) => {
minute: "2-digit",
});
};
const deleteItem = async (arg) => {
await API_CLIENT.deleteEvent({ id: arg });
const deleteItem = (arg) => {
API_CLIENT.deleteEvent({ id: arg }).then(() => {
removeEvent(arg);
typeof props.onRemove === "function" && props.onRemove(arg);
});
};
return (
<div className={styles.link}>
+49
View File
@@ -0,0 +1,49 @@
import { useState, createContext, useContext } from "react";
const events = {
value: [],
setter: null,
};
const validateData = (event) => {
// VALIDATE INPUT DATA
if (typeof event !== "object") throw new TypeError("Incorrect argument");
if (typeof event._id !== "string") throw new TypeError("Invalid ID");
if (typeof event.name !== "string") throw new TypeError("Incorrect value type: name");
if (typeof event.type !== "number") throw new TypeError("Incorrect value type: type");
if (event.description && typeof event.description !== "string") throw new TypeError("Incorrect value type: description");
if (event.address && typeof event.address !== "string") throw new TypeError("Incorrect value type: address");
if (typeof event.time !== "number") throw new TypeError("Incorrect value type: time");
if (event.lon && typeof event.lon !== "number") throw new TypeError("Incorrect value type: lon");
if (event.lat && typeof event.lat !== "number") throw new TypeError("Incorrect value type: lat");
if (typeof event.created !== "object") throw new TypeError("Incorrect value type: created");
if (typeof event.created.time !== "number") throw new TypeError("Incorrect value type: created.time");
if (typeof event.user !== "string") throw new TypeError("Incorrect value type: user");
};
export const EventContext = createContext(events.value);
export const useEvents = () => {
return useContext(EventContext);
};
export const addEvent = (event) => {
validateData(event);
events.setter([...events.value, event]);
};
export const removeEvent = (id) => {
const target = events.value.findIndex((event) => event._id === id);
if (target === -1) {
console.error(`Cannot find event with ID ${id}`);
return;
}
const list = events.value;
list.splice(target, 1);
events.setter([...list]);
};
export const Events = (props) => {
[events.value, events.setter] = useState(Array.isArray(props.initial) ? props.initial : []);
return <EventContext.Provider value={events.value}>{props.children}</EventContext.Provider>;
};
+5 -2
View File
@@ -4,14 +4,17 @@ import "./index.scss";
import { App } from "./App.js";
import "bootstrap/dist/css/bootstrap.min.css";
import { API_CLIENT, LOGIN_MANAGER } from "./constants";
import { Events } from "./context/Events";
(async () => {
await API_CLIENT.fetchEvents();
const events = await API_CLIENT.fetchEvents();
await LOGIN_MANAGER.fetchSession();
ReactDOM.render(
<React.StrictMode>
<App />
<Events initial={events}>
<App />
</Events>
</React.StrictMode>,
document.getElementById("root")
);
+3 -1
View File
@@ -11,6 +11,7 @@ import { spawnPopup } from "../../utils/popups/spawnPopup";
import { spawnError } from "../../utils/popups/spawnError";
import { getPlace } from "../../utils/map/getPlace";
import { MapPopup } from "./MapPopup";
import { addEvent } from "../../context/Events";
export const CreateEvent = () => {
const history = useHistory();
@@ -56,7 +57,8 @@ export const CreateEvent = () => {
user: LOGIN_MANAGER.state.email,
};
API_CLIENT.createEvent(rqObj)
.then(() => {
.then((res) => {
addEvent(res);
history.push("/");
})
.catch(async (e) => {
+30 -31
View File
@@ -1,5 +1,4 @@
import styles from "./index.module.scss";
import { useRef } from "react";
import { Stack } from "react-bootstrap";
import { useState, useEffect } from "react";
import { API_CLIENT } from "../../constants";
@@ -13,8 +12,7 @@ export const Dashboard = () => {
if (LOGIN_MANAGER.state === null) {
history.push("/");
}
const [list, updateList] = useState();
const array = useRef([]);
const [list, updateList] = useState([]);
useEffect(() => {
//chce wyrenderować eventpreview ale tylko dla eventów utworzonych przez danego użytkownika
API_CLIENT.isLogged().then((data) => {
@@ -22,44 +20,45 @@ export const Dashboard = () => {
if (data !== "not logged") {
API_CLIENT.getUserEvents(data.email).then((events) => {
if (events) {
array.current = events;
console.log(array);
render();
updateList(events);
}
});
} else {
history.push("/");
}
});
// eslint-disable-next-line
}, []);
}, [history]);
const render = () => {
if (!Array.isArray(array)) return;
updateList(
array.map((e, i) => {
return (
<EventPreviewEdit
data={{
name: e.name,
type: e.type,
lon: e.lon,
lat: e.lat,
createdTime: e.created?.time,
time: e.time,
id: e._id,
address: e.address,
}}
key={i}
/>
);
})
);
};
return (
<div className={container}>
<p className={styles.title}>Tu możesz zarządzać swoimi wydarzeniami</p>
<Stack gap={1}>{list}</Stack>
<Stack gap={1}>
{list.map((e, i) => {
return (
<EventPreviewEdit
onRemove={(id) => {
const newList = list;
const index = newList.findIndex((el) => id === el._id);
if (index !== -1) {
newList.splice(index, 1);
updateList(newList);
}
}}
data={{
name: e.name,
type: e.type,
lon: e.lon,
lat: e.lat,
createdTime: e.created?.time,
time: e.time,
id: e._id,
address: e.address,
}}
key={i}
/>
);
})}
</Stack>
</div>
);
};
+102 -149
View File
@@ -11,165 +11,55 @@ import { spawnPopup } from "../../utils/popups/spawnPopup";
import { spawnError } from "../../utils/popups/spawnError";
import { getUserPos } from "../../utils/getUserPos";
import { useLoginState } from "../../utils/useLoginState";
import { useDbEvents } from "../../utils/useDbEvents";
import { useEvents } from "../../context/Events";
export const Home = () => {
const sortingFn = (userPos, customMode) => {
return (a, b) => {
const [type, direction] = customMode ? customMode.split(":") : list.sortingMode.split(":");
switch (type) {
case "event-date":
return direction === "ascending" ? a.time - b.time : b.time - a.time;
case "creation-date":
return direction === "ascending" ? b.created.time - a.created.time : a.created.time - b.created.time;
case "distance":
if (userPos) {
const aDistance = a.lon && a.lat ? calculateDistance(userPos, [a.lon, a.lat]) : -1;
const bDistance = b.lon && b.lat ? calculateDistance(userPos, [b.lon, b.lat]) : -1;
return direction === "ascending" ? aDistance - bDistance : bDistance - aDistance;
} else return 0;
default:
return 0;
}
};
};
const getPos = async () => {
let userPos;
const permissions = await window.navigator.permissions.query({ name: "geolocation" });
if (permissions.state === "granted") {
userPos = await getUserPos().catch(async () => {
await spawnError("Wystąpił nieoczekiwany błąd podczas próby dostępu do usługi geolokalizacji");
return null;
});
} else if (permissions.state === "prompt") {
let closeFn;
spawnPopup((c) => {
closeFn = c;
return (
<div className={styles.popupContainer}>
<BIcon color="rgb(0,0,100)" icon="info-circle-fill" size={"120px"} />
<h3 style={{ textAlign: "center" }}>Oczekiwanie na przyznanie uprawnień przez użytkownika</h3>
</div>
);
});
userPos = await getUserPos().catch(async () => {
await spawnError("Nie udało się uzyskać lokalizacji urządzenia lub nastąpiła odmowa dostępu");
return null;
});
closeFn();
} else if (permissions.state === "denied") {
await spawnError("Odmówiono aplikacji dostępu do lokalizacji urządzenia");
}
return userPos ?? null;
};
const events = useDbEvents(async (list) => {
updateList({
sortingMode: list.sortingMode,
elements: list.sort(sortingFn(list.sortingMode.split(":")[0] === "distance" ? await getPos() : null)).map((e, i) => {
return (
<EventPreview
data={{ name: e.name, type: e.type, lon: e.lon, lat: e.lat, createdTime: e.created?.time, time: e.time, id: e._id, address: e.address, user: e.user }}
key={i}
/>
);
}),
});
});
const events = useEvents();
const history = useHistory();
const sortDropdown = useRef(null);
const createEventButton = useRef(null);
const [list, updateList] = useState({
sortingMode: window.localStorage.getItem("sorting") ?? "event-date:ascending",
elements: [],
});
const history = useHistory();
const sortDropdown = useRef(null);
const createEventButton = useRef(null);
useTitle();
useLoginState((state) => {
if (createEventButton.current) createEventButton.current.disabled = state === null;
});
useEffect(() => {
(async () => {
if (isDistance(list.sortingMode)) {
const pos = await getPos();
if (!pos) {
updateList((prev) => prev);
} else {
updateList({
sortingMode: list.sortingMode,
elements: events.sort(sortingFn(list.sortingMode, pos)).map(toComponent),
});
}
}
updateList({
sortingMode: list.sortingMode,
elements: events.sort(sortingFn(list.sortingMode, null)).map(toComponent),
});
})();
}, [events, list.sortingMode]);
useEffect(() => {
window.localStorage.setItem("sorting", list.sortingMode);
}, [list.sortingMode]);
useEffect(() => {
sortDropdown.current.addEventListener("input", async (e) => {
const [type] = e.target.value.split(":");
const pos = type === "distance" ? await getPos() : null;
if (type === "distance" && !pos) {
sortDropdown.current.value = list.sortingMode;
updateList({
sortingMode: list.sortingMode,
elements: events.sort(sortingFn(pos)).map((e, i) => {
return (
<EventPreview
data={{
name: e.name,
type: e.type,
lon: e.lon,
lat: e.lat,
createdTime: e.created?.time,
time: e.time,
id: e._id,
address: e.address,
user: e.user,
}}
key={i}
/>
);
}),
});
return;
}
updateList({
sortingMode: e.target.value,
elements: events.sort(sortingFn(pos, e.target.value)).map((e, i) => {
return (
<EventPreview
data={{ name: e.name, type: e.type, lon: e.lon, lat: e.lat, createdTime: e.created?.time, time: e.time, id: e._id, address: e.address, user: e.user }}
key={i}
/>
);
}),
});
});
sortDropdown.current.value = list.sortingMode;
list.sortingMode.split(":")[0] === "distance"
? getPos().then((pos) => {
updateList({
sortingMode: list.sortingMode,
elements: events.sort(sortingFn(pos)).map((e, i) => {
return (
<EventPreview
data={{
name: e.name,
type: e.type,
lon: e.lon,
lat: e.lat,
createdTime: e.created?.time,
time: e.time,
id: e._id,
address: e.address,
user: e.user,
}}
key={i}
/>
);
}),
});
})
: updateList({
sortingMode: list.sortingMode,
elements: events.sort(sortingFn(null)).map((e, i) => {
return (
<EventPreview
data={{ name: e.name, type: e.type, lon: e.lon, lat: e.lat, createdTime: e.created?.time, time: e.time, id: e._id, address: e.address, user: e.user }}
key={i}
/>
);
}),
});
// eslint-disable-next-line
}, []);
const el = sortDropdown.current;
const cb = (e) => {
updateList({ ...list, sortingMode: e.target.value });
};
el.addEventListener("input", cb);
el.value = list.sortingMode;
return () => {
el.removeEventListener("input", cb);
};
}, [events, list]);
useLoginState((state) => {
if (createEventButton.current) createEventButton.current.disabled = state === null;
});
return (
<div className={container}>
@@ -179,7 +69,7 @@ export const Home = () => {
<BIcon icon="sort-down-alt" /> Sortuj
</p>
<select ref={sortDropdown} className={styles.sortDropdownMenu}>
<option value="event-date:ascending">Nadzchodzące</option>
<option value="event-date:ascending">Nadchodzące</option>
<option value="event-date:descending">Najpóźniejsze</option>
<option value="creation-date:ascending">Najnowsze</option>
<option value="creation-date:descending">Najstarsze</option>
@@ -203,3 +93,66 @@ export const Home = () => {
</div>
);
};
function toComponent(e, i) {
return (
<EventPreview
data={{ name: e.name, type: e.type, lon: e.lon, lat: e.lat, createdTime: e.created?.time, time: e.time, id: e._id, address: e.address, user: e.user }}
key={i}
/>
);
}
function sortingFn(mode, userPos) {
return (a, b) => {
const [type, direction] = mode.split(":");
switch (type) {
case "event-date":
return direction === "ascending" ? a.time - b.time : b.time - a.time;
case "creation-date":
return direction === "ascending" ? b.created.time - a.created.time : a.created.time - b.created.time;
case "distance":
if (userPos) {
const aDistance = a.lon && a.lat ? calculateDistance(userPos, [a.lon, a.lat]) : -1;
const bDistance = b.lon && b.lat ? calculateDistance(userPos, [b.lon, b.lat]) : -1;
return direction === "ascending" ? aDistance - bDistance : bDistance - aDistance;
} else return 0;
default:
return 0;
}
};
}
async function getPos() {
let userPos;
const permissions = await window.navigator.permissions.query({ name: "geolocation" });
if (permissions.state === "granted") {
userPos = await getUserPos().catch(async () => {
await spawnError("Wystąpił nieoczekiwany błąd podczas próby dostępu do usługi geolokalizacji");
return null;
});
} else if (permissions.state === "prompt") {
let closeFn;
spawnPopup((c) => {
closeFn = c;
return (
<div className={styles.popupContainer}>
<BIcon color="rgb(0,0,100)" icon="info-circle-fill" size={"120px"} />
<h3 style={{ textAlign: "center" }}>Oczekiwanie na przyznanie uprawnień przez użytkownika</h3>
</div>
);
});
userPos = await getUserPos().catch(async () => {
await spawnError("Nie udało się uzyskać lokalizacji urządzenia lub nastąpiła odmowa dostępu");
return null;
});
closeFn();
} else if (permissions.state === "denied") {
await spawnError("Odmówiono aplikacji dostępu do lokalizacji urządzenia");
}
return userPos ?? null;
}
function isDistance(value) {
return value.split(":")[0] === "distance";
}
+1 -15
View File
@@ -3,7 +3,6 @@ import { EventEmitter } from "events";
export class APIClient extends EventEmitter {
#dbUrl = "";
#events = [];
constructor(dbUrl) {
super();
@@ -11,15 +10,9 @@ export class APIClient extends EventEmitter {
this.#dbUrl = dbUrl;
}
get events() {
return this.#events;
}
async fetchEvents() {
const rq = await axios.get(`${this.#dbUrl}/api/getEvents`);
if (rq.status !== 200) throw new Error(`Request failed with status code ${rq.status}: ${rq.statusText}`);
this.#events = rq.data;
this.emit("EVENT_RELOAD", this.#events, rq.data);
return rq.data;
}
@@ -27,7 +20,7 @@ export class APIClient extends EventEmitter {
const rq = await axios.post(`${this.#dbUrl}/api/deleteEvent`, item);
if (rq.status !== 200) throw new Error(`Request failed with status code ${rq.status}: ${rq.statusText}`);
await this.fetchEvents();
return rq.data;
return item.id;
}
async getUserEvents(data) {
@@ -43,11 +36,6 @@ export class APIClient extends EventEmitter {
return rq.data;
}
async getEvent(id) {
if (typeof id !== "string") throw new TypeError("Invalid ID type");
return this.#events.find((e) => e.id === id) ?? null;
}
async createEvent(data) {
//Check types
if (typeof data !== "object") throw new TypeError("Data object is required");
@@ -58,8 +46,6 @@ export class APIClient extends EventEmitter {
//Make POST request
const rq = await axios.post(`${this.#dbUrl}/api/createEvent`, data);
if (rq.status !== 200) throw new Error(`Request failed with status code ${rq.status}: ${rq.statusText}`);
this.#events.push(rq.data);
this.emit("EVENT_CREATE", this.#events, rq.data);
return rq.data;
}
}