In this tutorial, you'll learn how to build a Meetup.com clone we will touch:
- Create and join online events
- Add comments under an upcoming event
- [TRICK 1]: Building an authentication / authorization in 5 minutes with SuperTokens
- [TRICK 2]: Notify when someone joins and comment on their event in 10 minutes with Novu.
So Let's meet up!
Novu: Open-source notification infrastructure 🚀
Just a quick background about us. Novu is an open-source notification infrastructure. We basically help to manage all the product notifications. It can be In-App (the bell icon like you have in the Dev Community - Websockets), Emails, SMSs and so on.
Let's set it up! 🔥
Here, I'll walk you through creating the project setup for the application. We'll use React.js for the front end and Node.js for the backend server.
Create a folder for the web application as done below.
mkdir meetup-clone
cd meetup-clone
mkdir client server
Adding a Node.js server 🔌
Navigate into the server folder and create a package.json
file.
cd server & npm init -y
Install Express, Nodemon, and the CORS library.
npm install express cors nodemon
ExpressJS is a fast, minimalist framework that provides several features for building web applications in Node.js, CORS is a Node.js package that allows communication between different domains, and Nodemon is a Node.js tool that automatically restarts the server after detecting file changes.
Create an index.js
file - the entry point to the web server.
touch index.js
Set up a Node.js server using ExpressJS. The code snippet below returns a JSON object when you visit the http://localhost:4000/api
in your browser.
//👇🏻index.js
const express = require("express");
const cors = require("cors");
const app = express();
const PORT = 4000;
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cors());
app.get("/api", (req, res) => {
res.json({
message: "Hello world",
});
});
app.listen(PORT, () => {
console.log(`Server listening on ${PORT}`);
});
Configure Nodemon by adding the start command to the list of scripts in the package.json
file. The code snippet below starts the server using Nodemon.
//👇🏻 In server/package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon index.js"
},
Congratulations!🎉 You can now start the server by using the command below.
npm start
Setting up the React application ⚒️
Navigate into the client folder via your terminal and create a new React.js project with Vite.
npm create vite@latest
Install React Icons and React Router - a JavaScript library that enables us to navigate between pages in a React application.
npm install react-router-dom react-icons
Delete the redundant files, such as the logo and the test files from the React app, and update the App.jsx
file to display “Hello World” as done below.
function App() {
return (
<div>
<p>Hello World!</p>
</div>
);
}
export default App;
Copy the CSS file required for styling the project into the src/index.css
file.
Crafting the user interface 🧪
Here, we'll create the user interface for the Meetup clone to enable users to create and join events and view event details.
Create a pages folder within the client/src
folder containing the CreateEvent.jsx
, Dashboard.jsx
, EventDetails.jsx
, Events.jsx
, EventsCategories.jsx
, and Home.jsx
.
cd client/src
mkdir pages
cd pages
touch Home.jsx Dashboard.jsx CreateEvent.jsx EventDetails.jsx Events.jsx EventsCategories.jsx
- From the code snippet above,
- The
Home.jsx
component is the application's homepage where users can view all upcoming events. - The
Dashboard.jsx
component displays all the user's events. - The
CreateEvent.jsx
component enables users to provide details about an event and create a new event. - The
EventDetails.jsx
component provides all the details about an event. - The
Events.jsx
andEventsCategories.jsx
components are similar. TheEvents.jsx
component displays all the available events, and theEventsCategories.jsx
displays all the events under a particular category.
- The
Next, create a components folder within the client/src
folder containing the CategoriesSection.jsx
, EventsSection.jsx
, Footer.jsx
, Hero.jsx
, and Nav.jsx
components. These components are the different sections of the homepage.
cd client/src
mkdir components
cd components
touch CategoriesSection.jsx EventsSection.jsx Footer.jsx Hero.jsx Nav.jsx
Update the App.jsx
file to render the pages using React Router.
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import Dashboard from "./pages/Dashboard";
import Events from "./pages/Events";
import EventsCategory from "./pages/EventsCategory";
import CreateEvent from "./pages/CreateEvent";
import EventDetails from "./pages/EventDetails";
function App() {
return (
<Router>
<Routes>
<Route path='/' element={<Home />} />
<Route path='/dashboard' element={<Dashboard />} />
<Route path='/events/all' element={<Events />} />
<Route path='/events/:category' element={<EventsCategory />} />
<Route path='/create/event' element={<CreateEvent />} />
<Route path='/event/:slug' element={<EventDetails />} />
</Routes>
</Router>
);
}
export default App;
Home Page 🏠
The Home page displays all the available events within the application and enables users to navigate to other pages on the website. Copy the code below into the Home.jsx
file.
import CategoriesSection from "../components/CategoriesSection";
import EventsSection from "../components/EventsSection";
import Footer from "../components/Footer";
import Hero from "../components/Hero";
import Nav from "../components/Nav";
const Home = () => {
return (
<div>
<Nav />
<Hero />
<EventsSection />
<CategoriesSection />
<Footer />
</div>
);
};
export default Home;
Adding Events 📕
These pages display all the events within the application or within a particular category.
import event from "../assets/event.jpeg";
import Nav from "../components/Nav";
import { AiOutlineCalendar } from "react-icons/ai";
import { BsCheckCircle } from "react-icons/bs";
import { ImLocation2 } from "react-icons/im";
import { Link } from "react-router-dom";
const Events = () => {
return (
<>
<Nav />
<div className='home_events' style={{ paddingTop: "20px" }}>
<h1 style={{ fontSize: "30px", marginBottom: "20px" }}>All Events</h1>
<div className='body_events'>
<Link to={`/event/slug`} className='i_event'>
<img src={event} alt='Event' className='i_image' />
<div className='i_content'>
<h2 style={{ marginBottom: "10px" }}>Novu Community Call</h2>
<p style={{ marginBottom: "10px", opacity: 0.7 }}>
Hosted by: Novu Development Team
</p>
<div
style={{
display: "flex",
alignItems: "center",
opacity: 0.7,
marginBottom: "10px",
}}
>
<AiOutlineCalendar style={{ marginRight: "5px" }} />
<p>Starting at 8:00pm</p>
</div>
<div
style={{
display: "flex",
alignItems: "center",
opacity: 0.7,
marginBottom: "10px",
}}
>
<ImLocation2 style={{ marginRight: "5px", color: "red" }} />
<p>Online (Discord Channel)</p>
</div>
<div
style={{
display: "flex",
alignItems: "center",
opacity: 0.7,
marginBottom: "10px",
}}
>
<BsCheckCircle style={{ marginRight: "5px", color: "green" }} />
<p>12 going</p>
</div>
</div>
</Link>
</div>
</div>
</>
);
};
export default Events;
The Create Event page 📅
This page displays a form field that accepts the details of the new event. Copy the code snippet below into the CreateEvent.jsx
file.
import Nav from "../components/Nav";
import { useState } from "react";
import { postNewEvent } from "../utils/util";
import { useNavigate } from "react-router-dom";
const CreateEvent = () => {
const [title, setTitle] = useState("");
const navigate = useNavigate();
const [location, setLocation] = useState("");
const [category, setCategory] = useState("");
const [description, setDescription] = useState("");
const [startTime, setStartTime] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
console.log({ title, location, category, startTime, description });
setTitle("");
setLocation("");
setCategory("");
setDescription("");
setStartTime("");
};
return (
<div className='create_event'>
<Nav />
<div style={{ padding: "30px" }}>
<h2
style={{
textAlign: "center",
marginBottom: "30px",
color: "#1d5d9b",
}}
>
Create new event
</h2>
<form className='create_form' onSubmit={handleSubmit}>
<label htmlFor='title'>Title</label>
<input
type='text'
name='title'
id='title'
value={title}
onChange={(e) => setTitle(e.target.value)}
required
className='event_title'
/>
<label htmlFor='location'>Location</label>
<input
type='text'
name='location'
id='location'
value={location}
onChange={(e) => setLocation(e.target.value)}
className='event_title'
required
/>
<div
style={{
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
width: "50%",
marginRight: "7px",
}}
>
<label htmlFor='startTime'>Starting Time</label>
<input
type='time'
name='startTime'
id='startTime'
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className='event_title'
required
/>
</div>
<div
style={{ display: "flex", flexDirection: "column", width: "50%" }}
>
<label htmlFor='category'>Category</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className='event_title'
required
>
<option value='travel-and-outdoor'>Travel and Outdoor</option>
<option value='religion'>Religion</option>
<option value='sports-and-fitness'>Sports and Fitness</option>
<option value='social-activities'>Social Activities</option>
</select>
</div>
</div>
<label htmlFor='description'>Description</label>
<textarea
rows={8}
value={description}
onChange={(e) => setDescription(e.target.value)}
required
/>
<button className='createEventBtn' type='submit'>
Create Event
</button>
</form>
</div>
</div>
);
};
export default CreateEvent;
The Dashboard page
The Dashboard page shows the user's events and allows the user to create events.
import Nav from "../components/Nav";
import { Link } from "react-router-dom";
const Dashboard = () => {
return (
<div className='dashboard_container'>
<Nav />
<div className='dashboard_main'>
<section className='header_events'>
<h1 style={{ fontSize: "30px" }}>Your Events</h1>
<Link to='/create/event' className='link'>
Create new event
</Link>
</section>
<div>{/*--user's events*/}</div>
</div>
</div>
);
};
export default Dashboard;
The Event Details page 🗓️
This page displays information about an event, enables users to register for an event with just a click, and leave a comment about an event.
import Nav from "../components/Nav";
import event from "../assets/event.jpeg";
import { useState } from "react";
import { useParams } from "react-router-dom";
const EventDetails = () => {
const [comment, setComment] = useState("");
const { slug } = useParams();
const addComment = (e) => {
e.preventDefault();
console.log(comment, slug);
};
return (
<div>
<Nav />
<header className='details_header'>
<h2 style={{ marginBottom: "15px" }}>Title</h2>
<p style={{ opacity: 0.6 }}>
Hosted by: <span style={{ fontWeight: "bold" }}>Host</span>
</p>
</header>
<main className='details_main'>
<div className='details_content'>
<img src={event} alt='Event' className='details_image' />
<div style={{ marginBottom: "30px" }}>Description</div>
<div style={{ padding: "30px 0" }}>
<h2 style={{ color: "#1d5d9b", marginBottom: "15px" }}>
Attendees
</h2>
<p style={{ opacity: 0.6 }}>Attendees</p>
</div>
<div className='comments'>
<h2 style={{ color: "#1d5d9b" }}>Comments</h2>
<form className='comment_form' onSubmit={addComment}>
<textarea
rows={4}
className='commentInput'
value={comment}
onChange={(e) => setComment(e.target.value)}
required
/>
<button className='buttons commentBtn'>Comment</button>
</form>
<div className='comment_section'>
<div
style={{
padding: "15px",
border: "1px solid #ddd",
borderRadius: "3px",
marginBottom: "10px",
}}
key={comment.id}
>
<p style={{ color: "#1d5d9b", marginBottom: "3px" }}>@User</p>
<p style={{ opacity: 0.5 }}>Comment</p>
</div>
</div>
</div>
</div>
<div className='details_cta'>
<p style={{ marginBottom: "10px", opacity: "0.6" }}>
Click here to register
</p>
<button className='buttons registerBtn'>Register</button>
</div>
</main>
</div>
);
};
export default EventDetails;
Authentication and Authorization 🔑
SuperTokens is an open-source authentication service provider that enables you to add secure and seamless user authentication and session management to your software applications.
It also provides a prebuilt UI for various forms of authentication, such as email and password login, social login, and passwordless login. With SuperTokens, you can add authentication to both web and mobile applications in a few minutes.
Adding it to our app 🎉
Here, you'll learn how to add authentication to your React and Node.js applications with SuperTokens. You can add SuperTokens automatically to your application or manually to an existing project.
To install SuperTokens automatically, run the code snippet below to install a starter app.
npx create-supertokens-app@latest --recipe=emailpassword
💡 PS: The recipe flag represents the authentication method you want to set up with SuperTokens. You check the documentation for a complete guide.
Configuring SuperTokens
Go to the homepage and create an account.
Head to your dashboard and fill the Get Started form accordingly.
Next, switch to the Core Configuration Details menu tab to generate your Core connectionURI
and Core API key.
Connect SuperTokens with React app 📳
Install SuperTokens to the React app by running the code snippet below.
npm i -s supertokens-auth-react
Add the code snippet below to the App.jsx
file.
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui";
import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui";
import * as reactRouterDom from "react-router-dom";
import EmailPassword from "supertokens-auth-react/recipe/emailpassword";
import Session from "supertokens-auth-react/recipe/session";
import { SessionAuth } from "supertokens-auth-react/recipe/session";
SuperTokens.init({
appInfo: {
appName: "meetup-clone",
apiDomain: "http://localhost:4000",
websiteDomain: "http://localhost:5173",
apiBasePath: "/auth",
websiteBasePath: "/",
},
recipeList: [EmailPassword.init(), Session.init()],
});
The code snippet above initialises SuperTokens with Email & Password within the React app.
Finally, update the routes as done below.
return (
<SuperTokensWrapper>
<Router>
<Routes>
{getSuperTokensRoutesForReactRouterDom(reactRouterDom, [
EmailPasswordPreBuiltUI,
])}
<Route
path='/'
element={
<SessionAuth>
<Home />
</SessionAuth>
}
/>
<Route path='/' element={<Home />} />
<Route
path='/dashboard'
element={
<SessionAuth>
<Dashboard />
</SessionAuth>
}
/>
<Route
path='/events/all'
element={
<SessionAuth>
<Events />
</SessionAuth>
}
/>
<Route
path='/events/:category'
element={
<SessionAuth>
<EventsCategory />
</SessionAuth>
}
/>
<Route
path='/create/event'
element={
<SessionAuth>
<CreateEvent />
</SessionAuth>
}
/>
<Route
path='/event/:slug'
element={
<SessionAuth>
<EventDetails />
</SessionAuth>
}
/>
</Routes>
</Router>
</SuperTokensWrapper>
);
From the code snippet above, I wrapped all the routes with the <SessionAuth/>
component provided by SuperTokens to protect them from unauthenticated users until they sign into the application.
Adding SuperTokens to the Node.js server
Install SuperTokens to the Node.js app by running the code below.
npm i -s supertokens-node
Add the code snippet below to initialise SuperTokens.
const supertokens = require("supertokens-node");
const Session = require("supertokens-node/recipe/session");
const EmailPassword = require("supertokens-node/recipe/emailpassword");
const { middleware } = require("supertokens-node/framework/express");
const { errorHandler } = require("supertokens-node/framework/express");
supertokens.init({
framework: "express",
supertokens: {
connectionURI: "<YOUR_CONNECTION_URL>",
apiKey: "<YOUR_API_KEY>",
},
appInfo: {
appName: "meetup-clone",
apiDomain: "http://localhost:4000",
websiteDomain: "http://localhost:5173",
apiBasePath: "/auth",
websiteBasePath: "/",
},
recipeList: [EmailPassword.init(), Session.init()],
});
Update the server CORS as shown below.
app.use(
cors({
origin: "http://localhost:5173",
allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()],
credentials: true,
})
);
// IMPORTANT: CORS should be before the below line.
app.use(middleware());
Finally, add the error handler provided by SuperTokens.
// ...your API routes
// Add this AFTER all your routes
app.use(errorHandler());
Congratulations!🎉 You've successfully added SuperTokens to the React and Node.js application. If you encounter any issues, feel free to follow the SuperTokens installation guide.
Communicating with the Node.js server
In this section, you'll learn how to communicate with the Node.js server by retrieving and creating events within the application.
Before we begin, create a utils
folder containing a util.js
file within the React app.
cd client
mkdir utils
cd utils
touch util.js
Getting existing events
Create an events array within the index.js
file on the server containing the event's attributes.
const events = [
{
id: generateID(),
title: "Novu Community Call",
slug: "novu-community-call",
host: "Novu Development Team",
category: "social-activities",
start_time: "8:00pm",
location: "Online (Discord Channel)",
comments: [
{ user: "nevodavid", id: generateID(), comment: "Can't wait!😍" },
{ user: "emil_pearce", id: generateID(), comment: "Let's go!🚀" },
],
attendees: [
"nevodavid",
"emil_pearce",
"tomer_barnea",
"unicodeveloper",
"scopsy",
],
description:
"Dear attendee,\n We hope this message finds you well! We're excited to invite you to our upcoming Novu Community Call, where we will come together to share insights, updates, and engage in meaningful discussions. Your presence and contributions are highly valued as we continue to grow and strengthen our vibrant Novu community.",
},
{
id: generateID(),
title: "Novu Team Hangout",
slug: "novu-team-hangout",
host: "Novu Team",
category: "social-activities",
start_time: "12:30pm",
location: "Online (Google Meet)",
comments: [
{ user: "nevodavid", id: generateID(), comment: "Can't wait!😍" },
{ user: "emil_pearce", id: generateID(), comment: "Let's go!🚀" },
],
attendees: ["nevodavid", "tomer_barnea", "unicodeveloper", "scopsy"],
description:
"Dear attendee,\n We hope this message finds you well! We're excited to invite you to our upcoming Novu Community Call, where we will come together to share insights, updates, and engage in meaningful discussions. Your presence and contributions are highly valued as we continue to grow and strengthen our vibrant Novu community.",
},
];
Add another endpoint that returns the events in a JSON format.
app.get("/events", (req, res) => {
res.json({
message: "Success!",
events,
});
});
Next, create a function within the utils/util.js
file that sends a request to the endpoint from the React app.
export const fetchEvents = (setEvents) => {
fetch("http://localhost:4000/events")
.then((res) => res.json())
.then((data) => {
if (data.message) {
setEvents(data.events);
}
})
.catch((err) => console.error(err));
};
Finally, execute the function when the Home component mounts.
const Home = () => {
const [events, setEvents] = useState([]);
//generates a random string as ID
const generateID = () => Math.random().toString(36).substring(2, 10);
useEffect(() => {
fetchEvents(setEvents);
//save a user_id property to the database
if (!localStorage.getItem("user_id")) {
localStorage.setItem("user_id", generateID());
}
}, []);
return <div>{/*--render events from the server--*/}</div>;
};
The code snippet above retrieves all the events from the server and creates a user_id
property on the web browser to enable us to identify each user.
💡 PS: I'm using local storage because this is a small application. If you are using SuperTokens in a production environment, kindly check the SuperTokens backend guide.
Getting events by categories
To fetch the events under a particular category, the /events/:category
client route accepts the category name as part of its path name and sends it to the server to return all the events under the category.
<Route
path='/events/:category'
element={
<SessionAuth>
<EventsCategory />
</SessionAuth>
}
/>
Add an endpoint on the server that retrieves the events based on their category name.
app.post("/event/category", (req, res) => {
const { category } = req.body;
const result = events.filter((e) => e.category === category);
res.json({ message: "Success!", events: result });
});
Send a request to the endpoint on the server and display the events under a particular category.
export const fetchEventByCategory = (category, setEvents) => {
fetch("http://localhost:4000/event/category", {
method: "POST",
body: JSON.stringify({ category }),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((data) => {
if (data.message) {
console.log(data.events);
setEvents(data.events);
}
})
.catch((err) => console.error(err));
};
Retrieving event details via slug
To do this, add a POST route to the server that filters the events via the slug received from the React app.
app.post("/event/slug", (req, res) => {
const { slug } = req.body;
const result = events.filter((e) => e.slug === slug);
res.json({ message: "Success!", event: result[0] });
});
The code snippet above uses the event's slug to retrieve its entire event object from the server.
Create a function within the utils/util.js
that sends a request to the endpoint on the server.
//👇🏻Within the util.js file
export const fetchEventBySlug = (slug, setEvent) => {
fetch("http://localhost:4000/event/slug", {
method: "POST",
body: JSON.stringify({ slug }),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((data) => {
if (data.message) {
setEvent(data.event);
}
})
.catch((err) => console.error(err));
};
Execute the function on component mount within the EventDetails
component and display the properties accordingly.
import { useNavigate, useParams } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom"
import { useState, useEffect } from "react"
import { fetchEventBySlug } from "../utils/util"
export const EventDetails = () => {
const [eventDetails, setEventDetails] = useState({});
const { slug } = useParams();
useEffect(() => {
fetchEventBySlug(slug, setEventDetails);
setLoading(false);
}, [slug]);
return <div>{/*--displays event details --*/}</div>;
};
Creating new events
Create an endpoint that adds a new event to the events
array.
//👇🏻 generates slug from a text
const createSlug = (text) => {
let slug = text
.trim()
.toLowerCase()
.replace(/[^\w\s-]/g, "");
slug = slug.replace(/\s+/g, "-");
return slug;
};
//👇🏻 generates a random string as ID
const generateID = () => Math.random().toString(36).substring(2, 10);
//👇🏻 endpoint for creating new events
app.post("/create/event", async (req, res) => {
const { title, location, startTime, category, description, host } = req.body;
const eventObject = {
id: generateID(),
title,
slug: createSlug(title),
host,
category,
start_time: startTime,
location,
comments: [],
attendees: [],
description,
};
events.unshift(eventObject);
res.json({ message: "Event added successfully!✅" });
});
The code snippet above accepts the event attributes from the form within the CreateEvent
component and adds the event to the events
array when the user submits the form.
Execute the function below when the user submits the form.
//👇🏻 runs when a user submits the form
const handleSubmit = (e) => {
e.preventDefault();
postNewEvent(
title,
location,
category,
startTime,
description,
localStorage.getItem("user_id")
);
};
//👇🏻 makes a request to the server
const postNewEvent = () => {
fetch("http://localhost:4000/create/event", {
method: "POST",
body: JSON.stringify({
title,
location,
category,
startTime,
description,
host,
}),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((data) => {
if (data.message) {
alert(data.message);
navigate("/dashboard");
}
})
.catch((err) => console.error(err));
};
Adding comments to events
To add comments to an event, create another endpoint that accepts the user's id, the event's slug, and the comment from the React app.
app.post("/event/comment", async (req, res) => {
const { comment, user, slug } = req.body;
for (let i = 0; i < events.length; i++) {
if (events[i].slug === slug) {
events[i].comments.unshift({
user,
id: generateID(),
comment,
});
return res.json({ message: "Comment added successfully!✅" });
}
}
The code snippet above retrieves the event with the same slug from the request and updates the comment
property with the latest comment.
Execute the function when a user drops a new comment.
const addComment = (e) => {
e.preventDefault();
postNewComment(comment, localStorage.getItem("user_id"), slug);
};
const postNewComment = (comment, user, slug) => {
fetch("http://localhost:4000/event/comment", {
method: "POST",
body: JSON.stringify({ comment, user, slug }),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((data) => {
if (data.message) {
alert(data.message);
}
})
.catch((err) => console.error(err));
};
Registering for an event
When a user registers for an event, the user's id is added to the attendees
property (array) on the particular event. Although, you need to check if the user hasn't registered before you update the attendees
property.
Add a POST route to the server that carries out the function.
app.post("/register/event", async (req, res) => {
const { userID, eventID } = req.body;
for (let i = 0; i < events.length; i++) {
if (events[i].id === eventID) {
const validate = events[i].attendees.filter((user) => user === userID);
if (validate.length === 0) {
events[i].attendees.push(user);
return res.json({ message: "Registered successfully!✅" });
}
} else {
return res.json({ message: "You cannot register twice ❌" });
}
}
}
});
The code snippet above accepts the event and user id from the React app, filters the events array via the id, and updates the attendee's list on the event with the matching id.
Execute the function below when a user clicks the Register button.
const eventRegister = (user, id) => {
fetch("http://localhost:4000/register/event", {
method: "POST",
body: JSON.stringify({ user, id }),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((data) => {
if (data.message) {
alert(data.message);
navigate("/");
}
})
.catch((err) => console.error(err));
};
Retrieving users' events
Within the Dashboard
component, you can display all the events the user registered for.
First, create an endpoint that accepts the user's id, loops through the entire event, and returns the user's events.
app.post("/user/events", (req, res) => {
const { userID } = req.body;
let userEvents = [];
for (let i = 0; i < events.length; i++) {
let result = events[i].attendees.filter((user) => user === userID);
if (result.length > 0) {
userEvents.push(events[i]);
}
}
res.json({ message: "Successful", events: userEvents });
});
Display the events returned from the server when the Dashboard
page loads.
const fetchMyEvents = (userID, setEvents) => {
fetch("http://localhost:4000/user/events", {
method: "POST",
body: JSON.stringify({ userID }),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((data) => {
if (data.message) {
console.log(data);
setEvents(data.events);
}
})
.catch((err) => console.error(err));
};
Congratulations on making it thus far!🎉 In the upcoming section, you'll learn how to alert users when someone comments and registers for their events with Novu.
Trick 2: Adding notifications to your app ℹ️
Here, we need to notify the users when someone comments and registers for their events. Also, you can alert everyone when there is a new event.
To do this, we'll use Novu - an open-source notification infrastructure that enables you to send in-app, SMS, chat, push, and e-mail notifications from a single dashboard.
Initiate it ⚡️
Navigate into the client
folder and create a Novu project by running the code below.
cd client
npx novu init
Enter your application name and sign in to your Novu dashboard. The code snippet below contains the steps you should follow after running npx novu init.
Now let's setup your account and send your first notification
? What is your application name? Meetup Clone
? Now lets setup your environment. How would you like to proceed? Create a free cloud account (Recommended)
? Create your account with: Sign-in with GitHub
? I accept the Terms and Conditions (https://novu.co/terms) and have read the Privacy Policy (https://novu.co/privacy) Yes
✔ Created your account successfully.
Visit the demo page, copy your subscriber ID from the page, and click the Skip Tutorial
button.
Create a notification template with a workflow as shown below:
💡PS: You'll need to create three different notification templates to be triggered when someone comments on an event, registers for an event, and creates a new event.
Novu Digest allows you to control how you send notifications in your app. It collects multiple trigger events, schedules them, or sends them as a single message.
Update the In-App notification step to send this message to the event host when someone comments on their event.
{{user}} commented on your event.
Adding Novu notification bell to a React app
Novu in-app notification uses a notification bell to send alerts to users. Here, you'll learn how to add it to your React applications.
Install the Novu Notification package.
npm install @novu/notification-center
Create a Novu.jsx
file within the components folder and copy the below into the file.
import React from "react";
import {
NovuProvider,
PopoverNotificationCenter,
NotificationBell,
} from "@novu/notification-center";
import { useNavigate } from "react-router-dom";
function Novu() {
const navigate = useNavigate();
const onNotificationClick = (notification) =>
navigate(notification.cta.data.url);
return (
<>
<NovuProvider
subscriberId='<YOUR_SUBSCRIBER_ID>'
applicationIdentifier='<YOUR_APP_ID>'
>
<PopoverNotificationCenter
onNotificationClick={onNotificationClick}
colorScheme='light'
>
{({ unseenCount }) => <NotificationBell unseenCount={unseenCount} />}
</PopoverNotificationCenter>
</NovuProvider>
</>
);
}
The code snippet above enables us to add Novu's notification bell icon to the application. With this, you can view all the notifications within the app.
Select Settings on your Novu Admin Panel to copy your App ID and replace the subscriber's ID placeholder with yours.
Import the Novu.jsx
component into the Nav.jsx
component.
import meetup from "../assets/meetup.png"
import { Link } from "react-router-dom"
import Novu from "./Novu"
const Nav = () => {
return (
<nav className='navbar'>
<Link to="/">
<img src={meetup} alt="Meetup" className="logo"/>
</Link>
<div className="navBtn">
<Novu />
<button className="buttons signUpBtn">Log out</button>
</div>
</nav>
)
}
export default Nav
Configuring Novu on a Node.js server
Install the Novu SDK for Node.js into the server folder.
npm install @novu/node
Import Novu from the package and create an instance using your API Key.
const { Novu } = require("@novu/node");
const novu = new Novu("<YOUR_API_KEY>");
Create a function within the index.js
file that sends notification to the event host via Novu.
const addCommentNotification = async (userID) => {
await novu.subscribers.identify(userID, {
firstName: "inAppSubscriber",
});
const response = await novu.trigger("notify", {
to: {
subscriberId: "<YOUR_SUBSCRIBER_ID>",
},
payload: {
user: userID,
},
});
return response.data.data;
};
Execute the function when a user comments on an event.
app.post("/event/comment", async (req, res) => {
const { comment, user, slug } = req.body;
for (let i = 0; i < events.length; i++) {
if (events[i].slug === slug) {
events[i].comments.unshift({
user,
id: generateID(),
comment,
});
//👇🏻 sends notification via Novu
const sendNotification = await addCommentNotification(user);
if (sendNotification.acknowledged) {
return res.json({ message: "Comment added successfully!✅" });
}
}
}
});
Congratulations! You've completed the application.🎉
Conclusion
So far, you've learnt how to authenticate users with SuperTokens, communicate between a React and Node.js app, and send in-app notifications using the Novu Digest.
Novu enables you to create a rich notification system in your applications, thereby providing a great user experience for your users. You should also try out SuperTokens - it is easy to integrate and offers a seamless authentication process to your users.
The source code for this tutorial is available here:
https://github.com/novuhq/blog/tree/main/meetup-clone-react-supertokens-nodejs.
Thank you for reading!
Top comments (15)
Nice. SuperTokens also seems like a great open-source solution!
Thank you so much Vinny!
While the promise of building a Meetup.com clone with React in just 30 minutes is undoubtedly intriguing, it's crucial to approach such 'tricks' with a balanced perspective. Rapid development can be a great learning exercise, but it's equally important to emphasize the value of understanding the foundational concepts and best practices behind the tools we use. React is a powerful framework that empowers developers to create dynamic and interactive web applications, and learning it thoroughly can lead to more robust and maintainable solutions. So, while shortcuts are exciting, let's also celebrate the journey of mastering the technologies that drive our innovations.
Nice
Well Explained hey !
Novu + Open Source = Great Product
Well done !
This is a good article.
Great article, superb headline 🔥
Great article 💯
How did you animate your cover ? It looks very nice. I was wondering if this was AI, looks like it is. Which tool ?
Great..thanks for sharing, perharp has an example using next.js?
Wow, I really like this project. SuperTokens is also nice. I have used it in past for a project.
This is super cool. Novu also seems like an amazing solution, and I love that it is open source.
Crazy bro!