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

[CHE-15] Create Alumni Directory #82

Merged
merged 28 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d4b8396
Added basic direxctory page and snapshot test
brok3turtl3 Jan 9, 2024
e263ab6
CHE-16 adjustment made to GH workflow script
brok3turtl3 Jan 13, 2024
0039809
Merge pull request #38 from Code-Hammers/CHE-16/subtask/Create-Alumni…
brok3turtl3 Jan 13, 2024
eb6cf67
CHE-18 Added an example h2
dianemoon Jan 13, 2024
3a55677
CHE-18 Navigation and routing hooked up
brok3turtl3 Jan 29, 2024
322f93d
CHE-18 Updated snapshot tests
brok3turtl3 Jan 29, 2024
39c3604
[CHE-18] Create Navigation and Routes to Directory Page (#39)
dianemoon Jan 29, 2024
ff73433
Merge branch 'dev' into CHE-15/story/Create-Alumni-Directory
brok3turtl3 Mar 16, 2024
91d9d8f
Merge branch 'CHE-15/story/Create-Alumni-Directory' into CHE-18/subta…
brok3turtl3 Mar 16, 2024
e8fd24b
Merge pull request #43 from Code-Hammers/CHE-18/subtask/Create-Naviga…
brok3turtl3 Mar 16, 2024
6c0ff9c
CHE-17 Created alumni model, alumni type file, added routes and getAl…
brok3turtl3 Mar 16, 2024
888dc4e
Merge branch 'dev' into CHE-15/story/Create-Alumni-Directory
brok3turtl3 Mar 16, 2024
7543e95
Merge pull request #45 from Code-Hammers/CHE-17/subtask/Create-Backen…
brok3turtl3 Mar 28, 2024
42d8655
Merge branch 'dev' into CHE-15/story/Create-Alumni-Directory
brok3turtl3 Apr 18, 2024
f52832e
Merge branch 'CHE-15/story/Create-Alumni-Directory' into CHE-17/subta…
brok3turtl3 Apr 18, 2024
c8dd11b
CHE-17 A bunch of stuff
brok3turtl3 Apr 18, 2024
654878d
Merge branch 'CHE-15/story/Create-Alumni-Directory' into CHE-17/subta…
brok3turtl3 Apr 18, 2024
7dd6642
CHE-17 Reset snapshot
brok3turtl3 Apr 18, 2024
a8a3dba
Merge branch 'CHE-17/subtask/Create-Backend-Alumni-Routes' of https:/…
brok3turtl3 Apr 18, 2024
876db42
CHE-17 Removed unused useEffect
brok3turtl3 Apr 18, 2024
18cf94d
CHE-17 Adjusted test to reflect new component logic
brok3turtl3 Apr 18, 2024
e524b9a
Merge pull request #70 from Code-Hammers/CHE-17/subtask/Create-Backen…
brok3turtl3 Apr 18, 2024
a65b9cf
Updated some styling and added pagination functionality
brok3turtl3 Apr 18, 2024
47646cb
Merge pull request #71 from Code-Hammers/alumni-styling
brok3turtl3 Apr 18, 2024
eec796a
Modal and styling added
brok3turtl3 Apr 18, 2024
c0876f9
Created an Alum interface in the type file and used it to fix some ty…
brok3turtl3 Apr 18, 2024
cd07366
Merge pull request #72 from Code-Hammers/alumni-modal
brok3turtl3 Apr 18, 2024
deeeb42
CHE-15 Merging in dev and resolving conflicts
brok3turtl3 Apr 21, 2024
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
2 changes: 2 additions & 0 deletions client/src/AuthenticatedApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Forums from "./pages/Forums/Forums";
import Profiles from "./pages/Profiles/Profiles";
import Profile from "./pages/Profile/Profile";
import EditProfilePage from "./pages/EditProfilePage/EditProfilePage";
import Directory from "./pages/DirectoryPage/DirectoryPage";
import NotFoundPage from "./pages/NotFoundPage/NotFoundPage";
import { useNavigate } from "react-router-dom";

Expand Down Expand Up @@ -45,6 +46,7 @@ const AuthenticatedApp = () => {
<Route path="/profile/:userId" element={<Profile />} />
<Route path="/editProfile" element={<EditProfilePage />} />
<Route path="/forums" element={<Forums />} />
<Route path="/directory" element={<Directory />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</div>
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { configureStore } from "@reduxjs/toolkit";
import userReducer from "../features/user/userSlice";
import profilesReducer from "../features/profiles/profilesSlice";
import userProfileReducer from "../features/userProfile/userProfileSlice";
import alumniReducer from "../features/alumni/alumniSlice";

export const store = configureStore({
reducer: {
user: userReducer,
profiles: profilesReducer,
userProfile: userProfileReducer,
alumni: alumniReducer,
},
});

Expand Down
4 changes: 2 additions & 2 deletions client/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ const Header = (): JSX.Element => {
<div className="flex-grow mx-10">
<div className="flex justify-evenly space-x-4 md:space-x-6 lg:space-x-10">
<Link
to="/app/main"
to="/app/directory"
className={`text-lg md:text-xl ${
currentPath === "main" ? "text-gray-300" : "hover:text-gray-300"
} transition transform hover:scale-105`}
>
MainPage
Alumni
</Link>
<Link
to="/app/profiles"
Expand Down
9 changes: 9 additions & 0 deletions client/src/components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ const Navbar = (): JSX.Element => {
Forums
</Link>
)}

{location.pathname !== "/app/directory" && (
<Link
to="/app/directory"
className="text-teal-600 hover:text-teal-800 transition transform hover:scale-105"
>
Directory
</Link>
)}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ exports[`Navbar Component renders correctly 1`] = `
>
Forums
</a>
<a
className="text-teal-600 hover:text-teal-800 transition transform hover:scale-105"
href="/app/directory"
onClick={[Function]}
>
Directory
</a>
</div>
</div>
`;
36 changes: 36 additions & 0 deletions client/src/components/modals/AlumniModal/AlumniModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from "react";
import { Alum } from "../../../../types/alum";

interface ModalProps {
isOpen: boolean;
onClose: () => void;
alum: Alum | null;
}

const Modal: React.FC<ModalProps> = ({ isOpen, onClose, alum }) => {
if (!isOpen || !alum) return null;

return (
<div className="fixed inset-0 z-50 bg-black bg-opacity-75 flex items-center justify-center p-4">
<div className="bg-gradient-to-r from-gray-700 via-gray-800 to-gray-900 p-6 rounded-lg max-w-lg w-full m-4 shadow-xl">
<h2 className="text-2xl font-bold text-white">{alum.name}</h2>
<p className="text-white">Company: {alum.company}</p>
<p className="text-white">Email: {alum.email}</p>
<p className="text-white">LinkedIn: {alum.linkedIn || "N/A"}</p>
<p className="text-white">Campus: {alum.campus || "N/A"}</p>
<p className="text-white">Cohort: {alum.cohort}</p>
<p className="text-white">Job Title: {alum.jobTitle || "N/A"}</p>
<p className="text-white">Industry: {alum.industry || "N/A"}</p>
<p className="text-white">Cities: {alum.cities.join(", ")}</p>
<button
onClick={onClose}
className="mt-4 bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded focus:outline-none"
>
Close
</button>
</div>
</div>
);
};

export default Modal;
61 changes: 61 additions & 0 deletions client/src/features/alumni/alumniSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";

interface AlumniState {
alumni: any[];
status: "idle" | "loading" | "failed";
error: string | null;
page: number;
totalPages: number;
}

const initialState: AlumniState = {
alumni: [],
status: "idle",
error: null,
page: 1,
totalPages: 1,
};

export const fetchAlumni = createAsyncThunk(
"alumni/fetchAlumni",
async (
{ page, name, company }: { page: number; name: string; company: string },
thunkAPI
) => {
try {
const response = await axios.get(
`/api/alumni?page=${page}&name=${encodeURIComponent(
name
)}&company=${encodeURIComponent(company)}`
);
return response.data;
} catch (error) {
return thunkAPI.rejectWithValue("Error fetching alumni data");
}
}
);

const alumniSlice = createSlice({
name: "alumni",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchAlumni.pending, (state) => {
state.status = "loading";
})
.addCase(fetchAlumni.fulfilled, (state, action) => {
state.alumni = action.payload.alumni;
state.totalPages = action.payload.totalPages;
state.page = action.payload.currentPage;
state.status = "idle";
})
.addCase(fetchAlumni.rejected, (state, action) => {
state.status = "failed";
state.error = action.payload as string;
});
},
});

export default alumniSlice.reducer;
37 changes: 37 additions & 0 deletions client/src/pages/DirectoryPage/DirectoryPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from "react";
import { create } from "react-test-renderer";
import { Provider } from "react-redux";
import configureStore from "redux-mock-store";

import DirectoryPage from "./DirectoryPage";

interface State {
alumni: {
alumni: any[];
status: "idle" | "loading" | "failed";
page: number;
totalPages: number;
};
}

const mockStore = configureStore<State>([]);
const initialState: State = {
alumni: {
alumni: [],
status: "idle",
page: 1,
totalPages: 1,
},
};

describe("MainPage Component", () => {
it("renders correctly", () => {
const store = mockStore(initialState);
const tree = create(
<Provider store={store}>
<DirectoryPage />
</Provider>
).toJSON();
expect(tree).toMatchSnapshot();
});
});
116 changes: 116 additions & 0 deletions client/src/pages/DirectoryPage/DirectoryPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React, { useEffect, useState } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { fetchAlumni } from "../../features/alumni/alumniSlice";
import Modal from "../../components/modals/AlumniModal/AlumniModal";
import { Alum } from "../../../types/alum";

const DirectoryPage = (): JSX.Element => {
const dispatch = useAppDispatch();
const { alumni, status, page, totalPages } = useAppSelector(
(state) => state.alumni
);
const [nameSearch, setNameSearch] = useState("");
const [companySearch, setCompanySearch] = useState("");
const [modalOpen, setModalOpen] = useState(false);
const [selectedAlum, setSelectedAlum] = useState<Alum | null>(null);
useEffect(() => {
dispatch(fetchAlumni({ page, name: nameSearch, company: companySearch }));
}, [dispatch, page, nameSearch, companySearch]);

const handlePreviousPage = () => {
if (page > 1) {
dispatch(
fetchAlumni({
page: page - 1,
name: nameSearch,
company: companySearch,
})
);
}
};

const handleNextPage = () => {
if (page < totalPages) {
dispatch(
fetchAlumni({
page: page + 1,
name: nameSearch,
company: companySearch,
})
);
}
};

const handleAlumClick = (alum: Alum) => {
setSelectedAlum(alum);
setModalOpen(true);
};

const handleCloseModal = () => {
setModalOpen(false);
setSelectedAlum(null);
};

return (
<div className="min-h-screen bg-gray-900 text-white flex flex-col items-center justify-start pt-20 p-4">
<div className="sticky mt-20 z-10 bg-gray-900 w-full text-center py-4">
<h1 className="text-4xl font-extrabold mb-4">Alumni Directory</h1>
<div className="flex justify-center space-x-4 mb-4">
<input
type="text"
value={nameSearch}
onChange={(e) => setNameSearch(e.target.value)}
placeholder="Search by name"
className="mb-4 p-2 text-gray-900"
/>
<input
type="text"
value={companySearch}
onChange={(e) => setCompanySearch(e.target.value)}
placeholder="Search by company"
className="mb-4 p-2 text-gray-900"
/>
</div>
</div>
<div className="w-full max-w-3xl">
<div className="grid grid-cols-3 text-center mb-2">
<strong>Name</strong>
<strong>Company</strong>
<strong>Email</strong>
</div>
<div className="overflow-auto" style={{ maxHeight: "60vh" }}>
{status === "loading" ? (
<p>Loading...</p>
) : (
alumni.map((alum) => (
<div
key={alum._id}
className="grid grid-cols-3 text-center py-2 cursor-pointer"
onClick={() => handleAlumClick(alum)}
>
<div>{alum.name}</div>
<div>{alum.company}</div>
<div>{alum.email}</div>
</div>
))
)}
</div>
<div className="py-4 flex justify-between">
<button onClick={handlePreviousPage} disabled={page <= 1}>
Previous
</button>
<button onClick={handleNextPage} disabled={page >= totalPages}>
Next
</button>
</div>
</div>
<Modal
isOpen={modalOpen}
onClose={handleCloseModal}
alum={selectedAlum}
/>
</div>
);
};

export default DirectoryPage;
12 changes: 12 additions & 0 deletions client/types/alum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface Alum {
_id: string;
name: string;
company: string;
email: string;
linkedIn?: string;
campus?: string;
cohort: string | number;
jobTitle?: string;
industry?: string;
cities: string[];
}
40 changes: 40 additions & 0 deletions server/controllers/alumniControllers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Alumni from "../models/alumniModel";
import { Request, Response, NextFunction } from "express";
import { IAlumni } from "../types/alumni";

const getAllAlumniData = async (
req: Request,
res: Response,
next: NextFunction
) => {
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 10;
const nameSearch = (req.query.name as string) || "";
const companySearch = (req.query.company as string) || "";

try {
const searchQuery: any = {};
if (nameSearch) {
searchQuery.name = { $regex: nameSearch, $options: "i" };
}
if (companySearch) {
searchQuery.company = { $regex: companySearch, $options: "i" };
}

const alumni: IAlumni[] = await Alumni.find(searchQuery)
.limit(limit * 1)
.skip((page - 1) * limit)
.exec();

const count = await Alumni.countDocuments(searchQuery);
return res.status(200).json({
alumni,
totalPages: Math.ceil(count / limit),
currentPage: page,
});
} catch (error) {
console.log("Awesome error handling");
}
};

export { getAllAlumniData };
2 changes: 2 additions & 0 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import userRoutes from "./routes/userRoutes";
import profileRoutes from "./routes/profileRoutes";
import authRoutes from "./routes/authRoutes";
import imageRoutes from "./routes/imageRoutes";
import alumniRoutes from "./routes/alumniRoutes";
import connectDB from "./config/db";
import dotenv from "dotenv";
import cookieParser from "cookie-parser";
Expand All @@ -22,6 +23,7 @@ app.use("/api/users", userRoutes);
app.use("/api/profiles", profileRoutes);
app.use("/api/auth", authRoutes);
app.use("/api/images", imageRoutes);
app.use("/api/alumni", alumniRoutes);

console.log(`ENV BEFORE CHECK: ${process.env.NODE_ENV}`);

Expand Down
Loading
Loading