diff --git a/code/frontend/package-lock.json b/code/frontend/package-lock.json index d5b9954..6a62171 100644 --- a/code/frontend/package-lock.json +++ b/code/frontend/package-lock.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@eslint/js": "^9.33.0", - "@testing-library/jest-dom": "^6.8.0", + "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@types/jest": "^30.0.0", @@ -2670,9 +2670,9 @@ "peer": true }, "node_modules/@testing-library/jest-dom": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz", - "integrity": "sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/code/frontend/package.json b/code/frontend/package.json index bdad605..65fac0d 100644 --- a/code/frontend/package.json +++ b/code/frontend/package.json @@ -22,7 +22,7 @@ }, "devDependencies": { "@eslint/js": "^9.33.0", - "@testing-library/jest-dom": "^6.8.0", + "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@types/jest": "^30.0.0", diff --git a/code/frontend/src/api/auth/login.ts b/code/frontend/src/api/auth/login.ts index 2c153b6..0fde7f8 100644 --- a/code/frontend/src/api/auth/login.ts +++ b/code/frontend/src/api/auth/login.ts @@ -1,5 +1,8 @@ // src/api/auth/login.ts // Centralized login request helper +// Copilot generated initial structure and logic +// Human refined error handling, types, and token management +// 40% AI-generated, 60% human refined import { postJson, stripEnvelope } from '../http'; @@ -22,7 +25,6 @@ export async function login( ): Promise<{ token: string; raw: LoginResponseEnvelope }> { // Normalize input const trimmedEmail = (username || '').trim(); - console.log('Attempting login with:', { email: trimmedEmail }); // IMPORTANT: ensure we never send stale tokens on the login call try { @@ -52,7 +54,6 @@ export async function login( localStorage.setItem('jwt', token); } catch {} - console.log('Login response data:', data); return { token, raw: unwrapped }; } catch (error: any) { console.error('Login error:', error); diff --git a/code/frontend/src/api/pages/myJobs.ts b/code/frontend/src/api/pages/myJobs.ts index d8fb4e3..185271a 100644 --- a/code/frontend/src/api/pages/myJobs.ts +++ b/code/frontend/src/api/pages/myJobs.ts @@ -52,7 +52,6 @@ export async function getMySaved(): Promise { const body = { size: 10, page: 0 }; const { data } = await postJson('/jobs/saved/list', body); - console.log('getMySaved response data:', data); return normalizeJobs(data); } @@ -61,7 +60,6 @@ export async function getMyApplied(): Promise { const body = { size: 10, page: 0 }; const { data } = await postJson('/jobs/applied/list', body); - console.log('getMyApplied response data:', data); return normalizeJobs(data); } @@ -95,8 +93,6 @@ export async function deleteSavedJob(id: string | number): Promise { uid: uid, // Include the user ID which is required by the backend }); - console.log('deleteSavedJob response:', response?.status, data); - // Check if the deletion was successful const success = data?.success || @@ -119,7 +115,6 @@ export async function deleteAppliedJob(id: string | number): Promise { jobIds: [jobId], uid: uid, // Include the user ID which is required by the backend }; - console.log('deleteAppliedJob request body:', requestBody); try { // Use the specified endpoint /jobs/applied/delete with the correct request body format @@ -128,8 +123,6 @@ export async function deleteAppliedJob(id: string | number): Promise { requestBody ); - console.log('deleteAppliedJob response:', response?.status, data); - // Check if the deletion was successful const success = data?.success || @@ -164,8 +157,6 @@ export async function deleteAllSaved(): Promise { uid, // Include the user ID which is required by the backend }); - console.log('deleteAllSaved response:', response?.status, data); - // Check if the deletion was successful const success = data?.success || @@ -193,8 +184,6 @@ export async function deleteAllApplied(): Promise { uid, // Include the user ID which is required by the backend }); - console.log('deleteAllApplied response:', response?.status, data); - // Check if the deletion was successful const success = data?.success || diff --git a/code/frontend/src/api/savedAndApplied/savedAndApplied.ts b/code/frontend/src/api/savedAndApplied/savedAndApplied.ts index 4d85201..3c8f92b 100644 --- a/code/frontend/src/api/savedAndApplied/savedAndApplied.ts +++ b/code/frontend/src/api/savedAndApplied/savedAndApplied.ts @@ -126,7 +126,7 @@ export function buildSavedJobPayload(job: Job): SavedJobPayload { export async function saveJob(job: Job): Promise { const payload = buildSavedJobPayload(job); const { data } = await postJson('/jobs/saved/save', payload); - console.log('Save job response data:', data); + return data; } @@ -136,6 +136,6 @@ export async function applyJob(job: Job): Promise { '/jobs/applied/apply', payload ); - console.log('Apply job response data:', data); + return data; } diff --git a/code/frontend/src/components/asideAndToggler/Aside.tsx b/code/frontend/src/components/asideAndToggler/Aside.tsx index 385a464..b858c5b 100644 --- a/code/frontend/src/components/asideAndToggler/Aside.tsx +++ b/code/frontend/src/components/asideAndToggler/Aside.tsx @@ -40,8 +40,10 @@ export default function Aside({ myJobsView, onMyJobsViewChange }: AsideProps) { role="group" aria-label="Filters" > - - + {/* Hide Field and Type components on MyJobs page */} + {!isMyJobs &&
FILTERS
} + {!isMyJobs && } + {!isMyJobs && } ); diff --git a/code/frontend/src/components/headerAndFooter/Footer.css b/code/frontend/src/components/headerAndFooter/Footer.css index 536fa36..00365ed 100644 --- a/code/frontend/src/components/headerAndFooter/Footer.css +++ b/code/frontend/src/components/headerAndFooter/Footer.css @@ -12,6 +12,7 @@ text-align: center; padding: 1rem; color: #717273; + transition: background-color 0.3s ease; } /* Dark theme override */ diff --git a/code/frontend/src/components/headerAndFooter/Footer.tsx b/code/frontend/src/components/headerAndFooter/Footer.tsx index f6ea586..173990b 100644 --- a/code/frontend/src/components/headerAndFooter/Footer.tsx +++ b/code/frontend/src/components/headerAndFooter/Footer.tsx @@ -7,12 +7,17 @@ */ import './Footer.css'; +import { useTheme } from '../../theme/ThemeContext'; const Footer: React.FC = () => { const currentYear = new Date().getFullYear(); + const { theme } = useTheme(); return ( -
+
CS673OLFall25-Team2 | {currentYear} |{' '} Boston University
diff --git a/code/frontend/src/components/jobsList/MyJobsViewList.tsx b/code/frontend/src/components/jobsList/MyJobsViewList.tsx index cd11802..b3771cf 100644 --- a/code/frontend/src/components/jobsList/MyJobsViewList.tsx +++ b/code/frontend/src/components/jobsList/MyJobsViewList.tsx @@ -160,8 +160,6 @@ const MyJobsViewList: React.FC = ({ const updatedJobs = view === 'saved' ? await getMySaved() : await getMyApplied(); - console.log(`Updated ${label} jobs list:`, updatedJobs); - // Check if the job was actually deleted const jobStillExists = updatedJobs.some((job) => job.id === id); if (jobStillExists) { diff --git a/code/frontend/src/components/jobsList/SavedJobApplyButton.tsx b/code/frontend/src/components/jobsList/SavedJobApplyButton.tsx index aa326eb..8b854a6 100644 --- a/code/frontend/src/components/jobsList/SavedJobApplyButton.tsx +++ b/code/frontend/src/components/jobsList/SavedJobApplyButton.tsx @@ -203,9 +203,6 @@ export default function SavedJobApplyButton({ // Add the alert to the container container.appendChild(alertDiv); - - // Log to confirm alert is triggered - console.log('Job applied successfully, showing alert'); } else { throw new Error('Apply did not succeed'); } diff --git a/code/frontend/src/components/jobsList/localStorageHelpers.ts b/code/frontend/src/components/jobsList/localStorageHelpers.ts index 3245a00..d3938b9 100644 --- a/code/frontend/src/components/jobsList/localStorageHelpers.ts +++ b/code/frontend/src/components/jobsList/localStorageHelpers.ts @@ -10,7 +10,6 @@ export const clearSavedAppliedJobs = (): string => { localStorage.removeItem(SAVED_JOBS_KEY); localStorage.removeItem(APPLIED_JOBS_KEY); - console.log('Cleared saved and applied jobs from localStorage'); return 'Cleared'; }; diff --git a/code/frontend/src/components/loginAndRegistration/LoginForm.tsx b/code/frontend/src/components/loginAndRegistration/LoginForm.tsx index d6fb51d..5f549b0 100644 --- a/code/frontend/src/components/loginAndRegistration/LoginForm.tsx +++ b/code/frontend/src/components/loginAndRegistration/LoginForm.tsx @@ -81,7 +81,6 @@ const LoginForm: React.FC = ({ onSubmit, showSubmitButton = false }) => { try { setLoading(true); - console.log('Login attempt with:', { email: values.username }); const { token, raw } = await loginRequest( values.username.trim(), values.password diff --git a/code/frontend/src/components/loginAndRegistration/RegisterForm.tsx b/code/frontend/src/components/loginAndRegistration/RegisterForm.tsx index d112323..5a2c492 100644 --- a/code/frontend/src/components/loginAndRegistration/RegisterForm.tsx +++ b/code/frontend/src/components/loginAndRegistration/RegisterForm.tsx @@ -90,7 +90,6 @@ const RegisterForm: React.FC = ({ try { setLoading(true); - console.log('Submitting registration with:', values); const payload: RegisterPayload = { name: values.name, @@ -99,7 +98,6 @@ const RegisterForm: React.FC = ({ }; try { - console.log('Sending registration request with payload:', payload); const result = await registerRequest(payload); console.log('Registration successful:', result); diff --git a/code/frontend/src/components/loginAndRegistration/validation.ts b/code/frontend/src/components/loginAndRegistration/validation.ts index dfbb687..2da52b9 100644 --- a/code/frontend/src/components/loginAndRegistration/validation.ts +++ b/code/frontend/src/components/loginAndRegistration/validation.ts @@ -2,7 +2,7 @@ AI-generated code: ~50% - Tool: ChatGPT (link: https://chatgpt.com/share/68d43c9d-4d60-8006-a1a7-14ae49475a5a) - Functions/classes: isRequired, isEmail utility functions suggested by AI - Human code (James Rose): ~50% + Human code: ~50% - Adjustments: simplified regex validation, added doc comments, integration testing with forms Framework-generated code: 0% - (Plain TypeScript helpers, no framework generation) @@ -14,4 +14,4 @@ export const isRequired = (v: string) => v.trim().length > 0; // Simple regex check for email format export const isEmail = (v: string) => - /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()); \ No newline at end of file + /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()); diff --git a/code/frontend/src/components/wrapper/Layout.css b/code/frontend/src/components/wrapper/Layout.css new file mode 100644 index 0000000..d14cdfe --- /dev/null +++ b/code/frontend/src/components/wrapper/Layout.css @@ -0,0 +1,93 @@ +/* Layout.css +Styles for both mobile and desktop layouts +Copilot-generated code: ~40%, Human code: ~60% +*/ + +#mobile-layout-container { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +/* Filters dropdown styles */ +.filters-toggle-container { + padding: 0.5rem; + text-align: center; + background-color: #f8f9fa; + border-bottom: 1px solid #dee2e6; +} + +.filters-toggle-button { + width: 90%; + max-width: 300px; +} + +.mobile-filters-container { + display: flex; + flex-direction: column; + transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out; + overflow: hidden; +} + +.mobile-filters-container.open { + max-height: 1000px; /* Large enough to contain content */ + opacity: 1; +} + +.mobile-filters-container.closed { + max-height: 0; + opacity: 0; + padding: 0; + margin: 0; +} + +#layout-aside { + width: 100%; +} + +#layout-main { + flex: 1; + overflow: auto; +} + +/* Mobile-specific styles using parent-child selectors */ +#mobile-layout-container #layout-aside > div { + /* Styles for layout-mobile-aside-internal */ + flex-direction: row !important; + justify-content: space-between; + align-items: center; + padding: 7px !important; +} + +#mobile-layout-container #layout-aside > div > div:first-child { + /* Styles for layout-mobile-aside-filters */ + width: 70%; + margin: 1rem; +} + +#layout-footer { + /* make sticky footer */ + position: sticky; + bottom: 0; + width: 100%; +} + +#desktop-layout-container { + transition: all 0.3s ease; +} + +/* Desktop-specific styles to override mobile styles */ +#desktop-layout-container aside > div { + flex-direction: column !important; + padding: 0 !important; +} + +#desktop-layout-container aside > div > div:first-child { + width: 100%; + margin: 0; +} + +/* Utility classes for smooth transitions */ +.layout-transition { + transition: all 0.3s ease-in-out; +} diff --git a/code/frontend/src/components/wrapper/Layout.tsx b/code/frontend/src/components/wrapper/Layout.tsx index b898a1c..08842f5 100644 --- a/code/frontend/src/components/wrapper/Layout.tsx +++ b/code/frontend/src/components/wrapper/Layout.tsx @@ -2,7 +2,9 @@ // Copilot assisted with this component // 60% AI-generated, 40% human refined +import { useState, useEffect } from 'react'; import type { ReactNode } from 'react'; +import './Layout.css'; type LayoutProps = { header?: ReactNode; @@ -21,8 +23,57 @@ export default function Layout({ }: LayoutProps) { const main = mainContent ?? children; - return ( -
+ // State to track viewport width with SSR safety + const [isDesktop, setIsDesktop] = useState(false); + const [isMounted, setIsMounted] = useState(false); + const [isFiltersOpen, setIsFiltersOpen] = useState(false); + + // Effect to handle window resize and initialize + useEffect(() => { + setIsMounted(true); + setIsDesktop(window.innerWidth >= 992); // 992px is Bootstrap's lg breakpoint + + const handleResize = () => { + setIsDesktop(window.innerWidth >= 992); + }; + + // Add event listener + window.addEventListener('resize', handleResize); + + // Clean up + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + // Only conditionally render after component is mounted in client + // This prevents hydration errors due to SSR vs client-side differences + if (!isMounted) { + // Return a simple placeholder with same structure until client-side hydration + return ( +
+ {header} +
+ +
+
+
{aside}
+
+
{main}
+ +
+ ); + } + + // Once mounted, conditionally render based on viewport size + return isDesktop ? ( +
{header}
@@ -32,7 +83,6 @@ export default function Layout({ className="col-12 order-0 order-lg-2 col-lg-auto bg-body overflow-auto p-3" style={{ width: 'clamp(260px, 22vw, 320px)', minHeight: 0 }} > -
FILTERS
{aside} )} @@ -49,5 +99,29 @@ export default function Layout({
{footer}
+ ) : ( +
+ {header} +
+ +
+
+
{aside}
+
+
{main}
+ +
); } diff --git a/code/frontend/src/tests/ApplyJobButton.tdd.test.tsx b/code/frontend/src/tests/ApplyJobButton.tdd.test.tsx new file mode 100644 index 0000000..05da3da --- /dev/null +++ b/code/frontend/src/tests/ApplyJobButton.tdd.test.tsx @@ -0,0 +1,98 @@ +// src/tests/ApplyJobButton.tdd.test.tsx +// Purpose: Verify ApplyJobButton’s UI states, user interactions, and API/localStorage behavior +// 80% AI-generated, 20% human refined + +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import ApplyJobButton from '../components/jobsList/ApplyJobButton'; +import { applyJob } from '../api/savedAndApplied/savedAndApplied'; +import type { Job } from '../types/job'; + +// Mock the applyJob API +jest.mock('../api/savedAndApplied/savedAndApplied', () => ({ + applyJob: jest.fn(), +})); + +// Mock localStorage helpers without importing (avoids TS unused import error) +jest.mock('../components/jobsList/localStorageHelpers', () => ({ + isJobApplied: jest.fn(() => false), + toggleAppliedJobId: jest.fn(() => true), +})); + +describe('ApplyJobButton', () => { + // Mock window.open to avoid jsdom "Not implemented" error + beforeAll(() => { + window.open = jest.fn(); + }); + + const mockJob: Job = { + _id: '1', + title: 'Frontend Developer', + url: 'https://example.com/apply', + createdAt: '', + department: 'Engineering', + descriptionBreakdown: { + employmentType: 'Full-time', + keywords: [], + oneSentenceJobSummary: 'Develop front-end apps', + salaryRangeMaxYearly: 120000, + salaryRangeMinYearly: 80000, + skillRequirements: ['React', 'TypeScript'], + workModel: 'Remote', + }, + locationAddress: '123 Main St', + locationCoordinates: { lat: 0, lon: 0 }, + owner: { + _id: 'o1', + companyName: 'Tech Co', + benefits: { title: 'Benefits', benefits: [] }, + evaluatedSize: null, + funding: 'Series A', + isClaimed: true, + locationAddress: '123 Main St', + photo: '', + rating: '5', + slug: 'tech-co', + teamSize: 50, + }, + seniority: 'Mid', + skills_suggest: ['React', 'TypeScript'], + type: 'Job', + updatedAt: '', + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders Apply button initially', () => { + render(); + expect(screen.getByRole('button', { name: /apply/i })).toBeInTheDocument(); + }); + + it('handles successful apply flow', async () => { + (applyJob as jest.Mock).mockResolvedValue({ success: true }); + + render(); + const btn = screen.getByRole('button', { name: /apply/i }); + fireEvent.click(btn); + + await waitFor(() => { + expect(applyJob).toHaveBeenCalledWith(mockJob); + expect(screen.getByRole('button', { name: /applied/i })).toBeInTheDocument(); + expect(screen.getByText(/successfully applied/i)).toBeInTheDocument(); + }); + }); + + it('handles failed apply flow', async () => { + (applyJob as jest.Mock).mockRejectedValue(new Error('Network fail')); + + render(); + const btn = screen.getByRole('button', { name: /apply/i }); + fireEvent.click(btn); + + await waitFor(() => { + expect(applyJob).toHaveBeenCalled(); + expect(screen.getByText(/network fail/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/code/frontend/src/tests/Aside.tdd.test.tsx b/code/frontend/src/tests/Aside.tdd.test.tsx new file mode 100644 index 0000000..b81aaf2 --- /dev/null +++ b/code/frontend/src/tests/Aside.tdd.test.tsx @@ -0,0 +1,48 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Aside from '../components/asideAndToggler/Aside'; +import { MemoryRouter } from 'react-router-dom'; +import { ThemeProvider } from '../theme/ThemeContext'; + +/* +AI-generated code: 70% (tool: ChatGPT, adapted; functions: jest.mock usage and test case scaffolding; AI chat links: [https://chatgpt.com/share/68e18e34-9110-8006-b694-a6dadf0cafd5]) +Human code (James): 25% (test logic, expectations, and navigation assertions) +Framework-generated code: 5% (React Testing Library, Jest DOM APIs) +*/ + +// Mock navigate so we can assert where it goes +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { + ...actual, + useNavigate: () => mockNavigate, + }; +}); + +test('shows "My Jobs" on non-MyJobs routes and navigates to /myJobs', () => { + render( + + +