From fe1e1fe572c1f5669176dd13f7fc6e24da1e091a Mon Sep 17 00:00:00 2001 From: CasperL1218 Date: Sun, 27 Oct 2024 18:55:43 -0400 Subject: [PATCH 1/9] Implemented travelTime Firebase Data - Created travelTime collection - Implemented POST endpoint to calculate travel distances using Google Map Matrix API - Implemented testing POST endpoint to create single building document --- backend/src/app.ts | 146 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/backend/src/app.ts b/backend/src/app.ts index 27bca15b..1898b988 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -19,6 +19,7 @@ import { import { auth } from 'firebase-admin'; import { Timestamp } from '@firebase/firestore-types'; import nodemailer from 'nodemailer'; +import axios from 'axios'; import { db, FieldValue, FieldPath } from './firebase-config'; import { Faq } from './firebase-config/types'; import authenticate from './auth'; @@ -39,6 +40,8 @@ const usersCollection = db.collection('users'); const pendingBuildingsCollection = db.collection('pendingBuildings'); const contactQuestionsCollection = db.collection('contactQuestions'); +const travelTimesCollection = db.collection('travelTimes'); + // Middleware setup const app: Express = express(); @@ -943,4 +946,147 @@ app.post('/api/add-contact-question', authenticate, async (req, res) => { } }); +// 1. Fix the camelCase interface properties +interface TravelTimes { + artsQuadWalking: number; + artsQuadDriving: number; + duffieldWalking: number; + duffieldDriving: number; + ctbWalking: number; + ctbDriving: number; +} + +const {GOOGLE_MAPS_API_KEY} = process.env; +const LANDMARKS = { + arts_quad: 'Arts Quad, Cornell University, Ithaca, NY 14853', + duffield: 'Duffield Hall, Cornell University, Ithaca, NY 14853', + ctb: 'College Town Bagels, College Ave, Ithaca, NY 14850', +}; + +interface TravelTimeInput { + origin: string; // Can be either address or "latitude,longitude" +} + +/** + * Travel Times Calculator - Calculates walking and driving times from a given origin to Cornell landmarks. + * + * @remarks + * Uses Google Maps Distance Matrix API to calculate travel times to three landmarks: Arts Quad, + * Duffield Hall, and College Town Bagels. Returns both walking and driving durations in minutes. + * Origin can be either an address or coordinates in "latitude,longitude" format. + * + * @route POST /api/travel-times + * + * @input {string} req.body.origin - Starting point, either as address or "latitude,longitude" + * + * @status + * - 200: Successfully retrieved travel times + * - 400: Missing or invalid origin + * - 500: Server error or Google Maps API failure + */ +app.post('/api/travel-times', async (req, res) => { + try { + const { origin } = req.body as TravelTimeInput; + + if (!origin) { + return res.status(400).json({ error: 'Origin is required' }); + } + + // Create destinations array + const destinations = Object.values(LANDMARKS); + + // Make request to Google Distance Matrix API for walking times + const walkingResponse = await axios.get( + `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${encodeURIComponent( + origin + )}&destinations=${destinations + .map((dest) => encodeURIComponent(dest)) + .join('|')}&mode=walking&key=${GOOGLE_MAPS_API_KEY}` + ); + + // Make request to Google Distance Matrix API for driving times + const drivingResponse = await axios.get( + `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${encodeURIComponent( + origin + )}&destinations=${destinations + .map((dest) => encodeURIComponent(dest)) + .join('|')}&mode=driving&key=${GOOGLE_MAPS_API_KEY}` + ); + + // Fix the any types with a proper interface + interface DistanceMatrixElement { + duration: { + value: number; + }; + } + + const walkingTimes = walkingResponse.data.rows[0].elements.map( + (element: DistanceMatrixElement) => element.duration.value / 60 + ); + const drivingTimes = drivingResponse.data.rows[0].elements.map( + (element: DistanceMatrixElement) => element.duration.value / 60 + ); + + const travelTimes: TravelTimes = { + artsQuadWalking: walkingTimes[0], + artsQuadDriving: drivingTimes[0], + duffieldWalking: walkingTimes[1], + duffieldDriving: drivingTimes[1], + ctbWalking: walkingTimes[2], + ctbDriving: drivingTimes[2], + }; + + return res.status(200).json(travelTimes); + } catch (error) { + console.error('Error calculating travel times:', error); + return res.status(500).json({ error: 'Internal server error' }); + } +}); + +/** + * Test Travel Times Endpoint - Creates a travel times document for a specific building. + * + * @remarks + * Retrieves building coordinates from the buildings collection, calculates travel times to Cornell landmarks, + * and stores the results in the travelTimes collection. This endpoint is used for testing and populating + * travel time data for existing buildings. + * + * @param {string} buildingId - The ID of the building to calculate and store travel times for + * + * @return {Object} - Object containing success message, building ID, and calculated travel times + */ +app.post('/api/test-travel-times/:buildingId', async (req, res) => { + try { + const { buildingId } = req.params; + + // Get building data + const buildingDoc = await buildingsCollection.doc(buildingId).get(); + if (!buildingDoc.exists) { + return res.status(404).json({ error: 'Building not found' }); + } + + const buildingData = buildingDoc.data(); + if (!buildingData?.latitude || !buildingData?.longitude) { + return res.status(400).json({ error: 'Building missing coordinate data' }); + } + + // Calculate travel times using the main endpoint + const response = await axios.post(`${process.env.API_BASE_URL}/api/travel-times`, { + origin: `${buildingData.latitude},${buildingData.longitude}`, + }); + + // Store in Firebase + await travelTimesCollection.doc(buildingId).set(response.data); + + return res.status(200).json({ + message: 'Travel times calculated and stored successfully', + buildingId, + travelTimes: response.data, + }); + } catch (error) { + console.error('Error in test endpoint:', error); + return res.status(500).json({ error: 'Internal server error' }); + } +}); + export default app; From 28bb1e3b1e2d253a1b50f43b989f28a3981f152e Mon Sep 17 00:00:00 2001 From: CasperL1218 Date: Tue, 29 Oct 2024 17:28:27 -0400 Subject: [PATCH 2/9] Implemented travel time documents creation - Implemented travel documents document batch creation endpoint - Tested on smaller batches (size 10) - Created building travel time documents for all buildings --- backend/src/app.ts | 151 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 117 insertions(+), 34 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 1898b988..48339843 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -956,7 +956,7 @@ interface TravelTimes { ctbDriving: number; } -const {GOOGLE_MAPS_API_KEY} = process.env; +const { REACT_APP_MAPS_API_KEY } = process.env; const LANDMARKS = { arts_quad: 'Arts Quad, Cornell University, Ithaca, NY 14853', duffield: 'Duffield Hall, Cornell University, Ithaca, NY 14853', @@ -967,6 +967,31 @@ interface TravelTimeInput { origin: string; // Can be either address or "latitude,longitude" } +/** + * Makes a request to Google Distance Matrix API and extracts travel times + * @param origin - Starting point (address or coordinates) + * @param destinations - Array of destination addresses + * @param mode - Travel mode ('walking' or 'driving') + * @returns Array of travel times in minutes + */ +async function getTravelTimes( + origin: string, + destinations: string[], + mode: 'walking' | 'driving' +): Promise { + const response = await axios.get( + `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${encodeURIComponent( + origin + )}&destinations=${destinations + .map((dest) => encodeURIComponent(dest)) + .join('|')}&mode=${mode}&key=${REACT_APP_MAPS_API_KEY}` + ); + + return response.data.rows[0].elements.map( + (element: { duration: { value: number } }) => element.duration.value / 60 + ); +} + /** * Travel Times Calculator - Calculates walking and driving times from a given origin to Cornell landmarks. * @@ -992,40 +1017,13 @@ app.post('/api/travel-times', async (req, res) => { return res.status(400).json({ error: 'Origin is required' }); } - // Create destinations array const destinations = Object.values(LANDMARKS); - // Make request to Google Distance Matrix API for walking times - const walkingResponse = await axios.get( - `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${encodeURIComponent( - origin - )}&destinations=${destinations - .map((dest) => encodeURIComponent(dest)) - .join('|')}&mode=walking&key=${GOOGLE_MAPS_API_KEY}` - ); - - // Make request to Google Distance Matrix API for driving times - const drivingResponse = await axios.get( - `https://maps.googleapis.com/maps/api/distancematrix/json?origins=${encodeURIComponent( - origin - )}&destinations=${destinations - .map((dest) => encodeURIComponent(dest)) - .join('|')}&mode=driving&key=${GOOGLE_MAPS_API_KEY}` - ); - - // Fix the any types with a proper interface - interface DistanceMatrixElement { - duration: { - value: number; - }; - } - - const walkingTimes = walkingResponse.data.rows[0].elements.map( - (element: DistanceMatrixElement) => element.duration.value / 60 - ); - const drivingTimes = drivingResponse.data.rows[0].elements.map( - (element: DistanceMatrixElement) => element.duration.value / 60 - ); + // Get walking and driving times using the helper function + const [walkingTimes, drivingTimes] = await Promise.all([ + getTravelTimes(origin, destinations, 'walking'), + getTravelTimes(origin, destinations, 'driving'), + ]); const travelTimes: TravelTimes = { artsQuadWalking: walkingTimes[0], @@ -1071,7 +1069,7 @@ app.post('/api/test-travel-times/:buildingId', async (req, res) => { } // Calculate travel times using the main endpoint - const response = await axios.post(`${process.env.API_BASE_URL}/api/travel-times`, { + const response = await axios.post(`http://localhost:3000/api/travel-times`, { origin: `${buildingData.latitude},${buildingData.longitude}`, }); @@ -1088,5 +1086,90 @@ app.post('/api/test-travel-times/:buildingId', async (req, res) => { return res.status(500).json({ error: 'Internal server error' }); } }); +/** + * Batch Travel Times Endpoint - Calculates and stores travel times for a batch of buildings. + * + * @remarks + * Processes a batch of buildings from the buildings collection, calculating travel times to Cornell landmarks + * for each one and storing the results in the travelTimes collection. This endpoint handles buildings in batches + * with rate limiting between requests. Supports pagination through the startAfter parameter. + * + * @param {number} batchSize - Number of buildings to process in this batch (defaults to 50) + * @param {string} [startAfter] - Optional ID of last processed building for pagination + * @return {Object} - Summary object containing: + * - Message indicating batch completion + * - Total number of buildings processed in this batch + * - Count of successful calculations + * - Arrays of successful building IDs and failed buildings with error details + */ + +// Helper function for rate limiting API requests +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +app.post('/api/batch-travel-times/:batchSize/:startAfter?', async (req, res) => { + try { + const batchSize = parseInt(req.params.batchSize, 10) || 50; + const { startAfter } = req.params; + + let query = buildingsCollection.limit(batchSize); + if (startAfter) { + const lastDoc = await buildingsCollection.doc(startAfter).get(); + query = query.startAfter(lastDoc); + } + + const buildingDocs = (await query.get()).docs; + const results = { + success: [] as string[], + failed: [] as { id: string; error: string }[], + }; + + // Replace for...of loop with Promise.all and map + const processPromises = buildingDocs.map(async (doc) => { + try { + await delay(100); // 100ms delay between requests + const buildingData = doc.data(); + if (!buildingData?.latitude || !buildingData?.longitude) { + results.failed.push({ + id: doc.id, + error: 'Missing coordinates', + }); + return; + } + + const response = await axios.post(`http://localhost:3000/api/travel-times`, { + origin: `${buildingData.latitude},${buildingData.longitude}`, + }); + + await travelTimesCollection.doc(doc.id).set(response.data); + results.success.push(doc.id); + } catch (error) { + results.failed.push({ + id: doc.id, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + }); + + await Promise.all(processPromises); + + res.status(200).json({ + message: 'Batch processing completed', + totalProcessed: buildingDocs.length, + successCount: results.success.length, + failureCount: results.failed.length, + hasMore: buildingDocs.length === batchSize, + lastProcessedId: buildingDocs[buildingDocs.length - 1]?.id, + results, + }); + } catch (error) { + console.error('Batch processing error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Add this simple test endpoint +app.post('/api/batch-travel-times/test', async (req, res) => { + res.status(200).json({ message: 'Endpoint working' }); +}); export default app; From a58bc1a55e5c5052fecdcd4f1b6a74741dbc0fdc Mon Sep 17 00:00:00 2001 From: CasperL1218 Date: Tue, 29 Oct 2024 17:55:20 -0400 Subject: [PATCH 3/9] Implemented Endpoint for Frontend Travel Time Retrieval - Created endpoint for frontend travel time retrieval - Improved documentation for helper function --- backend/src/app.ts | 71 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 48339843..ae27a94d 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -946,7 +946,6 @@ app.post('/api/add-contact-question', authenticate, async (req, res) => { } }); -// 1. Fix the camelCase interface properties interface TravelTimes { artsQuadWalking: number; artsQuadDriving: number; @@ -968,11 +967,16 @@ interface TravelTimeInput { } /** - * Makes a request to Google Distance Matrix API and extracts travel times - * @param origin - Starting point (address or coordinates) - * @param destinations - Array of destination addresses - * @param mode - Travel mode ('walking' or 'driving') - * @returns Array of travel times in minutes + * getTravelTimes – Calculates travel times between an origin and multiple destinations using Google Distance Matrix API. + * + * @remarks + * Makes an HTTP request to the Google Distance Matrix API and processes the response to extract duration values. + * Times are converted from seconds to minutes before being returned. + * + * @param {string} origin - Starting location as either an address or coordinates in "lat,lng" format + * @param {string[]} destinations - Array of destination addresses to calculate times to + * @param {'walking' | 'driving'} mode - Mode of transportation to use for calculations + * @return {Promise} - Array of travel times in minutes to each destination */ async function getTravelTimes( origin: string, @@ -1123,7 +1127,6 @@ app.post('/api/batch-travel-times/:batchSize/:startAfter?', async (req, res) => failed: [] as { id: string; error: string }[], }; - // Replace for...of loop with Promise.all and map const processPromises = buildingDocs.map(async (doc) => { try { await delay(100); // 100ms delay between requests @@ -1167,9 +1170,57 @@ app.post('/api/batch-travel-times/:batchSize/:startAfter?', async (req, res) => } }); -// Add this simple test endpoint -app.post('/api/batch-travel-times/test', async (req, res) => { - res.status(200).json({ message: 'Endpoint working' }); +/** + * Get Travel Times By Coordinates – Calculates travel times from specific coordinates to Cornell landmarks. + * + * @remarks + * Uses Google Maps Distance Matrix API to calculate walking and driving times from given coordinates + * to three landmarks: Arts Quad, Duffield Hall, and College Town Bagels. Returns durations in minutes. + * + * @route GET /api/travel-times/coordinates/:latitude/:longitude + * + * @input {string} req.params.latitude - Latitude coordinate of starting location + * @input {string} req.params.longitude - Longitude coordinate of starting location + * + * @status + * - 200: Successfully retrieved travel times + * - 400: Invalid coordinates provided + * - 500: Server error or Google Maps API failure + */ +app.get('/api/travel-times/coordinates/:latitude/:longitude', async (req, res) => { + try { + const { latitude, longitude } = req.params; + + // Validate coordinates + const lat = parseFloat(latitude); + const lng = parseFloat(longitude); + + if (Number.isNaN(lat) || Number.isNaN(lng)) { + return res.status(400).json({ error: 'Invalid coordinates' }); + } + + const destinations = Object.values(LANDMARKS); + + // Get walking and driving times using the helper function + const [walkingTimes, drivingTimes] = await Promise.all([ + getTravelTimes(`${lat},${lng}`, destinations, 'walking'), + getTravelTimes(`${lat},${lng}`, destinations, 'driving'), + ]); + + const travelTimes: TravelTimes = { + artsQuadWalking: walkingTimes[0], + artsQuadDriving: drivingTimes[0], + duffieldWalking: walkingTimes[1], + duffieldDriving: drivingTimes[1], + ctbWalking: walkingTimes[2], + ctbDriving: drivingTimes[2], + }; + + return res.status(200).json(travelTimes); + } catch (error) { + console.error('Error calculating travel times:', error); + return res.status(500).json({ error: 'Internal server error' }); + } }); export default app; From 76c933d31145dee6398dd72df194c4dacaf9e74e Mon Sep 17 00:00:00 2001 From: CasperL1218 Date: Tue, 12 Nov 2024 17:24:54 -0500 Subject: [PATCH 4/9] Implemented Frontend Display of Travel Time Data - Implemented frontend travel time display - Mofified ApartmentWithId type to include travelTimes attribute --- backend/scripts/add_buildings.ts | 7 +- backend/src/app.ts | 134 ++++++++++-------- common/types/db-types.ts | 14 +- frontend/src/components/Apartment/MapInfo.tsx | 23 ++- .../src/components/Apartment/MapModal.tsx | 49 +++---- frontend/src/pages/ApartmentPage.tsx | 6 +- 6 files changed, 128 insertions(+), 105 deletions(-) diff --git a/backend/scripts/add_buildings.ts b/backend/scripts/add_buildings.ts index 07976aaa..d4b3cd12 100644 --- a/backend/scripts/add_buildings.ts +++ b/backend/scripts/add_buildings.ts @@ -48,8 +48,11 @@ const formatBuilding = ({ area: getAreaType(area), latitude, longitude, - walkTime: 0, - driveTime: 0, + travelTimes: { + agQuad: { walk: 0, drive: 0 }, + engQuad: { walk: 0, drive: 0 }, + hoPlaza: { walk: 0, drive: 0 }, + }, }); const makeBuilding = async (apartmentWithId: ApartmentWithId) => { diff --git a/backend/src/app.ts b/backend/src/app.ts index ae27a94d..611dd643 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -14,6 +14,7 @@ import { ApartmentWithId, CantFindApartmentForm, QuestionForm, + LocationTravelTimes, } from '@common/types/db-types'; // Import Firebase configuration and types import { auth } from 'firebase-admin'; @@ -211,14 +212,46 @@ app.get('/api/apts/:ids', async (req, res) => { try { const { ids } = req.params; const idsList = ids.split(','); - // Fetching each apartment from the database and returning an array of apartment objects + // Fetching each apartment and its travel times from the database const aptsArr = await Promise.all( idsList.map(async (id) => { - const snapshot = await buildingsCollection.doc(id).get(); + const [snapshot, travelTimeDoc] = await Promise.all([ + buildingsCollection.doc(id).get(), + travelTimesCollection.doc(id).get(), + ]); + if (!snapshot.exists) { throw new Error('Invalid id'); } - return { id, ...snapshot.data() } as ApartmentWithId; + + // Transform travel times to match LocationTravelTimes schema + const rawTravelTimes = travelTimeDoc.exists ? travelTimeDoc.data() : null; + const travelTimes: LocationTravelTimes = rawTravelTimes + ? { + agQuad: { + walk: rawTravelTimes.agQuadWalking, + drive: rawTravelTimes.agQuadDriving, + }, + engQuad: { + walk: rawTravelTimes.engQuadWalking, + drive: rawTravelTimes.engQuadDriving, + }, + hoPlaza: { + walk: rawTravelTimes.hoPlazaWalking, + drive: rawTravelTimes.hoPlazaDriving, + }, + } + : { + agQuad: { walk: 0, drive: 0 }, + engQuad: { walk: 0, drive: 0 }, + hoPlaza: { walk: 0, drive: 0 }, + }; + + return { + id, + ...snapshot.data(), + travelTimes, + } as ApartmentWithId; }) ); res.status(200).send(JSON.stringify(aptsArr)); @@ -947,19 +980,19 @@ app.post('/api/add-contact-question', authenticate, async (req, res) => { }); interface TravelTimes { - artsQuadWalking: number; - artsQuadDriving: number; - duffieldWalking: number; - duffieldDriving: number; - ctbWalking: number; - ctbDriving: number; + agQuadWalking: number; + agQuadDriving: number; + engQuadWalking: number; + engQuadDriving: number; + hoPlazaWalking: number; + hoPlazaDriving: number; } const { REACT_APP_MAPS_API_KEY } = process.env; const LANDMARKS = { - arts_quad: 'Arts Quad, Cornell University, Ithaca, NY 14853', - duffield: 'Duffield Hall, Cornell University, Ithaca, NY 14853', - ctb: 'College Town Bagels, College Ave, Ithaca, NY 14850', + eng_quad: '42.4445,-76.4836', // Duffield Hall + ag_quad: '42.4489,-76.4780', // Mann Library + ho_plaza: '42.4468,-76.4851', // Ho Plaza }; interface TravelTimeInput { @@ -1000,8 +1033,8 @@ async function getTravelTimes( * Travel Times Calculator - Calculates walking and driving times from a given origin to Cornell landmarks. * * @remarks - * Uses Google Maps Distance Matrix API to calculate travel times to three landmarks: Arts Quad, - * Duffield Hall, and College Town Bagels. Returns both walking and driving durations in minutes. + * Uses Google Maps Distance Matrix API to calculate travel times to three landmarks: Engineering Quad, + * Agriculture Quad, and Ho Plaza. Returns both walking and driving durations in minutes. * Origin can be either an address or coordinates in "latitude,longitude" format. * * @route POST /api/travel-times @@ -1013,15 +1046,17 @@ async function getTravelTimes( * - 400: Missing or invalid origin * - 500: Server error or Google Maps API failure */ -app.post('/api/travel-times', async (req, res) => { +app.post('/api/calculate-travel-times', async (req, res) => { try { const { origin } = req.body as TravelTimeInput; + console.log('Origin:', origin); if (!origin) { return res.status(400).json({ error: 'Origin is required' }); } const destinations = Object.values(LANDMARKS); + console.log('Destinations array:', destinations); // Get walking and driving times using the helper function const [walkingTimes, drivingTimes] = await Promise.all([ @@ -1029,15 +1064,19 @@ app.post('/api/travel-times', async (req, res) => { getTravelTimes(origin, destinations, 'driving'), ]); + console.log('Raw walking times:', walkingTimes); + console.log('Raw driving times:', drivingTimes); + const travelTimes: TravelTimes = { - artsQuadWalking: walkingTimes[0], - artsQuadDriving: drivingTimes[0], - duffieldWalking: walkingTimes[1], - duffieldDriving: drivingTimes[1], - ctbWalking: walkingTimes[2], - ctbDriving: drivingTimes[2], + engQuadWalking: walkingTimes[0], + engQuadDriving: drivingTimes[0], + agQuadWalking: walkingTimes[1], + agQuadDriving: drivingTimes[1], + hoPlazaWalking: walkingTimes[2], + hoPlazaDriving: drivingTimes[2], }; + console.log('Final travel times:', travelTimes); return res.status(200).json(travelTimes); } catch (error) { console.error('Error calculating travel times:', error); @@ -1073,7 +1112,7 @@ app.post('/api/test-travel-times/:buildingId', async (req, res) => { } // Calculate travel times using the main endpoint - const response = await axios.post(`http://localhost:3000/api/travel-times`, { + const response = await axios.post(`http://localhost:3000/api/calculate-travel-times`, { origin: `${buildingData.latitude},${buildingData.longitude}`, }); @@ -1110,7 +1149,7 @@ app.post('/api/test-travel-times/:buildingId', async (req, res) => { // Helper function for rate limiting API requests const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); -app.post('/api/batch-travel-times/:batchSize/:startAfter?', async (req, res) => { +app.post('/api/batch-create-travel-times/:batchSize/:startAfter?', async (req, res) => { try { const batchSize = parseInt(req.params.batchSize, 10) || 50; const { startAfter } = req.params; @@ -1139,7 +1178,7 @@ app.post('/api/batch-travel-times/:batchSize/:startAfter?', async (req, res) => return; } - const response = await axios.post(`http://localhost:3000/api/travel-times`, { + const response = await axios.post(`http://localhost:3000/api/calculate-travel-times`, { origin: `${buildingData.latitude},${buildingData.longitude}`, }); @@ -1171,54 +1210,35 @@ app.post('/api/batch-travel-times/:batchSize/:startAfter?', async (req, res) => }); /** - * Get Travel Times By Coordinates – Calculates travel times from specific coordinates to Cornell landmarks. + * Get Travel Times By Building ID - Retrieves pre-calculated travel times from the travel times collection. * * @remarks - * Uses Google Maps Distance Matrix API to calculate walking and driving times from given coordinates - * to three landmarks: Arts Quad, Duffield Hall, and College Town Bagels. Returns durations in minutes. + * Looks up the travel times document for the given building ID and returns the stored walking and driving + * times to Cornell landmarks: Engineering Quad, Agriculture Quad, and Ho Plaza. * - * @route GET /api/travel-times/coordinates/:latitude/:longitude + * @route GET /api/travel-times/:buildingId * - * @input {string} req.params.latitude - Latitude coordinate of starting location - * @input {string} req.params.longitude - Longitude coordinate of starting location + * @input {string} req.params.buildingId - ID of the building to get travel times for * * @status * - 200: Successfully retrieved travel times - * - 400: Invalid coordinates provided - * - 500: Server error or Google Maps API failure + * - 404: Building travel times not found + * - 500: Server error */ -app.get('/api/travel-times/coordinates/:latitude/:longitude', async (req, res) => { +app.get('/api/travel-times-by-id/:buildingId', async (req, res) => { try { - const { latitude, longitude } = req.params; + const { buildingId } = req.params; - // Validate coordinates - const lat = parseFloat(latitude); - const lng = parseFloat(longitude); + const travelTimeDoc = await travelTimesCollection.doc(buildingId).get(); - if (Number.isNaN(lat) || Number.isNaN(lng)) { - return res.status(400).json({ error: 'Invalid coordinates' }); + if (!travelTimeDoc.exists) { + return res.status(404).json({ error: 'Travel times not found for this building' }); } - const destinations = Object.values(LANDMARKS); - - // Get walking and driving times using the helper function - const [walkingTimes, drivingTimes] = await Promise.all([ - getTravelTimes(`${lat},${lng}`, destinations, 'walking'), - getTravelTimes(`${lat},${lng}`, destinations, 'driving'), - ]); - - const travelTimes: TravelTimes = { - artsQuadWalking: walkingTimes[0], - artsQuadDriving: drivingTimes[0], - duffieldWalking: walkingTimes[1], - duffieldDriving: drivingTimes[1], - ctbWalking: walkingTimes[2], - ctbDriving: drivingTimes[2], - }; - + const travelTimes = travelTimeDoc.data() as TravelTimes; return res.status(200).json(travelTimes); } catch (error) { - console.error('Error calculating travel times:', error); + console.error('Error retrieving travel times:', error); return res.status(500).json({ error: 'Internal server error' }); } }); diff --git a/common/types/db-types.ts b/common/types/db-types.ts index 8fb75259..fd0ea36a 100644 --- a/common/types/db-types.ts +++ b/common/types/db-types.ts @@ -56,8 +56,18 @@ export type Apartment = { readonly area: 'COLLEGETOWN' | 'WEST' | 'NORTH' | 'DOWNTOWN' | 'OTHER'; readonly latitude: number; readonly longitude: number; - readonly walkTime: number; - readonly driveTime: number; + readonly travelTimes: LocationTravelTimes; +}; + +type TravelTime = { + walk: number; + drive: number; +}; + +export type LocationTravelTimes = { + agQuad: TravelTime; + engQuad: TravelTime; + hoPlaza: TravelTime; }; export type ApartmentWithId = Apartment & Id; diff --git a/frontend/src/components/Apartment/MapInfo.tsx b/frontend/src/components/Apartment/MapInfo.tsx index ab3c525b..78296a51 100644 --- a/frontend/src/components/Apartment/MapInfo.tsx +++ b/frontend/src/components/Apartment/MapInfo.tsx @@ -10,6 +10,7 @@ import recenterIcon from '../../assets/recenter-icon.svg'; import blackPinIcon from '../../assets/ph_map-pin-fill.svg'; import { config } from 'dotenv'; import { Marker } from './Marker'; +import { LocationTravelTimes } from '../../../../common/types/db-types'; config(); @@ -17,8 +18,7 @@ export type BaseProps = { readonly address: string | null; readonly latitude?: number; readonly longitude?: number; - readonly walkTime?: number; - readonly driveTime?: number; + readonly travelTimes?: LocationTravelTimes; }; type MapInfoProps = BaseProps & { @@ -28,7 +28,7 @@ type MapInfoProps = BaseProps & { export type distanceProps = { location: string; - walkDistance: number; + walkDistance: number | undefined; }; const WalkDistanceInfo = ({ location, walkDistance }: distanceProps) => { @@ -145,7 +145,7 @@ export default function MapInfo({ address, latitude = 0, longitude = 0, - walkTime = 0, + travelTimes, handleClick, isMobile, }: MapInfoProps): ReactElement { @@ -274,9 +274,18 @@ export default function MapInfo({ Distance from Campus - - - + + + diff --git a/frontend/src/components/Apartment/MapModal.tsx b/frontend/src/components/Apartment/MapModal.tsx index 20b26684..c3c43e27 100644 --- a/frontend/src/components/Apartment/MapModal.tsx +++ b/frontend/src/components/Apartment/MapModal.tsx @@ -118,8 +118,7 @@ const MapModal = ({ address, latitude = 0, longitude = 0, - walkTime = 0, - driveTime = 0, + travelTimes, }: MapModalProps) => { const classes = useStyles(); const theme = useTheme(); @@ -171,9 +170,13 @@ const MapModal = ({ const DistanceInfo = ({ location, - walkDistance, - driveDistance, - }: distanceProps & { driveDistance: number }) => ( + walkTime, + driveTime, + }: { + location: string; + walkTime: number; + driveTime: number; + }) => ( - + - + ); @@ -328,42 +331,22 @@ const MapModal = ({ > {address} - {/* {isMediumScreen && ( - - Distance from Campus - - - - - )} */} Distance from Campus diff --git a/frontend/src/pages/ApartmentPage.tsx b/frontend/src/pages/ApartmentPage.tsx index 02eab58a..4e2b4566 100644 --- a/frontend/src/pages/ApartmentPage.tsx +++ b/frontend/src/pages/ApartmentPage.tsx @@ -382,8 +382,7 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { address={apt!.address} longitude={apt!.longitude} latitude={apt!.latitude} - walkTime={apt!.walkTime} - driveTime={apt!.driveTime} + travelTimes={apt!.travelTimes} /> { address={apt!.address} longitude={apt!.longitude} latitude={apt!.latitude} - walkTime={apt!.walkTime} - driveTime={apt!.driveTime} + travelTimes={apt!.travelTimes} handleClick={() => setMapOpen(true)} isMobile={isMobile} /> From 09b3fe08413aca52825589ec15131aae3f085263 Mon Sep 17 00:00:00 2001 From: CasperL1218 Date: Thu, 14 Nov 2024 19:24:50 -0500 Subject: [PATCH 5/9] Improved Traveltimes Data Display & Frontend Map Feature - Modified TravelTimes type (isolated from Apartments type) - Implemented map recenter feature upon map modal closure - Implemented mobile map modal --- backend/scripts/add_buildings.ts | 5 -- backend/src/app.ts | 41 ++----------- common/types/db-types.ts | 15 ++--- frontend/src/components/Apartment/MapInfo.tsx | 58 +++++++------------ .../src/components/Apartment/MapModal.tsx | 47 ++++++++------- frontend/src/pages/ApartmentPage.tsx | 40 +++++++++++-- 6 files changed, 91 insertions(+), 115 deletions(-) diff --git a/backend/scripts/add_buildings.ts b/backend/scripts/add_buildings.ts index d4b3cd12..a2bc4718 100644 --- a/backend/scripts/add_buildings.ts +++ b/backend/scripts/add_buildings.ts @@ -48,11 +48,6 @@ const formatBuilding = ({ area: getAreaType(area), latitude, longitude, - travelTimes: { - agQuad: { walk: 0, drive: 0 }, - engQuad: { walk: 0, drive: 0 }, - hoPlaza: { walk: 0, drive: 0 }, - }, }); const makeBuilding = async (apartmentWithId: ApartmentWithId) => { diff --git a/backend/src/app.ts b/backend/src/app.ts index 611dd643..1a8ecde4 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -212,46 +212,14 @@ app.get('/api/apts/:ids', async (req, res) => { try { const { ids } = req.params; const idsList = ids.split(','); - // Fetching each apartment and its travel times from the database + // Fetching each apartment from the database and returning an array of apartment objects const aptsArr = await Promise.all( idsList.map(async (id) => { - const [snapshot, travelTimeDoc] = await Promise.all([ - buildingsCollection.doc(id).get(), - travelTimesCollection.doc(id).get(), - ]); - + const snapshot = await buildingsCollection.doc(id).get(); if (!snapshot.exists) { throw new Error('Invalid id'); } - - // Transform travel times to match LocationTravelTimes schema - const rawTravelTimes = travelTimeDoc.exists ? travelTimeDoc.data() : null; - const travelTimes: LocationTravelTimes = rawTravelTimes - ? { - agQuad: { - walk: rawTravelTimes.agQuadWalking, - drive: rawTravelTimes.agQuadDriving, - }, - engQuad: { - walk: rawTravelTimes.engQuadWalking, - drive: rawTravelTimes.engQuadDriving, - }, - hoPlaza: { - walk: rawTravelTimes.hoPlazaWalking, - drive: rawTravelTimes.hoPlazaDriving, - }, - } - : { - agQuad: { walk: 0, drive: 0 }, - engQuad: { walk: 0, drive: 0 }, - hoPlaza: { walk: 0, drive: 0 }, - }; - - return { - id, - ...snapshot.data(), - travelTimes, - } as ApartmentWithId; + return { id, ...snapshot.data() } as ApartmentWithId; }) ); res.status(200).send(JSON.stringify(aptsArr)); @@ -1235,7 +1203,8 @@ app.get('/api/travel-times-by-id/:buildingId', async (req, res) => { return res.status(404).json({ error: 'Travel times not found for this building' }); } - const travelTimes = travelTimeDoc.data() as TravelTimes; + const travelTimes = travelTimeDoc.data() as LocationTravelTimes; + return res.status(200).json(travelTimes); } catch (error) { console.error('Error retrieving travel times:', error); diff --git a/common/types/db-types.ts b/common/types/db-types.ts index fd0ea36a..14d4e0b4 100644 --- a/common/types/db-types.ts +++ b/common/types/db-types.ts @@ -56,18 +56,15 @@ export type Apartment = { readonly area: 'COLLEGETOWN' | 'WEST' | 'NORTH' | 'DOWNTOWN' | 'OTHER'; readonly latitude: number; readonly longitude: number; - readonly travelTimes: LocationTravelTimes; -}; - -type TravelTime = { - walk: number; - drive: number; }; export type LocationTravelTimes = { - agQuad: TravelTime; - engQuad: TravelTime; - hoPlaza: TravelTime; + agQuadDriving: number; + agQuadWalking: number; + engQuadDriving: number; + engQuadWalking: number; + hoPlazaDriving: number; + hoPlazaWalking: number; }; export type ApartmentWithId = Apartment & Id; diff --git a/frontend/src/components/Apartment/MapInfo.tsx b/frontend/src/components/Apartment/MapInfo.tsx index 78296a51..3e8c83d3 100644 --- a/frontend/src/components/Apartment/MapInfo.tsx +++ b/frontend/src/components/Apartment/MapInfo.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, useRef } from 'react'; +import React, { ReactElement, useRef, useEffect } from 'react'; import { Box, Grid, IconButton, Typography, makeStyles } from '@material-ui/core'; import GoogleMapReact from 'google-map-react'; import aptIcon from '../../assets/location-pin.svg'; @@ -6,7 +6,6 @@ import schoolIcon from '../../assets/school-pin.svg'; import expandIcon from '../../assets/expand-button.svg'; import zoomInIcon from '../../assets/zoom-in-icon.png'; import zoomOutIcon from '../../assets/zoom-out-icon.png'; -import recenterIcon from '../../assets/recenter-icon.svg'; import blackPinIcon from '../../assets/ph_map-pin-fill.svg'; import { config } from 'dotenv'; import { Marker } from './Marker'; @@ -19,11 +18,12 @@ export type BaseProps = { readonly latitude?: number; readonly longitude?: number; readonly travelTimes?: LocationTravelTimes; + isMobile: boolean; }; type MapInfoProps = BaseProps & { handleClick: () => void; - isMobile: boolean; + mapToggle: boolean; }; export type distanceProps = { @@ -141,57 +141,39 @@ const useStyles = makeStyles((theme) => ({ * - `longitude`: The longitude of the apartment location. * - `walkTime`: The walk time from the apartment to campus landmarks. */ -export default function MapInfo({ +function MapInfo({ address, latitude = 0, longitude = 0, travelTimes, handleClick, + mapToggle, isMobile, }: MapInfoProps): ReactElement { - const { outerMapDiv, innerMapDiv, mapExpandButton, recenterButton, zoomInButton, zoomOutButton } = - useStyles(); + const { outerMapDiv, innerMapDiv, mapExpandButton, zoomInButton, zoomOutButton } = useStyles(); const mapRef = useRef(null); const handleApiLoaded = ({ map, maps }: { map: google.maps.Map; maps: typeof google.maps }) => { mapRef.current = map; }; - const handleRecenter = () => { + useEffect(() => { if (mapRef.current) { mapRef.current.setCenter({ lat: latitude, lng: longitude }); mapRef.current.setZoom(16); } - }; + }, [mapToggle, latitude, longitude]); - // Function to handle zoom in/out of the map const handleZoom = (zoomChange: number) => { if (mapRef.current) { - const currentZoom = mapRef.current.getZoom() || 16; // Ensure there is a valid value for currentZoom + const currentZoom = mapRef.current.getZoom() || 16; const newZoom = currentZoom + zoomChange; if (newZoom > 11 && newZoom < 20) { - // Ensure the new zoom is within the allowed range mapRef.current.setZoom(newZoom); } } }; - const expandOrRecenter = (isMobile: boolean) => { - return isMobile ? ( - - {'recenter-icon'} - - ) : ( - - {'expand-icon'} - - ); - }; - return ( @@ -216,12 +198,6 @@ export default function MapInfo({ src={schoolIcon} altText="Engineering Quad icon" /> - - {expandOrRecenter(isMobile)} + + {'expand-icon'} + {
handleZoom(1)}> @@ -276,15 +258,15 @@ export default function MapInfo({ @@ -292,3 +274,5 @@ export default function MapInfo({ ); } + +export default MapInfo; diff --git a/frontend/src/components/Apartment/MapModal.tsx b/frontend/src/components/Apartment/MapModal.tsx index c3c43e27..24914359 100644 --- a/frontend/src/components/Apartment/MapModal.tsx +++ b/frontend/src/components/Apartment/MapModal.tsx @@ -27,24 +27,26 @@ import { BaseProps, distanceProps } from './MapInfo'; const useStyles = makeStyles((theme) => ({ paper: { - borderRadius: '13.895px', - maxWidth: '70%', - maxHeight: '94%', + borderRadius: ({ isMobile }: { isMobile: boolean }) => (isMobile ? '0px' : '13.895px'), + maxWidth: ({ isMobile }: { isMobile: boolean }) => (isMobile ? '100%' : '70%'), + maxHeight: ({ isMobile }: { isMobile: boolean }) => (isMobile ? '100%' : '94%'), + height: ({ isMobile }: { isMobile: boolean }) => (isMobile ? '100%' : 'auto'), + margin: ({ isMobile }: { isMobile: boolean }) => (isMobile ? '0' : undefined), overflow: 'hidden', }, outerMapDiv: { - height: '50vh', - width: '94%', - borderRadius: '12.764px', + height: ({ isMobile }: { isMobile: boolean }) => (isMobile ? '65vh' : '50vh'), + width: ({ isMobile }: { isMobile: boolean }) => (isMobile ? '100%' : '94%'), + borderRadius: '0px', overflow: 'hidden', outline: 'none', position: 'relative', - marginBottom: '30px', + marginBottom: ({ isMobile }: { isMobile: boolean }) => (isMobile ? '0' : '30px'), }, innerMapDiv: { height: '130%', width: '100%', - borderRadius: '12.764px', + borderRadius: '0px', overflow: 'hidden', outline: 'none', position: 'absolute', @@ -68,7 +70,8 @@ const useStyles = makeStyles((theme) => ({ width: '100%', overflow: 'auto', alignItems: 'center', - marginBottom: '30px', + marginBottom: ({ isMobile }: { isMobile: boolean }) => (isMobile ? '0' : '30px'), + padding: ({ isMobile }: { isMobile: boolean }) => (isMobile ? '0' : undefined), }, addressTypography: { fontWeight: 600, @@ -110,6 +113,7 @@ interface MapModalProps extends BaseProps { * - `walkTime`: The walk time from the apartment to campus landmarks (default: 0). * - `driveTime`: The drive time from the apartment to campus landmarks (default: 0). */ + const MapModal = ({ aptName, open, @@ -119,11 +123,11 @@ const MapModal = ({ latitude = 0, longitude = 0, travelTimes, + isMobile, }: MapModalProps) => { - const classes = useStyles(); + const classes = useStyles({ isMobile }); const theme = useTheme(); const mapRef = useRef(null); - const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); const isMediumScreen = useMediaQuery(theme.breakpoints.up('lg')); const handleApiLoaded = ({ map, maps }: { map: google.maps.Map; maps: typeof google.maps }) => { @@ -217,6 +221,7 @@ const MapModal = ({ }} maxWidth="md" fullWidth + fullScreen={isMobile} > @@ -229,7 +234,7 @@ const MapModal = ({ - setOpen(false)} style={{ padding: 0 }}> + {'close-icon'} - Distance from Campus diff --git a/frontend/src/pages/ApartmentPage.tsx b/frontend/src/pages/ApartmentPage.tsx index 4e2b4566..d5fd2ef8 100644 --- a/frontend/src/pages/ApartmentPage.tsx +++ b/frontend/src/pages/ApartmentPage.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, useState, useEffect } from 'react'; +import React, { ReactElement, useState, useEffect, useRef } from 'react'; import { IconButton, Button, @@ -25,6 +25,7 @@ import { Apartment, ApartmentWithId, DetailedRating, + LocationTravelTimes, } from '../../../common/types/db-types'; import Toast from '../components/utils/Toast'; import LinearProgress from '../components/utils/LinearProgress'; @@ -54,6 +55,10 @@ export type RatingInfo = { rating: number; }; +interface MapInfoRef { + recenter: () => void; +} + const useStyles = makeStyles((theme) => ({ reviewButton: { borderRadius: '30px', @@ -136,6 +141,7 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { const [buildings, setBuildings] = useState([]); const [aptData, setAptData] = useState([]); const [apt, setApt] = useState(undefined); + const [travelTimes, setTravelTimes] = useState(undefined); const [loaded, setLoaded] = useState(false); const [showSignInError, setShowSignInError] = useState(false); const [sortBy, setSortBy] = useState('date'); @@ -149,6 +155,17 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { const saved = savedIcon; const unsaved = unsavedIcon; const [isSaved, setIsSaved] = useState(false); + const mapInfoRef = useRef(null); + const [mapToggle, setMapToggle] = useState(false); + + const dummyTravelTimes: LocationTravelTimes = { + agQuadDriving: -1, + agQuadWalking: -1, + engQuadDriving: -1, + engQuadWalking: -1, + hoPlazaDriving: -1, + hoPlazaWalking: -1, + }; // Set the number of results to show based on mobile or desktop view. useEffect(() => { @@ -189,6 +206,14 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { }); }, [aptId]); + // Fetch travel times data for the current apartment + useEffect(() => { + get(`/api/travel-times-by-id/${aptId}`, { + callback: setTravelTimes, + errorHandler: handlePageNotFound, + }); + }, [aptId]); + // Set the apartment data once it's fetched. useEffect(() => { setApt(aptData[0]); @@ -372,17 +397,23 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { setReviewOpen(true); }; + const handleMapModalClose = () => { + setMapOpen(false); + setMapToggle((prev) => !prev); + }; + const Modals = landlordData && apt && ( <> setMapOpen(false)} + onClose={handleMapModalClose} setOpen={setMapOpen} address={apt!.address} longitude={apt!.longitude} latitude={apt!.latitude} - travelTimes={apt!.travelTimes} + travelTimes={travelTimes} + isMobile={isMobile} /> { address={apt!.address} longitude={apt!.longitude} latitude={apt!.latitude} - travelTimes={apt!.travelTimes} + travelTimes={travelTimes} handleClick={() => setMapOpen(true)} + mapToggle={mapToggle} isMobile={isMobile} /> From bbb29300692f22983ac18da85b026860a49f6880 Mon Sep 17 00:00:00 2001 From: CasperL1218 Date: Sun, 17 Nov 2024 01:03:11 -0500 Subject: [PATCH 6/9] Improved Frontend Mobile Map Modal - Added spacing between address and map - Increased location and traveltime font size for better visibility --- .../src/components/Apartment/MapModal.tsx | 88 +++++++++++-------- 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/frontend/src/components/Apartment/MapModal.tsx b/frontend/src/components/Apartment/MapModal.tsx index 24914359..a766ebac 100644 --- a/frontend/src/components/Apartment/MapModal.tsx +++ b/frontend/src/components/Apartment/MapModal.tsx @@ -73,17 +73,44 @@ const useStyles = makeStyles((theme) => ({ marginBottom: ({ isMobile }: { isMobile: boolean }) => (isMobile ? '0' : '30px'), padding: ({ isMobile }: { isMobile: boolean }) => (isMobile ? '0' : undefined), }, + dataBox: { + width: '94%', + padding: ({ isMobile }: { isMobile: boolean }) => (isMobile ? '28px 0 0 20px' : 0), + display: 'flex', + justifyContent: 'space-between', + }, + aptNameTypography: { + fontWeight: 600, + fontSize: '24px', + }, + closeIcon: { + width: 'clamp(20px, 5vw, 26.9px)', + height: 'auto', + }, + controlButton: { + width: '100%', + height: 'auto', + }, addressTypography: { fontWeight: 600, - fontSize: 'clamp(16px, 4vw, 20px)', - marginBottom: '16px', + fontSize: '20px', }, distanceTypography: { fontWeight: 600, - fontSize: 'clamp(14px, 3.5vw, 18px)', + fontSize: 'clamp(16px, 18px, 18px)', lineHeight: '28px', marginBottom: '10px', }, + travelIcon: { + width: 'clamp(16px, 4vw, 24px)', + height: 'auto', + }, + travelTimeTypography: { + fontSize: '16px', + }, + locationTypography: { + fontSize: '16px', + }, })); interface MapModalProps extends BaseProps { @@ -162,10 +189,10 @@ const MapModal = ({ }) => ( - {altText} + {altText} - + {distance} min @@ -197,7 +224,7 @@ const MapModal = ({ /> - + {location} @@ -224,22 +251,20 @@ const MapModal = ({ fullScreen={isMobile} > - - - + + + {aptName} - {'close-icon'} + {'close-icon'} @@ -284,47 +309,32 @@ const MapModal = ({
- {'recenter-icon'} + {'recenter-icon'} handleZoom(1)} > - {'zoom-in-icon'} + {'zoom-in-icon'} handleZoom(-1)} > - {'zoom-out-icon'} + {'zoom-out-icon'} From 9a0a91d5aa5a7ff5fa51d0e06ce04a4f8a64d348 Mon Sep 17 00:00:00 2001 From: CasperL1218 Date: Tue, 3 Dec 2024 20:00:46 -0500 Subject: [PATCH 7/9] Revert "Merge branch 'main' into map-backend" This reverts commit ddeacc09cb5665f784d3fa9d6e42a6dfe1d9fb9a, reversing changes made to bbb29300692f22983ac18da85b026860a49f6880. --- README.md | 15 +-- backend/src/app.ts | 50 +++---- frontend/src/assets/helpful-icon.svg | 6 - frontend/src/components/Admin/AdminReview.tsx | 23 +--- frontend/src/components/Apartment/Header.tsx | 18 ++- .../src/components/Apartment/MapModal.tsx | 3 +- frontend/src/components/Landlord/Header.tsx | 13 +- .../PhotoCarousel/PhotoCarousel.tsx | 77 ++--------- .../PhotoCarousel/usePhotoCarousel.ts | 39 ------ frontend/src/components/Review/Review.tsx | 126 ++++++++---------- .../components/utils/Footer/ContactModal.tsx | 2 +- frontend/src/pages/AdminPage.tsx | 24 ---- frontend/src/pages/ApartmentPage.tsx | 99 ++++---------- frontend/src/pages/BookmarksPage.tsx | 53 +++----- frontend/src/pages/HomePage.tsx | 15 --- frontend/src/pages/LandlordPage.tsx | 47 +------ frontend/src/pages/LocationPage.tsx | 17 --- frontend/src/pages/NotFoundPage.tsx | 9 -- frontend/src/pages/Policies.tsx | 10 -- frontend/src/pages/ProfilePage.tsx | 63 +-------- frontend/src/pages/SearchResultsPage.tsx | 15 --- 21 files changed, 166 insertions(+), 558 deletions(-) delete mode 100644 frontend/src/assets/helpful-icon.svg delete mode 100644 frontend/src/components/PhotoCarousel/usePhotoCarousel.ts diff --git a/README.md b/README.md index 944c6f79..19a9aca1 100644 --- a/README.md +++ b/README.md @@ -24,17 +24,6 @@ on its own, run `yarn frontend-dev` or `yarn backend-dev`. ## Contributors -### 2024-2025 - -- **Ella Krechmer** - Product Manager -- **Janet Luo** - Associate Product Manager -- **Jacob Green** - Product Marketing Manager -- **Gunyasorn (Grace) Sawatyanon** - Technical Product Manager -- **David Martinez Lopez** - Designer -- **Vicky Wang** - Designer -- **Kea-Roy Ong** - Developer -- **Casper Liao** - Developer - ### 2023-2024 - **Tina Ou** - Product Manager @@ -45,7 +34,7 @@ on its own, run `yarn frontend-dev` or `yarn backend-dev`. - **Cyrus Irani** - Developer - **Ankit Lakkapragada** - Developer - **Jessica Han** - Developer -- **Gunyasorn (Grace) Sawatyanon** - Developer +- **Grace Sawatyanon** - Developer - **Miranda Luo** - Developer - **Kea-Roy Ong** - Developer - **Casper Liao** - Developer @@ -62,7 +51,7 @@ on its own, run `yarn frontend-dev` or `yarn backend-dev`. - **Daniel Jin** - Developer - **Ankit Lakkapragada** - Developer - **Jessica Han** - Developer -- **Gunyasorn (Grace) Sawatyanon** - Developer +- **Grace Sawatyanon** - Developer ### 2021-2022 diff --git a/backend/src/app.ts b/backend/src/app.ts index 26c2cca7..a00f1f8f 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -156,48 +156,28 @@ app.get('/api/review/:status', async (req, res) => { }); /** - * review/like/:userId – Fetches reviews liked by a user. - * - * @remarks - * This endpoint retrieves reviews that a user has marked as helpful (liked). It can also filter by review status by passing in a query parameter. If no parameter is provided, no additional filter is applied. - * - * @route GET /api/review/like/:userId - * - * @status - * - 200: Successfully retrieved the liked reviews. - * - 401: Error due to unauthorized access or authentication issues. + * Return list of reviews that user marked as helpful (like) */ -app.get('/api/review/like/:userId', authenticate, async (req, res) => { - if (!req.user) { - throw new Error('not authenticated'); - } - const realUserId = req.user.uid; - const { userId } = req.params; - const statusType = req.query.status; - if (userId !== realUserId) { - res.status(401).send("Error: user is not authorized to access another user's likes"); - return; - } - const likesDoc = await likesCollection.doc(realUserId).get(); +// TODO: uid param is unused here but when remove it encounter 304 status and req.user is null +app.get('/api/review/like/:uid', authenticate, async (req, res) => { + if (!req.user) throw new Error('not authenticated'); + const { uid } = req.user; + const likesDoc = await likesCollection.doc(uid).get(); if (likesDoc.exists) { const data = likesDoc.data(); if (data) { const reviewIds = Object.keys(data); const matchingReviews: ReviewWithId[] = []; - if (reviewIds.length > 0) { - let query = reviewCollection.where(FieldPath.documentId(), 'in', reviewIds); - if (statusType) { - // filter by status if provided - query = query.where('status', '==', statusType); - } - const querySnapshot = await query.get(); - querySnapshot.forEach((doc) => { - const data = doc.data(); - const reviewData = { ...data, date: data.date.toDate() }; - matchingReviews.push({ ...reviewData, id: doc.id } as ReviewWithId); - }); - } + const querySnapshot = await reviewCollection + .where(FieldPath.documentId(), 'in', reviewIds) + .where('status', '==', 'APPROVED') + .get(); + querySnapshot.forEach((doc) => { + const data = doc.data(); + const reviewData = { ...data, date: data.date.toDate() }; + matchingReviews.push({ ...reviewData, id: doc.id } as ReviewWithId); + }); res.status(200).send(JSON.stringify(matchingReviews)); return; } diff --git a/frontend/src/assets/helpful-icon.svg b/frontend/src/assets/helpful-icon.svg deleted file mode 100644 index 4755ddf9..00000000 --- a/frontend/src/assets/helpful-icon.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/src/components/Admin/AdminReview.tsx b/frontend/src/components/Admin/AdminReview.tsx index 56ca6c82..2dfe0c06 100644 --- a/frontend/src/components/Admin/AdminReview.tsx +++ b/frontend/src/components/Admin/AdminReview.tsx @@ -37,8 +37,6 @@ type Props = { readonly setToggle: React.Dispatch>; /** Indicates if the review is in the declined section. */ readonly declinedSection: boolean; - /** Function to trigger the photo carousel. */ - readonly triggerPhotoCarousel: (photos: readonly string[], startIndex: number) => void; }; /** @@ -72,24 +70,14 @@ const useStyles = makeStyles(() => ({ borderRadius: '4px', height: '15em', width: '15em', - cursor: 'pointer', - transition: '0.3s ease-in-out', - '&:hover': { - filter: 'brightness(0.85)', - boxShadow: '0 4px 4px rgba(0, 0, 0, 0.1)', - transform: 'scale(1.02)', - }, }, photoRowStyle: { overflowX: 'auto', - overflowY: 'hidden', display: 'flex', flexDirection: 'row', gap: '1vw', paddingTop: '2%', paddingLeft: '0.6%', - paddingRight: '0.6%', - paddingBottom: '2%', }, })); @@ -99,12 +87,7 @@ const useStyles = makeStyles(() => ({ * @param review review - The review to approve * @returns The rendered component. */ -const AdminReviewComponent = ({ - review, - setToggle, - declinedSection, - triggerPhotoCarousel, -}: Props): ReactElement => { +const AdminReviewComponent = ({ review, setToggle, declinedSection }: Props): ReactElement => { const { detailedRatings, overallRating, bedrooms, price, date, reviewText, photos } = review; const formattedDate = format(new Date(date), 'MMM dd, yyyy').toUpperCase(); const { root, dateText, bedroomsPriceText, ratingInfo, photoStyle, photoRowStyle } = useStyles(); @@ -219,7 +202,7 @@ const AdminReviewComponent = ({ {photos.length > 0 && ( - {photos.map((photo, i) => { + {photos.map((photo) => { return ( triggerPhotoCarousel(photos, i)} - loading="lazy" /> ); })} diff --git a/frontend/src/components/Apartment/Header.tsx b/frontend/src/components/Apartment/Header.tsx index 7df889ba..056714ec 100644 --- a/frontend/src/components/Apartment/Header.tsx +++ b/frontend/src/components/Apartment/Header.tsx @@ -1,5 +1,14 @@ import React, { ReactElement, useState, useEffect } from 'react'; -import { CardHeader, CardMedia, Grid, withStyles, makeStyles, ButtonBase } from '@material-ui/core'; +import { + CardHeader, + CardMedia, + Grid, + Button, + withStyles, + makeStyles, + Avatar, + ButtonBase, +} from '@material-ui/core'; import styles from './Header.module.scss'; import { ApartmentWithId } from '../../../../common/types/db-types'; import defaultHeader from '../../assets/default_header.svg'; @@ -129,6 +138,12 @@ const useStyles = makeStyles((theme) => ({ flexDirection: 'column', justifyContent: 'center', }, + btnSection: { + height: '94%', + [theme.breakpoints.down('sm')]: { + height: '97%', + }, + }, logoGrid: { marginRight: '1em', flex: '0 0 auto', @@ -174,6 +189,7 @@ const ApartmentHeader = ({ apartment, handleClick }: Props): ReactElement => { aptAddress, headerSection, mobileHeaderSection, + btnSection, logoGrid, logoGridMobile, mobileAptName, diff --git a/frontend/src/components/Apartment/MapModal.tsx b/frontend/src/components/Apartment/MapModal.tsx index 1fadf907..a766ebac 100644 --- a/frontend/src/components/Apartment/MapModal.tsx +++ b/frontend/src/components/Apartment/MapModal.tsx @@ -1,4 +1,4 @@ -import React, { Dispatch, SetStateAction, useRef } from 'react'; +import React, { Dispatch, SetStateAction, useRef, useState, useEffect } from 'react'; import { Box, Grid, @@ -6,6 +6,7 @@ import { Dialog, DialogTitle, DialogContent, + DialogActions, makeStyles, IconButton, useTheme, diff --git a/frontend/src/components/Landlord/Header.tsx b/frontend/src/components/Landlord/Header.tsx index b979f129..c996887e 100644 --- a/frontend/src/components/Landlord/Header.tsx +++ b/frontend/src/components/Landlord/Header.tsx @@ -1,6 +1,14 @@ import React, { ReactElement, useState, useEffect } from 'react'; -import { CardHeader, CardMedia, Grid, Button, withStyles, makeStyles } from '@material-ui/core'; +import { + CardHeader, + CardMedia, + Grid, + Button, + withStyles, + makeStyles, + Avatar, +} from '@material-ui/core'; import styles from './Header.module.scss'; import { Landlord } from '../../../../common/types/db-types'; import defaultHeader from '../../assets/default_header.svg'; @@ -138,7 +146,8 @@ const useStyles = makeStyles((theme) => ({ })); const LandlordHeader = ({ landlord, handleClick }: Props): ReactElement => { - const { name, photos } = landlord; + const { name, profilePhoto, photos } = landlord; + const icon = profilePhoto ? profilePhoto : DefaultIcon; const photoLink = photos.length ? photos[0] : defaultHeader; const [isMobile, setIsMobile] = useState(false); diff --git a/frontend/src/components/PhotoCarousel/PhotoCarousel.tsx b/frontend/src/components/PhotoCarousel/PhotoCarousel.tsx index dd0695d0..c2d127e8 100644 --- a/frontend/src/components/PhotoCarousel/PhotoCarousel.tsx +++ b/frontend/src/components/PhotoCarousel/PhotoCarousel.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import { Box, styled, Container, CardMedia, Dialog, makeStyles } from '@material-ui/core'; +import { Modal, Box, styled, Container, CardMedia, Dialog, makeStyles } from '@material-ui/core'; import Carousel from 'react-material-ui-carousel'; interface Props { photos: readonly string[]; open: boolean; onClose?: () => void; - startIndex: number; } const useStyles = makeStyles((theme) => ({ @@ -19,91 +18,33 @@ const useStyles = makeStyles((theme) => ({ backgroundColor: 'rgba(0, 0, 0, 0.5)', opacity: 1, }, - indicatorContainer: { - position: 'absolute', - bottom: '-10px', - width: '100%', - textAlign: 'center', - }, - carouselContainer: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'column', - overflow: 'visible', - height: '80vh', - [theme.breakpoints.down('md')]: { - height: '60dvw', - }, - cursor: 'pointer', - }, })); const ImageBox = styled(Box)({ + width: 'fit-content', + margin: 'auto', borderRadius: '10px', - maxHeight: '80dvh', - backgroundColor: 'transparent', - '& img': { - borderRadius: '10px', - maxHeight: '80dvh', - objectFit: 'contain', - width: 'calc(69dvw - 96px)', - margin: 'auto', - cursor: 'default', - }, + overflow: 'hidden', }); -/** - * PhotoCarousel - this component displays a modal with a carousel of photos. - * - * @remarks - * This component is used to display a modal with a carousel of photos. - * It dynamically adjusts to different screen sizes and can be navigated using the arrow buttons. - * - * @component - * @param {readonly string[]} props.photos - An array of photo URLs to display in the carousel. - * @param {boolean} props.open - A boolean indicating whether the modal is open. - * @param {() => void} [props.onClose] - An optional callback function to handle closing the modal. - * @param {number} props.startIndex - The starting index of the carousel. - * - * @returns {JSX.Element} The rendered PhotoCarousel component. - */ -const PhotoCarousel = ({ photos, open, onClose, startIndex }: Props) => { - const { modalBackground, navButton, indicatorContainer, carouselContainer } = useStyles(); +const PhotoCarousel = ({ photos, open, onClose }: Props) => { + const { modalBackground, navButton } = useStyles(); return ( - { - const target = e.target as HTMLElement; - if ( - target.tagName !== 'IMG' && - target.tagName !== 'BUTTON' && - target.tagName !== 'svg' && - target.tagName !== 'circle' - ) { - console.log(target.tagName); - onClose?.(); - } - }} - > + {photos.map((src, index) => { return ( - + ); diff --git a/frontend/src/components/PhotoCarousel/usePhotoCarousel.ts b/frontend/src/components/PhotoCarousel/usePhotoCarousel.ts deleted file mode 100644 index 7997344a..00000000 --- a/frontend/src/components/PhotoCarousel/usePhotoCarousel.ts +++ /dev/null @@ -1,39 +0,0 @@ -// src/hooks/usePhotoCarousel.ts -import { useState } from 'react'; - -const usePhotoCarousel = (defaultPhotos: readonly string[] = []) => { - const [carouselPhotos, setCarouselPhotos] = useState(defaultPhotos); - const [carouselStartIndex, setCarouselStartIndex] = useState(0); - const [carouselOpen, setCarouselOpen] = useState(false); - - /** - * showPhotoCarousel – Opens the photo carousel modal with the provided photos and start index. - * - * @remarks - * This function sets the photos and start index for the photo carousel and then opens the carousel modal. - * If no photos are provided, it defaults to [defaultPhotos]. - * - * @param {readonly string[]} [photos] – The array of photo URLs to display in the carousel. - * @param {number} [startIndex] – The index of the photo to start the carousel from. - * @return {void} – This function does not return anything. - */ - const showPhotoCarousel = (photos: readonly string[] = defaultPhotos, startIndex: number = 0) => { - setCarouselPhotos(photos); - setCarouselStartIndex(startIndex); - setCarouselOpen(true); - }; - - const closePhotoCarousel = () => { - setCarouselOpen(false); - }; - - return { - carouselPhotos, - carouselStartIndex, - carouselOpen, - showPhotoCarousel, - closePhotoCarousel, - }; -}; - -export default usePhotoCarousel; diff --git a/frontend/src/components/Review/Review.tsx b/frontend/src/components/Review/Review.tsx index 3747bc8e..050f2a87 100644 --- a/frontend/src/components/Review/Review.tsx +++ b/frontend/src/components/Review/Review.tsx @@ -17,7 +17,6 @@ import HeartRating from '../utils/HeartRating'; import { format } from 'date-fns'; import { makeStyles } from '@material-ui/styles'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import HelpfulIcon from '../../assets/helpful-icon.svg'; import EditIcon from '@material-ui/icons/Edit'; import DeleteIcon from '@material-ui/icons/Delete'; import { @@ -56,7 +55,6 @@ type Props = { setToggle: React.Dispatch>; readonly triggerEditToast: () => void; readonly triggerDeleteToast: () => void; - readonly triggerPhotoCarousel: (photos: readonly string[], startIndex: number) => void; user: firebase.User | null; setUser: React.Dispatch>; readonly showLabel: boolean; @@ -103,13 +101,6 @@ const useStyles = makeStyles(() => ({ borderRadius: '4px', height: '15em', width: '15em', - cursor: 'pointer', - transition: '0.3s ease-in-out', - '&:hover': { - filter: 'brightness(0.85)', - boxShadow: '0 4px 4px rgba(0, 0, 0, 0.1)', - transform: 'scale(1.02)', - }, }, photoRowStyle: { overflowX: 'auto', @@ -118,9 +109,6 @@ const useStyles = makeStyles(() => ({ gap: '1vw', paddingTop: '2%', paddingLeft: '0.6%', - overflowY: 'hidden', - paddingRight: '0.6%', - paddingBottom: '2%', }, bedroomsPrice: { display: 'flex', @@ -144,11 +132,6 @@ const useStyles = makeStyles(() => ({ width: '21px', height: '21px', }, - helpfulIcon: { - width: '21px', - height: '21px', - marginRight: '5px', - }, submitButton: { borderRadius: '30px', minWidth: '80px', @@ -219,13 +202,13 @@ const ReviewComponent = ({ setToggle, triggerEditToast, triggerDeleteToast, - triggerPhotoCarousel, user, setUser, showLabel, }: Props): ReactElement => { - const formattedDate = format(new Date(review.date), 'MMM dd, yyyy').toUpperCase(); - const shortenedDate = format(new Date(review.date), 'MMM yyyy').toUpperCase(); + const [reviewData, setReviewData] = useState(review); + const formattedDate = format(new Date(reviewData.date), 'MMM dd, yyyy').toUpperCase(); + const shortenedDate = format(new Date(reviewData.date), 'MMM yyyy').toUpperCase(); const { root, expand, @@ -242,7 +225,6 @@ const ReviewComponent = ({ priceWithIcon, bedPriceIcon, bedroomsPriceText, - helpfulIcon, deleteDialogTitle, deleteDialogDesc, deleteDialogActions, @@ -259,17 +241,14 @@ const ReviewComponent = ({ const isSmallScreen = useMediaQuery('(max-width:391px)'); const toastTime = 3500; - const updateReviewData = () => { - get(`/api/review-by-id/${review.id}`, { + const onSuccessfulEdit = () => { + get(`/api/review-by-id/${reviewData.id}`, { callback: (updatedReview: ReviewWithId) => { // Update the review state with the new data - review = updatedReview; + setReviewData(updatedReview); setToggle((cur) => !cur); }, }); - }; - const onSuccessfulEdit = () => { - updateReviewData(); if (triggerEditToast) triggerEditToast(); }; const deleteModal = () => { @@ -320,13 +299,13 @@ const ReviewComponent = ({ open={reviewOpen} onClose={() => setReviewOpen(false)} setOpen={setReviewOpen} - landlordId={review.landlordId} + landlordId={reviewData.landlordId} onSuccess={onSuccessfulEdit} toastTime={toastTime} - aptId={review.aptId ?? ''} + aptId={reviewData.aptId ?? ''} aptName={apt?.[0]?.name ?? ''} user={user} - initialReview={review} + initialReview={reviewData} /> {deleteModal()} @@ -336,12 +315,12 @@ const ReviewComponent = ({ }; //Retrieving apartment data useEffect(() => { - if (review.aptId !== null) { - get(`/api/apts/${review.aptId}`, { + if (reviewData.aptId !== null) { + get(`/api/apts/${reviewData.aptId}`, { callback: setApt, }); } - }, [review]); + }, [reviewData]); const getRatingInfo = (ratings: DetailedRating): RatingInfo[] => { return [ @@ -370,7 +349,7 @@ const ReviewComponent = ({ }; const reportAbuseHandler = async (reviewId: string) => { - const endpoint = `/api/update-review-status/${review.id}/PENDING`; + const endpoint = `/api/update-review-status/${reviewData.id}/PENDING`; if (user) { const token = await user.getIdToken(true); await axios.put(endpoint, {}, createAuthHeaders(token)); @@ -392,7 +371,7 @@ const ReviewComponent = ({ const handleDeleteModalClose = async (deleteit: Boolean) => { if (deleteit) { - const endpoint = `/api/update-review-status/${review.id}/DELETED`; + const endpoint = `/api/update-review-status/${reviewData.id}/DELETED`; if (user) { const token = await user.getIdToken(true); await axios.put(endpoint, {}, createAuthHeaders(token)); @@ -414,16 +393,16 @@ const ReviewComponent = ({ }; const landlordNotFound = useCallback(() => { - console.error('Landlord with id ' + review.landlordId + ' not found.'); - }, [review.landlordId]); + console.error('Landlord with id ' + reviewData.landlordId + ' not found.'); + }, [reviewData.landlordId]); // Fetch landlord data when the component mounts or when landlordId changes useEffect(() => { - get(`/api/landlord/${review.landlordId}`, { + get(`/api/landlord/${reviewData.landlordId}`, { callback: setLandlordData, errorHandler: landlordNotFound, }); - }, [review.landlordId, landlordNotFound]); + }, [reviewData.landlordId, landlordNotFound]); const propertyLandlordLabel = () => { return ( @@ -434,7 +413,10 @@ const ReviewComponent = ({ 0 ? `/apartment/${review.aptId}` : `/landlord/${review.landlordId}`, + to: + apt.length > 0 + ? `/apartment/${reviewData.aptId}` + : `/landlord/${reviewData.landlordId}`, style: { color: 'black', textDecoration: 'underline', @@ -472,16 +454,24 @@ const ReviewComponent = ({ }; const bedroomsPriceLabel = (rowNum: number) => { return ( - - {review.bedrooms > 0 && ( + + {reviewData.bedrooms > 0 && (
- {review.bedrooms}{' '} - {review.bedrooms === 1 + {reviewData.bedrooms}{' '} + {reviewData.bedrooms === 1 ? isSmallScreen ? 'Bed' : 'Bedroom' @@ -491,7 +481,7 @@ const ReviewComponent = ({
)} - {review.price > 0 && ( + {reviewData.price > 0 && (
{' '} - {getPriceRange(review.price) || 0} + {getPriceRange(reviewData.price) || 0}
)} @@ -515,7 +505,7 @@ const ReviewComponent = ({ - + {useMediaQuery( - user && review.userId && user.uid === review.userId + user && reviewData.userId && user.uid === reviewData.userId ? '(min-width:1410px)' : '(min-width:1075px)' ) && bedroomsPriceLabel(1)} @@ -540,17 +530,20 @@ const ReviewComponent = ({ {isSmallScreen ? shortenedDate : formattedDate} - {user && review.userId && user.uid === review.userId && editDeleteButtons()} + {user && + reviewData.userId && + user.uid === reviewData.userId && + editDeleteButtons()} {useMediaQuery( - user && review.userId && user.uid === review.userId + user && reviewData.userId && user.uid === reviewData.userId ? '(max-width:1409px)' : '(max-width:1074px)' ) && bedroomsPriceLabel(2)} - + @@ -562,19 +555,19 @@ const ReviewComponent = ({ - {expandedText ? review.reviewText : review.reviewText.substring(0, 500)} - {!expandedText && review.reviewText.length > 500 && '...'} - {review.reviewText.length > 500 ? ( + {expandedText ? reviewData.reviewText : reviewData.reviewText.substring(0, 500)} + {!expandedText && reviewData.reviewText.length > 500 && '...'} + {reviewData.reviewText.length > 500 ? ( ) : null} - {review.photos.length > 0 && ( + {reviewData.photos.length > 0 && ( - {review.photos.map((photo, i) => { + {reviewData.photos.map((photo) => { return ( triggerPhotoCarousel(review.photos, i)} - loading="lazy" /> ); })} @@ -598,24 +589,21 @@ const ReviewComponent = ({ - diff --git a/frontend/src/components/utils/Footer/ContactModal.tsx b/frontend/src/components/utils/Footer/ContactModal.tsx index 04216936..46c199af 100644 --- a/frontend/src/components/utils/Footer/ContactModal.tsx +++ b/frontend/src/components/utils/Footer/ContactModal.tsx @@ -670,7 +670,7 @@ const ContactModal = ({ user }: Props) => { {currModal === 'apartment' && cantFindApartmentModal} {currModal === 'question' && questionModal} - {currModal !== 'contact' && ( + {currModal != 'contact' && ( ({ container: { @@ -53,14 +51,6 @@ const AdminPage = (): ReactElement => { const [pendingApartment, setPendingApartmentData] = useState([]); const [pendingContactQuestions, setPendingContactQuestions] = useState([]); - const { - carouselPhotos, - carouselStartIndex, - carouselOpen, - showPhotoCarousel, - closePhotoCarousel, - } = usePhotoCarousel([]); - const { container } = useStyles(); useTitle('Admin'); @@ -125,17 +115,6 @@ const AdminPage = (): ReactElement => { }); }, [toggle]); - const Modals = ( - <> - - - ); - // Reviews tab const reviews = ( @@ -178,7 +157,6 @@ const AdminPage = (): ReactElement => { review={review} setToggle={setToggle} declinedSection={false} - triggerPhotoCarousel={showPhotoCarousel} /> ))} @@ -197,7 +175,6 @@ const AdminPage = (): ReactElement => { review={review} setToggle={setToggle} declinedSection={true} - triggerPhotoCarousel={showPhotoCarousel} /> ))} @@ -269,7 +246,6 @@ const AdminPage = (): ReactElement => { {selectedTab === 'Reviews' && reviews} {selectedTab === 'Contact' && contact} - {Modals} ); }; diff --git a/frontend/src/pages/ApartmentPage.tsx b/frontend/src/pages/ApartmentPage.tsx index 443c89b8..93667dc4 100644 --- a/frontend/src/pages/ApartmentPage.tsx +++ b/frontend/src/pages/ApartmentPage.tsx @@ -13,7 +13,6 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import ReviewModal from '../components/LeaveReview/ReviewModal'; import PhotoCarousel from '../components/PhotoCarousel/PhotoCarousel'; -import usePhotoCarousel from '../components/PhotoCarousel/usePhotoCarousel'; import ReviewComponent from '../components/Review/Review'; import ReviewHeader from '../components/Review/ReviewHeader'; import { useTitle } from '../utils'; @@ -41,8 +40,8 @@ import { getAverageRating } from '../utils/average'; import { colors } from '../colors'; import clsx from 'clsx'; import { sortReviews } from '../utils/sortReviews'; -import savedIcon from '../assets/saved-icon-filled.svg'; -import unsavedIcon from '../assets/saved-icon-unfilled.svg'; +import savedIcon from '../assets/filled-large-saved-icon.png'; +import unsavedIcon from '../assets/unfilled-large-saved-icon.png'; import MapModal from '../components/Apartment/MapModal'; import DropDownWithLabel from '../components/utils/DropDownWithLabel'; @@ -83,6 +82,9 @@ const useStyles = makeStyles((theme) => ({ container: { marginTop: '20px', }, + root: { + borderRadius: '10px', + }, expand: { transform: 'rotate(0deg)', marginLeft: 'auto', @@ -91,22 +93,22 @@ const useStyles = makeStyles((theme) => ({ expandOpen: { transform: 'rotate(180deg)', }, - saveButton: { - backgroundColor: 'transparent', - width: '107px', - margin: '10px 16px', - borderRadius: '30px', - border: '2px solid', - fontSize: '15px', - borderColor: colors.red1, - '&:focus': { - borderColor: `${colors.red1} !important`, + dateText: { + color: colors.gray1, + }, + button: { + textTransform: 'none', + '&.Mui-disabled': { + color: 'inherit', }, }, - bookmarkRibbon: { - width: '19px', - height: '25px', - marginRight: '10px', + horizontalLine: { + borderTop: '1px solid #C4C4C4', + width: '95%', + marginTop: '20px', + borderLeft: 'none', + borderRight: 'none', + borderBottom: 'none', }, })); @@ -133,6 +135,7 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { const [likeStatuses, setLikeStatuses] = useState({}); const [reviewOpen, setReviewOpen] = useState(false); const [mapOpen, setMapOpen] = useState(false); + const [carouselOpen, setCarouselOpen] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false); const [showEditSuccessConfirmation, setShowEditSuccessConfirmation] = useState(false); const [showDeleteSuccessConfirmation, setShowDeleteSuccessConfirmation] = useState(false); @@ -140,13 +143,6 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { const [aptData, setAptData] = useState([]); const [apt, setApt] = useState(undefined); const [travelTimes, setTravelTimes] = useState(undefined); - const { - carouselPhotos, - carouselStartIndex, - carouselOpen, - showPhotoCarousel, - closePhotoCarousel, - } = usePhotoCarousel(apt ? apt.photos : []); const [loaded, setLoaded] = useState(false); const [showSignInError, setShowSignInError] = useState(false); const [sortBy, setSortBy] = useState('date'); @@ -195,8 +191,6 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { container, expand, expandOpen, - saveButton, - bookmarkRibbon, } = useStyles(); // Set the page title based on whether apartment data is loaded. @@ -280,32 +274,6 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { return subscribeLikes(setLikedReviews); }, []); - // Fetch the reviews that the user has liked and set the liked reviews and like statuses. - useEffect(() => { - getUser(false).then((user) => { - if (user) { - user.getIdToken(true).then((token) => { - get( - `/api/review/like/${user.uid}`, - { - callback: (reviews) => { - const likedReviewsMap: Likes = {}; - const likeStatusesMap: Likes = {}; - reviews.forEach((review) => { - likedReviewsMap[review.id] = true; - likeStatusesMap[review.id] = false; - }); - setLikedReviews(likedReviewsMap); - setLikeStatuses(likeStatusesMap); - }, - }, - createAuthHeaders(token) - ); - }); - } - }); - }, []); - useEffect(() => { get(`/api/buildings/all/${apt?.landlordId}`, { callback: setOtherproperties, @@ -464,10 +432,9 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { user={user} /> setCarouselOpen(false)} /> ); @@ -501,7 +468,7 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { )} - {/* { alt={isSaved ? 'Saved' : 'Unsaved'} style={{ width: '107px', height: '43px' }} /> - */} - + @@ -617,7 +573,7 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { color="secondary" variant="contained" disableElevation - onClick={() => showPhotoCarousel()} + onClick={() => setCarouselOpen(true)} > Show all photos @@ -711,7 +667,7 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { averageRating={getAverageRating(reviewData)} apartment={apt!} numReviews={reviewData.length} - handleClick={() => showPhotoCarousel()} + handleClick={() => setCarouselOpen(true)} /> )} @@ -802,7 +758,6 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { setToggle={setToggle} triggerEditToast={showEditSuccessConfirmationToast} triggerDeleteToast={showDeleteSuccessConfirmationToast} - triggerPhotoCarousel={showPhotoCarousel} user={user} setUser={setUser} /> diff --git a/frontend/src/pages/BookmarksPage.tsx b/frontend/src/pages/BookmarksPage.tsx index 1da50272..43f37cc6 100644 --- a/frontend/src/pages/BookmarksPage.tsx +++ b/frontend/src/pages/BookmarksPage.tsx @@ -16,8 +16,6 @@ import { sortReviews } from '../utils/sortReviews'; import DropDownWithLabel from '../components/utils/DropDownWithLabel'; import { AptSortFields, sortApartments } from '../utils/sortApartments'; import Toast from '../components/utils/Toast'; -import PhotoCarousel from '../components/PhotoCarousel/PhotoCarousel'; -import usePhotoCarousel from '../components/PhotoCarousel/usePhotoCarousel'; type Props = { user: firebase.User | null; @@ -111,13 +109,6 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { const [isMobile, setIsMobile] = useState(false); const [showEditSuccessConfirmation, setShowEditSuccessConfirmation] = useState(false); const [showDeleteSuccessConfirmation, setShowDeleteSuccessConfirmation] = useState(false); - const { - carouselPhotos, - carouselStartIndex, - carouselOpen, - showPhotoCarousel, - closePhotoCarousel, - } = usePhotoCarousel([]); useEffect(() => { const handleResize = () => setIsMobile(window.innerWidth <= 600); @@ -130,15 +121,17 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { // Fetch helpful reviews data when the component mounts or when user changes or when toggle changes useEffect(() => { - if (user) { - user.getIdToken(true).then((token) => { + const fetchLikedReviews = async () => { + if (user) { + const token = await user.getIdToken(true); get( - `/api/review/like/${user.uid}?status=APPROVED`, + `/api/review/like/${user.uid}`, { callback: setHelpfulReviewsData, }, createAuthHeaders(token) ); + // this is here so we can get the token when it's fetched and not cause an unauthorized error get( savedAPI, @@ -149,8 +142,9 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { }, createAuthHeaders(token) ); - }); - } + } + }; + fetchLikedReviews(); }, [user, toggle, savedAPI, sortAptsBy]); // Define the type of the properties used for sorting reviews @@ -194,17 +188,13 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { const token = await user.getIdToken(true); const endpoint = dislike ? '/api/remove-like' : '/api/add-like'; await axios.post(endpoint, { reviewId }, createAuthHeaders(token)); - if (dislike) { - setHelpfulReviewsData((reviews) => reviews.filter((review) => review.id !== reviewId)); - } else { - setHelpfulReviewsData((reviews) => - reviews.map((review) => - review.id === reviewId - ? { ...review, likes: (review.likes || defaultLikes) + offsetLikes } - : review - ) - ); - } + setHelpfulReviewsData((reviews) => + reviews.map((review) => + review.id === reviewId + ? { ...review, likes: (review.likes || defaultLikes) + offsetLikes } + : review + ) + ); } catch (err) { throw new Error('Error with liking review'); } @@ -217,17 +207,6 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { const addLike = likeHelper(false); const removeLike = likeHelper(true); - const Modals = ( - <> - - - ); - return (
{showEditSuccessConfirmation && ( @@ -385,7 +364,6 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { setToggle={setToggle} triggerEditToast={showEditSuccessConfirmationToast} triggerDeleteToast={showDeleteSuccessConfirmationToast} - triggerPhotoCarousel={showPhotoCarousel} user={user} setUser={setUser} showLabel={true} @@ -413,7 +391,6 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { )} - {Modals}
); }; diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index d22d96fa..211f113b 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -40,21 +40,6 @@ type Props = { setUser: React.Dispatch>; }; -/** - * HomePage Component – This component represents CUAPTS' home page. - * - * @remarks - * This page displays a search bar (autocomplete), featured properties, and adjusts its layout - * responsively for desktop and mobile views. - * - * @param {firebase.User | null} props.user – The currently logged-in Firebase user or null - * if no user is logged in. - * @param {React.Dispatch>} props.setUser - A - * function to update the `user` state. - * - * @return {ReactElement} The JSX structure of the HomePage component. - */ - const HomePage = ({ user, setUser }: Props): ReactElement => { const classes = useStyles(); const [data, setData] = useState({ buildingData: [], isEnded: false }); diff --git a/frontend/src/pages/LandlordPage.tsx b/frontend/src/pages/LandlordPage.tsx index fc00a1cb..8dff19b0 100644 --- a/frontend/src/pages/LandlordPage.tsx +++ b/frontend/src/pages/LandlordPage.tsx @@ -12,7 +12,6 @@ import React, { ReactElement, useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import ReviewModal from '../components/LeaveReview/ReviewModal'; import PhotoCarousel from '../components/PhotoCarousel/PhotoCarousel'; -import usePhotoCarousel from '../components/PhotoCarousel/usePhotoCarousel'; import InfoFeatures from '../components/Review/InfoFeatures'; import ReviewComponent from '../components/Review/Review'; import ReviewHeader from '../components/Review/ReviewHeader'; @@ -93,13 +92,7 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { const [likedReviews, setLikedReviews] = useState({}); const [likeStatuses, setLikeStatuses] = useState({}); const [reviewOpen, setReviewOpen] = useState(false); - const { - carouselPhotos, - carouselStartIndex, - carouselOpen, - showPhotoCarousel, - closePhotoCarousel, - } = usePhotoCarousel(landlordData ? landlordData.photos : []); + const [carouselOpen, setCarouselOpen] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false); const [showEditSuccessConfirmation, setShowEditSuccessConfirmation] = useState(false); const [showDeleteSuccessConfirmation, setShowDeleteSuccessConfirmation] = useState(false); @@ -186,32 +179,6 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { return subscribeLikes(setLikedReviews); }, []); - // Fetch the reviews that the user has liked and set the liked reviews and like statuses. - useEffect(() => { - getUser(false).then((user) => { - if (user) { - user.getIdToken(true).then((token) => { - get( - `/api/review/like/${user.uid}`, - { - callback: (reviews) => { - const likedReviewsMap: Likes = {}; - const likeStatusesMap: Likes = {}; - reviews.forEach((review) => { - likedReviewsMap[review.id] = true; - likeStatusesMap[review.id] = false; - }); - setLikedReviews(likedReviewsMap); - setLikeStatuses(likeStatusesMap); - }, - }, - createAuthHeaders(token) - ); - }); - } - }); - }, []); - useEffect(() => { const checkIfSaved = async () => { try { @@ -339,10 +306,9 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { user={user} /> setCarouselOpen(false)} /> ); @@ -380,7 +346,7 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { color="secondary" variant="contained" disableElevation - onClick={() => showPhotoCarousel()} + onClick={() => setCarouselOpen(true)} > Show all photos @@ -478,7 +444,7 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { color="secondary" variant="contained" disableElevation - onClick={() => showPhotoCarousel()} + onClick={() => setCarouselOpen(true)} > Show all photos @@ -538,7 +504,7 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { <> {landlordData && ( - showPhotoCarousel()} /> + setCarouselOpen(true)} /> )} @@ -596,7 +562,6 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { setToggle={setToggle} triggerEditToast={showEditSuccessConfirmationToast} triggerDeleteToast={showDeleteSuccessConfirmationToast} - triggerPhotoCarousel={showPhotoCarousel} user={user} setUser={setUser} showLabel={true} diff --git a/frontend/src/pages/LocationPage.tsx b/frontend/src/pages/LocationPage.tsx index 9dbe9f7f..990e98c6 100644 --- a/frontend/src/pages/LocationPage.tsx +++ b/frontend/src/pages/LocationPage.tsx @@ -19,23 +19,6 @@ type Props = { setUser: React.Dispatch>; }; -/** - * LocationPage Component – Displays information and apartments for a specific location - * (i.e. Collegetown, West, North, Downtown). - * - * @remarks - * The LocationPage displays the name and description of the location. It also fetches and - * displays apartment cards that are located in the specific location (e.g. all apartments - * in Collegetown). - * - * @param {firebase.User | null} props.user – The currently logged-in Firebase user or null - * if no user is logged in. - * @param {React.Dispatch>} props.setUser - A - * function to update the `user` state. - * - * @return {ReactElement} The JSX structure of the LocationPage component. - */ - const LocationPage = ({ user, setUser }: Props): ReactElement => { const [isMobile, setIsMobile] = useState(false); const [data, setData] = useState([]); diff --git a/frontend/src/pages/NotFoundPage.tsx b/frontend/src/pages/NotFoundPage.tsx index 25bee9b0..fe19c51b 100644 --- a/frontend/src/pages/NotFoundPage.tsx +++ b/frontend/src/pages/NotFoundPage.tsx @@ -3,15 +3,6 @@ import React, { ReactElement } from 'react'; import NotFoundIcon from '../assets/not-found.svg'; import styles from './NotFoundPage.module.css'; -/** - * NotFoundPage Component – Displays a 404 error page when a requested route is not found. - * - * @remarks - * The NotFoundPage displays a message and an icon to indicate that the requested page does not exist. - * - * @return {ReactElement} – The JSX structure of the NotFoundPage. - */ - const NotFoundPage = (): ReactElement => { return ( diff --git a/frontend/src/pages/Policies.tsx b/frontend/src/pages/Policies.tsx index d3318bc9..094e0484 100644 --- a/frontend/src/pages/Policies.tsx +++ b/frontend/src/pages/Policies.tsx @@ -41,16 +41,6 @@ const useStyles = makeStyles(() => ({ }, })); -/** - * Policies Component – Displays the privacy policy for CUAPTS. - * - * @remarks - * This component displays the privacy policy, describing data collection, usage, user rights etc. - * It adapts content styling based on the screen size. - * - * @return {ReactElement} – The rendered JSX structure of the Policies page. - */ - const Policies = (): ReactElement => { const { h2, h3, h4, body, link } = useStyles(); diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx index 4b91701d..b02e7e53 100644 --- a/frontend/src/pages/ProfilePage.tsx +++ b/frontend/src/pages/ProfilePage.tsx @@ -21,8 +21,6 @@ import { createAuthHeaders, getUser } from '../utils/firebase'; import defaultProfilePic from '../assets/cuapts-bear.png'; import { useTitle } from '../utils'; import { sortReviews } from '../utils/sortReviews'; -import PhotoCarousel from '../components/PhotoCarousel/PhotoCarousel'; -import usePhotoCarousel from '../components/PhotoCarousel/usePhotoCarousel'; type Props = { user: firebase.User | null; @@ -169,13 +167,6 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { const [showEditSuccessConfirmation, setShowEditSuccessConfirmation] = useState(false); const [showDeleteSuccessConfirmation, setShowDeleteSuccessConfirmation] = useState(false); const toastTime = 3500; - const { - carouselPhotos, - carouselStartIndex, - carouselOpen, - showPhotoCarousel, - closePhotoCarousel, - } = usePhotoCarousel([]); useTitle('Profile'); @@ -191,32 +182,6 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { }); }, [user?.uid, toggle]); - // Fetch the reviews that the user has liked and set the liked reviews and like statuses. - useEffect(() => { - getUser(false).then((user) => { - if (user) { - user.getIdToken(true).then((token) => { - get( - `/api/review/like/${user.uid}`, - { - callback: (reviews) => { - const likedReviewsMap: Likes = {}; - const likeStatusesMap: Likes = {}; - reviews.forEach((review) => { - likedReviewsMap[review.id] = true; - likeStatusesMap[review.id] = false; - }); - setLikedReviews(likedReviewsMap); - setLikeStatuses(likeStatusesMap); - }, - }, - createAuthHeaders(token) - ); - }); - } - }); - }, []); - const likeHelper = (dislike = false) => { return async (reviewId: string) => { setLikeStatuses((reviews) => ({ ...reviews, [reviewId]: true })); @@ -234,16 +199,7 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { const token = await user.getIdToken(true); const endpoint = dislike ? '/api/remove-like' : '/api/add-like'; await axios.post(endpoint, { reviewId }, createAuthHeaders(token)); - setLikedReviews((reviews) => { - return { ...reviews, [reviewId]: !dislike }; - }); - setPendingReviews((reviews) => - reviews.map((review) => - review.id === reviewId - ? { ...review, likes: (review.likes || defaultLikes) + offsetLikes } - : review - ) - ); + setLikedReviews((reviews) => ({ ...reviews, [reviewId]: !dislike })); setApprovedReviews((reviews) => reviews.map((review) => review.id === reviewId @@ -305,17 +261,6 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { }; }, [isModalOpen, setIsModalOpen]); - const Modals = ( - <> - - - ); - return (
@@ -382,7 +327,7 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { {/* Maps list of pending reviews and calls Review Component with fields for each user*/} {sortReviews(pendingReviews, 'date').map((review, index) => ( - + { setToggle={setToggle} triggerEditToast={showEditSuccessConfirmationToast} triggerDeleteToast={showDeleteSuccessConfirmationToast} - triggerPhotoCarousel={showPhotoCarousel} user={user} setUser={setUser} showLabel={true} @@ -408,7 +352,6 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { {sortReviews(approvedReviews, 'date').map((review, index) => ( { setToggle={setToggle} triggerEditToast={showEditSuccessConfirmationToast} triggerDeleteToast={showDeleteSuccessConfirmationToast} - triggerPhotoCarousel={showPhotoCarousel} user={user} setUser={setUser} showLabel={true} @@ -442,7 +384,6 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => {
)} - {Modals} ); }; diff --git a/frontend/src/pages/SearchResultsPage.tsx b/frontend/src/pages/SearchResultsPage.tsx index 2f8fdb98..4a60ce37 100644 --- a/frontend/src/pages/SearchResultsPage.tsx +++ b/frontend/src/pages/SearchResultsPage.tsx @@ -19,21 +19,6 @@ type Props = { setUser: React.Dispatch>; }; -/** - * SearchResultsPage Component – Displays search results with apartment cards based on a user's query. - * - * @remarks - * The page fetches the search results by calling a backend API. The resulting apartments and landlords - * are then listed and displayed. The page adjusts for mobile and desktop versions. - * - * @param {firebase.User | null} props.user – The currently logged-in Firebase user or null - * if no user is logged in. - * @param {React.Dispatch>} props.setUser - A - * function to update the `user` state. - * - * @return {ReactElement} – The rendered JSX structure of the SearchResultsPage. - */ - const SearchResultsPage = ({ user, setUser }: Props): ReactElement => { const { searchText } = useStyles(); const location = useLocation(); From 8a0266262ebaef427fbf86b509d38f0092814f63 Mon Sep 17 00:00:00 2001 From: Grace Sawatyanon Date: Tue, 3 Dec 2024 20:46:23 -0500 Subject: [PATCH 8/9] Reapply "Merge branch 'main' into map-backend" This reverts commit 9a0a91d5aa5a7ff5fa51d0e06ce04a4f8a64d348. --- README.md | 15 ++- backend/src/app.ts | 50 ++++--- frontend/src/assets/helpful-icon.svg | 6 + frontend/src/components/Admin/AdminReview.tsx | 23 +++- frontend/src/components/Apartment/Header.tsx | 18 +-- .../src/components/Apartment/MapModal.tsx | 3 +- frontend/src/components/Landlord/Header.tsx | 13 +- .../PhotoCarousel/PhotoCarousel.tsx | 77 +++++++++-- .../PhotoCarousel/usePhotoCarousel.ts | 39 ++++++ frontend/src/components/Review/Review.tsx | 126 ++++++++++-------- .../components/utils/Footer/ContactModal.tsx | 2 +- frontend/src/pages/AdminPage.tsx | 24 ++++ frontend/src/pages/ApartmentPage.tsx | 99 ++++++++++---- frontend/src/pages/BookmarksPage.tsx | 53 +++++--- frontend/src/pages/HomePage.tsx | 15 +++ frontend/src/pages/LandlordPage.tsx | 47 ++++++- frontend/src/pages/LocationPage.tsx | 17 +++ frontend/src/pages/NotFoundPage.tsx | 9 ++ frontend/src/pages/Policies.tsx | 10 ++ frontend/src/pages/ProfilePage.tsx | 63 ++++++++- frontend/src/pages/SearchResultsPage.tsx | 15 +++ 21 files changed, 558 insertions(+), 166 deletions(-) create mode 100644 frontend/src/assets/helpful-icon.svg create mode 100644 frontend/src/components/PhotoCarousel/usePhotoCarousel.ts diff --git a/README.md b/README.md index 19a9aca1..944c6f79 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,17 @@ on its own, run `yarn frontend-dev` or `yarn backend-dev`. ## Contributors +### 2024-2025 + +- **Ella Krechmer** - Product Manager +- **Janet Luo** - Associate Product Manager +- **Jacob Green** - Product Marketing Manager +- **Gunyasorn (Grace) Sawatyanon** - Technical Product Manager +- **David Martinez Lopez** - Designer +- **Vicky Wang** - Designer +- **Kea-Roy Ong** - Developer +- **Casper Liao** - Developer + ### 2023-2024 - **Tina Ou** - Product Manager @@ -34,7 +45,7 @@ on its own, run `yarn frontend-dev` or `yarn backend-dev`. - **Cyrus Irani** - Developer - **Ankit Lakkapragada** - Developer - **Jessica Han** - Developer -- **Grace Sawatyanon** - Developer +- **Gunyasorn (Grace) Sawatyanon** - Developer - **Miranda Luo** - Developer - **Kea-Roy Ong** - Developer - **Casper Liao** - Developer @@ -51,7 +62,7 @@ on its own, run `yarn frontend-dev` or `yarn backend-dev`. - **Daniel Jin** - Developer - **Ankit Lakkapragada** - Developer - **Jessica Han** - Developer -- **Grace Sawatyanon** - Developer +- **Gunyasorn (Grace) Sawatyanon** - Developer ### 2021-2022 diff --git a/backend/src/app.ts b/backend/src/app.ts index a00f1f8f..26c2cca7 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -156,28 +156,48 @@ app.get('/api/review/:status', async (req, res) => { }); /** - * Return list of reviews that user marked as helpful (like) + * review/like/:userId – Fetches reviews liked by a user. + * + * @remarks + * This endpoint retrieves reviews that a user has marked as helpful (liked). It can also filter by review status by passing in a query parameter. If no parameter is provided, no additional filter is applied. + * + * @route GET /api/review/like/:userId + * + * @status + * - 200: Successfully retrieved the liked reviews. + * - 401: Error due to unauthorized access or authentication issues. */ -// TODO: uid param is unused here but when remove it encounter 304 status and req.user is null -app.get('/api/review/like/:uid', authenticate, async (req, res) => { - if (!req.user) throw new Error('not authenticated'); - const { uid } = req.user; - const likesDoc = await likesCollection.doc(uid).get(); +app.get('/api/review/like/:userId', authenticate, async (req, res) => { + if (!req.user) { + throw new Error('not authenticated'); + } + const realUserId = req.user.uid; + const { userId } = req.params; + const statusType = req.query.status; + if (userId !== realUserId) { + res.status(401).send("Error: user is not authorized to access another user's likes"); + return; + } + const likesDoc = await likesCollection.doc(realUserId).get(); if (likesDoc.exists) { const data = likesDoc.data(); if (data) { const reviewIds = Object.keys(data); const matchingReviews: ReviewWithId[] = []; - const querySnapshot = await reviewCollection - .where(FieldPath.documentId(), 'in', reviewIds) - .where('status', '==', 'APPROVED') - .get(); - querySnapshot.forEach((doc) => { - const data = doc.data(); - const reviewData = { ...data, date: data.date.toDate() }; - matchingReviews.push({ ...reviewData, id: doc.id } as ReviewWithId); - }); + if (reviewIds.length > 0) { + let query = reviewCollection.where(FieldPath.documentId(), 'in', reviewIds); + if (statusType) { + // filter by status if provided + query = query.where('status', '==', statusType); + } + const querySnapshot = await query.get(); + querySnapshot.forEach((doc) => { + const data = doc.data(); + const reviewData = { ...data, date: data.date.toDate() }; + matchingReviews.push({ ...reviewData, id: doc.id } as ReviewWithId); + }); + } res.status(200).send(JSON.stringify(matchingReviews)); return; } diff --git a/frontend/src/assets/helpful-icon.svg b/frontend/src/assets/helpful-icon.svg new file mode 100644 index 00000000..4755ddf9 --- /dev/null +++ b/frontend/src/assets/helpful-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/components/Admin/AdminReview.tsx b/frontend/src/components/Admin/AdminReview.tsx index 2dfe0c06..56ca6c82 100644 --- a/frontend/src/components/Admin/AdminReview.tsx +++ b/frontend/src/components/Admin/AdminReview.tsx @@ -37,6 +37,8 @@ type Props = { readonly setToggle: React.Dispatch>; /** Indicates if the review is in the declined section. */ readonly declinedSection: boolean; + /** Function to trigger the photo carousel. */ + readonly triggerPhotoCarousel: (photos: readonly string[], startIndex: number) => void; }; /** @@ -70,14 +72,24 @@ const useStyles = makeStyles(() => ({ borderRadius: '4px', height: '15em', width: '15em', + cursor: 'pointer', + transition: '0.3s ease-in-out', + '&:hover': { + filter: 'brightness(0.85)', + boxShadow: '0 4px 4px rgba(0, 0, 0, 0.1)', + transform: 'scale(1.02)', + }, }, photoRowStyle: { overflowX: 'auto', + overflowY: 'hidden', display: 'flex', flexDirection: 'row', gap: '1vw', paddingTop: '2%', paddingLeft: '0.6%', + paddingRight: '0.6%', + paddingBottom: '2%', }, })); @@ -87,7 +99,12 @@ const useStyles = makeStyles(() => ({ * @param review review - The review to approve * @returns The rendered component. */ -const AdminReviewComponent = ({ review, setToggle, declinedSection }: Props): ReactElement => { +const AdminReviewComponent = ({ + review, + setToggle, + declinedSection, + triggerPhotoCarousel, +}: Props): ReactElement => { const { detailedRatings, overallRating, bedrooms, price, date, reviewText, photos } = review; const formattedDate = format(new Date(date), 'MMM dd, yyyy').toUpperCase(); const { root, dateText, bedroomsPriceText, ratingInfo, photoStyle, photoRowStyle } = useStyles(); @@ -202,7 +219,7 @@ const AdminReviewComponent = ({ review, setToggle, declinedSection }: Props): Re {photos.length > 0 && ( - {photos.map((photo) => { + {photos.map((photo, i) => { return ( triggerPhotoCarousel(photos, i)} + loading="lazy" /> ); })} diff --git a/frontend/src/components/Apartment/Header.tsx b/frontend/src/components/Apartment/Header.tsx index 056714ec..7df889ba 100644 --- a/frontend/src/components/Apartment/Header.tsx +++ b/frontend/src/components/Apartment/Header.tsx @@ -1,14 +1,5 @@ import React, { ReactElement, useState, useEffect } from 'react'; -import { - CardHeader, - CardMedia, - Grid, - Button, - withStyles, - makeStyles, - Avatar, - ButtonBase, -} from '@material-ui/core'; +import { CardHeader, CardMedia, Grid, withStyles, makeStyles, ButtonBase } from '@material-ui/core'; import styles from './Header.module.scss'; import { ApartmentWithId } from '../../../../common/types/db-types'; import defaultHeader from '../../assets/default_header.svg'; @@ -138,12 +129,6 @@ const useStyles = makeStyles((theme) => ({ flexDirection: 'column', justifyContent: 'center', }, - btnSection: { - height: '94%', - [theme.breakpoints.down('sm')]: { - height: '97%', - }, - }, logoGrid: { marginRight: '1em', flex: '0 0 auto', @@ -189,7 +174,6 @@ const ApartmentHeader = ({ apartment, handleClick }: Props): ReactElement => { aptAddress, headerSection, mobileHeaderSection, - btnSection, logoGrid, logoGridMobile, mobileAptName, diff --git a/frontend/src/components/Apartment/MapModal.tsx b/frontend/src/components/Apartment/MapModal.tsx index a766ebac..1fadf907 100644 --- a/frontend/src/components/Apartment/MapModal.tsx +++ b/frontend/src/components/Apartment/MapModal.tsx @@ -1,4 +1,4 @@ -import React, { Dispatch, SetStateAction, useRef, useState, useEffect } from 'react'; +import React, { Dispatch, SetStateAction, useRef } from 'react'; import { Box, Grid, @@ -6,7 +6,6 @@ import { Dialog, DialogTitle, DialogContent, - DialogActions, makeStyles, IconButton, useTheme, diff --git a/frontend/src/components/Landlord/Header.tsx b/frontend/src/components/Landlord/Header.tsx index c996887e..b979f129 100644 --- a/frontend/src/components/Landlord/Header.tsx +++ b/frontend/src/components/Landlord/Header.tsx @@ -1,14 +1,6 @@ import React, { ReactElement, useState, useEffect } from 'react'; -import { - CardHeader, - CardMedia, - Grid, - Button, - withStyles, - makeStyles, - Avatar, -} from '@material-ui/core'; +import { CardHeader, CardMedia, Grid, Button, withStyles, makeStyles } from '@material-ui/core'; import styles from './Header.module.scss'; import { Landlord } from '../../../../common/types/db-types'; import defaultHeader from '../../assets/default_header.svg'; @@ -146,8 +138,7 @@ const useStyles = makeStyles((theme) => ({ })); const LandlordHeader = ({ landlord, handleClick }: Props): ReactElement => { - const { name, profilePhoto, photos } = landlord; - const icon = profilePhoto ? profilePhoto : DefaultIcon; + const { name, photos } = landlord; const photoLink = photos.length ? photos[0] : defaultHeader; const [isMobile, setIsMobile] = useState(false); diff --git a/frontend/src/components/PhotoCarousel/PhotoCarousel.tsx b/frontend/src/components/PhotoCarousel/PhotoCarousel.tsx index c2d127e8..dd0695d0 100644 --- a/frontend/src/components/PhotoCarousel/PhotoCarousel.tsx +++ b/frontend/src/components/PhotoCarousel/PhotoCarousel.tsx @@ -1,11 +1,12 @@ import React from 'react'; -import { Modal, Box, styled, Container, CardMedia, Dialog, makeStyles } from '@material-ui/core'; +import { Box, styled, Container, CardMedia, Dialog, makeStyles } from '@material-ui/core'; import Carousel from 'react-material-ui-carousel'; interface Props { photos: readonly string[]; open: boolean; onClose?: () => void; + startIndex: number; } const useStyles = makeStyles((theme) => ({ @@ -18,33 +19,91 @@ const useStyles = makeStyles((theme) => ({ backgroundColor: 'rgba(0, 0, 0, 0.5)', opacity: 1, }, + indicatorContainer: { + position: 'absolute', + bottom: '-10px', + width: '100%', + textAlign: 'center', + }, + carouselContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'column', + overflow: 'visible', + height: '80vh', + [theme.breakpoints.down('md')]: { + height: '60dvw', + }, + cursor: 'pointer', + }, })); const ImageBox = styled(Box)({ - width: 'fit-content', - margin: 'auto', borderRadius: '10px', - overflow: 'hidden', + maxHeight: '80dvh', + backgroundColor: 'transparent', + '& img': { + borderRadius: '10px', + maxHeight: '80dvh', + objectFit: 'contain', + width: 'calc(69dvw - 96px)', + margin: 'auto', + cursor: 'default', + }, }); -const PhotoCarousel = ({ photos, open, onClose }: Props) => { - const { modalBackground, navButton } = useStyles(); +/** + * PhotoCarousel - this component displays a modal with a carousel of photos. + * + * @remarks + * This component is used to display a modal with a carousel of photos. + * It dynamically adjusts to different screen sizes and can be navigated using the arrow buttons. + * + * @component + * @param {readonly string[]} props.photos - An array of photo URLs to display in the carousel. + * @param {boolean} props.open - A boolean indicating whether the modal is open. + * @param {() => void} [props.onClose] - An optional callback function to handle closing the modal. + * @param {number} props.startIndex - The starting index of the carousel. + * + * @returns {JSX.Element} The rendered PhotoCarousel component. + */ +const PhotoCarousel = ({ photos, open, onClose, startIndex }: Props) => { + const { modalBackground, navButton, indicatorContainer, carouselContainer } = useStyles(); return ( - + { + const target = e.target as HTMLElement; + if ( + target.tagName !== 'IMG' && + target.tagName !== 'BUTTON' && + target.tagName !== 'svg' && + target.tagName !== 'circle' + ) { + console.log(target.tagName); + onClose?.(); + } + }} + > {photos.map((src, index) => { return ( - + ); diff --git a/frontend/src/components/PhotoCarousel/usePhotoCarousel.ts b/frontend/src/components/PhotoCarousel/usePhotoCarousel.ts new file mode 100644 index 00000000..7997344a --- /dev/null +++ b/frontend/src/components/PhotoCarousel/usePhotoCarousel.ts @@ -0,0 +1,39 @@ +// src/hooks/usePhotoCarousel.ts +import { useState } from 'react'; + +const usePhotoCarousel = (defaultPhotos: readonly string[] = []) => { + const [carouselPhotos, setCarouselPhotos] = useState(defaultPhotos); + const [carouselStartIndex, setCarouselStartIndex] = useState(0); + const [carouselOpen, setCarouselOpen] = useState(false); + + /** + * showPhotoCarousel – Opens the photo carousel modal with the provided photos and start index. + * + * @remarks + * This function sets the photos and start index for the photo carousel and then opens the carousel modal. + * If no photos are provided, it defaults to [defaultPhotos]. + * + * @param {readonly string[]} [photos] – The array of photo URLs to display in the carousel. + * @param {number} [startIndex] – The index of the photo to start the carousel from. + * @return {void} – This function does not return anything. + */ + const showPhotoCarousel = (photos: readonly string[] = defaultPhotos, startIndex: number = 0) => { + setCarouselPhotos(photos); + setCarouselStartIndex(startIndex); + setCarouselOpen(true); + }; + + const closePhotoCarousel = () => { + setCarouselOpen(false); + }; + + return { + carouselPhotos, + carouselStartIndex, + carouselOpen, + showPhotoCarousel, + closePhotoCarousel, + }; +}; + +export default usePhotoCarousel; diff --git a/frontend/src/components/Review/Review.tsx b/frontend/src/components/Review/Review.tsx index 050f2a87..3747bc8e 100644 --- a/frontend/src/components/Review/Review.tsx +++ b/frontend/src/components/Review/Review.tsx @@ -17,6 +17,7 @@ import HeartRating from '../utils/HeartRating'; import { format } from 'date-fns'; import { makeStyles } from '@material-ui/styles'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import HelpfulIcon from '../../assets/helpful-icon.svg'; import EditIcon from '@material-ui/icons/Edit'; import DeleteIcon from '@material-ui/icons/Delete'; import { @@ -55,6 +56,7 @@ type Props = { setToggle: React.Dispatch>; readonly triggerEditToast: () => void; readonly triggerDeleteToast: () => void; + readonly triggerPhotoCarousel: (photos: readonly string[], startIndex: number) => void; user: firebase.User | null; setUser: React.Dispatch>; readonly showLabel: boolean; @@ -101,6 +103,13 @@ const useStyles = makeStyles(() => ({ borderRadius: '4px', height: '15em', width: '15em', + cursor: 'pointer', + transition: '0.3s ease-in-out', + '&:hover': { + filter: 'brightness(0.85)', + boxShadow: '0 4px 4px rgba(0, 0, 0, 0.1)', + transform: 'scale(1.02)', + }, }, photoRowStyle: { overflowX: 'auto', @@ -109,6 +118,9 @@ const useStyles = makeStyles(() => ({ gap: '1vw', paddingTop: '2%', paddingLeft: '0.6%', + overflowY: 'hidden', + paddingRight: '0.6%', + paddingBottom: '2%', }, bedroomsPrice: { display: 'flex', @@ -132,6 +144,11 @@ const useStyles = makeStyles(() => ({ width: '21px', height: '21px', }, + helpfulIcon: { + width: '21px', + height: '21px', + marginRight: '5px', + }, submitButton: { borderRadius: '30px', minWidth: '80px', @@ -202,13 +219,13 @@ const ReviewComponent = ({ setToggle, triggerEditToast, triggerDeleteToast, + triggerPhotoCarousel, user, setUser, showLabel, }: Props): ReactElement => { - const [reviewData, setReviewData] = useState(review); - const formattedDate = format(new Date(reviewData.date), 'MMM dd, yyyy').toUpperCase(); - const shortenedDate = format(new Date(reviewData.date), 'MMM yyyy').toUpperCase(); + const formattedDate = format(new Date(review.date), 'MMM dd, yyyy').toUpperCase(); + const shortenedDate = format(new Date(review.date), 'MMM yyyy').toUpperCase(); const { root, expand, @@ -225,6 +242,7 @@ const ReviewComponent = ({ priceWithIcon, bedPriceIcon, bedroomsPriceText, + helpfulIcon, deleteDialogTitle, deleteDialogDesc, deleteDialogActions, @@ -241,14 +259,17 @@ const ReviewComponent = ({ const isSmallScreen = useMediaQuery('(max-width:391px)'); const toastTime = 3500; - const onSuccessfulEdit = () => { - get(`/api/review-by-id/${reviewData.id}`, { + const updateReviewData = () => { + get(`/api/review-by-id/${review.id}`, { callback: (updatedReview: ReviewWithId) => { // Update the review state with the new data - setReviewData(updatedReview); + review = updatedReview; setToggle((cur) => !cur); }, }); + }; + const onSuccessfulEdit = () => { + updateReviewData(); if (triggerEditToast) triggerEditToast(); }; const deleteModal = () => { @@ -299,13 +320,13 @@ const ReviewComponent = ({ open={reviewOpen} onClose={() => setReviewOpen(false)} setOpen={setReviewOpen} - landlordId={reviewData.landlordId} + landlordId={review.landlordId} onSuccess={onSuccessfulEdit} toastTime={toastTime} - aptId={reviewData.aptId ?? ''} + aptId={review.aptId ?? ''} aptName={apt?.[0]?.name ?? ''} user={user} - initialReview={reviewData} + initialReview={review} /> {deleteModal()} @@ -315,12 +336,12 @@ const ReviewComponent = ({ }; //Retrieving apartment data useEffect(() => { - if (reviewData.aptId !== null) { - get(`/api/apts/${reviewData.aptId}`, { + if (review.aptId !== null) { + get(`/api/apts/${review.aptId}`, { callback: setApt, }); } - }, [reviewData]); + }, [review]); const getRatingInfo = (ratings: DetailedRating): RatingInfo[] => { return [ @@ -349,7 +370,7 @@ const ReviewComponent = ({ }; const reportAbuseHandler = async (reviewId: string) => { - const endpoint = `/api/update-review-status/${reviewData.id}/PENDING`; + const endpoint = `/api/update-review-status/${review.id}/PENDING`; if (user) { const token = await user.getIdToken(true); await axios.put(endpoint, {}, createAuthHeaders(token)); @@ -371,7 +392,7 @@ const ReviewComponent = ({ const handleDeleteModalClose = async (deleteit: Boolean) => { if (deleteit) { - const endpoint = `/api/update-review-status/${reviewData.id}/DELETED`; + const endpoint = `/api/update-review-status/${review.id}/DELETED`; if (user) { const token = await user.getIdToken(true); await axios.put(endpoint, {}, createAuthHeaders(token)); @@ -393,16 +414,16 @@ const ReviewComponent = ({ }; const landlordNotFound = useCallback(() => { - console.error('Landlord with id ' + reviewData.landlordId + ' not found.'); - }, [reviewData.landlordId]); + console.error('Landlord with id ' + review.landlordId + ' not found.'); + }, [review.landlordId]); // Fetch landlord data when the component mounts or when landlordId changes useEffect(() => { - get(`/api/landlord/${reviewData.landlordId}`, { + get(`/api/landlord/${review.landlordId}`, { callback: setLandlordData, errorHandler: landlordNotFound, }); - }, [reviewData.landlordId, landlordNotFound]); + }, [review.landlordId, landlordNotFound]); const propertyLandlordLabel = () => { return ( @@ -413,10 +434,7 @@ const ReviewComponent = ({ 0 - ? `/apartment/${reviewData.aptId}` - : `/landlord/${reviewData.landlordId}`, + to: apt.length > 0 ? `/apartment/${review.aptId}` : `/landlord/${review.landlordId}`, style: { color: 'black', textDecoration: 'underline', @@ -454,24 +472,16 @@ const ReviewComponent = ({ }; const bedroomsPriceLabel = (rowNum: number) => { return ( - - {reviewData.bedrooms > 0 && ( + + {review.bedrooms > 0 && (
- {reviewData.bedrooms}{' '} - {reviewData.bedrooms === 1 + {review.bedrooms}{' '} + {review.bedrooms === 1 ? isSmallScreen ? 'Bed' : 'Bedroom' @@ -481,7 +491,7 @@ const ReviewComponent = ({
)} - {reviewData.price > 0 && ( + {review.price > 0 && (
{' '} - {getPriceRange(reviewData.price) || 0} + {getPriceRange(review.price) || 0}
)} @@ -505,7 +515,7 @@ const ReviewComponent = ({ - + {useMediaQuery( - user && reviewData.userId && user.uid === reviewData.userId + user && review.userId && user.uid === review.userId ? '(min-width:1410px)' : '(min-width:1075px)' ) && bedroomsPriceLabel(1)} @@ -530,20 +540,17 @@ const ReviewComponent = ({ {isSmallScreen ? shortenedDate : formattedDate} - {user && - reviewData.userId && - user.uid === reviewData.userId && - editDeleteButtons()} + {user && review.userId && user.uid === review.userId && editDeleteButtons()} {useMediaQuery( - user && reviewData.userId && user.uid === reviewData.userId + user && review.userId && user.uid === review.userId ? '(max-width:1409px)' : '(max-width:1074px)' ) && bedroomsPriceLabel(2)} - + @@ -555,19 +562,19 @@ const ReviewComponent = ({ - {expandedText ? reviewData.reviewText : reviewData.reviewText.substring(0, 500)} - {!expandedText && reviewData.reviewText.length > 500 && '...'} - {reviewData.reviewText.length > 500 ? ( + {expandedText ? review.reviewText : review.reviewText.substring(0, 500)} + {!expandedText && review.reviewText.length > 500 && '...'} + {review.reviewText.length > 500 ? ( ) : null} - {reviewData.photos.length > 0 && ( + {review.photos.length > 0 && ( - {reviewData.photos.map((photo) => { + {review.photos.map((photo, i) => { return ( triggerPhotoCarousel(review.photos, i)} + loading="lazy" /> ); })} @@ -589,21 +598,24 @@ const ReviewComponent = ({ - diff --git a/frontend/src/components/utils/Footer/ContactModal.tsx b/frontend/src/components/utils/Footer/ContactModal.tsx index 46c199af..04216936 100644 --- a/frontend/src/components/utils/Footer/ContactModal.tsx +++ b/frontend/src/components/utils/Footer/ContactModal.tsx @@ -670,7 +670,7 @@ const ContactModal = ({ user }: Props) => { {currModal === 'apartment' && cantFindApartmentModal} {currModal === 'question' && questionModal} - {currModal != 'contact' && ( + {currModal !== 'contact' && ( ({ container: { @@ -51,6 +53,14 @@ const AdminPage = (): ReactElement => { const [pendingApartment, setPendingApartmentData] = useState([]); const [pendingContactQuestions, setPendingContactQuestions] = useState([]); + const { + carouselPhotos, + carouselStartIndex, + carouselOpen, + showPhotoCarousel, + closePhotoCarousel, + } = usePhotoCarousel([]); + const { container } = useStyles(); useTitle('Admin'); @@ -115,6 +125,17 @@ const AdminPage = (): ReactElement => { }); }, [toggle]); + const Modals = ( + <> + + + ); + // Reviews tab const reviews = ( @@ -157,6 +178,7 @@ const AdminPage = (): ReactElement => { review={review} setToggle={setToggle} declinedSection={false} + triggerPhotoCarousel={showPhotoCarousel} /> ))} @@ -175,6 +197,7 @@ const AdminPage = (): ReactElement => { review={review} setToggle={setToggle} declinedSection={true} + triggerPhotoCarousel={showPhotoCarousel} /> ))} @@ -246,6 +269,7 @@ const AdminPage = (): ReactElement => { {selectedTab === 'Reviews' && reviews} {selectedTab === 'Contact' && contact} + {Modals} ); }; diff --git a/frontend/src/pages/ApartmentPage.tsx b/frontend/src/pages/ApartmentPage.tsx index 93667dc4..443c89b8 100644 --- a/frontend/src/pages/ApartmentPage.tsx +++ b/frontend/src/pages/ApartmentPage.tsx @@ -13,6 +13,7 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import ReviewModal from '../components/LeaveReview/ReviewModal'; import PhotoCarousel from '../components/PhotoCarousel/PhotoCarousel'; +import usePhotoCarousel from '../components/PhotoCarousel/usePhotoCarousel'; import ReviewComponent from '../components/Review/Review'; import ReviewHeader from '../components/Review/ReviewHeader'; import { useTitle } from '../utils'; @@ -40,8 +41,8 @@ import { getAverageRating } from '../utils/average'; import { colors } from '../colors'; import clsx from 'clsx'; import { sortReviews } from '../utils/sortReviews'; -import savedIcon from '../assets/filled-large-saved-icon.png'; -import unsavedIcon from '../assets/unfilled-large-saved-icon.png'; +import savedIcon from '../assets/saved-icon-filled.svg'; +import unsavedIcon from '../assets/saved-icon-unfilled.svg'; import MapModal from '../components/Apartment/MapModal'; import DropDownWithLabel from '../components/utils/DropDownWithLabel'; @@ -82,9 +83,6 @@ const useStyles = makeStyles((theme) => ({ container: { marginTop: '20px', }, - root: { - borderRadius: '10px', - }, expand: { transform: 'rotate(0deg)', marginLeft: 'auto', @@ -93,22 +91,22 @@ const useStyles = makeStyles((theme) => ({ expandOpen: { transform: 'rotate(180deg)', }, - dateText: { - color: colors.gray1, - }, - button: { - textTransform: 'none', - '&.Mui-disabled': { - color: 'inherit', + saveButton: { + backgroundColor: 'transparent', + width: '107px', + margin: '10px 16px', + borderRadius: '30px', + border: '2px solid', + fontSize: '15px', + borderColor: colors.red1, + '&:focus': { + borderColor: `${colors.red1} !important`, }, }, - horizontalLine: { - borderTop: '1px solid #C4C4C4', - width: '95%', - marginTop: '20px', - borderLeft: 'none', - borderRight: 'none', - borderBottom: 'none', + bookmarkRibbon: { + width: '19px', + height: '25px', + marginRight: '10px', }, })); @@ -135,7 +133,6 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { const [likeStatuses, setLikeStatuses] = useState({}); const [reviewOpen, setReviewOpen] = useState(false); const [mapOpen, setMapOpen] = useState(false); - const [carouselOpen, setCarouselOpen] = useState(false); const [showConfirmation, setShowConfirmation] = useState(false); const [showEditSuccessConfirmation, setShowEditSuccessConfirmation] = useState(false); const [showDeleteSuccessConfirmation, setShowDeleteSuccessConfirmation] = useState(false); @@ -143,6 +140,13 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { const [aptData, setAptData] = useState([]); const [apt, setApt] = useState(undefined); const [travelTimes, setTravelTimes] = useState(undefined); + const { + carouselPhotos, + carouselStartIndex, + carouselOpen, + showPhotoCarousel, + closePhotoCarousel, + } = usePhotoCarousel(apt ? apt.photos : []); const [loaded, setLoaded] = useState(false); const [showSignInError, setShowSignInError] = useState(false); const [sortBy, setSortBy] = useState('date'); @@ -191,6 +195,8 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { container, expand, expandOpen, + saveButton, + bookmarkRibbon, } = useStyles(); // Set the page title based on whether apartment data is loaded. @@ -274,6 +280,32 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { return subscribeLikes(setLikedReviews); }, []); + // Fetch the reviews that the user has liked and set the liked reviews and like statuses. + useEffect(() => { + getUser(false).then((user) => { + if (user) { + user.getIdToken(true).then((token) => { + get( + `/api/review/like/${user.uid}`, + { + callback: (reviews) => { + const likedReviewsMap: Likes = {}; + const likeStatusesMap: Likes = {}; + reviews.forEach((review) => { + likedReviewsMap[review.id] = true; + likeStatusesMap[review.id] = false; + }); + setLikedReviews(likedReviewsMap); + setLikeStatuses(likeStatusesMap); + }, + }, + createAuthHeaders(token) + ); + }); + } + }); + }, []); + useEffect(() => { get(`/api/buildings/all/${apt?.landlordId}`, { callback: setOtherproperties, @@ -432,9 +464,10 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { user={user} /> setCarouselOpen(false)} + startIndex={carouselStartIndex} + onClose={closePhotoCarousel} /> ); @@ -468,7 +501,7 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { )} - { alt={isSaved ? 'Saved' : 'Unsaved'} style={{ width: '107px', height: '43px' }} /> - + */} + @@ -573,7 +617,7 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { color="secondary" variant="contained" disableElevation - onClick={() => setCarouselOpen(true)} + onClick={() => showPhotoCarousel()} > Show all photos @@ -667,7 +711,7 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { averageRating={getAverageRating(reviewData)} apartment={apt!} numReviews={reviewData.length} - handleClick={() => setCarouselOpen(true)} + handleClick={() => showPhotoCarousel()} />
)} @@ -758,6 +802,7 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { setToggle={setToggle} triggerEditToast={showEditSuccessConfirmationToast} triggerDeleteToast={showDeleteSuccessConfirmationToast} + triggerPhotoCarousel={showPhotoCarousel} user={user} setUser={setUser} /> diff --git a/frontend/src/pages/BookmarksPage.tsx b/frontend/src/pages/BookmarksPage.tsx index 43f37cc6..1da50272 100644 --- a/frontend/src/pages/BookmarksPage.tsx +++ b/frontend/src/pages/BookmarksPage.tsx @@ -16,6 +16,8 @@ import { sortReviews } from '../utils/sortReviews'; import DropDownWithLabel from '../components/utils/DropDownWithLabel'; import { AptSortFields, sortApartments } from '../utils/sortApartments'; import Toast from '../components/utils/Toast'; +import PhotoCarousel from '../components/PhotoCarousel/PhotoCarousel'; +import usePhotoCarousel from '../components/PhotoCarousel/usePhotoCarousel'; type Props = { user: firebase.User | null; @@ -109,6 +111,13 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { const [isMobile, setIsMobile] = useState(false); const [showEditSuccessConfirmation, setShowEditSuccessConfirmation] = useState(false); const [showDeleteSuccessConfirmation, setShowDeleteSuccessConfirmation] = useState(false); + const { + carouselPhotos, + carouselStartIndex, + carouselOpen, + showPhotoCarousel, + closePhotoCarousel, + } = usePhotoCarousel([]); useEffect(() => { const handleResize = () => setIsMobile(window.innerWidth <= 600); @@ -121,17 +130,15 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { // Fetch helpful reviews data when the component mounts or when user changes or when toggle changes useEffect(() => { - const fetchLikedReviews = async () => { - if (user) { - const token = await user.getIdToken(true); + if (user) { + user.getIdToken(true).then((token) => { get( - `/api/review/like/${user.uid}`, + `/api/review/like/${user.uid}?status=APPROVED`, { callback: setHelpfulReviewsData, }, createAuthHeaders(token) ); - // this is here so we can get the token when it's fetched and not cause an unauthorized error get( savedAPI, @@ -142,9 +149,8 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { }, createAuthHeaders(token) ); - } - }; - fetchLikedReviews(); + }); + } }, [user, toggle, savedAPI, sortAptsBy]); // Define the type of the properties used for sorting reviews @@ -188,13 +194,17 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { const token = await user.getIdToken(true); const endpoint = dislike ? '/api/remove-like' : '/api/add-like'; await axios.post(endpoint, { reviewId }, createAuthHeaders(token)); - setHelpfulReviewsData((reviews) => - reviews.map((review) => - review.id === reviewId - ? { ...review, likes: (review.likes || defaultLikes) + offsetLikes } - : review - ) - ); + if (dislike) { + setHelpfulReviewsData((reviews) => reviews.filter((review) => review.id !== reviewId)); + } else { + setHelpfulReviewsData((reviews) => + reviews.map((review) => + review.id === reviewId + ? { ...review, likes: (review.likes || defaultLikes) + offsetLikes } + : review + ) + ); + } } catch (err) { throw new Error('Error with liking review'); } @@ -207,6 +217,17 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { const addLike = likeHelper(false); const removeLike = likeHelper(true); + const Modals = ( + <> + + + ); + return (
{showEditSuccessConfirmation && ( @@ -364,6 +385,7 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { setToggle={setToggle} triggerEditToast={showEditSuccessConfirmationToast} triggerDeleteToast={showDeleteSuccessConfirmationToast} + triggerPhotoCarousel={showPhotoCarousel} user={user} setUser={setUser} showLabel={true} @@ -391,6 +413,7 @@ const BookmarksPage = ({ user, setUser }: Props): ReactElement => { )} + {Modals}
); }; diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 211f113b..d22d96fa 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -40,6 +40,21 @@ type Props = { setUser: React.Dispatch>; }; +/** + * HomePage Component – This component represents CUAPTS' home page. + * + * @remarks + * This page displays a search bar (autocomplete), featured properties, and adjusts its layout + * responsively for desktop and mobile views. + * + * @param {firebase.User | null} props.user – The currently logged-in Firebase user or null + * if no user is logged in. + * @param {React.Dispatch>} props.setUser - A + * function to update the `user` state. + * + * @return {ReactElement} The JSX structure of the HomePage component. + */ + const HomePage = ({ user, setUser }: Props): ReactElement => { const classes = useStyles(); const [data, setData] = useState({ buildingData: [], isEnded: false }); diff --git a/frontend/src/pages/LandlordPage.tsx b/frontend/src/pages/LandlordPage.tsx index 8dff19b0..fc00a1cb 100644 --- a/frontend/src/pages/LandlordPage.tsx +++ b/frontend/src/pages/LandlordPage.tsx @@ -12,6 +12,7 @@ import React, { ReactElement, useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import ReviewModal from '../components/LeaveReview/ReviewModal'; import PhotoCarousel from '../components/PhotoCarousel/PhotoCarousel'; +import usePhotoCarousel from '../components/PhotoCarousel/usePhotoCarousel'; import InfoFeatures from '../components/Review/InfoFeatures'; import ReviewComponent from '../components/Review/Review'; import ReviewHeader from '../components/Review/ReviewHeader'; @@ -92,7 +93,13 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { const [likedReviews, setLikedReviews] = useState({}); const [likeStatuses, setLikeStatuses] = useState({}); const [reviewOpen, setReviewOpen] = useState(false); - const [carouselOpen, setCarouselOpen] = useState(false); + const { + carouselPhotos, + carouselStartIndex, + carouselOpen, + showPhotoCarousel, + closePhotoCarousel, + } = usePhotoCarousel(landlordData ? landlordData.photos : []); const [showConfirmation, setShowConfirmation] = useState(false); const [showEditSuccessConfirmation, setShowEditSuccessConfirmation] = useState(false); const [showDeleteSuccessConfirmation, setShowDeleteSuccessConfirmation] = useState(false); @@ -179,6 +186,32 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { return subscribeLikes(setLikedReviews); }, []); + // Fetch the reviews that the user has liked and set the liked reviews and like statuses. + useEffect(() => { + getUser(false).then((user) => { + if (user) { + user.getIdToken(true).then((token) => { + get( + `/api/review/like/${user.uid}`, + { + callback: (reviews) => { + const likedReviewsMap: Likes = {}; + const likeStatusesMap: Likes = {}; + reviews.forEach((review) => { + likedReviewsMap[review.id] = true; + likeStatusesMap[review.id] = false; + }); + setLikedReviews(likedReviewsMap); + setLikeStatuses(likeStatusesMap); + }, + }, + createAuthHeaders(token) + ); + }); + } + }); + }, []); + useEffect(() => { const checkIfSaved = async () => { try { @@ -306,9 +339,10 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { user={user} /> setCarouselOpen(false)} + startIndex={carouselStartIndex} + onClose={closePhotoCarousel} /> ); @@ -346,7 +380,7 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { color="secondary" variant="contained" disableElevation - onClick={() => setCarouselOpen(true)} + onClick={() => showPhotoCarousel()} > Show all photos @@ -444,7 +478,7 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { color="secondary" variant="contained" disableElevation - onClick={() => setCarouselOpen(true)} + onClick={() => showPhotoCarousel()} > Show all photos @@ -504,7 +538,7 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { <> {landlordData && ( - setCarouselOpen(true)} /> + showPhotoCarousel()} /> )} @@ -562,6 +596,7 @@ const LandlordPage = ({ user, setUser }: Props): ReactElement => { setToggle={setToggle} triggerEditToast={showEditSuccessConfirmationToast} triggerDeleteToast={showDeleteSuccessConfirmationToast} + triggerPhotoCarousel={showPhotoCarousel} user={user} setUser={setUser} showLabel={true} diff --git a/frontend/src/pages/LocationPage.tsx b/frontend/src/pages/LocationPage.tsx index 990e98c6..9dbe9f7f 100644 --- a/frontend/src/pages/LocationPage.tsx +++ b/frontend/src/pages/LocationPage.tsx @@ -19,6 +19,23 @@ type Props = { setUser: React.Dispatch>; }; +/** + * LocationPage Component – Displays information and apartments for a specific location + * (i.e. Collegetown, West, North, Downtown). + * + * @remarks + * The LocationPage displays the name and description of the location. It also fetches and + * displays apartment cards that are located in the specific location (e.g. all apartments + * in Collegetown). + * + * @param {firebase.User | null} props.user – The currently logged-in Firebase user or null + * if no user is logged in. + * @param {React.Dispatch>} props.setUser - A + * function to update the `user` state. + * + * @return {ReactElement} The JSX structure of the LocationPage component. + */ + const LocationPage = ({ user, setUser }: Props): ReactElement => { const [isMobile, setIsMobile] = useState(false); const [data, setData] = useState([]); diff --git a/frontend/src/pages/NotFoundPage.tsx b/frontend/src/pages/NotFoundPage.tsx index fe19c51b..25bee9b0 100644 --- a/frontend/src/pages/NotFoundPage.tsx +++ b/frontend/src/pages/NotFoundPage.tsx @@ -3,6 +3,15 @@ import React, { ReactElement } from 'react'; import NotFoundIcon from '../assets/not-found.svg'; import styles from './NotFoundPage.module.css'; +/** + * NotFoundPage Component – Displays a 404 error page when a requested route is not found. + * + * @remarks + * The NotFoundPage displays a message and an icon to indicate that the requested page does not exist. + * + * @return {ReactElement} – The JSX structure of the NotFoundPage. + */ + const NotFoundPage = (): ReactElement => { return ( diff --git a/frontend/src/pages/Policies.tsx b/frontend/src/pages/Policies.tsx index 094e0484..d3318bc9 100644 --- a/frontend/src/pages/Policies.tsx +++ b/frontend/src/pages/Policies.tsx @@ -41,6 +41,16 @@ const useStyles = makeStyles(() => ({ }, })); +/** + * Policies Component – Displays the privacy policy for CUAPTS. + * + * @remarks + * This component displays the privacy policy, describing data collection, usage, user rights etc. + * It adapts content styling based on the screen size. + * + * @return {ReactElement} – The rendered JSX structure of the Policies page. + */ + const Policies = (): ReactElement => { const { h2, h3, h4, body, link } = useStyles(); diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx index b02e7e53..4b91701d 100644 --- a/frontend/src/pages/ProfilePage.tsx +++ b/frontend/src/pages/ProfilePage.tsx @@ -21,6 +21,8 @@ import { createAuthHeaders, getUser } from '../utils/firebase'; import defaultProfilePic from '../assets/cuapts-bear.png'; import { useTitle } from '../utils'; import { sortReviews } from '../utils/sortReviews'; +import PhotoCarousel from '../components/PhotoCarousel/PhotoCarousel'; +import usePhotoCarousel from '../components/PhotoCarousel/usePhotoCarousel'; type Props = { user: firebase.User | null; @@ -167,6 +169,13 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { const [showEditSuccessConfirmation, setShowEditSuccessConfirmation] = useState(false); const [showDeleteSuccessConfirmation, setShowDeleteSuccessConfirmation] = useState(false); const toastTime = 3500; + const { + carouselPhotos, + carouselStartIndex, + carouselOpen, + showPhotoCarousel, + closePhotoCarousel, + } = usePhotoCarousel([]); useTitle('Profile'); @@ -182,6 +191,32 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { }); }, [user?.uid, toggle]); + // Fetch the reviews that the user has liked and set the liked reviews and like statuses. + useEffect(() => { + getUser(false).then((user) => { + if (user) { + user.getIdToken(true).then((token) => { + get( + `/api/review/like/${user.uid}`, + { + callback: (reviews) => { + const likedReviewsMap: Likes = {}; + const likeStatusesMap: Likes = {}; + reviews.forEach((review) => { + likedReviewsMap[review.id] = true; + likeStatusesMap[review.id] = false; + }); + setLikedReviews(likedReviewsMap); + setLikeStatuses(likeStatusesMap); + }, + }, + createAuthHeaders(token) + ); + }); + } + }); + }, []); + const likeHelper = (dislike = false) => { return async (reviewId: string) => { setLikeStatuses((reviews) => ({ ...reviews, [reviewId]: true })); @@ -199,7 +234,16 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { const token = await user.getIdToken(true); const endpoint = dislike ? '/api/remove-like' : '/api/add-like'; await axios.post(endpoint, { reviewId }, createAuthHeaders(token)); - setLikedReviews((reviews) => ({ ...reviews, [reviewId]: !dislike })); + setLikedReviews((reviews) => { + return { ...reviews, [reviewId]: !dislike }; + }); + setPendingReviews((reviews) => + reviews.map((review) => + review.id === reviewId + ? { ...review, likes: (review.likes || defaultLikes) + offsetLikes } + : review + ) + ); setApprovedReviews((reviews) => reviews.map((review) => review.id === reviewId @@ -261,6 +305,17 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { }; }, [isModalOpen, setIsModalOpen]); + const Modals = ( + <> + + + ); + return (
@@ -327,7 +382,7 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { {/* Maps list of pending reviews and calls Review Component with fields for each user*/} {sortReviews(pendingReviews, 'date').map((review, index) => ( - + { setToggle={setToggle} triggerEditToast={showEditSuccessConfirmationToast} triggerDeleteToast={showDeleteSuccessConfirmationToast} + triggerPhotoCarousel={showPhotoCarousel} user={user} setUser={setUser} showLabel={true} @@ -352,6 +408,7 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => { {sortReviews(approvedReviews, 'date').map((review, index) => ( { setToggle={setToggle} triggerEditToast={showEditSuccessConfirmationToast} triggerDeleteToast={showDeleteSuccessConfirmationToast} + triggerPhotoCarousel={showPhotoCarousel} user={user} setUser={setUser} showLabel={true} @@ -384,6 +442,7 @@ const ProfilePage = ({ user, setUser }: Props): ReactElement => {
)} + {Modals} ); }; diff --git a/frontend/src/pages/SearchResultsPage.tsx b/frontend/src/pages/SearchResultsPage.tsx index 4a60ce37..2f8fdb98 100644 --- a/frontend/src/pages/SearchResultsPage.tsx +++ b/frontend/src/pages/SearchResultsPage.tsx @@ -19,6 +19,21 @@ type Props = { setUser: React.Dispatch>; }; +/** + * SearchResultsPage Component – Displays search results with apartment cards based on a user's query. + * + * @remarks + * The page fetches the search results by calling a backend API. The resulting apartments and landlords + * are then listed and displayed. The page adjusts for mobile and desktop versions. + * + * @param {firebase.User | null} props.user – The currently logged-in Firebase user or null + * if no user is logged in. + * @param {React.Dispatch>} props.setUser - A + * function to update the `user` state. + * + * @return {ReactElement} – The rendered JSX structure of the SearchResultsPage. + */ + const SearchResultsPage = ({ user, setUser }: Props): ReactElement => { const { searchText } = useStyles(); const location = useLocation(); From b89c450f550937a19d43cc89812ea872700f7f0f Mon Sep 17 00:00:00 2001 From: CasperL1218 Date: Tue, 3 Dec 2024 21:02:47 -0500 Subject: [PATCH 9/9] Remove unused elements --- backend/src/app.ts | 17 ++++------------- frontend/src/components/Apartment/MapModal.tsx | 2 -- frontend/src/pages/ApartmentPage.tsx | 6 ------ 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 26c2cca7..2c5b1ac8 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -984,15 +984,6 @@ app.post('/api/add-contact-question', authenticate, async (req, res) => { } }); -interface TravelTimes { - agQuadWalking: number; - agQuadDriving: number; - engQuadWalking: number; - engQuadDriving: number; - hoPlazaWalking: number; - hoPlazaDriving: number; -} - const { REACT_APP_MAPS_API_KEY } = process.env; const LANDMARKS = { eng_quad: '42.4445,-76.4836', // Duffield Hall @@ -1072,7 +1063,7 @@ app.post('/api/calculate-travel-times', async (req, res) => { console.log('Raw walking times:', walkingTimes); console.log('Raw driving times:', drivingTimes); - const travelTimes: TravelTimes = { + const travelTimes: LocationTravelTimes = { engQuadWalking: walkingTimes[0], engQuadDriving: drivingTimes[0], agQuadWalking: walkingTimes[1], @@ -1117,7 +1108,7 @@ app.post('/api/test-travel-times/:buildingId', async (req, res) => { } // Calculate travel times using the main endpoint - const response = await axios.post(`http://localhost:3000/api/calculate-travel-times`, { + const response = await axios.post(`/api/calculate-travel-times`, { origin: `${buildingData.latitude},${buildingData.longitude}`, }); @@ -1183,7 +1174,7 @@ app.post('/api/batch-create-travel-times/:batchSize/:startAfter?', async (req, r return; } - const response = await axios.post(`http://localhost:3000/api/calculate-travel-times`, { + const response = await axios.post(`/api/calculate-travel-times`, { origin: `${buildingData.latitude},${buildingData.longitude}`, }); @@ -1221,7 +1212,7 @@ app.post('/api/batch-create-travel-times/:batchSize/:startAfter?', async (req, r * Looks up the travel times document for the given building ID and returns the stored walking and driving * times to Cornell landmarks: Engineering Quad, Agriculture Quad, and Ho Plaza. * - * @route GET /api/travel-times/:buildingId + * @route GET /api/travel-times-by-id/:buildingId * * @input {string} req.params.buildingId - ID of the building to get travel times for * diff --git a/frontend/src/components/Apartment/MapModal.tsx b/frontend/src/components/Apartment/MapModal.tsx index 1fadf907..0b3ed270 100644 --- a/frontend/src/components/Apartment/MapModal.tsx +++ b/frontend/src/components/Apartment/MapModal.tsx @@ -116,7 +116,6 @@ interface MapModalProps extends BaseProps { aptName: string; open: boolean; onClose: () => void; - setOpen: Dispatch>; } /** @@ -144,7 +143,6 @@ const MapModal = ({ aptName, open, onClose, - setOpen, address, latitude = 0, longitude = 0, diff --git a/frontend/src/pages/ApartmentPage.tsx b/frontend/src/pages/ApartmentPage.tsx index 443c89b8..26f0ec40 100644 --- a/frontend/src/pages/ApartmentPage.tsx +++ b/frontend/src/pages/ApartmentPage.tsx @@ -56,10 +56,6 @@ export type RatingInfo = { rating: number; }; -interface MapInfoRef { - recenter: () => void; -} - const useStyles = makeStyles((theme) => ({ reviewButton: { borderRadius: '30px', @@ -160,7 +156,6 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { const saved = savedIcon; const unsaved = unsavedIcon; const [isSaved, setIsSaved] = useState(false); - const mapInfoRef = useRef(null); const [mapToggle, setMapToggle] = useState(false); const dummyTravelTimes: LocationTravelTimes = { @@ -445,7 +440,6 @@ const ApartmentPage = ({ user, setUser }: Props): ReactElement => { aptName={apt!.name} open={mapOpen} onClose={handleMapModalClose} - setOpen={setMapOpen} address={apt!.address} longitude={apt!.longitude} latitude={apt!.latitude}