CRUD redux redux-saga (ok)
https://github.com/roldanjr/next-crud/tree/master
Last updated
Was this helpful?
https://github.com/roldanjr/next-crud/tree/master
Last updated
Was this helpful?
package.json
{
"name": "next-crud",
"version": "1.0.0",
"main": "index.js",
"repository": "https://github.com/roldanjr/next-crud.git",
"author": "Roldan Montilla Jr <roldanjrmontilla@gmail.com>",
"license": "MIT",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,css,scss,json,md}\""
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"dependencies": {
"clsx": "^1.1.1",
"next": "^9.5.3",
"next-redux-wrapper": "^6.0.2",
"path": "^0.12.7",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-hook-form": "^6.8.6",
"react-redux": "^7.2.1",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.8",
"redux-saga": "^1.1.3",
"sass": "^1.26.11",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7"
},
"devDependencies": {
"@types/react": "^19.0.7",
"husky": "^4.3.0",
"lint-staged": "^10.4.0",
"prettier": "^2.1.2"
}
}
utils\dbConnect.js
const sqlite = require('sqlite');
const sqlite3 = require('sqlite3');
export function openDb() {
return sqlite.open({
filename: '../pages/data/mydb.sqlite',
driver: sqlite3.Database,
});
};
C:\Users\Administrator\Desktop\TMP\next-crud\styles.zip
store\types.js
export const MODAL_OPEN = "MODAL_OPEN";
export const EMPLOYEE_SELECTED = "EMPLOYEE_SELECTED";
export const EMPLOYEE_FETCH_REQUESTED = "EMPLOYEE_FETCH_REQUESTED";
export const EMPLOYEE_FETCH_SUCCEEDED = "EMPLOYEE_FETCH_SUCCEEDED";
export const EMPLOYEE_FETCH_FAILED = "EMPLOYEE_FETCH_FAILED";
export const EMPLOYEE_ADD_REQUESTED = "EMPLOYEE_ADD_REQUESTED";
export const EMPLOYEE_ADD_SUCCEEDED = "EMPLOYEE_ADD_SUCCEEDED";
export const EMPLOYEE_ADD_FAILED = "EMPLOYEE_ADD_FAILED";
export const EMPLOYEE_UPDATE_REQUESTED = "EMPLOYEE_UPDATE_REQUESTED";
export const EMPLOYEE_UPDATE_SUCCEEDED = "EMPLOYEE_UPDATE_SUCCEEDED";
export const EMPLOYEE_UPDATE_FAILED = "EMPLOYEE_UPDATE_FAILED";
export const EMPLOYEE_DELETE_REQUESTED = "EMPLOYEE_DELETE_REQUESTED";
export const EMPLOYEE_DELETE_SUCCEEDED = "EMPLOYEE_DELETE_SUCCEEDED";
export const EMPLOYEE_DELETE_FAILED = "EMPLOYEE_DELETE_FAILED";
store\store.js
import { createStore, applyMiddleware } from "redux";
import { createWrapper } from "next-redux-wrapper";
import { composeWithDevTools } from "redux-devtools-extension";
import createMiddleware from "redux-saga";
import rootReducer from "./reducers";
import rootSaga from "./sagas";
const sagaMiddleware = createMiddleware();
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(sagaMiddleware))
);
sagaMiddleware.run(rootSaga);
const makeStore = () => store;
export const wrapper = createWrapper(makeStore);
store\index.js
export * from "./store";
export * from "./actions";
store\sagas\employee.js
import { all, put, takeLatest } from "redux-saga/effects";
import * as t from "../types";
function* fetchEmployees() {
try {
const response = yield fetch("/api/employees");
const employeeList = yield response.json();
yield put({
type: t.EMPLOYEE_FETCH_SUCCEEDED,
payload: employeeList.data,
});
} catch (error) {
yield put({
type: t.EMPLOYEE_FETCH_FAILED,
payload: error.message,
});
}
}
function* watchFetchEmployees() {
yield takeLatest(t.EMPLOYEE_FETCH_REQUESTED, fetchEmployees);
}
function* addEmployee(action) {
try {
const response = yield fetch("/api/employees", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(action.payload),
});
const newEmployee = yield response.json();
yield put({
type: t.EMPLOYEE_ADD_SUCCEEDED,
payload: newEmployee.data,
});
} catch (error) {
yield put({
type: t.EMPLOYEE_ADD_FAILED,
payload: error.message,
});
}
}
function* watchAddEmployee() {
yield takeLatest(t.EMPLOYEE_ADD_REQUESTED, addEmployee);
}
function* deleteEmployee(action) {
try {
const response = yield fetch("/api/employees/" + action.payload, {
method: "DELETE",
});
const deletedEmployee = yield response.json();
yield put({
type: t.EMPLOYEE_DELETE_SUCCEEDED,
payload: deletedEmployee.data.id,
});
} catch (error) {
yield put({
type: t.EMPLOYEE_DELETE_FAILED,
payload: error.message,
});
}
}
function* watchRemoveEmployee() {
yield takeLatest(t.EMPLOYEE_DELETE_REQUESTED, deleteEmployee);
}
function* updateEmployee(action) {
try {
const response = yield fetch("/api/employees/" + action.payload.id, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(action.payload),
});
const updatedEmployee = yield response.json();
yield put({
type: t.EMPLOYEE_UPDATE_SUCCEEDED,
payload: updatedEmployee.data,
});
} catch (error) {
yield put({
type: t.EMPLOYEE_UPDATE_FAILED,
payload: error.message,
});
}
}
function* watchUpdateEmployee() {
yield takeLatest(t.EMPLOYEE_UPDATE_REQUESTED, updateEmployee);
}
export default function* rootSaga() {
yield all([
watchFetchEmployees(),
watchAddEmployee(),
watchRemoveEmployee(),
watchUpdateEmployee(),
]);
}
store\sagas\index.js
export { default } from "./employee";
store\reducers\employee.js
import { HYDRATE } from "next-redux-wrapper";
import * as t from "../types";
const initialState = {
employeeList: [],
selectedEmployee: undefined,
isModalOpen: false,
};
const mainReducer = (state = initialState, action) => {
switch (action.type) {
case HYDRATE:
return { ...state, ...action.payload };
case t.MODAL_OPEN:
return {
...state,
isModalOpen: action.payload,
};
case t.EMPLOYEE_FETCH_SUCCEEDED:
return {
...state,
employeeList: action.payload,
};
case t.EMPLOYEE_ADD_SUCCEEDED:
return {
...state,
employeeList: [action.payload, ...state.employeeList],
};
case t.EMPLOYEE_UPDATE_SUCCEEDED:
const updatedEmployee = state.employeeList.map((employee) => {
if (employee.id === action.payload.id) {
return {
...employee,
name: action.payload.name,
email: action.payload.email,
address: action.payload.address,
phone: action.payload.phone,
};
}
return employee;
});
return { ...state, employeeList: updatedEmployee };
case t.EMPLOYEE_DELETE_SUCCEEDED:
const newEmployeeList = state.employeeList.filter(
(employee) => employee.id !== action.payload
);
return {
...state,
employeeList: newEmployeeList,
};
case t.EMPLOYEE_SELECTED:
const selectedEmployee = state.employeeList.find(
(employee) => employee.id === action.payload
);
return {
...state,
selectedEmployee,
};
default:
return state;
}
};
export default mainReducer;
store\reducers\index.js
import { combineReducers } from "redux";
import employeeReducer from "./employee";
const rootReducer = combineReducers({
employee: employeeReducer,
});
export default rootReducer;
store\actions\employee.js
import * as t from "../types";
export const setModalOpen = (isModalOpen) => {
return {
type: t.MODAL_OPEN,
payload: isModalOpen,
};
};
export const fetchEmployees = () => {
return {
type: t.EMPLOYEE_FETCH_REQUESTED,
};
};
export const addEmployee = (employee) => {
return {
type: t.EMPLOYEE_ADD_REQUESTED,
payload: employee,
};
};
export const updateEmployee = (employee) => {
return {
type: t.EMPLOYEE_UPDATE_REQUESTED,
payload: employee,
};
};
export const deleteEmployee = (id) => {
return {
type: t.EMPLOYEE_DELETE_REQUESTED,
payload: id,
};
};
export const setSelectedEmployee = (id) => {
return {
type: t.EMPLOYEE_SELECTED,
payload: id,
};
};
store\actions\index.js
export * from "./employee";
C:\Users\Administrator\Desktop\TMP\next-crud\pages\api\employees\mydb.sqlite
pages\api\employees\index.js
const path = require("path");
const sqlite = require('sqlite');
const sqlite3 = require('sqlite3');
async function openDb() {
return sqlite.open({
filename: path.resolve("pages/data", "mydb.sqlite"),
driver: sqlite3.Database,
});
};
export default async (req, res) => {
const { method } = req;
const db = await openDb();
switch (method) {
case "GET":
try {
const employees = await db.all('SELECT * FROM Employee');
return res.status(200).json({
success: true,
data: employees
});
} catch (error) {
return res.status(400).json({
success: false,
path: path.resolve("pages/data", "mydb.sqlite")
});
}
case "POST":
try {
const statement = await db.prepare('INSERT INTO Employee(name, email, address,phone,createdAt) values (?, ?, ?, ?, ?)');
const result = await statement.run(req.body.name, req.body.email, req.body.address, req.body.phone, "58-05-5995");
const employees = await db.all('SELECT * FROM Employee');
return res.status(201).json({
success: true,
data: employees
});
} catch (error) {
return res.status(400).json({
success: false
});
}
default:
res.setHeaders("Allow", ["GET", "POST"]);
return res
.status(405)
.json({ success: false })
.end(`Method ${method} Not Allowed`);
}
};
pages\api\employees\[id].js
const path = require("path");
const sqlite = require('sqlite');
const sqlite3 = require('sqlite3');
async function openDb() {
return sqlite.open({
filename: path.resolve("pages/data", "mydb.sqlite"),
driver: sqlite3.Database,
});
};
export default async (req, res) => {
const db = await openDb();
const {
query: { id },
method,
} = req;
switch (method) {
case "GET":
try {
const employee = await db.get(`SELECT * FROM Employee WHERE id=?`, [id]);
return res.status(200).json({
success: true,
data: employee
});
} catch (error) {
return res.status(404).json({
success: false,
});
}
case "PUT":
try {
const statement = await db.prepare('UPDATE Employee SET name= ?, email = ?, address =?, phone =? where id = ?');
await statement.run(req.body.name,req.body.email,req.body.address,req.body.phone,id);
const employee = await db.get(`SELECT * FROM Employee WHERE id=?`, [id]);
return res.status(200).json({
success: true,
data: "employee"
});
} catch (error) {
return res.status(400).json({
success: false,
});
}
case "DELETE":
try {
const statement = await db.prepare('DELETE FROM Employee where id = ?');
await statement.run(id);
return res.status(200).json({
success: true,
data: { id },
});
} catch (error) {
return res.status(400).json({
success: false,
});
}
default:
res.setHeaders("Allow", ["GET", "PUT", "DELETE"]);
return res
.status(405)
.json({ success: false })
.end(`Method ${method} Not Allowed`);
}
};
pages\_app.js
import { wrapper } from "@/store";
import "@/styles/main.scss";
function App({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default wrapper.withRedux(App);
pages\index.js
import { Header, Layout, Modal, Pagination, Table } from "@/components";
function Landing() {
return (
<Layout>
<Header />
<Table />
<Pagination />
<Modal />
</Layout>
);
}
export default Landing;
models\Employee.js
import mongoose from "mongoose";
const EmployeeSchema = new mongoose.Schema({
name: {
type: String,
required: [true, "Name is required!"],
trim: true,
},
email: {
type: String,
required: [true, "Email is required!"],
trim: true,
},
address: {
type: String,
required: [true, "Address is required!"],
trim: true,
},
phone: {
type: String,
required: [true, "Phone is required!"],
trim: true,
},
createdAt: { type: Date, default: Date.now },
});
export default mongoose.models.Employee || mongoose.model("Employee", EmployeeSchema);
components\Header.js
import { useDispatch } from "react-redux";
import { PersonAddSVG } from "@/icons";
import { setModalOpen } from "@/store";
export function Header() {
const dispatch = useDispatch();
return (
<header className="header">
<h1 className="header__h1">
Manage <span>Employees</span>
</h1>
<button
className="btn btn__primary btn__icon"
onClick={() => {
dispatch(setModalOpen(true));
}}
>
<PersonAddSVG /> <span>Add new</span>
</button>
</header>
);
}
components\index.js
export * from "./Layout";
export * from "./Header";
export * from "./Table";
export * from "./Pagination";
export * from "./Modal";
components\Layout.js
import Head from "next/head";
export function Layout({ children }) {
return (
<main className="layout">
<Head>
<title>NextJS | Full-stack CRUD App</title>
</Head>
{children}
</main>
);
}
components\Modal.js
import { useEffect } from "react";
import ReactDOM from "react-dom";
import { useDispatch, useSelector } from "react-redux";
import { useForm } from "react-hook-form";
import cx from "clsx";
import { CheckSVG, CloseSVG } from "@/icons";
import {
addEmployee,
setModalOpen,
setSelectedEmployee,
updateEmployee,
} from "@/store";
export function Modal() {
const { register, handleSubmit, errors, reset, setValue } = useForm();
const state = useSelector((state) => state.employee);
const dispatch = useDispatch();
const closeModal = () => {
reset();
dispatch(setModalOpen(false));
dispatch(setSelectedEmployee(undefined));
};
const onSubmitHandler = (data) => {
if (data) {
closeModal();
}
if (state.selectedEmployee) {
dispatch(
updateEmployee({
id: state.selectedEmployee.id,
...data,
})
);
} else {
dispatch(addEmployee(data));
}
};
useEffect(() => {
if (state.selectedEmployee) {
setValue("name", state.selectedEmployee.name);
setValue("email", state.selectedEmployee.email);
setValue("address", state.selectedEmployee.address);
setValue("phone", state.selectedEmployee.phone);
}
}, [state.selectedEmployee, setValue]);
return state.isModalOpen
? ReactDOM.createPortal(
<div className="modal">
<div className="modal__content">
<header className="header modal__header">
<h1 className="header__h2">
{state.selectedEmployee ? (
<>
Edit <span>Employee</span>
</>
) : (
<>
Add <span>Employee</span>
</>
)}
</h1>
<button
className="btn btn__compact btn__close"
onClick={closeModal}
>
<CloseSVG />
</button>
</header>
<form
className="form modal__form"
onSubmit={handleSubmit(onSubmitHandler)}
noValidate
>
<div className="form__element">
<label
htmlFor="nameInput"
className={cx("label", errors.name && "label--error")}
>
{errors.name ? (
"Full name is required!"
) : (
<>
Full name <span className="label__required">*</span>
</>
)}
</label>
<input
type="text"
id="nameInput"
name="name"
placeholder="Full name"
className={cx("input", errors.name && "input--error")}
ref={register({ required: true })}
/>
</div>
<div className="form__element">
<label
htmlFor="emailInput"
className={cx("label", errors.email && "label--error")}
>
{errors.email ? (
`${errors.email.message}`
) : (
<>
Email <span className="label__required">*</span>
</>
)}
</label>
<input
type="email"
id="emailInput"
name="email"
placeholder="Email"
className={cx("input", errors.email && "input--error")}
ref={register({
required: "Email is required!",
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i,
message: "Invalid email address!",
},
})}
/>
</div>
<div className="form__element">
<label
htmlFor="addressArea"
className={cx("label", errors.address && "label--error")}
>
{errors.address ? (
"Address is required!"
) : (
<>
Address <span className="label__required">*</span>
</>
)}
</label>
<textarea
type="text"
id="addressArea"
name="address"
placeholder="Address"
className={cx("area", errors.address && "input--error")}
ref={register({ required: true })}
/>
</div>
<div className="form__element">
<label
htmlFor="phoneNumber"
className={cx("label", errors.phone && "label--error")}
>
{errors.phone ? (
`${errors.phone.message}`
) : (
<>
Phone <span className="label__required">*</span>
</>
)}
</label>
<input
type="number"
id="phoneNumber"
name="phone"
placeholder="Phone"
className={cx("input", errors.phone && "input--error")}
ref={register({
required: "Phone is required!",
minLength: {
value: 11,
message: "Minimum of 11 digits",
},
maxLength: {
value: 12,
message: "Maximum of 12 digits",
},
})}
/>
</div>
<div className="form__action">
<button
className="btn btn__icon btn__cancel"
type="button"
onClick={closeModal}
>
<CloseSVG /> Cancel
</button>
<button className="btn btn__primary btn__icon" type="submit">
<CheckSVG /> {state.selectedEmployee ? "Update" : "Submit"}
</button>
</div>
</form>
</div>
</div>,
document.body
)
: null;
}
components\Pagination.js
export function Pagination() {
return <div className="pagination"></div>;
}
components\Table.js
import { useSelector, useDispatch } from "react-redux";
import { PencilSVG, TrashSVG } from "@/icons";
import { deleteEmployee, fetchEmployees,setModalOpen,setSelectedEmployee} from "@/store";
import { useEffect } from "react";
export function Table() {
const state = useSelector((state) => state.employee);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchEmployees());
}, [dispatch]);
return (
<table className="table">
<thead className="table__head">
<tr>
<th>Full name</th>
<th>Email</th>
<th>Address</th>
<th>Phone</th>
<th>Actions</th>
</tr>
</thead>
<tbody className="table__body">
{state.employeeList.map(({ id, name, email, address, phone }) => (
<tr key={id}>
<td>{name}</td>
<td>{email}</td>
<td>{address}</td>
<td>{phone}</td>
<td>
<button
className="btn btn__compact btn__edit"
onClick={() => {
dispatch(setSelectedEmployee(id));
dispatch(setModalOpen(true));
}}
>
<PencilSVG />
</button>
<button
className="btn btn__compact btn__delete"
onClick={() => {
dispatch(deleteEmployee(id));
}}
>
<TrashSVG />
</button>
</td>
</tr>
))}
</tbody>
</table>
);
}
icons\checkmark.js
export function CheckSVG() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="512"
height="512"
viewBox="0 0 512 512"
>
<polyline
points="416 128 192 384 96 288"
style={{
fill: "none",
stroke: "currentcolor",
strokeLinecap: "square",
strokeMiterlimit: 10,
strokeWidth: 44,
}}
/>
</svg>
);
}
icons\close.js
export function CloseSVG() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="512"
height="512"
viewBox="0 0 512 512"
>
<polygon points="400 145.49 366.51 112 256 222.51 145.49 112 112 145.49 222.51 256 112 366.51 145.49 400 256 289.49 366.51 400 400 366.51 289.49 256 400 145.49" />
</svg>
);
}
icons\eye.js
export function EyeSVG() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="512"
height="512"
viewBox="0 0 512 512"
>
<circle cx="256" cy="256" r="64" />
<path d="M490.84,238.6c-26.46-40.92-60.79-75.68-99.27-100.53C349,110.55,302,96,255.66,96c-42.52,0-84.33,12.15-124.27,36.11C90.66,156.54,53.76,192.23,21.71,238.18a31.92,31.92,0,0,0-.64,35.54c26.41,41.33,60.4,76.14,98.28,100.65C162,402,207.9,416,255.66,416c46.71,0,93.81-14.43,136.2-41.72,38.46-24.77,72.72-59.66,99.08-100.92A32.2,32.2,0,0,0,490.84,238.6ZM256,352a96,96,0,1,1,96-96A96.11,96.11,0,0,1,256,352Z" />
</svg>
);
}
icons\index.js
export * from "./close";
export * from "./checkmark";
export * from "./person-add";
export * from "./pencil";
export * from "./trash";
export * from "./eye";
icons\pencil.js
export function PencilSVG() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="512"
height="512"
viewBox="0 0 512 512"
>
<polygon points="103 464 48 464 48 409 358.14 98.09 413.91 153.87 103 464" />
<path d="M425.72,142,370,86.28l31.66-30.66C406.55,50.7,414.05,48,421,48a25.91,25.91,0,0,1,18.42,7.62l17,17A25.87,25.87,0,0,1,464,91c0,7-2.71,14.45-7.62,19.36ZM418.2,71.17h0Z" />
</svg>
);
}
icons\person-add.js
export function PersonAddSVG() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="512"
height="512"
viewBox="0 0 512 512"
>
<polygon points="106 304 106 250 160 250 160 214 106 214 106 160 70 160 70 214 16 214 16 250 70 250 70 304 106 304" />
<circle cx="288" cy="144" r="112" />
<path d="M288,288c-69.42,0-208,42.88-208,128v64H496V416C496,330.88,357.42,288,288,288Z" />
</svg>
);
}
icons\trash.js
export function TrashSVG() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="512"
height="512"
viewBox="0 0 512 512"
>
<path
d="M296,64H216a7.91,7.91,0,0,0-8,8V96h96V72A7.91,7.91,0,0,0,296,64Z"
style={{ fill: "none" }}
/>
<path
d="M292,64H220a4,4,0,0,0-4,4V96h80V68A4,4,0,0,0,292,64Z"
style={{ fill: "none" }}
/>
<path d="M447.55,96H336V48a16,16,0,0,0-16-16H192a16,16,0,0,0-16,16V96H64.45L64,136H97l20.09,314A32,32,0,0,0,149,480H363a32,32,0,0,0,31.93-29.95L415,136h33ZM176,416l-9-256h33l9,256Zm96,0H240V160h32ZM296,96H216V68a4,4,0,0,1,4-4h72a4,4,0,0,1,4,4Zm40,320H303l9-256h33Z" />
</svg>
);
}