使用NextJ,MongoDB,React-big-Calendar和Redux构建日历应用程序。
#redux #nextjs #mongodb

使用NextJS构建的日历应用程序

我们开始之前,这是我们正在构建的代码和演示

在本教程中,我们将使用nextjsreact-big-calendarmaterial-uireduxmongodb构建一个日历应用程序。

创建项目

为了构建我们的项目,我们需要运行以下命令:

npx creat-next-app calendar-app-nextjs

在上述命令之后创建项目后,您需要在VS Code或您选择的任何IDE中打开文件夹。

现在运行以下命令将依赖项添加到您的项目

npm install @reduxjs/toolkit date-fns date-fns-tz mongoose react-big-calendar react-redux redux redux-logger redux-persist redux-saga redux-thunk @emotion/styled @emotion/react @mui/material

此命令将为项目添加所需的依赖项。

现在,我们需要开始构建应用程序

此时您的文件夹结构应该像这样:

- pages
- public
- styles

现在从向项目添加jsconfig.json文件开始。这将帮助您在项目中配置绝对导入。

{
    "compilerOptions": {
        "baseUrl": "./"
    }
}

现在创建theme目录并在其中添加一个index.js文件。该文件将负责设置材料UI主题。将以下代码添加到文件:

import { createTheme } from "@mui/material";
const theme = createTheme({
    typography: {
        fontFamily: "Montserrat, sans-serif",
    },
    palette: {
        primary: {
            main: "#07617D",
            dark: "#07617D",
        },
    },
});

export default theme;

之后,我们将为国家管理申请添加Redux。

为国家管理添加REDUX

在您的项目中创建一个名为redux的目录。然后创建名为configureStore.jsrootReducer.jsrootSaga.js和一个名为events的文件,events添加了名为events.helpers.jsevents.saga.jseventsSlice.js的文件。
让我们将代码添加到文件:

`eventsSlice.js`;

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
    event: {},
    events: [],
};

export const eventsSlice = createSlice({
    name: "events",
    initialState,
    reducers: {
        setEventData(state, action) {
            state.event = action.payload;
        },
        fetchEventsStart() {},
        setEvents(state, action) {
            state.events = action.payload;
        },
    },
});

// Action creators are generated for each case reducer function
export const { setEventData, setEvents, fetchEventsStart, setSchedules } =
    eventsSlice.actions;

export default eventsSlice.reducer;

events.helper.js

export const handleFetchEvents = () => {
    return new Promise((resolve, reject) => {
        fetch("/api/events")
            .then((res) => res.json())
            .then((json) => {
                return resolve(json);
            })
            .catch((error) => {
                reject(error);
            });
    });
};

events.saga.js

import { all, call, put, takeLatest } from "redux-saga/effects";
import { handleFetchEvents } from "./events.helpers";
import { fetchEventsStart, setEvents } from "./eventsSlice";

export function* fetchEvents({ payload }) {
    try {
        const events = yield handleFetchEvents();
        yield put(setEvents(events));
    } catch (error) {
        console.log(error);
    }
}
export function* onFetchEventsStart() {
    yield takeLatest(fetchEventsStart.type, fetchEvents);
}
export default function* eventsSagas() {
    yield all([call(onFetchEventsStart)]);
}

rootsaga.js

import { all, call } from "redux-saga/effects";
import eventsSagas from "./events/events.saga";

export default function* rootSaga() {
    yield all([call(eventsSagas)]);
}

rootreducer.js

import { combineReducers } from "@reduxjs/toolkit";
import storage from "redux-persist/lib/storage";
import { persistReducer } from "redux-persist";
import eventsSlice from "./events/eventsSlice";

export const rootReducer = combineReducers({
    eventsData: eventsSlice,
});

const configStorage = {
    key: "root",
    storage,
    whitelist: ["events"],
};

export default persistReducer(configStorage, rootReducer);

configurestore.js

