Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implemented redux and rtk query #5

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions frontend/umass_isa/src/app/api/apiSlice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

export const apiSlice = createApi({
baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:3500" }),
tagTypes: ["Notes", "User"],
endpoints: (builder) => ({}),
});
9 changes: 9 additions & 0 deletions frontend/umass_isa/src/app/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
import { apiSlice } from "./api/apiSlice";

export const store = configureStore({
reducer: { [apiSlice.reducerPath]: apiSlice.reducer },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware.concat(apiSlice.middleware),
devTools: true,
});
49 changes: 49 additions & 0 deletions frontend/umass_isa/src/features/notes/Note.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPenToSquare } from "@fortawesome/free-solid-svg-icons";
import { useNavigate } from "react-router-dom";

import { useSelector } from "react-redux";
import { selectNoteById } from "./notesApiSlice";

const Note = ({ noteId }) => {
const note = useSelector((state) => selectNoteById(state, noteId));

const navigate = useNavigate();

if (note) {
const created = new Date(note.createdAt).toLocaleString("en-US", {
day: "numeric",
month: "long",
});

const updated = new Date(note.updatedAt).toLocaleString("en-US", {
day: "numeric",
month: "long",
});

const handleEdit = () => navigate(`/dash/notes/${noteId}`);

return (
<tr className="table__row">
<td className="table__cell note__status">
{note.completed ? (
<span className="note__status--completed">Completed</span>
) : (
<span className="note__status--open">Open</span>
)}
</td>
<td className="table__cell note__created">{created}</td>
<td className="table__cell note__updated">{updated}</td>
<td className="table__cell note__title">{note.title}</td>
<td className="table__cell note__username">{note.username}</td>

<td className="table__cell">
<button className="icon-button table__button" onClick={handleEdit}>
<FontAwesomeIcon icon={faPenToSquare} />
</button>
</td>
</tr>
);
} else return null;
};
export default Note;
62 changes: 57 additions & 5 deletions frontend/umass_isa/src/features/notes/NotesList.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,59 @@
import { useGetNotesQuery } from "./notesApiSlice";
import Note from "./Note";

const NotesList = () => {
return (
<h1>NotesList</h1>
)
}
const {
data: notes,
isLoading,
isSuccess,
isError,
error,
} = useGetNotesQuery();

let content;

if (isLoading) content = <p>Loading...</p>;

if (isError) {
content = <p className="errmsg">{error?.data?.message}</p>;
}

if (isSuccess) {
const { ids } = notes;

const tableContent = ids?.length
? ids.map((noteId) => <Note key={noteId} noteId={noteId} />)
: null;

content = (
<table className="table table--notes">
<thead className="table__thead">
<tr>
<th scope="col" className="table__th note__status">
Username
</th>
<th scope="col" className="table__th note__created">
Created
</th>
<th scope="col" className="table__th note__updated">
Updated
</th>
<th scope="col" className="table__th note__title">
Title
</th>
<th scope="col" className="table__th note__username">
Owner
</th>
<th scope="col" className="table__th note__edit">
Edit
</th>
</tr>
</thead>
<tbody>{tableContent}</tbody>
</table>
);
}

export default NotesList
return content;
};
export default NotesList;
57 changes: 57 additions & 0 deletions frontend/umass_isa/src/features/notes/notesApiSlice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { createSelector, createEntityAdapter } from "@reduxjs/toolkit";
import { apiSlice } from "../../app/api/apiSlice";

const notesAdapter = createEntityAdapter({
sortComparer: (a, b) =>
a.completed === b.completed ? 0 : a.completed ? 1 : -1,
});

const initialState = notesAdapter.getInitialState();

export const notesApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
getNotes: builder.query({
query: () => "/notes",
validateStatus: (response, result) => {
return response.status === 200 && !result.isError;
},
keepUnusedDataFor: 5,
transformResponse: (responseData) => {
const loadedNotes = responseData.map((note) => {
note.id = note._id;
return note;
});
return notesAdapter.setAll(initialState, loadedNotes);
},
providesTags: (result, error, arg) => {
if (result?.ids) {
return [
{ type: "Note", id: "LIST" },
...result.ids.map((id) => ({ type: "Note", id })),
];
} else return [{ type: "Note", id: "LIST" }];
},
}),
}),
});

export const { useGetNotesQuery } = notesApiSlice;

// returns the query result object
export const selectNotesResult = notesApiSlice.endpoints.getNotes.select();

// creates memoized selector
const selectNotesData = createSelector(
selectNotesResult,
(notesResult) => notesResult.data // normalized state object with ids & entities
);

