使用 React、Nodejs 和 EmailJS 创建一个调度应用程序

2025-05-25

使用 React、Nodejs 和 EmailJS 创建一个调度应用程序

这篇文章是关于什么的?

在本文中,您将学习如何构建一个日程安排应用程序,该应用程序允许您设置您的空闲时间并分享您的个人资料链接,以便其他人与您预约。当有人安排预约时,您还会收到电子邮件通知。

图片描述

Novu——第一个开源通知基础设施

简单介绍一下我们。Novu 是第一个开源通知基础设施。我们主要负责管理所有产品通知。这些通知可以是应用内通知(类似 Facebook 的铃铛图标 - Websockets)、电子邮件、短信等等。

诺武

如果你能给我们一颗星,我会非常高兴!也请在评论区告诉我❤️
https://github.com/novuhq/novu

项目设置

在这里,我将指导您创建调度应用程序的项目环境。

通过运行以下代码为调度应用程序创建项目文件夹:

mkdir scheduling-app
cd scheduling-app
mkdir client server
Enter fullscreen mode Exit fullscreen mode

设置 Node.js 服务器

导航到服务器文件夹并创建一个package.json文件。

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

安装 Express、Nodemon 和 CORS 库。

npm install express cors nodemon
Enter fullscreen mode Exit fullscreen mode

ExpressJS是一个快速、简约的框架,它提供了在 Node.js 中构建 Web 应用程序的多种功能,  CORS是一个允许不同域之间通信的 Node.js 包,  Nodemon是一个在检测到文件更改后自动重启服务器的 Node.js 工具。

创建一个index.js文件——Web 服务器的入口点。

touch index.js
Enter fullscreen mode Exit fullscreen mode

使用 Express.js 设置 Node.js 服务器。当您http://localhost:4000/api在浏览器中访问时,下面的代码片段会返回一个 JSON 对象。

//👇🏻index.js
const express = require("express");
const app = express();
const PORT = 4000;

app.use(express.urlencoded({ extended: true }));
app.use(express.json());

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

我们将在本教程的后面创建各种 API 路由。现在,让我们设计应用程序的用户界面。

设置 React 应用程序

在这里,我将指导您使用 React.js 创建应用程序的用户界面。

通过终端导航到客户端文件夹并创建一个新的 React.js 项目。

cd client
npx create-react-app ./
Enter fullscreen mode Exit fullscreen mode

安装 React Router、React-Toastify 和 React Timezone Select。

npm install react-toastify react-router-dom react-timezone-select
Enter fullscreen mode Exit fullscreen mode

React Router 是一个 JavaScript 库,它使我们能够在 React 应用程序中的页面之间导航,而  React Toastify用于向用户显示彩色通知。React  Timezone Select 是一个简单的库,它为每个位置提供各种可用的时区。

从 React 应用程序中删除冗余文件(例如徽标和测试文件),并更新App.js文件以显示如下所示的 Hello World。