import logger from "redux-logger";
import createSagaMiddle from "redux-saga";
import {
    persistStore,
    FLUSH,
    REHYDRATE,
    PAUSE,
    PERSIST,
    PURGE,
    REGISTER,
} from "redux-persist";
import rootReducer from "./rootReducer";
import { configureStore } from "@reduxjs/toolkit";
import rootSaga from "./rootSaga";

const sagaMiddleware = createSagaMiddle();

export const store = configureStore({
    reducer: rootReducer,
    middleware:
        process.env.NODE_ENV !== "production"
            ? (getDefaultMiddleware) =>
                    getDefaultMiddleware({
                        serializableCheck: false,
                    })
                        .concat(logger)
                        .concat(sagaMiddleware)
            : (getDefaultMiddleware) =>
                    getDefaultMiddleware({
                        serializableCheck: {
                            ignoredActions: [
                                FLUSH,
                                REHYDRATE,
                                PAUSE,
                                PERSIST,
                                PURGE,
                                REGISTER,
                            ],
                        },
                    }).concat(sagaMiddleware),
    devTools: process.env.NODE_ENV !== "production",
});

export const persistor = persistStore(store);
sagaMiddleware.run(rootSaga);

export default {
    store,
    persistor,
};

在这里我们成功完成了Redux设置。

现在,我们需要将Redux Store和主题添加到应用程序中。为此,我们将编辑pages目录中的_app.js文件。
用以下代码替换现有代码:

import { Provider } from "react-redux";
import { persistor, store } from "redux/configureStore";
import { PersistGate } from "redux-persist/integration/react";
import "styles/globals.css";
import { CssBaseline, ThemeProvider } from "@mui/material";
import theme from "theme";

function MyApp({ Component, pageProps }) {
    return (
        <Provider store={store}>
            <PersistGate persistor={persistor}>
                <ThemeProvider theme={theme}>
                    <CssBaseline />
                    <Component {...pageProps} />
                </ThemeProvider>
            </PersistGate>
        </Provider>
    );
}

export default MyApp;

创建后端API

现在我们将创建一个名为backend的目录。这将具有连接到mongodb数据库所需的所有后端代码。
backend目录中,创建两个名为controllersmodels的目录。在models中,我们将创建一个文件eventModel.js,这将是events的架构文件。
将以下代码添加到eventmodel.js文件:

const mongoose = require("mongoose");

const Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
const eventSchema = new Schema(
    {
        title: {
            type: String,
            required: true,
        },
        start: {
            type: Date,
            required: true,
        },
        end: {
            type: Date,
            required: true,
        },
        description: String,
        timezone: String,
        start_date: String,
        end_date: String,
        start_time: String,
        end_time: String,
        background: String,
    },
    { timestamps: true, _id: true },
);

module.exports = mongoose.models.Event || mongoose.model("Event", eventSchema);

现在在controllers目录中添加一个名为eventController.js的文件并添加以下代码

import Event from "../models/eventModel";
import mongoose from "mongoose";

// get all Events
const getEvents = async (req, res) => {
    const events = await Event.find({}).sort({ createdAt: -1 });

    res.status(200).json(events);
};

// get a single Event
const getEvent = async (req, res) => {
    const {
        query: { id },
    } = req;

    if (!mongoose.Types.ObjectId.isValid(id)) {
        return res.status(404).json({ error: "No such Event" });
    }

    const event = await Event.findById(id);

    if (!event) {
        return res.status(404).json({ error: "No such Event" });
    }

    res.status(200).json(event);
};

// create a new Event
const createEvent = async (req, res) => {
    const { title } = req.body;

    let emptyFields = [];

    if (!title) {
        emptyFields.push("title");
    }
    if (emptyFields.length > 0) {
        return res
            .status(400)
            .json({ error: "Please fill in all fields", emptyFields });
    }

    // add to the database
    try {
        const event = await Event.create({
            ...req.body,
        });
        res.status(200).json({ result: event, status: "success" });
    } catch (error) {
        res.status(400).json({ error: error.message });
    }
};