//getSelectors creates these selectors and we rename them with aliases using destructuring
export const {
selectAll: selectAllNotes,
selectById: selectNoteById,
selectIds: selectNoteIds,
// Pass in a selector that returns the notes slice of state
} = notesAdapter.getSelectors(
(state) => selectNotesData(state) ?? initialState
);
33 changes: 33 additions & 0 deletions frontend/umass_isa/src/features/users/User.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPenToSquare } from "@fortawesome/free-solid-svg-icons";
import { useNavigate } from "react-router-dom";

import { useSelector } from "react-redux";
import { selectUserById } from "./usersApiSlice";

const User = ({ userId }) => {
const user = useSelector((state) => selectUserById(state, userId));

const navigate = useNavigate();

if (user) {
const handleEdit = () => navigate(`/dash/users/${userId}`);

const userRolesString = user.roles.toString().replaceAll(",", ", ");

const cellStatus = user.active ? "" : "table__cell--inactive";

return (
<tr className="table__row user">
<td className={`table__cell ${cellStatus}`}>{user.username}</td>
<td className={`table__cell ${cellStatus}`}>{userRolesString}</td>
<td className={`table__cell ${cellStatus}`}>
<button className="icon-button table__button" onClick={handleEdit}>
<FontAwesomeIcon icon={faPenToSquare} />
</button>
</td>
</tr>
);
} else return null;
};
export default User;
53 changes: 48 additions & 5 deletions frontend/umass_isa/src/features/users/UsersList.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,50 @@
import { useGetUsersQuery } from "./usersApiSlice";
import User from "./User";

const UsersList = () => {
return (
<h1>UsersList</h1>
)
}
const {
data: users,
isLoading,
isSuccess,
isError,
error,
} = useGetUsersQuery();

let content;

if (isLoading) content = <p>Loading...</p>;

if (isError) {
content = <p className="errmsg">{error?.data?.message}</p>;
}

if (isSuccess) {
const { ids } = users;

const tableContent = ids?.length
? ids.map((userId) => <User key={userId} userId={userId} />)
: null;

content = (
<table className="table table--users">
<thead className="table__thead">
<tr>
<th scope="col" className="table__th user__username">
Username
</th>
<th scope="col" className="table__th user__roles">
Roles
</th>
<th scope="col" className="table__th user__edit">
Edit
</th>
</tr>
</thead>
<tbody>{tableContent}</tbody>
</table>
);
}

export default UsersList
return content;
};
export default UsersList;
54 changes: 54 additions & 0 deletions frontend/umass_isa/src/features/users/usersApiSlice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { createSelector, createEntityAdapter } from "@reduxjs/toolkit";
import { apiSlice } from "../../app/api/apiSlice";

const usersAdapter = createEntityAdapter({});

const initialState = usersAdapter.getInitialState();

export const usersApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query({
query: () => "/users",
validateStatus: (response, result) => {
return response.status === 200 && !result.isError;
},
keepUnusedDataFor: 5,
transformResponse: (responseData) => {
const loadedUsers = responseData.map((user) => {
user.id = user._id;
return user;
});
return usersAdapter.setAll(initialState, loadedUsers);
},
providesTags: (result, error, arg) => {
if (result?.ids) {
return [
{ type: "User", id: "LIST" },
...result.ids.map((id) => ({ type: "User", id })),
];
} else return [{ type: "User", id: "LIST" }];
},
}),
}),
});

export const { useGetUsersQuery } = usersApiSlice;

// returns the query result object
export const selectUsersResult = usersApiSlice.endpoints.getUsers.select();

// creates memoized selector
const selectUsersData = createSelector(
selectUsersResult,
(usersResult) => usersResult.data // normalized state object with ids & entities
);

//getSelectors creates these selectors and we rename them with aliases using destructuring
export const {
selectAll: selectAllUsers,
selectById: selectUserById,
selectIds: selectUserIds,
// Pass in a selector that returns the users slice of state
} = usersAdapter.getSelectors(
(state) => selectUsersData(state) ?? initialState
);
27 changes: 16 additions & 11 deletions frontend/umass_isa/src/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { BrowserRouter, Routes, Route} from 'react-router-dom';
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { BrowserRouter, Routes, Route } from "react-router-dom";

const root = ReactDOM.createRoot(document.getElementById('root'));
import { store } from "./app/store";
import { Provider } from "react-redux";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path ="/*" element={<App />} />
</Routes>
</BrowserRouter>
<Provider store={store}>
<BrowserRouter>
<Routes>
<Route path="/*" element={<App />} />
</Routes>
</BrowserRouter>
</Provider>
</React.StrictMode>
);
Loading