function App() {
    return (
        <div>
            <p>Hello World!</p>
        </div>
    );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

将此处项目样式所需的 CSS 文件复制 src/index.css文件中。

构建用户界面

在本节中,我将引导您创建构建应用程序所需的各种组件。

更新App.js文件以呈现以下组件:

import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
//👇🏻 component
import Dashboard from "./components/Dashboard";
import Login from "./components/Login";
import Signup from "./components/Signup";
import Profile from "./components/Profile";
import BookUser from "./components/BookUser";
//👇🏻 React-Toastify configuration
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";

const App = () => {
    return (
        <div>
            <BrowserRouter>
                <Routes>
                    <Route path='/' element={<Login />} />
                    <Route path='/register' element={<Signup />} />
                    <Route path='/dashboard' element={<Dashboard />} />
                    <Route path='/profile/:id' element={<Profile />} />
                    <Route path='/book/:user' element={<BookUser />} />
                </Routes>
            </BrowserRouter>
            <ToastContainer />
        </div>
    );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

从上面的代码片段中,我导入了Login、、、和组件。创建一个包含这些文件的components 文件夹Signup如下所示:DashboardProfileBookUser

cd client
mkdir components
cd components
touch Login.js Signup.js Dashboard.js Profile.js BookUser.js
Enter fullscreen mode Exit fullscreen mode
  • 登录和注册组件是身份验证路由。
  • 仪表板组件是向经过身份验证的用户显示的主页 - 他们可以在此设置他们的可用性。
  • Profile 组件向用户显示可用性,而 BookUser 组件允许其他人与他们安排约会。

身份验证组件 - 登录和注册

将以下代码复制到Login.js文件中以接受用户的用户名和密码。

import React, { useState } from "react";
import { useNavigate, Link } from "react-router-dom";

const Login = () => {
    const [username, setUsername] = useState("");
    const [password, setPassword] = useState("");
    const navigate = useNavigate();

    const handleSubmit = (e) => {
        if (username.trim() && password.trim()) {
            e.preventDefault();
            console.log({ username, password });
            setPassword("");
            setUsername("");
        }
    };

    return (
        <main className='login'>
            <form className='login__form' onSubmit={handleSubmit}>
                <h2 className='login__title'>Log into your account</h2>
                <label htmlFor='username'>Username</label>
                <input
                    id='username'
                    name='username'
                    type='text'
                    value={username}
                    onChange={(e) => setUsername(e.target.value)}
                    className='username'
                />
                <label htmlFor='password'>Password</label>
                <input
                    id='password'
                    type='password'
                    name='password'
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                    className='password'
                />
                <button className='loginButton'>LOG IN</button>
                <p style={{ textAlign: "center", marginTop: "30px" }}>
                    Don't have an account?{" "}
                    <Link className='link' to='/register'>
                        Create one
                    </Link>
                </p>
            </form>
        </main>
    );
};

export default Login;
Enter fullscreen mode Exit fullscreen mode

日程

更新Signup.js组件以允许用户使用他们的用户名、电子邮件和密码创建帐户。

import React, { useState } from "react";
import { useNavigate, Link } from "react-router-dom";
import { handleRegister } from "../utils/resource";

const Signup = () => {
    const [username, setUsername] = useState("");
    const [password, setPassword] = useState("");
    const [email, setEmail] = useState("");
    const navigate = useNavigate();

    const handleSubmit = (e) => {
        e.preventDefault();
        if (username.trim() && password.trim() && email.trim()) {
            console.log(email, username, password);
            setPassword("");
            setUsername("");
            setEmail("");
        }
    };

    return (
        <main className='signup'>
            <form className='signup__form' onSubmit={handleSubmit}>
                <h2 className='signup__title'>Create an account</h2>
                <label htmlFor='email'>Email Address</label>
                <input
                    id='email'
                    name='email'
                    type='email'
                    required
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                />
                <label htmlFor='username'>Username</label>
                <input
                    id='username'
                    name='username'
                    required
                    type='text'
                    value={username}
                    onChange={(e) => setUsername(e.target.value)}
                />
                <label htmlFor='password'>Password</label>
                <input
                    id='password'
                    type='password'
                    name='password'
                    required
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                />
                <button className='signupButton'>REGISTER</button>
                <p style={{ textAlign: "center", marginTop: "30px" }}>
                    Already have an account?{" "}
                    <Link className='link' to='/'>
                        Sign in
                    </Link>
                </p>
            </form>
        </main>
    );
};

export default Signup;
Enter fullscreen mode Exit fullscreen mode

登记

仪表板组件

在这里,我们将创建一个用户界面,允许用户根据其位置或首选时区设置其可用性。React  Timezone Select允许我们从每个位置的时区列表中进行选择。

Dashboard.js按照如下所示更新组件:

import React, { useState, useEffect } from "react";
import TimezoneSelect from "react-timezone-select";
import { useNavigate } from "react-router-dom";

const Dashboard = () => {
    const [selectedTimezone, setSelectedTimezone] = useState({});
    const navigate = useNavigate();

    //👇🏻 Runs when a user sign out
    const handleLogout = () => {
        localStorage.removeItem("_id");
        localStorage.removeItem("_myEmail");
        navigate("/");
    };

    return (
        <div>
            <nav className='dashboard__nav'>
                <h2>BookMe</h2>
                <button onClick={handleLogout} className='logout__btn'>
                    Log out
                </button>
            </nav>
            <main className='dashboard__main'>
                <h2 className='dashboard__heading'>Select your availability</h2>

                <div className='timezone__wrapper'>
                    <p>Pick your timezone</p>
                    <TimezoneSelect
                        value={selectedTimezone}
                        onChange={setSelectedTimezone}
                    />
                </div>
            </main>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

上面的代码片段显示了组件,如下图所示。该handleLogout函数通过从本地存储中删除电子邮件和 ID 来将用户从应用程序中注销。

代码片段

在时区选择字段下方,我们需要创建一组输入字段,允许用户设置每天的可用时间或工作时间。

可用性

为此,请在仪表板组件内创建一个状态来保存每天的时间表。

const [schedule, setSchedule] = useState([
    { day: "Sun", startTime: "", endTime: "" },
    { day: "Mon", startTime: "", endTime: "" },
    { day: "Tue", startTime: "", endTime: "" },
    { day: "Wed", startTime: "", endTime: "" },
    { day: "Thu", startTime: "", endTime: "" },
    { day: "Fri", startTime: "", endTime: "" },
    { day: "Sat", startTime: "", endTime: "" },
]);
Enter fullscreen mode Exit fullscreen mode

创建一个utils包含resource.js文件的文件夹。该文件将包含向服务器发出 API 请求所需的异步函数。

cd src
mkdir utils
cd utils
touch resource.js
Enter fullscreen mode Exit fullscreen mode

创建一个可能的工作时间列表,供用户选择。

export const time = [
    { id: "null", t: "Select" },
    { id: "7", t: "7:00am" },
    { id: "8", t: "8:00am" },
    { id: "9", t: "9:00am" },
    { id: "10", t: "10:00am" },
    { id: "11", t: "11:00am" },
    { id: "12", t: "12:00pm" },
    { id: "13", t: "13:00pm" },
    { id: "14", t: "14:00pm" },
    { id: "15", t: "15:00pm" },
    { id: "16", t: "16:00pm" },
    { id: "17", t: "17:00pm" },
    { id: "18", t: "18:00pm" },
    { id: "19", t: "19:00pm" },
];
Enter fullscreen mode Exit fullscreen mode

更新Dashboard.js文件以显示每天的工作时间列表。

import { time } from "../utils/resource";
import { toast } from "react-toastify";

const Dashboard = () => {
    const [selectedTimezone, setSelectedTimezone] = useState({})
    //👇🏻 This updates the schedule array with the start and end time.
    const handleTimeChange = (e, id) => {
        const { name, value } = e.target;
        if (value === "Select") return;
        const list = [...schedule];
        list[id][name] = value;
        setSchedule(list);
    };
    //👇🏻 Logs the user's schedule to the console after setting the availability
    const handleSaveSchedules = () => {
        if (JSON.stringify(selectedTimezone) !== "{}") {
            console.log(schedule);
        } else {
            toast.error("Select your timezone");
        }
    };
    return (
        <div>
            <nav className='dashboard__nav'>
                <h2>BookMe</h2>
                <button onClick={handleLogout} className='logout__btn'>
                    Log out
                </button>
            </nav>
            <main className='dashboard__main'>
                <h2 className='dashboard__heading'>Select your availability</h2>
                <div className='timezone__wrapper'>
                    <p>Pick your timezone</p>
                    <TimezoneSelect
                        value={selectedTimezone}
                        onChange={setSelectedTimezone}
                    />

                    {schedule.map((sch, id) => (
                        <div className='form' key={id}>
                            <p>{sch.day}</p>
                            <div className='select__wrapper'>
                                <label htmlFor='startTime'>Start Time</label>
                                <select
                                    name='startTime'
                                    id='startTime'
                                    onChange={(e) => handleTimeChange(e, id)}
                                >
                                    {time.map((t) => (
                                        <option key={t.id} value={t.t} id={t.id}>
                                            {t.t}
                                        </option>
                                    ))}
                                </select>
                            </div>
                            <div className='select__wrapper'>
                                <label htmlFor='endTime'>End Time</label>
                                <select
                                    name='endTime'
                                    id='endTime'
                                    onChange={(e) => handleTimeChange(e, id)}
                                >
                                    {time.map((t) => (
                                        <option key={t.id} value={t.t} id={t.id}>
                                            {t.t}
                                        </option>
                                    ))}
                                </select>
                            </div>
                        </div>
                    ))}
                </div>

                <div className='saveBtn__container'>
                    <button onClick={handleSaveSchedules}>SAVE SCHEDULES</button>
                </div>
            </main>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

配置文件组件

Profile 组件是一个简单的组件,它显示用户的日程安排,如下所示:

调度

将下面的代码复制到Profile.js文件中。在本教程的后面,我们将从服务器获取其数据。

import React from "react";
import { useParams } from "react-router-dom";

const Profile = () => {
    //👇🏻 The ID is the URL parameter for fetching the user's details.
    const { id } = useParams();

    return (
        <main className='profile'>
            <div style={{ width: "70%" }}>
                <h2>Hey, nevodavid</h2>
                <p>Here is your schedule: WAT</p>
                <table>
                    <tbody>
                        <tr>
                            <td>MON</td>
                            <td>8:00am</td>
                            <td>10:00pm</td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </main>
    );
};

export default Profile;
Enter fullscreen mode Exit fullscreen mode

BookUser 组件

此页面根据 URL 中的用户名显示用户的可用性,并允许人们与该用户预约会议。

图书用户

将下面的代码复制到BookUser.js组件中。

import React, { useState } from "react";
import { useParams } from "react-router-dom";

const BookUser = () => {
    const [fullName, setFullName] = useState("");
    const [email, setEmail] = useState("");
    const [message, setMessage] = useState("");
    const { user } = useParams();

    //👇🏻 logs the user's details to the console
    const handleSubmit = (e) => {
        e.preventDefault();
        console.log(email, fullName, message);
        setFullName("");
        setMessage("");
    };

    return (
        <div className='bookContainer'>
            <h2 className='bookTitle'>Book a session with {user}</h2>
            <form onSubmit={handleSubmit} className='booking__form'>
                <label htmlFor='fullName'>Full Name</label>
                <input
                    id='fullName'
                    name='fullName'
                    type='text'
                    required
                    value={fullName}
                    onChange={(e) => setFullName(e.target.value)}
                />
                <label htmlFor='email'>Email Address</label>
                <input
                    id='email'
                    name='email'
                    required
                    type='email'
                    value={email}
                    onChange={(e) => setEmail(e.target.value)}
                />

                <label htmlFor='message'>Any important note? (optional)</label>
                <textarea
                    rows={5}
                    name='message'
                    id='message'
                    value={message}
                    onChange={(e) => setMessage(e.target.value)}
                />

                <label htmlFor='session'>
                    Select your preferred session - GMT+2 Jerusalem
                </label>

                <button className='bookingBtn'>SEND</button>
            </form>
        </div>
    );
};

export default BookUser;
Enter fullscreen mode Exit fullscreen mode

上面的代码片段显示了一个预订表单,用于输入客户的全名、电子邮件和消息。在本教程的后面,我们将改进该组件,以便与用户预订会话并向用户发送确认电子邮件。

ErrorPage 组件

当发生错误时,此组件会显示给用户。

import React from "react";
import { Link } from "react-router-dom";

const ErrorPage = ({ error }) => {
    return (
        <div className='errorContainer'>
            <h2 style={{ marginBottom: "30px" }}>{error}</h2>
            <Link to='/'>Go Home</Link>
        </div>
    );
};

export default ErrorPage;
Enter fullscreen mode Exit fullscreen mode

使用 React 和 Node.js 进行用户身份验证

在这里,我将指导您验证用户身份以及如何仅允许授权用户访问 Web 应用程序中受保护的页面。

创建新用户

在服务器上添加一个注册 POST 路由,从 React 应用程序接受用户的用户名、电子邮件和密码。

app.post("/register", (req, res) => {
    const { username, email, password } = req.body;
    console.log(req.body);
});
Enter fullscreen mode Exit fullscreen mode

在文件中创建一个utils/resource.js接受用户凭证的异步函数。

export async function handleRegister(email, username, password, navigate) {
    //...data
}
Enter fullscreen mode Exit fullscreen mode

handleRegister函数导入Signup组件并传入所需参数。

import { handleRegister } from "../utils/resource";
import { useNavigate } from "react-router-dom";

const navigate = useNavigate();

const handleSubmit = (e) => {
    e.preventDefault();
    if (username.trim() && password.trim() && email.trim()) {
        handleRegister(email, username, password, navigate);
        setPassword("");
        setUsername("");
        setEmail("");
    }
};
Enter fullscreen mode Exit fullscreen mode

更新handleRegister函数以向服务器发出 POST 请求。

export async function handleRegister(email, username, password, navigate) {
    try {
        const request = await fetch("http://localhost:4000/register", {
            method: "POST",
            body: JSON.stringify({
                email,
                username,
                password,
            }),
            headers: {
                Accept: "application/json",
                "Content-Type": "application/json",
            },
        });
        const data = await request.json();
        if (data.error_message) {
            toast.error(data.error_message);
        } else {
            toast.success(data.message);
            navigate("/");
        }
    } catch (err) {
        console.error(err);
        toast.error("Account creation failed");
    }
}
Enter fullscreen mode Exit fullscreen mode

接受用户的凭证并在服务器上创建一个帐户。

//👇🏻 array representing the data
const database = [];
//👇🏻 generates a random string as ID
const generateID = () => Math.random().toString(36).substring(2, 10);

app.post("/register", (req, res) => {
    const { username, email, password } = req.body;
    //👇🏻 checks if the user does not exist
    let result = database.filter(
        (user) => user.email === email || user.username === username
    );
    //👇🏻 creates the user's data structure on the server
    if (result.length === 0) {
        database.push({
            id: generateID(),
            username,
            password,
            email,
            timezone: {},
            schedule: [],
        });
        return res.json({ message: "Account created successfully!" });
    }
    //👇🏻 returns an error
    res.json({ error_message: "User already exists!" });
});
Enter fullscreen mode Exit fullscreen mode

将用户登录到应用程序

在服务器上添加一个登录 POST 路由,接受来自 React 应用程序的用户名和密码。

app.post("/login", (req, res) => {
    const { username, password } = req.body;
    console.log(req.body);
});
Enter fullscreen mode Exit fullscreen mode

创建一个异步函数,接受文件中用户的用户名和密码utils/resource.js

export async function handleLogin(username, password, navigate) {
    //...data
}
Enter fullscreen mode Exit fullscreen mode

将该handleLogin函数导入到 Login 组件中,如下所示:

import { handleLogin } from "../utils/resource";
import { useNavigate } from "react-router-dom";

const navigate = useNavigate();

//👇🏻 The Login button function
const handleSubmit = (e) => {
    if (username.trim() && password.trim()) {
        e.preventDefault();
        //👇🏻 accepts the user's password and email
        handleLogin(username, password, navigate);
        setPassword("");
        setUsername("");
    }
};
Enter fullscreen mode Exit fullscreen mode

更新handleLogin函数以向服务器发出 POST 请求。

export async function handleLogin(username, password, navigate) {
    try {
        const request = await fetch("http://localhost:4000/login", {
            method: "POST",
            body: JSON.stringify({
                username,
                password,
            }),
            headers: {
                Accept: "application/json",
                "Content-Type": "application/json",
            },
        });
        const data = await request.json();
        if (data.error_message) {
            toast.error(data.error_message);
        } else {
            //👇🏻If login successful
            toast.success(data.message);
            //👇🏻 saves the email and id for identification
            localStorage.setItem("_id", data.data._id);
            localStorage.setItem("_myEmail", data.data._email);
            navigate("/dashboard");
        }
    } catch (err) {
        console.error(err);
    }
}
Enter fullscreen mode Exit fullscreen mode

接受并验证服务器上的用户凭据。

app.post("/login", (req, res) => {
    const { username, password } = req.body;
    let result = database.filter(
        (user) => user.username === username && user.password === password
    );
    //👇🏻 user doesn't exist
    if (result.length !== 1) {
        return res.json({
            error_message: "Incorrect credentials",
        });
    }
    //👇🏻 user exists
    res.json({
        message: "Login successfully",
        data: {
            _id: result[0].id,
            _email: result[0].email,
        },
    });
});
Enter fullscreen mode Exit fullscreen mode

由于我们将发出需要在服务器上进行身份验证的请求,因此将下面的代码片段添加到DashboardProfile组件中。

useEffect(() => {
    if (!localStorage.getItem("_id")) {
        navigate("/");
    }
}, [navigate]);
Enter fullscreen mode Exit fullscreen mode

创建时间表

在本节中,我将引导您完成创建计划并将其显示给用户的过程。

在服务器上添加一个 POST 路由,允许用户创建新的计划。

app.post("/schedule/create", (req, res) => {
    const { userId, timezone, schedule } = req.body;
    console.log(req.body);
});
Enter fullscreen mode Exit fullscreen mode

在文件中创建一个接受用户时区和时间表的handleCreateSchedule函数。utils/resource.js

export async function handleCreateSchedule(
    selectedTimezone,
    schedule,
    navigate
) {
    //..other data
}
Enter fullscreen mode Exit fullscreen mode

handleCreateSchedule在仪表板组件中导入该功能。

import { handleCreateSchedule } from "../utils/resource";

const handleSaveSchedules = () => {
//👇🏻 ensures the user's timezone has been selected
    if (JSON.stringify(selectedTimezone) !== "{}") {
        handleCreateSchedule(selectedTimezone, schedule, navigate);
    } else {
        toast.error("Select your timezone");
    }
};
Enter fullscreen mode Exit fullscreen mode

更新handleCreateSchedule函数以发出包含时间表和时区的 POST 请求。

export async function handleCreateSchedule(
    selectedTimezone,
    schedule,
    navigate
) {
    try {
        await fetch("http://localhost:4000/schedule/create", {
            method: "POST",
            body: JSON.stringify({
                userId: localStorage.getItem("_id"),
                timezone: selectedTimezone,
                schedule,
            }),
            headers: {
                Accept: "application/json",
                "Content-Type": "application/json",
            },
        });
        //👇🏻 navigates to the profile page
        navigate(`/profile/${localStorage.getItem("_id")}`);
    } catch (err) {
        console.error(err);
    }
}
Enter fullscreen mode Exit fullscreen mode

更新服务器上的 POST 路由以接受来自 React 应用程序的数据并为用户创建新的时间表。

app.post("/schedule/create", (req, res) => {
    const { userId, timezone, schedule } = req.body;
    //👇🏻 filters the database via the id
    let result = database.filter((db) => db.id === userId);
    //👇🏻 updates the user's schedule and timezone
    result[0].timezone = timezone;
    result[0].schedule = schedule;
    res.json({ message: "OK" });
});
Enter fullscreen mode Exit fullscreen mode

恭喜!🎉 我们已经能够更新用户的日程安排和时区。

显示时间表

在这里,我将引导您从服务器获取用户的日程安排。

在服务器上添加一个 GET 路由,从数据库数组中检索用户的数据。

app.get("/schedules/:id", (req, res) => {
    const { id } = req.params;
    //👇🏻 filters the array via the ID
    let result = database.filter((db) => db.id === id);
    //👇🏻 returns the schedule, time and username
    if (result.length === 1) {
        return res.json({
            message: "Schedules successfully retrieved!",
            schedules: result[0].schedule,
            username: result[0].username,
            timezone: result[0].timezone,
        });
    }
    //👇🏻 if user not found
    return res.json({ error_message: "Sign in again, an error occured..." });
});
Enter fullscreen mode Exit fullscreen mode

在文件中创建一个函数Profile.js,当页面加载时向 GET 路由发送请求。

const [schedules, setSchedules] = useState([]);
const [loading, setLoading] = useState(true);
const [username, setUsername] = useState("");
const [timezone, setTimezone] = useState("");

useEffect(() => {
    function getUserDetails() {
        if (id) {
            fetch(`http://localhost:4000/schedules/${id}`)
                .then((res) => res.json())
                .then((data) => {
                    setUsername(data.username);
                    setSchedules(data.schedules);
                    setTimezone(data.timezone.label);
                    setLoading(false);
                })
                .catch((err) => console.error(err));
        }
    }
    getUserDetails();
}, [id]);
Enter fullscreen mode Exit fullscreen mode

并显示数据如下图所示:

return (
    <main className='profile'>
        {loading ? (
            <p>Loading...</p>
        ) : (
            <div>
                <h2>Hey, {username}</h2>
                <p>Here is your schedule: - {timezone}</p>
                <table>
                    <tbody>
                        {schedules.map((sch) => (
                            <tr key={sch.day}>
                                <td style={{ fontWeight: "bold" }}>{sch.day.toUpperCase()}</td>
                                <td>{sch.startTime || "Unavailable"}</td>
                                <td>{sch.endTime || "Unavailable"}</td>
                            </tr>
                        ))}
                    </tbody>
                </table>
            </div>
        )}
    </main>
);
Enter fullscreen mode Exit fullscreen mode

使用 EmailJS 预约

在本节中,您将了解当客户与用户预约时如何通过 EmailJS 发送电子邮件通知。

EmailJS 是一个 JavaScript 库,它使我们能够仅通过客户端技术发送电子邮件,而无需服务器。使用 EmailJS,您可以发送文本和电子邮件模板,并在电子邮件中添加附件。

在服务器上创建一个用于获取用户数据的 POST 路由。

app.post("/schedules/:username", (req, res) => {
    const { username } = req.body;
    //👇🏻 filter the databse via the username
    let result = database.filter((db) => db.username === username);
    if (result.length === 1) {
        const scheduleArray = result[0].schedule;
        //👇🏻 return only the selected schedules
        const filteredArray = scheduleArray.filter((sch) => sch.startTime !== "");
        //return the schedules and other information
        return res.json({
            message: "Schedules successfully retrieved!",
            schedules: filteredArray,
            timezone: result[0].timezone,
            receiverEmail: result[0].email,
        });
    }
    return res.json({ error_message: "User doesn't exist" });
});
Enter fullscreen mode Exit fullscreen mode

在文件中添加一个fetchBookingDetails函数resource.js

export function fetchBookingDetails(
    user,
    setError,
    setTimezone,
    setSchedules,
    setReceiverEmail
) {
    //...data
}
Enter fullscreen mode Exit fullscreen mode

将函数导入BookUser.js组件并在页面加载时使用其必要的参数调用它。

const [schedules, setSchedules] = useState([]);
const [timezone, setTimezone] = useState("");
const [error, setError] = useState(false);
const [receiverEmail, setReceiverEmail] = useState("");

useEffect(() => {
    fetchBookingDetails(
        user,
        setError,
        setTimezone,
        setSchedules,
        setReceiverEmail
    );
}, [user]);

if (error) {
    return <ErrorPage error="User doesn't exist" />;
}
Enter fullscreen mode Exit fullscreen mode

更新fetchBookingDetails函数以从服务器检索信息并更新状态参数。

export function fetchBookingDetails(
    user,
    setError,
    setTimezone,
    setSchedules,
    setReceiverEmail
) {
    fetch(`http://localhost:4000/schedules/${user}`, {
        method: "POST",
        body: JSON.stringify({
            username: user,
        }),
        headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
        },
    })
        .then((res) => res.json())
        .then((data) => {
            if (data.error_message) {
                toast.error(data.error_message);
                setError(true);
            } else {
                setTimezone(data.timezone.label);
                setSchedules(data.schedules);
                setReceiverEmail(data.receiverEmail);
            }
        })
        .catch((err) => console.error(err));
}
Enter fullscreen mode Exit fullscreen mode

在表单中呈现时间表,以便用户选择他们喜欢的预约时间。

<select name='duration' onChange={(e) => setDuration(e.target.value)}>
    {schedules.map((schedule) => (
        <option
            value={`${schedule.day} - ${schedule.startTime} : ${schedule.endTime}`}
            key={schedule.day}
        >{`${schedule.day} - ${schedule.startTime} : ${schedule.endTime}`}</option>
    ))}
</select>
Enter fullscreen mode Exit fullscreen mode

使用 EmailJS 发送电子邮件通知

在这里,我将指导您将 EmailJS 添加到 React.js 应用程序,以及如何在有人与用户预约时向用户发送电子邮件。

通过运行以下代码将 EmailJS 安装到 React 应用程序:

npm install @emailjs/browser
Enter fullscreen mode Exit fullscreen mode

在此创建一个 EmailJS 帐户  并将电子邮件服务提供商添加到您的帐户。

添加电子邮件模板,如下图所示:

电子邮件模板

花括号中的单词代表可以保存动态数据的变量。

将 EmailJS 导入utils/resource.js文件并创建向用户发送电子邮件通知的函数。

import emailjs from "@emailjs/browser";

export const sendEmail = (
    receiverEmail,
    email,
    fullName,
    message,
    duration
) => {
    emailjs
        .send(
            "YOUR_SERVICE_ID",
            "YOUR_TEMPLATE_ID",
            {
                to_email: receiverEmail,
                from_email: email,
                fullName,
                message,
                duration,
            },
            "YOUR_PUBLIC_KEY"
        )
        .then(
            (result) => {
                console.log(result.text);
                toast.success("Session booked successfully!");
            },
            (error) => {
                console.log(error.text);
                toast.error(error.text);
            }
        );
};
Enter fullscreen mode Exit fullscreen mode

您可以从 EmailJS 仪表板的帐户部分获取 EmailJS 公钥。

将该功能添加到组件sendEmail中,BookUser以便在每次提交表单时向用户发送包含预订信息的电子邮件。

const handleSubmit = (e) => {
    e.preventDefault();
    sendEmail(receiverEmail, email, fullName, message, duration);
    setFullName("");
    setMessage("");
};
Enter fullscreen mode Exit fullscreen mode

恭喜!🎉您已完成本教程的项目。

结论

到目前为止,您已经学习了如何创建一个日程安排应用程序,使用户能够设置他们的可用性并在有预约时通过 EmailJS 收到通知。

本教程将引导您完成一个使用 React 和 Node.js 构建的迷你项目。您可以通过添加身份验证库并将数据存储在数据库中来改进该应用程序。

本教程的源代码可以在这里找到:

https://github.com/novuhq/blog/tree/main/scheduling-app-with-react-nodejs-emailjs

感谢您的阅读!

帮帮我!

如果您觉得这篇文章帮助您更好地理解了 WebSocket!请给我们一个 Star,我会非常高兴!也请在评论区告诉我❤️
https://github.com/novuhq/novu

图片描述

文章来源:https://dev.to/novu/creating-a-scheduling-app-i-wish-somebody-showed-me-this-technique-when-i-first-started-coding-2icd
PREV
使用 ChatGPT、React 和 Node.js 创建网站聚合器 🚀
NEXT
使用 React、NodeJS 和 AI 创建简历生成器 🚀 TL;DR