Skip to content

Commit

Permalink
✨ Added Notifications section v1.0
Browse files Browse the repository at this point in the history
notifications, other minor changes and version bump (v0.0.2-prealpha)
j1-dev committed Feb 25, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 37f037a commit 7017337
Showing 13 changed files with 304 additions and 49 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@
"react": "^18.2.0",
"react-burger-menu": "^3.0.9",
"react-dom": "^18.2.0",
"react-firebase-hooks": "^5.0.3",
"react-firebase-hooks": "^5.1.1",
"react-icons": "^4.6.0",
"react-kofi-button": "^0.7.1",
"react-popper": "^2.3.0",
2 changes: 2 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ import PostPage from "./pages/PostPage";
import { Route, Routes } from "react-router-dom";
import { AuthProvider, useAuth } from "./api/authContext";
import { auth } from "./api/firebase-config";
import Notifications from "./components/Notifications";

function App() {
const [windowSize, setWindowSize] = useState([
@@ -64,6 +65,7 @@ function App() {
<Route path="/Perfil" element={<Perfil />} />
<Route path="/Shop" element={<Shop />} />
<Route path="/About" element={<About />} />
<Route path="/Notifications" element={<Notifications />} />
<Route path="/:username" element={<Perfil />} />
<Route exact path="/Post" element={<PostPage />} />
<Route exact path="/Post/:id" element={<PostPage />} />
19 changes: 19 additions & 0 deletions src/components/Navbar.jsx
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import {
HiOutlineShoppingCart,
HiOutlineCog,
} from "react-icons/hi";
import { RiNotification3Line } from "react-icons/ri";
import { Avatar } from "@mui/material";
import { Tooltip } from "@mui/material";

@@ -181,6 +182,24 @@ const Navbar = () => {
)}
</NavLink>
)}
{/*
Notifications button
*/}
<NavLink
className={({ isActive }) =>
isActive ? "button-active" : "button"
}
to="/Notifications"
>
<RiNotification3Line className="float-left text-4xl" />
{windowSize[0] < 1024 ? (
<></>
) : (
<p className="float-left pl-4 text-3xl font-normal">
Notificaciones
</p>
)}
</NavLink>
{/*
Shop button
@todo Turn it into a ko-fi donation button
67 changes: 67 additions & 0 deletions src/components/Notification.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { doc } from "firebase/firestore";
import React, { useEffect, useState } from "react";
import { useDocument } from "react-firebase-hooks/firestore";
import { Link } from "react-router-dom";
import { db } from "../api/firebase-config";

const Notification = ({ data, key }) => {
const type = data.type;
const senderUid = data.uid;
const time = data.sentAt;
const senderQ = doc(db, "users", data.from);
const [value, loading] = useDocument(senderQ);

useEffect(() => {
console.log(data.type);
}, [data]);

function timeDiff(secs) {
let str = "";
let ms = Date.now();
let s = Math.floor(ms / 1000);
let sDiff = s - secs;
if (sDiff >= 0 && sDiff < 60) {
str += Math.round(sDiff) + "s ago";
} else if (sDiff >= 60 && sDiff < 3600) {
sDiff /= 60;
str += Math.round(sDiff) + "m ago";
} else if (sDiff >= 3600 && sDiff < 86400) {
sDiff /= 60;
sDiff /= 60;
str += Math.round(sDiff) + "h ago";
} else if (sDiff >= 86400 && sDiff < 2.628e6) {
sDiff /= 60;
sDiff /= 60;
sDiff /= 24;
str += Math.round(sDiff) + "d ago";
} else if (sDiff >= 2.628e6 && sDiff < 3.154e7) {
sDiff /= 60;
sDiff /= 60;
sDiff /= 24;
sDiff /= 30;
str += Math.round(sDiff) + "m ago";
} else {
sDiff /= 60;
sDiff /= 60;
sDiff /= 24;
sDiff /= 30;
sDiff /= 12;
str += Math.round(sDiff) + "y ago";
}
return str;
}

return (
<div key={key} className="notification-panel">
<hr />
<p>{type}</p>
<p>
From: {loading ? <p>loading...</p> : <p>{value.data().displayName}</p>}
</p>
<p>{timeDiff(time)}</p>
<hr />
</div>
);
};

export default Notification;
168 changes: 168 additions & 0 deletions src/components/Notifications.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { auth, db } from "../api/firebase-config";
import {
collection,
orderBy,
query,
onSnapshot,
limit,
startAfter,
} from "firebase/firestore";
import Notification from "./Notification";
import React, { useEffect, useState, useCallback } from "react";

function Notifications() {
const user = auth.currentUser;
/**
* The state variable that stores an array of notifications fetched from the backend.
* @type {Array}
*/
const [notifications, setNotifications] = useState([]);

/**
* The state variable that stores the number of notifications to be fetched from the backend.
* @type {number}
*/
const [limite] = useState(5);

/**
* The state variable that stores a boolean flag to indicate whether notifications are currently being loaded.
* @type {boolean}
*/
const [loading, setLoading] = useState(false);

/**
* The state variable that stores a boolean flag to indicate whether the user has scrolled to the bottom of the page.
* @type {boolean}
*/
const [atBottom, setAtBottom] = useState(false);

/**
* The state variable that stores the last loaded post for cursor based pagination
* @type {DocumentSnapshot}
*/
const [cursor, setCursor] = useState(null);

/**
* Hook that fetches posts from the Firestore database when the component is mounted.
* Sets cursor to the last post loaded for proper cursor pagination
*
* @function
* @returns {function} A cleanup function to remove the Firestore listener.
*/
useEffect(() => {
const NotificationCollectionRef = collection(
db,
`users/${user.uid}/notifications`
);
const unsub = () => {
const q = query(
NotificationCollectionRef,
orderBy("sentAt", "desc"),
limit(limite)
);
onSnapshot(q, (snapshot) => {
const data = snapshot.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}));
setCursor(snapshot.docs[4]);
setNotifications(data);
setLoading(false);
});
};

unsub();
}, [limite]);