// delete a Event
const deleteEvent = async (req, res) => {
    const { id } = req.body;

    // console.log({ body: req.body, id: req.body.id });
    if (!mongoose.Types.ObjectId.isValid(id)) {
        return res.status(400).json({
            error: "Something is wrong",
        });
    }

    const event = await Event.findOneAndDelete({ _id: id });

    if (!event) {
        return res.status(400).json({ error: "No such Event" });
    }

    res.status(200).json(event);
};

// update a Event
const updateEvent = async (req, res) => {
    const { id } = req.query;

    if (!mongoose.Types.ObjectId.isValid(id)) {
        return res.status(400).json({ error: "No such Event" });
    }

    const event = await Event.findOneAndUpdate(
        { _id: id },
        {
            ...req.body,
        },
    );

    if (!event) {
        return res.status(400).json({ error: "No such Event" });
    }

    res.status(200).json(event);
};

module.exports = {
    getEvents,
    getEvent,
    createEvent,
    deleteEvent,
    updateEvent,
};

现在我们已经完成了创建一个实用程序文件所需的文件,该文件将有助于与MongoDB建立连接。
现在,创建一个名为utils的目录,并在其中添加一个名为mongoDbConnect.js的文件。将以下代码添加到此文件:

import mongoose from "mongoose";

const MONGODB_URI = process.env.MONGODB_URI;

if (!MONGODB_URI) {
    throw new Error(
        "Please define the MONGODB_URI environment variable inside .env.local",
    );
}

let cached = global.mongoose;

if (!cached) {
    cached = global.mongoose = { conn: null, promise: null };
}

async function dbConnect() {
    if (cached.conn) {
        return cached.conn;
    }

    if (!cached.promise) {
        const opts = {
            bufferCommands: false,
        };

        cached.promise = mongoose
            .connect(MONGODB_URI, opts)
            .then((mongoose) => {
                return mongoose;
            });
    }

    try {
        cached.conn = await cached.promise;
    } catch (e) {
        cached.promise = null;
        throw e;
    }

    return cached.conn;
}

export default dbConnect;
  • 这里唯一的要求是MONGODB_URI,您可以从mongodb_atlas仪表板中获得此要求。这是连接到MongoDB Atlas的连接字符串。

现在我们将创建用于获取数据的API。
为此,我们必须在pages/api目录中创建events目录。在events中创建index.js和以下代码:

import dbConnect from "utils/mongoDbConnect";
import {
    getEvents,
    createEvent,
    deleteEvent,
} from "backend/controllers/eventController";

export default async function handler(req, res) {
    const { method } = req;

    await dbConnect();

    switch (method) {
        case "GET":
            await getEvents(req, res);
            break;
        case "POST":
            await createEvent(req, res);
            break;
        case "DELETE":
            await deleteEvent(req, res);
            break;
        default:
            res.status(400).json({ success: false });
            break;
    }
}

这就是我们的后端准备就绪
您可以在此处尝试此集成https://localhost:3000/api/events

它将显示一个空数组。

现在,让我们构建前端

建立日历

让我们创建一个名为components的文件夹,然后在components内部创建一个Buttons文件夹和一个Dialog文件夹。
Buttons中创建PrimayButton.js文件并添加以下代码:

import { Button, styled } from "@mui/material";
import React from "react";

const StyledButton = styled(Button)(({ theme, ...props }) => ({
  textTransform: "initial",
  textTransform: "inherit",
  borderColor: "white",
  borderRadius: "50px",
  color: "white",
  paddingRight: "24px",
  paddingLeft: "24px",
  height: "48px",
  background: " #07617D",

  "&:hover": {
    background: " #07617D",
  },
}));

const PrimaryButton = ({ children, ...props }) => {
  return (
    <StyledButton
      variant="contained"
      sx={{
        ...props.sx,
      }}
      {...props}
    >
      {children}
    </StyledButton>
  );
};

