diff --git a/backend/src/main/java/COMP_49X_our_search/backend/fetcher/DepartmentFetcher.java b/backend/src/main/java/COMP_49X_our_search/backend/fetcher/DepartmentFetcher.java index 31bd1bf..e5b613f 100644 --- a/backend/src/main/java/COMP_49X_our_search/backend/fetcher/DepartmentFetcher.java +++ b/backend/src/main/java/COMP_49X_our_search/backend/fetcher/DepartmentFetcher.java @@ -49,7 +49,7 @@ private void validateRequest(FetcherRequest request) { String.format("Expected fetcher_type 'direct_fetcher', but got '%s'", request.getFetcherTypeCase().toString().toLowerCase())); } - if (request.getDirectFetcher().getDirectType() != DirectType.DEPARTMENTS) { + if (request.getDirectFetcher().getDirectType() != DirectType.DIRECT_TYPE_DEPARTMENTS) { throw new IllegalArgumentException(String.format( "Expected DirectType 'DEPARTMENTS', but got '%s'. This fetcher only supports DEPARTMENTS type", request.getDirectFetcher().getDirectType())); diff --git a/backend/src/main/java/COMP_49X_our_search/backend/fetcher/FetcherModuleController.java b/backend/src/main/java/COMP_49X_our_search/backend/fetcher/FetcherModuleController.java index 5ea346a..7ce6173 100644 --- a/backend/src/main/java/COMP_49X_our_search/backend/fetcher/FetcherModuleController.java +++ b/backend/src/main/java/COMP_49X_our_search/backend/fetcher/FetcherModuleController.java @@ -54,7 +54,7 @@ private void validateConfig(ModuleConfig moduleConfig) { } private FetcherResponse handleDirectFetcher(FetcherRequest request) { - if (request.getDirectFetcher().getDirectType() == DirectType.DEPARTMENTS) { + if (request.getDirectFetcher().getDirectType() == DirectType.DIRECT_TYPE_DEPARTMENTS) { return departmentFetcher.fetch(request); } // Add more cases as we add more direct types. @@ -64,7 +64,7 @@ private FetcherResponse handleDirectFetcher(FetcherRequest request) { private FetcherResponse handleFilteredFetcher(FetcherRequest request) { if (request.getFilteredFetcher() - .getFilteredType() == FilteredType.PROJECTS) { + .getFilteredType() == FilteredType.FILTERED_TYPE_PROJECTS) { return projectFetcher.fetch(request); } // Add more cases as we add more filtered types. diff --git a/backend/src/main/java/COMP_49X_our_search/backend/gateway/GatewayController.java b/backend/src/main/java/COMP_49X_our_search/backend/gateway/GatewayController.java index e1c9200..4946990 100644 --- a/backend/src/main/java/COMP_49X_our_search/backend/gateway/GatewayController.java +++ b/backend/src/main/java/COMP_49X_our_search/backend/gateway/GatewayController.java @@ -37,7 +37,7 @@ public ResponseEntity> getProjects() { ModuleConfig moduleConfig = ModuleConfig.newBuilder() .setFetcherRequest( FetcherRequest.newBuilder().setFilteredFetcher(FilteredFetcher - .newBuilder().setFilteredType(FilteredType.PROJECTS))) + .newBuilder().setFilteredType(FilteredType.FILTERED_TYPE_PROJECTS))) .build(); ModuleResponse moduleResponse = moduleInvoker.processConfig(moduleConfig); return ResponseEntity.ok(moduleResponse.getFetcherResponse() diff --git a/backend/src/main/proto/fetcher/fetcher_module.proto b/backend/src/main/proto/fetcher/fetcher_module.proto index b603108..70017d5 100644 --- a/backend/src/main/proto/fetcher/fetcher_module.proto +++ b/backend/src/main/proto/fetcher/fetcher_module.proto @@ -8,7 +8,6 @@ message FetcherRequest { oneof fetcher_type { DirectFetcher direct_fetcher = 1; FilteredFetcher filtered_fetcher = 2; - // We will need a filtered fetcher type in the future. } } @@ -25,7 +24,8 @@ message DirectFetcher { } enum DirectType { - DEPARTMENTS = 0; + DIRECT_TYPE_UNSPECIFIED = 0; + DIRECT_TYPE_DEPARTMENTS = 1; // Add more if needed, e.g. Majors, Umbrella topics, etc. } @@ -34,6 +34,7 @@ message FilteredFetcher { } enum FilteredType { - PROJECTS = 0; + FILTERED_TYPE_UNSPECIFIED = 0; + FILTERED_TYPE_PROJECTS = 1; // Add more if needed, e.g. Students. } diff --git a/backend/src/test/java/COMP_49X_our_search/backend/fetcher/DepartmentFetcherTest.java b/backend/src/test/java/COMP_49X_our_search/backend/fetcher/DepartmentFetcherTest.java index c87c96a..314219c 100644 --- a/backend/src/test/java/COMP_49X_our_search/backend/fetcher/DepartmentFetcherTest.java +++ b/backend/src/test/java/COMP_49X_our_search/backend/fetcher/DepartmentFetcherTest.java @@ -39,7 +39,7 @@ public void testFetch_validRequest_returnsExpectedResponse() { FetcherRequest request = FetcherRequest.newBuilder() .setDirectFetcher( - DirectFetcher.newBuilder().setDirectType(DirectType.DEPARTMENTS)) + DirectFetcher.newBuilder().setDirectType(DirectType.DIRECT_TYPE_DEPARTMENTS)) .build(); FetcherResponse response = departmentFetcher.fetch(request); diff --git a/backend/src/test/java/COMP_49X_our_search/backend/fetcher/FetcherModuleControllerTest.java b/backend/src/test/java/COMP_49X_our_search/backend/fetcher/FetcherModuleControllerTest.java index 1a501f5..380b467 100644 --- a/backend/src/test/java/COMP_49X_our_search/backend/fetcher/FetcherModuleControllerTest.java +++ b/backend/src/test/java/COMP_49X_our_search/backend/fetcher/FetcherModuleControllerTest.java @@ -42,7 +42,7 @@ void setUp() { public void testProcessConfig_validRequest_directType_returnsExpectedResponse() { FetcherRequest mockRequest = FetcherRequest.newBuilder() .setDirectFetcher( - DirectFetcher.newBuilder().setDirectType(DirectType.DEPARTMENTS)) + DirectFetcher.newBuilder().setDirectType(DirectType.DIRECT_TYPE_DEPARTMENTS)) .build(); FetcherResponse mockResponse = FetcherResponse.newBuilder() @@ -64,7 +64,7 @@ public void testProcessConfig_validRequest_directType_returnsExpectedResponse() public void testProcessConfig_validRequest_filteredType_returnsExpectedResponse() { FetcherRequest mockRequest = FetcherRequest.newBuilder() .setFilteredFetcher( - FilteredFetcher.newBuilder().setFilteredType(FilteredType.PROJECTS)) + FilteredFetcher.newBuilder().setFilteredType(FilteredType.FILTERED_TYPE_PROJECTS)) .build(); MajorWithProjects majorWithProjects = diff --git a/backend/src/test/java/COMP_49X_our_search/backend/fetcher/ProjectFetcherTest.java b/backend/src/test/java/COMP_49X_our_search/backend/fetcher/ProjectFetcherTest.java index b6be55d..5800186 100644 --- a/backend/src/test/java/COMP_49X_our_search/backend/fetcher/ProjectFetcherTest.java +++ b/backend/src/test/java/COMP_49X_our_search/backend/fetcher/ProjectFetcherTest.java @@ -80,7 +80,7 @@ public void testFetch_validRequest_returnsExpectedResponse() { FetcherRequest request = FetcherRequest.newBuilder() .setFilteredFetcher( - FilteredFetcher.newBuilder().setFilteredType(FilteredType.PROJECTS)) + FilteredFetcher.newBuilder().setFilteredType(FilteredType.FILTERED_TYPE_PROJECTS)) .build(); FetcherResponse response = projectFetcher.fetch(request); @@ -141,7 +141,7 @@ public void testFetch_projectWithMultipleMajors_returnsCorrectHierarchy() { // Execute test FetcherRequest request = FetcherRequest.newBuilder() .setFilteredFetcher( - FilteredFetcher.newBuilder().setFilteredType(FilteredType.PROJECTS)) + FilteredFetcher.newBuilder().setFilteredType(FilteredType.FILTERED_TYPE_PROJECTS)) .build(); FetcherResponse response = projectFetcher.fetch(request); @@ -237,7 +237,7 @@ public void testFetch_projectInMultipleDepartments_returnsCorrectHierarchy() { // Execute test FetcherRequest request = FetcherRequest.newBuilder() .setFilteredFetcher( - FilteredFetcher.newBuilder().setFilteredType(FilteredType.PROJECTS)) + FilteredFetcher.newBuilder().setFilteredType(FilteredType.FILTERED_TYPE_PROJECTS)) .build(); FetcherResponse response = projectFetcher.fetch(request); diff --git a/backend/src/test/java/COMP_49X_our_search/backend/gateway/ModuleInvokerTest.java b/backend/src/test/java/COMP_49X_our_search/backend/gateway/ModuleInvokerTest.java index 7b74e8e..25d7ba3 100644 --- a/backend/src/test/java/COMP_49X_our_search/backend/gateway/ModuleInvokerTest.java +++ b/backend/src/test/java/COMP_49X_our_search/backend/gateway/ModuleInvokerTest.java @@ -33,7 +33,7 @@ void setUp() { public void testProcessConfig_validFetcherRequest_returnsExpectedResponse() { FetcherRequest mockRequest = FetcherRequest.newBuilder() .setDirectFetcher( - DirectFetcher.newBuilder().setDirectType(DirectType.DEPARTMENTS)) + DirectFetcher.newBuilder().setDirectType(DirectType.DIRECT_TYPE_DEPARTMENTS)) .build(); FetcherResponse mockResponse = FetcherResponse.newBuilder() .setDepartmentCollection( diff --git a/frontend/public/USD_Logo.png b/frontend/public/USD_Logo.png new file mode 100644 index 0000000..ac16eba Binary files /dev/null and b/frontend/public/USD_Logo.png differ diff --git a/frontend/public/index.html b/frontend/public/index.html index aa069f2..b7c7a27 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -2,14 +2,14 @@ - + - + - React App + OUR SEARCH diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index 080d6c7..77abedf 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -8,12 +8,12 @@ "type": "image/x-icon" }, { - "src": "logo192.png", + "src": "USD_Logo.png", "type": "image/png", "sizes": "192x192" }, { - "src": "logo512.png", + "src": "USD_Logo.png", "type": "image/png", "sizes": "512x512" } diff --git a/frontend/src/__tests__/MainAccordion.test.js b/frontend/src/__tests__/MainAccordion.test.js index c2302b1..3e752d9 100644 --- a/frontend/src/__tests__/MainAccordion.test.js +++ b/frontend/src/__tests__/MainAccordion.test.js @@ -1,41 +1,51 @@ -import React, { act } from 'react' +import React from 'react' import { render, screen } from '@testing-library/react' -import MainAccordion from '../components/MainAccordion' -import { mockResearchOps } from '../resources/mockData' -import { errorLoadingPostingsMessage } from '../resources/constants' +import userEvent from '@testing-library/user-event' +import MajorAccordion from '../components/MajorAccordion' +import { mockMajorNoPosts, mockMajorOnePost } from '../resources/mockData' -const mockSetSelectedPost = jest.fn() +describe('MajorAccordion', () => { + const mockSetSelectedPost = jest.fn() -describe('MainAccordion', () => { - test('renders correct number of departments and majors', () => { - render() + test('renders major name', () => { + render( + + ) - mockResearchOps.forEach(department => { - const departmentHeader = screen.getByText(department.name) - expect(departmentHeader).toBeInTheDocument() + // Find the major name using test ID instead of text content + // to account for multiple elements that include the name of the + // major. + const majorNameEl = screen.getByTestId('major-name') + expect(majorNameEl).toHaveTextContent(mockMajorNoPosts.name) + expect(screen.getByText('(0 opportunities)')).toBeInTheDocument() + }) - // Expand the accordion to check for majors - act(() => { - departmentHeader.click() - }) + test('renders posts for the major', async () => { + render( + + ) - department.majors.forEach(major => { - expect(screen.getByText(major.name)).toBeInTheDocument() - }) - }) - }) + // Find the major name using test ID + const majorNameEl = screen.getByTestId('major-name') + expect(majorNameEl).toHaveTextContent(mockMajorOnePost.name) + expect(screen.getByText('(1 opportunities)')).toBeInTheDocument() - test('renders message if no students/research opportunities exist', () => { - render() + // Find and click the accordion summary + const accordionButton = screen.getByRole('button') + await userEvent.click(accordionButton) - expect(screen.getByText(errorLoadingPostingsMessage)).toBeInTheDocument() + // After expansion, verify the post content + const firstPostName = mockMajorOnePost.posts[0].name + expect(screen.getByText(firstPostName)).toBeInTheDocument() }) }) diff --git a/frontend/src/__tests__/MajorAccordion.test.js b/frontend/src/__tests__/MajorAccordion.test.js index f566050..86a199b 100644 --- a/frontend/src/__tests__/MajorAccordion.test.js +++ b/frontend/src/__tests__/MajorAccordion.test.js @@ -1,35 +1,56 @@ -import React, { act } from 'react' -import { render, screen } from '@testing-library/react' +import React from 'react' +import { render, screen, act, within } from '@testing-library/react' import MajorAccordion from '../components/MajorAccordion' import { mockMajorNoPosts, mockMajorOnePost } from '../resources/mockData' -describe('MajorAccordion', () => { - const mockSetSelectedPost = jest.fn() - - test('renders major name', () => { - render() +describe('MajorAccordion', function () { + function getMockSetSelectedPost () { + return jest.fn() + } + it('renders major name with 0 opportunities', function () { + render( + + ) expect(screen.getByText(mockMajorNoPosts.name)).toBeInTheDocument() + expect(screen.getByText('(0 opportunities)')).toBeInTheDocument() }) - test('renders posts for the major', () => { - render() + it('renders major name and posts when present', function () { + render( + + ) + + // Find the expandable summary button + const majorButton = screen.getByRole('button', { + name: new RegExp(`${mockMajorOnePost.name}.*\\(${mockMajorOnePost.posts.length} opportunities\\)`, 'i') + }) + + expect(majorButton).toBeInTheDocument() - const majorHeader = screen.getByText(mockMajorOnePost.name) - expect(majorHeader).toBeInTheDocument() + // Check for the major name within the summary (button) + expect(within(majorButton).getByText(mockMajorOnePost.name)).toBeInTheDocument() - // Expand the accordion to check for posts + // Expand the accordion act(() => { - majorHeader.click() - expect(screen.getByText(mockMajorOnePost.posts[0].name)).toBeInTheDocument() // refer to post[0] because there is only one post in this mock data + majorButton.click() }) + + // After expansion, find the region and check for the post name + const accordionRegion = screen.getByRole('region') + expect(accordionRegion).toBeInTheDocument() + + const postName = mockMajorOnePost.posts[0].name + expect(within(accordionRegion).getByText(postName)).toBeInTheDocument() }) }) diff --git a/frontend/src/__tests__/PostDialog.test.js b/frontend/src/__tests__/PostDialog.test.js index 8e9e4f7..377cb24 100644 --- a/frontend/src/__tests__/PostDialog.test.js +++ b/frontend/src/__tests__/PostDialog.test.js @@ -37,7 +37,8 @@ describe('PostDialog Component', () => { // Faculty expect(screen.getByText('Faculty:')).toBeInTheDocument() - expect(screen.getByText('John Doe (john.doe@sandiego.edu)')).toBeInTheDocument() + expect(screen.getByText('John Doe')).toBeInTheDocument() + expect(screen.getByText('john.doe@sandiego.edu')).toBeInTheDocument() }) it('renders the close button and triggers onClose when clicked', () => { diff --git a/frontend/src/__tests__/PostList.test.js b/frontend/src/__tests__/PostList.test.js index a5931b4..b65e872 100644 --- a/frontend/src/__tests__/PostList.test.js +++ b/frontend/src/__tests__/PostList.test.js @@ -1,5 +1,6 @@ import React from 'react' -import { render, screen, fireEvent } from '@testing-library/react' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import PostList from '../components/PostList' import { mockThreeActiveProjects, mockTwoInactiveProjects } from '../resources/mockData' import { noPostsMessage } from '../resources/constants' @@ -7,57 +8,89 @@ import { noPostsMessage } from '../resources/constants' describe('PostList', () => { const mockSetSelectedPost = jest.fn() - test('renders the names, research periods, and faculty info of all projects if isStudent', () => { - render() - - mockThreeActiveProjects.forEach((project) => { // all of these should render because they are all active - const rowText = `${project.name} ${project.researchPeriods} ${project.faculty.firstName} ${project.faculty.lastName} ${project.faculty.email}` - const row = screen.getByRole('row', { name: rowText }) - - expect(row).toHaveTextContent(project.name) - expect(row).toHaveTextContent(project.researchPeriods) - expect(row).toHaveTextContent(project.faculty.lastName) - expect(row).toHaveTextContent(project.faculty.email) - }) + afterEach(() => { + jest.clearAllMocks() + }) + + test('renders no postings message if not a student', () => { + render( + + ) + + expect(screen.getByText(noPostsMessage)).toBeInTheDocument() }) - test('renders message if no students/research opportunities exist', () => { - render() + test('renders no postings message if there are no postings', () => { + render( + + ) expect(screen.getByText(noPostsMessage)).toBeInTheDocument() }) - test('renders message if no ACTIVE students/research opportunities exist', () => { - render() + test('renders no active postings message if no ACTIVE postings exist', () => { + render( + + ) expect(screen.getByText(noPostsMessage)).toBeInTheDocument() }) - test('calls setSelectedPost if one if the postings is clicked', () => { - render() + test('renders active postings with correct details for students', () => { + render( + + ) + + mockThreeActiveProjects.forEach((project) => { + // Verify the project name + expect(screen.getByText(project.name)).toBeInTheDocument() + + // Verify faculty name and research period using a more flexible approach + const detailsText = screen.getByText((content, element) => { + return ( + element.tagName.toLowerCase() === 'p' && + content.includes(project.faculty.firstName) && + content.includes(project.faculty.lastName) && + content.includes(project.researchPeriods) + ) + }) + expect(detailsText).toBeInTheDocument() + }) + }) + + test('calls setSelectedPost when a card is clicked', async () => { + render( + + ) + + const firstProject = mockThreeActiveProjects[0] + const card = screen.getByText(firstProject.name).closest('.MuiCard-root') + expect(card).not.toBeNull() - const firstMockProject = mockThreeActiveProjects[0] - const firstMockName = `${firstMockProject.name} ${firstMockProject.researchPeriods} ${firstMockProject.faculty.firstName} ${firstMockProject.faculty.lastName} ${firstMockProject.faculty.email}` - const tableRow = screen.getByRole('row', { name: firstMockName }) - fireEvent.click(tableRow) + // Use userEvent instead of fireEvent for better interaction simulation + await userEvent.click(card) expect(mockSetSelectedPost).toHaveBeenCalledTimes(1) - expect(mockSetSelectedPost).toHaveBeenCalledWith(mockThreeActiveProjects[0]) + expect(mockSetSelectedPost).toHaveBeenCalledWith(firstProject) }) }) diff --git a/frontend/src/components/MainAccordion.js b/frontend/src/components/MainAccordion.js index 5201e1f..a29a0e0 100644 --- a/frontend/src/components/MainAccordion.js +++ b/frontend/src/components/MainAccordion.js @@ -2,38 +2,101 @@ import React from 'react' import { Accordion, AccordionSummary, AccordionDetails, Typography, Box } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import MajorAccordion from './MajorAccordion' -import { errorLoadingPostingsMessage } from '../resources/constants' +import { errorLoadingPostingsMessage, noPostsMessage } from '../resources/constants' import PropTypes from 'prop-types' function MainAccordion ({ postings, setSelectedPost, isStudent }) { - if (postings.length === 0) { - return {errorLoadingPostingsMessage} + // renderMajors handles the logic for displaying majors of a given department + const renderMajors = (department) => { + if (!isStudent || department.majors.length === 0) { + return ( + + {noPostsMessage} + + ) + } + + return department.majors.map((major) => ( + + )) } + // Render departments logic: + // If no postings it shows an error message + // Otherwise, it creates an accordion for each department + const renderDepartments = () => { + if (postings.length === 0) { + return {errorLoadingPostingsMessage} + } - return ( - - {postings.map((department) => ( - - } - aria-controls={`panel${department.id}-content`} - id={`panel${department.id}-header`} - > - {department.name} - + return postings.map((department) => ( + + } + aria-controls={`panel${department.id}-content`} + id={`panel${department.id}-header`} + sx={{ + bgcolor: '#F0F0F0', + borderRadius: '8px !important', + minHeight: '48px', + '& .MuiAccordionSummary-content': { + margin: '12px 0' + }, + '&.Mui-expanded': { + borderRadius: '8px 8px 0 0 !important' + } + }} + > + + + {department.name} + + + - - {department.majors.map((major) => ( - - ))} - - - ))} + + {renderMajors(department)} + + + )) + } + + return ( + + {renderDepartments()} ) } diff --git a/frontend/src/components/MainLayout.js b/frontend/src/components/MainLayout.js index 35c5582..c7eb697 100644 --- a/frontend/src/components/MainLayout.js +++ b/frontend/src/components/MainLayout.js @@ -21,7 +21,11 @@ function MainLayout ({ isStudent, fetchPostings }) { }, [fetchPostings, isStudent]) return ( - + {/* The outermost box that puts the header, search bar, and view profile button next to each other */} - + // Disable and remove the expand icon if there are no posts + } + expandIcon={numPosts > 0 ? : null} aria-controls={`panel${major.id}-content`} id={`panel${major.id}-header`} + sx={{ bgcolor: '#FAFAFA' }} > - {major.name} + + + {major.name} + + + ({numPosts} opportunities) + + - - - - - + {numPosts > 0 && ( + + + + )} ) } MajorAccordion.propTypes = { major: PropTypes.shape({ + id: PropTypes.any.isRequired, name: PropTypes.string.isRequired, posts: PropTypes.array.isRequired - }), + }).isRequired, + numPosts: PropTypes.number.isRequired, setSelectedPost: PropTypes.func.isRequired, isStudent: PropTypes.bool.isRequired } diff --git a/frontend/src/components/PostDialog.js b/frontend/src/components/PostDialog.js index 71983f6..d2f7485 100644 --- a/frontend/src/components/PostDialog.js +++ b/frontend/src/components/PostDialog.js @@ -1,9 +1,9 @@ import React from 'react' -import PropTypes from 'prop-types' import { Dialog, DialogTitle, DialogContent, Button, Typography } from '@mui/material' const PostDialog = ({ onClose, post }) => { if (!post) return null + if (!post) return null const { name, @@ -19,53 +19,121 @@ const PostDialog = ({ onClose, post }) => { // TODO in later sprint: if isStudent render x, but if isFaculty render y return ( - + {/* Close Button */} - {/* Title */} - {name} + + {name} + {/* Content */} - + {/* Description */} - + Description: {description} {/* Layout for Remaining Fields */} -
+
{/* Column 1 */}
- + Qualifications: {desiredQualifications} - - Status: {isActive ? 'Active' : 'Inactive'} + + Status: {' '} + + {isActive ? 'Active' : 'Inactive'} +
{/* Column 2 */}
- + Topics: {umbrellaTopics.join(', ')} - + Periods: {researchPeriods.join(', ')}
{/* Column 3 */}
- + Majors: {majors.join(', ')} - - Faculty: {`${faculty.firstName} ${faculty.lastName} (${faculty.email})`} + + Faculty: {`${faculty.firstName} ${faculty.lastName}`} +
+ + {faculty.email} +
- {/* {TODO: this only displays the first faculty member listed, but what if there are multiple?} */}
@@ -73,23 +141,4 @@ const PostDialog = ({ onClose, post }) => { ) } -PostDialog.propTypes = { - onClose: PropTypes.func.isRequired, - post: PropTypes.shape({ - name: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - desiredQualifications: PropTypes.string, - umbrellaTopics: PropTypes.arrayOf(PropTypes.string), - researchPeriods: PropTypes.arrayOf(PropTypes.string), - isActive: PropTypes.bool.isRequired, - majors: PropTypes.arrayOf(PropTypes.string), - faculty: PropTypes.shape({ - firstName: PropTypes.string, - lastName: PropTypes.string, - email: PropTypes.string - }) - - }) -} - export default PostDialog diff --git a/frontend/src/components/PostList.js b/frontend/src/components/PostList.js index a92ab1e..0a9d2c3 100644 --- a/frontend/src/components/PostList.js +++ b/frontend/src/components/PostList.js @@ -1,57 +1,133 @@ import React from 'react' -import { Typography, Table, TableBody, TableCell, TableContainer, TableRow, Paper } from '@mui/material' +import { + Typography, + Box, + Card, + CardContent, + Stack, + IconButton, + Chip +} from '@mui/material' +import EmailIcon from '@mui/icons-material/Email' +import SchoolIcon from '@mui/icons-material/School' +import LocalOfferIcon from '@mui/icons-material/LocalOffer' import { noPostsMessage } from '../resources/constants' import PropTypes from 'prop-types' function PostList ({ postings, setSelectedPost, isStudent }) { - const activePosts = postings.filter((post) => post.isActive) + // Filter out inactive postings. + const activePostings = postings.filter((post) => post.isActive) - if (!isStudent) { + if (!isStudent || postings.length === 0) { return ( - - {noPostsMessage} - + + {noPostsMessage} + ) } - if (postings.length === 0) { + if (activePostings.length === 0) { return ( - - {noPostsMessage} - - ) - } - - if (activePosts.length === 0) { - return ( - - {noPostsMessage} - + + {noPostsMessage} + ) } return ( - - - - {activePosts.map((post) => ( - setSelectedPost(post)} - style={{ cursor: 'pointer' }} - > - {post.name} - {post.researchPeriods} - + + + {activePostings.map((post) => ( + setSelectedPost(post)} + sx={{ + cursor: 'pointer', + '&:hover': { + boxShadow: 3 + }, + position: 'relative' + }} + > + + + + + + + {post.name} + + + {post.faculty.firstName} {post.faculty.lastName} - - {post.faculty.email} - - ))} - -
-
+   •   + {post.researchPeriods.join(', ')} + + + + + + + {post.majors?.map((major, index) => ( + + ))} + + + + {post.umbrellaTopics?.length > 0 && ( + + + + {post.umbrellaTopics.slice(0, 3).map((topic, index) => ( + + ))} + {post.umbrellaTopics.length > 3 && ( + + +{post.umbrellaTopics.length - 3} more + + )} + + + )} + + + + ))} + + ) } diff --git a/frontend/src/components/SearchBar.js b/frontend/src/components/SearchBar.js index a0ebded..9f0639a 100644 --- a/frontend/src/components/SearchBar.js +++ b/frontend/src/components/SearchBar.js @@ -1,14 +1,23 @@ import React from 'react' -import { Box, Typography } from '@mui/material' +import { Box, TextField, InputAdornment } from '@mui/material' +import SearchIcon from '@mui/icons-material/Search' function SearchBar () { + // A simple search bar with a search icon at the end return ( - - Fake search bar - + + + + ) + }} + /> ) } diff --git a/frontend/src/components/TitleButton.js b/frontend/src/components/TitleButton.js index 8cf10c1..f4ff4f2 100644 --- a/frontend/src/components/TitleButton.js +++ b/frontend/src/components/TitleButton.js @@ -1,12 +1,28 @@ import React from 'react' +import { createTheme, ThemeProvider } from '@mui/material/styles' import { Box, Typography } from '@mui/material' import { appTitle } from '../resources/constants' +const theme = createTheme({ + typography: { + fontFamily: [ + 'Arial Narrow', + 'Arial', + 'sans-serif' + ].join(','), + h5: { + fontWeight: 900, + letterSpacing: '0.02em', + lineHeight: 1 + } + } +}) + function TitleButton () { const handleReload = () => { - window.location.reload() // Reloads the page + window.location.reload() } - + // Clickable title that uses a custom theme and reloads the page return ( - - {appTitle} - + + + {appTitle} + + ) } diff --git a/frontend/src/resources/mockData.js b/frontend/src/resources/mockData.js index 866bea5..662e6e1 100644 --- a/frontend/src/resources/mockData.js +++ b/frontend/src/resources/mockData.js @@ -14,13 +14,11 @@ export const mockOneActiveProject = { researchPeriods: ['Spring 2024', 'Fall 2024'], isActive: true, majors: ['Computer Science', 'Education'], - faculty: - { - firstName: 'John', - lastName: 'Doe', - email: 'john.doe@sandiego.edu' - } - + faculty: { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@sandiego.edu' + } } export const mockTwoInactiveProjects = [ @@ -33,13 +31,11 @@ export const mockTwoInactiveProjects = [ researchPeriods: ['Spring 2025'], isActive: false, majors: mockThreeMajorsList, - faculty: - { - firstName: 'Dr.', - lastName: 'Coding', - email: 'drcoding@sandiego.edu' - } - + faculty: { + firstName: 'Dr.', + lastName: 'Coding', + email: 'drcoding@sandiego.edu' + } }, { id: 1002, @@ -50,13 +46,11 @@ export const mockTwoInactiveProjects = [ researchPeriods: ['Fall 2025'], isActive: false, majors: mockThreeMajorsList, - faculty: - { - firstName: 'Dr.', - lastName: 'Debugger', - email: 'debugger@sandiego.edu' - } - + faculty: { + firstName: 'Dr.', + lastName: 'Debugger', + email: 'debugger@sandiego.edu' + } } ] @@ -70,13 +64,11 @@ export const mockThreeActiveProjects = [ researchPeriods: ['Spring 2025'], isActive: true, majors: mockThreeMajorsList, - faculty: - { - firstName: 'Dr.', - lastName: 'Coding', - email: 'drcoding@sandiego.edu' - } - + faculty: { + firstName: 'Dr.', + lastName: 'Coding', + email: 'drcoding@sandiego.edu' + } }, { id: 1002, @@ -87,12 +79,11 @@ export const mockThreeActiveProjects = [ researchPeriods: ['Fall 2025'], isActive: true, majors: mockThreeMajorsList, - faculty: - { - firstName: 'Dr.', - lastName: 'Debugger', - email: 'debugger@sandiego.edu' - } + faculty: { + firstName: 'Dr.', + lastName: 'Debugger', + email: 'debugger@sandiego.edu' + } }, { id: 1003, @@ -103,12 +94,11 @@ export const mockThreeActiveProjects = [ researchPeriods: ['Spring 2025'], isActive: true, majors: mockOneMajorList, - faculty: - { - firstName: 'Dr.', - lastName: 'React', - email: 'react@sandiego.edu' - } + faculty: { + firstName: 'Dr.', + lastName: 'React', + email: 'react@sandiego.edu' + } } ] @@ -131,12 +121,11 @@ export const mockMajorOnePost = { researchPeriods: ['Spring 2025'], isActive: true, majors: ['Sociology'], - faculty: - { - firstName: 'Dr.', - lastName: 'Social', - email: 'social@sandiego.edu' - } + faculty: { + firstName: 'Dr.', + lastName: 'Social', + email: 'social@sandiego.edu' + } } ] } @@ -164,12 +153,11 @@ export const mockResearchOps = [ researchPeriods: ['Fall 2025'], isActive: true, majors: ['Electrical Engineering'], - faculty: - { - firstName: 'Dr.', - lastName: 'Semiconductor', - email: 'semi@sandiego.edu' - } + faculty: { + firstName: 'Dr.', + lastName: 'Semiconductor', + email: 'semi@sandiego.edu' + } } ] } @@ -192,12 +180,11 @@ export const mockResearchOps = [ researchPeriods: ['Spring 2025'], isActive: true, majors: ['Visual Arts'], - faculty: - { - firstName: 'Dr.', - lastName: 'Rainbow', - email: 'rainbow@sandiego.edu' - } + faculty: { + firstName: 'Dr.', + lastName: 'Rainbow', + email: 'rainbow@sandiego.edu' + } } ] }, @@ -214,12 +201,11 @@ export const mockResearchOps = [ researchPeriods: ['Fall 2025'], isActive: true, majors: ['Music', 'Art History'], - faculty: - { - firstName: 'Dr.', - lastName: 'Mozart', - email: 'wannabeMozart@sandiego.edu' - } + faculty: { + firstName: 'Dr.', + lastName: 'Mozart', + email: 'wannabeMozart@sandiego.edu' + } } ] }