使用NextJS构建的日历应用程序
我们开始之前,这是我们正在构建的代码和演示
- github存储库:https://github.com/himohitmehta/calendar-app-tutorial
- Demo : https://calendar-app.mohitmehta.dev/
在本教程中,我们将使用nextjs
,react-big-calendar
,material-ui
,redux
和mongodb
构建一个日历应用程序。
创建项目
为了构建我们的项目,我们需要运行以下命令:
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.js
,rootReducer.js
,rootSaga.js
和一个名为events
的文件,events
添加了名为events.helpers.js
,events.saga.js
和eventsSlice.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
目录中,创建两个名为controllers
和models
的目录。在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.js
,CreateEventPopup.js
和DeleteEventPopup.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>
);
}
这就是现在的前端。您可以添加或删除事件。
恭喜!您已经成功创建了日历应用程序。
这是我们刚刚构建的代码和演示,您可以克隆该项目并测试
- github存储库:https://github.com/himohitmehta/calendar-app-tutorial
- Demo : https://calendar-app.mohitmehta.dev/
在接下来的几天里,我还将添加身份验证和更新日历中的事件的能力。
我希望这对每个人都有帮助。