export default PrimaryButton;

Dialog文件夹中创建index.js文件并添加以下代码

import { Dialog, DialogContent, DialogTitle, IconButton } from "@mui/material";
import React from "react";
import { MdClose } from "react-icons/md";
const BaseDialog = ({ open, handleClose, children, title }) => {
    return (
        <Dialog open={open} onClose={handleClose} sx={{}} scroll="paper">
            <IconButton
                onClick={() => handleClose()}
                sx={{ position: "absolute", top: "10px", right: "10px" }}
            >
                <MdClose />
            </IconButton>
            {title && <DialogTitle>{title}</DialogTitle>}
            <DialogContent>{children}</DialogContent>
        </Dialog>
    );
};

export default BaseDialog;

现在在components文件夹中创建一个文件夹CustomCalendar,创建三个名为:index.jsCreateEventPopup.jsDeleteEventPopup.js的文件,然后将以下代码添加到文件

index.js

import React, { useState } from "react";
import { Calendar, dateFnsLocalizer } from "react-big-calendar";
import format from "date-fns/format";
import parse from "date-fns/parse";
import startOfWeek from "date-fns/startOfWeek";
import getDay from "date-fns/getDay";
import enUS from "date-fns/locale/en-US";
import "react-big-calendar/lib/css/react-big-calendar.css";
import withDragAndDrop from "react-big-calendar/lib/addons/dragAndDrop";
import { useDispatch, useSelector } from "react-redux";
import CreateEventPopUp from "./CreateEventPopup";
import { setEventData } from "redux/events/eventsSlice";
import DeleteEventPopup from "./DeleteEventPopup";
const DragAndDropCalendar = withDragAndDrop(Calendar);

const locales = {
  "en-US": enUS,
};

let currentDate = new Date();
let currentDay = currentDate.getDay();

const localizer = dateFnsLocalizer({
  format,
  parse,
  startOfWeek: () => startOfWeek(currentDate, { weekStartsOn: currentDay }),
  getDay,
  locales,
});

const customDayPropGetter = (date) => {
  const currentDate = new Date();
  if (date < currentDate)
    return {
      className: "disabled-day",
      style: {
        cursor: "not-allowed",
        background: "rgba(184, 184, 184, 0.1)",
      },
    };
  else return {};
};

const CustomCalendar = ({ events = [], height, style, ...calendarProps }) => {
  const calendarRef = React.createRef();
  const dispatch = useDispatch();
  const [openDialog, setOpenDialog] = useState(false);
  const [openRemoveDialog, setOpenRemoveDialog] = useState(false);
  const [data, setData] = useState({});

  const setEventCellStyling = (event) => {
    if (event.background) {
      let style = {
        background: "rgba(7, 97, 125, 0.1)",
        border: `1px solid ${event.background}`,
        color: "#07617D",
        borderLeft: `3px solid ${event.background}`,

        fontWeight: 600,
        fontSize: "11px",
      };
      return { style };
    }
    let style = {
      background: "rgba(7, 97, 125, 0.1)",
      border: `1px solid #07617D`,
      color: "#07617D",
      borderLeft: "3px solid #07617D",

      fontWeight: 600,
      fontSize: "11px",
    };
    return { style };
  };

  const formats = {
    weekdayFormat: "EEE",
    timeGutterFormat: "hh a",
  };

  const handleSelect = ({ start, end }) => {
    const currentDate = new Date();
    if (start < currentDate) {
      return null;
    }
    if (start > end) return;

    handleOpenPopup();
    dispatch(setEventData({ start, end }));
  };
  const handleOpenPopup = () => {
    setOpenDialog(true);
  };
  const handleEventSelect = (event) => {
    handleRemoveDialogOpen();
    setData(event);
  };
  const handleRemoveDialogOpen = () => {
    setOpenRemoveDialog(true);
  };
  const handleRemoveDialogClose = () => {
    setOpenRemoveDialog(false);
    setEventData({});
  };
  const handleDialogClose = () => {
    setOpenDialog(false);
    dispatch(setEventData({}));
  };

  return (
    <>
      <DragAndDropCalendar
        ref={calendarRef}
        localizer={localizer}
        formats={formats}
        popup={true}
        events={events}
        selectable
        resizable
        longPressThreshold={1}
        eventPropGetter={setEventCellStyling}
        dayPropGetter={customDayPropGetter}
        onSelectSlot={handleSelect}
        onSelectEvent={handleEventSelect}
        views={{ week: true }}
        step={30}
        drilldownView={"week"}
        scrollToTime={currentDate.getHours()}
        defaultView={"week"}
        style={{ height: height ? height : "68vh", ...style }}
        {...calendarProps}
      />

      <CreateEventPopUp open={openDialog} handleClose={handleDialogClose} />
      <DeleteEventPopup
        open={openRemoveDialog}
        handleClose={handleRemoveDialogClose}
        event={data}
      />
    </>
  );
};