/**
* Handles loading more posts when the "load more" button is clicked.
* Calls a onSnapshot listener for the next 5 posts after the cursor.
*
* @callback
* @returns {void}
*/
const handleLoadMore = useCallback(() => {
setLoading(true);
const NotificationCollectionRef = collection(
db,
`users/${user.uid}/notifications`
);
const unsub = () => {
const q = query(
NotificationCollectionRef,
orderBy("sentAt", "desc"),
limit(limite),
startAfter(cursor)
);
onSnapshot(q, (snapshot) => {
const data = snapshot.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}));
setCursor(snapshot.docs[4]);
setNotifications((p) => p.concat(data));
setLoading(false);
});
};
unsub();
setAtBottom(false);
}, [limite, cursor]);

/**
* Registers a scroll event listener on the window object to detect when the user
* has scrolled to the bottom of the page, triggering the `handleLoadMore` callback
* function to load additional posts.
*
* @callback
* @returns {void}
*/
const handleScroll = useCallback(() => {
if (loading || atBottom) return;

// Check if the user has scrolled to the bottom of the page
if (
window.innerHeight + document.documentElement.scrollTop >=
document.documentElement.offsetHeight
) {
console.log("Scrolled to bottom!");
setAtBottom(true);
if (!loading) handleLoadMore();
}
}, [loading, atBottom, handleLoadMore]);

/**
* Registers a scroll event listener on the window object and removes it when the
* component unmounts.
*
* @callback
* @returns {void}
*/
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [handleScroll]);

/**
* Clears the "atBottom" state if it's set to true and the component is not loading.
*
* @function
* @returns {void}
*/
useEffect(() => {
if (!loading && atBottom) {
setAtBottom(false);
}
}, [loading, atBottom]);
return (
<div className="notification-feed text-center">
{notifications.map((doc) => {
return <Notification data={doc} key={doc.id} />;
})}
{loading && <p>Loading...</p>}
</div>
);
}

export default Notifications;
52 changes: 23 additions & 29 deletions src/components/Post.jsx
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import {
import React, { useState, useMemo, useEffect, useCallback } from "react";
import { auth, db } from "../api/firebase-config";
import { UserCollectionRef } from "../api/user.services";
import { useCollection } from "react-firebase-hooks/firestore";
import { useDocument } from "react-firebase-hooks/firestore";
import { NavLink, Link } from "react-router-dom";
import YouTube from "react-youtube";
import { GoThumbsdown, GoThumbsup } from "react-icons/go";
@@ -68,13 +68,14 @@ const Post = ({ data, path, className }) => {
* A Firestore query object used to retrieve the user with matching uid
* @type {Object}
*/
const q = query(UserCollectionRef, where("uid", "==", data.uid));
const q = doc(db, "users", data.uid);

/**
* An array of the user of the post and a loading state variable
* @todo change useCollection for useDocument
* @type {array}
*/
const [value, loading] = useCollection(q);
const [value, loading] = useDocument(q);

/**
* A collection object used to perform CRUD operations on the Likes subcollection
@@ -363,17 +364,11 @@ const Post = ({ data, path, className }) => {
);

/**
* Converts a Unix timestamp (in seconds) to a JavaScript Date object.
* Converts a Unix timestamp (in seconds) to formatted time difference.
* @function
* @param {number} secs - The Unix timestamp to convert, in seconds.
* @returns {Date} The corresponding Date object.
*/
function toDateTime(secs) {
var t = new Date(1970, 0, 1); // Epoch
t.setSeconds(secs);
return t;
}

function timeDiff(secs) {
let str = "";
let ms = Date.now();
@@ -439,25 +434,24 @@ const Post = ({ data, path, className }) => {
*/}
<div className="mb-10 w-full pb-7">
{loading && ""}
{value?.docs.map((doc) => {
const path = `/${doc.data().displayName}`;
const date = timeDiff(data.createdAt);
return (
<div className="float-left w-11/12">
<p className="text-base">
<Avatar
alt={doc.data().displayName}
src={doc.data().photo}
className="float-left mr-3"
/>
<NavLink to={path} className="underline hover:no-underline">
{doc.data().displayName}
</NavLink>
</p>
<div className="text-xs ">Posted {date}</div>
</div>
);
})}
{typeof value !== "undefined" ? (
<div className="float-left w-11/12">
<p className="text-base">
<Avatar
alt={value.data().displayName}
src={value.data().photo}
className="float-left mr-3"
/>
<NavLink
to={`/${value.data().displayName}`}
className="underline hover:no-underline"
>
{value.data().displayName}
</NavLink>
</p>
<div className="text-xs ">Posted {timeDiff(data.createdAt)}</div>
</div>
) : null}
</div>

{/*
Loading

0 comments on commit 7017337

Please sign in to comment.