DEV Community

Cover image for 🔥 2 TRICKS 🔥 to build a Meetup.com clone with React in 30 minutes 🪄✨
Nevo David for novu

Posted on

🔥 2 TRICKS 🔥 to build a Meetup.com clone with React in 30 minutes 🪄✨

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!

Meetup


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.

Like


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
Enter fullscreen mode Exit fullscreen mode

Adding a Node.js server 🔌

Navigate into the server folder and create a package.json file.

cd server & npm init -y
Enter fullscreen mode Exit fullscreen mode

Install Express, Nodemon, and the CORS library.

npm install express cors nodemon
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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"
  },
Enter fullscreen mode Exit fullscreen mode

Congratulations!🎉 You can now start the server by using the command below.

npm start
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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 and EventsCategories.jsx components are similar. The Events.jsx component displays all the available events, and the EventsCategories.jsx displays all the events under a particular category.

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Home Page

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;
Enter fullscreen mode Exit fullscreen mode

The Create Event page

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;
Enter fullscreen mode Exit fullscreen mode

The Dashboard page

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;
Enter fullscreen mode Exit fullscreen mode

The Event Details page

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

💡 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.

Configuring SuperTokens

Next, switch to the Core Configuration Details menu tab to generate your Core connectionURI and Core API key.

connectionURI

Connect SuperTokens with React app 📳

Install SuperTokens to the React app by running the code snippet below.

npm i -s supertokens-auth-react
Enter fullscreen mode Exit fullscreen mode

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()],
});
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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.

SessionAuth

Adding SuperTokens to the Node.js server

Install SuperTokens to the Node.js app by running the code below.

npm i -s supertokens-node
Enter fullscreen mode Exit fullscreen mode

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()],
});
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

Finally, add the error handler provided by SuperTokens.

// ...your API routes

// Add this AFTER all your routes
app.use(errorHandler());
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.",
    },
];
Enter fullscreen mode Exit fullscreen mode

Add another endpoint that returns the events in a JSON format.

app.get("/events", (req, res) => {
    res.json({
        message: "Success!",
        events,
    });
});
Enter fullscreen mode Exit fullscreen mode

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));
};
Enter fullscreen mode Exit fullscreen mode

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>;
};
Enter fullscreen mode Exit fullscreen mode

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>
    }
/>
Enter fullscreen mode Exit fullscreen mode

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 });
});
Enter fullscreen mode Exit fullscreen mode

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));
};
Enter fullscreen mode Exit fullscreen mode

Category

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] });
});
Enter fullscreen mode Exit fullscreen mode

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));
};
Enter fullscreen mode Exit fullscreen mode

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>;
};
Enter fullscreen mode Exit fullscreen mode

Creating new events

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!✅" });
});
Enter fullscreen mode Exit fullscreen mode

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.

CreateEvent

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));
};
Enter fullscreen mode Exit fullscreen mode

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!✅" });
        }
    }
Enter fullscreen mode Exit fullscreen mode

The code snippet above retrieves the event with the same slug from the request and updates the comment property with the latest comment.

Add 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));
};
Enter fullscreen mode Exit fullscreen mode

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 ❌" });
            }
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

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));
};
Enter fullscreen mode Exit fullscreen mode

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 });
});
Enter fullscreen mode Exit fullscreen mode

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));
};
Enter fullscreen mode Exit fullscreen mode

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 fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Visit the demo page, copy your subscriber ID from the page, and click the Skip Tutorial button.

Notification template

Create a notification template with a workflow as shown below:

different

💡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.
Enter fullscreen mode Exit fullscreen mode

Novu notification bell

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
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    );
}
Enter fullscreen mode Exit fullscreen mode

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.jsxcomponent.

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
Enter fullscreen mode Exit fullscreen mode

Configuring Novu on a Node.js server

Configuring Novu on a Node.js server

Install the Novu SDK for Node.js into the server folder.

npm install @novu/node
Enter fullscreen mode Exit fullscreen mode

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>");
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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!✅" });
            }
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

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!


Like

Top comments (16)

Collapse
 
vincanger profile image
vincanger

Nice. SuperTokens also seems like a great open-source solution!

Collapse
 
nevodavid profile image
Nevo David

Thank you so much Vinny!

Collapse
 
iamcymentho profile image
Odumosu Matthew

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.

Collapse
 
anandhureji profile image
anandhureji

Nice

Collapse
 
keyurparalkar profile image
Keyur Paralkar

Great article 💯

Collapse
 
syaddadh1211 profile image
Syaddad Hilmi Bahalwan

Great..thanks for sharing, perharp has an example using next.js?

Collapse
 
ashutoshmishra profile image
Ashutosh Mishra • Edited

Wow, I really like this project. SuperTokens is also nice. I have used it in past for a project.

Collapse
 
matijasos profile image
Matija Sosic

This is super cool. Novu also seems like an amazing solution, and I love that it is open source.

Collapse
 
bcouetil profile image
Benoit COUETIL 💫 • Edited

How did you animate your cover ? It looks very nice. I was wondering if this was AI, looks like it is. Which tool ?

Collapse
 
respect17 profile image
Kudzai Murimi

Well Explained hey !

Collapse
 
devdynamics profile image
DevDynamics

Novu + Open Source = Great Product

Collapse
 
respect17 profile image
Kudzai Murimi

Well done !

Collapse
 
mfts profile image
Marc Seitz

Great article, superb headline 🔥

Collapse
 
sohrab09 profile image
Mohammad Sohrab Hossain

This is a good article.

Collapse
 
waleedbutt profile image
Waleed Butt

Nice, amazing

Collapse
 
dhiru06 profile image
Dheeraj Dommaraju

Crazy bro!