export default CustomCalendar;

createeeventpopup.js

import React from "react";
import {
  Container,
  Dialog,
  DialogTitle,
  TextField,
  Typography,
} from "@mui/material";
import { useState } from "react";
import PrimaryButton from "components/Common/Buttons/PrimaryButton";
import { useDispatch, useSelector } from "react-redux";
import { format } from "date-fns";
import { fetchEventsStart } from "redux/events/eventsSlice";
import BaseDialog from "components/Common/Dialog";

const mapState = ({ eventsData }) => ({
  event: eventsData.event,
});

const CreateEventPopUp = ({ handleClose, open }) => {
  const { event } = useSelector(mapState);
  const startTimeAndDate = event.start;
  const endTimeAndDate = event.end;
  const from_time = startTimeAndDate && format(startTimeAndDate, "hh:mma");
  const formattedStartDate =
    startTimeAndDate && format(startTimeAndDate, "eeee, MMMM dd, yyyy ");
  const to_time = endTimeAndDate && format(endTimeAndDate, "hh:mma");
  const [title, setTitle] = useState("");
  const [backgroundColor, setBackgroundColor] = useState("#000000");
  const dispatch = useDispatch();

  const handleCreateEvent = (e) => {
    e.preventDefault();

    try {
      const schema = {
        title: title,
        description: "",
        background: backgroundColor,
        start: startTimeAndDate,
        end: endTimeAndDate,
      };
      const url = "/api/events";
      fetch(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(schema),
      })
        .then((res) => res.json())
        .then((json) => {
          dispatch(fetchEventsStart({ url: "/api/events" }));
          setTitle("");
        });
    } catch (error) {
      console.log(error);
    }
    handleClose();
  };

  return (
    <BaseDialog open={open} handleClose={handleClose} scroll={`body`}>
      <DialogTitle
        style={{
          fontSize: "21px",
          fontWeight: "bold",
          marginTop: "-24px",
        }}
      >
        Add Event
      </DialogTitle>

      <Container
        sx={{
          background: "white",
          top: "30%",
          left: "10%",
          minWidth: "450px",
          paddingBottom: "64px",
        }}
      >
        {formattedStartDate && (
          <Typography sx={{ fontSize: "18px", fontWeight: "500" }}>
            {formattedStartDate}, {from_time} - {to_time}
          </Typography>
        )}
        <TextField
          fullWidth
          sx={{ marginTop: "16px" }}
          placeholder="Title"
          label="Title"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
        <div>
          <div style={{ paddingTop: "16px" }}>
            <label style={{ fontWeight: 700 }}>Select Color</label>
            <div style={{ display: "flex" }}>
              {colorsList.map((item) => {
                return (
                  <div
                    key={item}
                    style={{
                      width: "20px",
                      height: "20px",
                      background: item,
                      marginRight: "8px",
                    }}
                    onClick={() => setBackgroundColor(item)}
                  ></div>
                );
              })}
            </div>

            <input
              type={"color"}
              value={backgroundColor}
              onChange={(e) => setBackgroundColor(e.target.value)}
              style={{
                width: "100%",
                marginTop: "4px",
                border: "none",
                background: "none",
              }}
            />
            <Typography>
              Selected color: <b>{backgroundColor}</b>
            </Typography>
          </div>
        </div>

        <div
          style={{
            display: "flex",
            padding: "8px",
            justifyContent: "center",
            paddingTop: "32px",
          }}
        >
          <PrimaryButton
            onClick={handleCreateEvent}
            sx={{
              paddingRight: "32px",
              paddingLeft: "32px",
            }}
          >
            Confirm
          </PrimaryButton>
        </div>
      </Container>
    </BaseDialog>
  );
};

export default CreateEventPopUp;

const colorsList = [
  "#624b4b",
  "#bc2020",
  "#bc20b6",
  " #420b40",
  "#1fad96",
  "#3538ed",
  " #1c474a",
  "#32bb30",
  "#cae958",
  "#dc3e09",
];

deleteeventpopup.js

import React from "react";
import { useDispatch } from "react-redux";
import { useSelector } from "react-redux";
import { FormControlLabel, Radio, Typography } from "@mui/material";
import BaseDialog from "components/Common/Dialog";
import { fetchEventsStart } from "redux/events/eventsSlice";
import PrimaryButton from "components/Common/Buttons/PrimaryButton";

const DeleteEventPopup = ({ event, open, handleClose }) => {
  const dispatch = useDispatch();

  const handleRemoveEvent = () => {
    const data = {
      id: event._id,
    };
    fetch("/api/events", {
      method: "DELETE",
      headers: {
        "Content-Type": "application/json",
      },

      body: JSON.stringify(data),
    })
      .then((res) => res.json())
      .then((json) => {
        handleClose();

        dispatch(fetchEventsStart());
      });
  };

  return (
    <BaseDialog open={open} handleClose={handleClose}>
      <div
        style={{
          display: "flex",
          justifyContent: "center",
          maxWidth: "480px",
          flexDirection: "column",
          paddingLeft: "8px",
          paddingRight: "8px",
          marginTop: "16px",
        }}
      >
        <Typography fontSize={`20px`} fontWeight={`700`} paddingBottom="16px">
          Do you really want to delete this event?
        </Typography>

        <div
          style={{
            justifyContent: "center",
            display: "flex",
          }}
        >
          <PrimaryButton title={`Confirm`} onClick={handleRemoveEvent}>
            Confirm
          </PrimaryButton>
        </div>
      </div>
    </BaseDialog>
  );
};

export default DeleteEventPopup;

现在我们已经创建了日历了,现在是时候通过以下代码替换所有现有代码将日历添加到pages/index.js中了:

import CustomCalendar from "components/CustomCalendar";
import Head from "next/head";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { fetchEventsStart } from "redux/events/eventsSlice";

const mapState = ({ eventsData }) => ({
  events: eventsData.events,
});
export default function Home() {
  const dispatch = useDispatch();
  const { events } = useSelector(mapState);

  const getAllEvents = () => {
    // fetch()
    dispatch(fetchEventsStart());
  };
  useEffect(() => {
    getAllEvents();
  }, []);
  const calendarEvents = events.map((item) => {
    const { start, end } = item;
    return {
      ...item,
      start: new Date(start),
      end: new Date(end),
    };
  });
  return (
    <div>
      <Head>
        <title>Calendar App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <CustomCalendar height={"100vh"} events={calendarEvents} />
    </div>
  );
}

这就是现在的前端。您可以添加或删除事件。

恭喜!您已经成功创建了日历应用程序。

这是我们刚刚构建的代码和演示,您可以克隆该项目并测试

在接下来的几天里,我还将添加身份验证和更新日历中的事件的能力。

我希望这对每个人都有帮助。