Skip to content

Commit bc63f97

Browse files
authored
Merge pull request #398 from cornell-dti/home-page-redesign-backend
Home Page and Search Result Page Re-Design
2 parents d725bd2 + c2bc24d commit bc63f97

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+3976
-521
lines changed

backend/scripts/add_buildings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type BuildingData = {
1212
area: string;
1313
latitude?: number;
1414
longitude?: number;
15+
distanceToCampus?: number;
1516
};
1617

1718
const getAreaType = (areaName: string): 'COLLEGETOWN' | 'WEST' | 'NORTH' | 'DOWNTOWN' | 'OTHER' => {
@@ -48,6 +49,8 @@ const formatBuilding = ({
4849
area: getAreaType(area),
4950
latitude,
5051
longitude,
52+
price: 0,
53+
distanceToCampus: 0,
5154
});
5255

5356
const makeBuilding = async (apartmentWithId: ApartmentWithId) => {

backend/src/app.ts

Lines changed: 299 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,21 @@ app.get('/api/review/:location/count', async (req, res) => {
239239
res.status(200).send(JSON.stringify({ count: approvedReviewCount }));
240240
});
241241

242-
// API endpoint to get apartments by a list of IDs
242+
/**
243+
* Get Multiple Apartments by IDs – Retrieves multiple apartments by their document IDs.
244+
*
245+
* @remarks
246+
* This endpoint accepts a comma-separated list of apartment IDs and returns the corresponding apartment data.
247+
* Each ID is validated to ensure it exists in the database before returning the data.
248+
*
249+
* @route GET /api/apts/:ids
250+
*
251+
* @input {string} req.params.ids - Comma-separated list of apartment document IDs
252+
*
253+
* @status
254+
* - 200: Successfully retrieved apartment data for all provided IDs
255+
* - 400: Error retrieving apartments or invalid ID provided
256+
*/
243257
app.get('/api/apts/:ids', async (req, res) => {
244258
try {
245259
const { ids } = req.params;
@@ -376,6 +390,21 @@ app.post('/api/set-data', async (req, res) => {
376390
}
377391
});
378392

393+
/**
394+
* Search Apartments and Landlords – Performs a fuzzy search across both apartments and landlords.
395+
*
396+
* @remarks
397+
* This endpoint searches through apartment and landlord names/addresses using fuzzy matching.
398+
* Returns up to 5 combined results, with each result labeled as either LANDLORD or APARTMENT.
399+
*
400+
* @route GET /api/search
401+
*
402+
* @input {string} req.query.q - The search query string to match against names and addresses
403+
*
404+
* @status
405+
* - 200: Successfully retrieved and labeled search results
406+
* - 400: Error occurred while processing the search request
407+
*/
379408
app.get('/api/search', async (req, res) => {
380409
try {
381410
const query = req.query.q as string;
@@ -402,6 +431,21 @@ app.get('/api/search', async (req, res) => {
402431
}
403432
});
404433

434+
/**
435+
* Search Results - Retrieves enriched apartment data based on a text search query.
436+
*
437+
* @remarks
438+
* This endpoint performs a fuzzy search on apartment names and addresses using the provided query string.
439+
* The matching results are enriched with additional data like reviews and ratings before being returned.
440+
*
441+
* @route GET /api/search-results
442+
*
443+
* @input {string} req.query.q - The search query string to match against apartment names and addresses
444+
*
445+
* @status
446+
* - 200: Successfully retrieved and enriched the matching apartment results
447+
* - 400: Error occurred while processing the search request
448+
*/
405449
app.get('/api/search-results', async (req, res) => {
406450
try {
407451
const query = req.query.q as string;
@@ -423,13 +467,140 @@ app.get('/api/search-results', async (req, res) => {
423467
}
424468
});
425469

426-
/*
427-
* assumption, what you return for a given page may be different
428-
* currently, we are assuming 'home' as a potential input and a default case for everything else for the page
429-
* for the home case, we are returning the top (size) apartments, top being having the most reviews
470+
/**
471+
* Search With Query And Filters - Searches apartments based on text query and multiple filter criteria.
472+
*
473+
* @remarks
474+
* This endpoint allows searching apartments using a combination of text search and filters.
475+
* The filters include location, price range, number of bedrooms and bathrooms.
476+
* Results are filtered progressively based on which parameters are provided.
477+
*
478+
* @route GET /api/search-with-query-and-filters
479+
*
480+
* @input {object} req.query - The search parameters
481+
* @input {string} req.query.q - Optional text search query
482+
* @input {string} req.query.locations - Optional comma-separated list of locations
483+
* @input {number} req.query.minPrice - Optional minimum price filter
484+
* @input {number} req.query.maxPrice - Optional maximum price filter
485+
* @input {number} req.query.bedrooms - Optional number of bedrooms filter
486+
* @input {number} req.query.bathrooms - Optional number of bathrooms filter
487+
*
488+
* @status
489+
* - 200: Successfully retrieved filtered search results
490+
* - 400: Error occurred while processing the search request
491+
*/
492+
493+
app.get('/api/search-with-query-and-filters', async (req, res) => {
494+
try {
495+
// Extract all query parameters
496+
const query = req.query.q as string;
497+
const locations = req.query.locations as string;
498+
const minPrice = req.query.minPrice ? parseInt(req.query.minPrice as string, 10) : null;
499+
const maxPrice = req.query.maxPrice ? parseInt(req.query.maxPrice as string, 10) : null;
500+
const bedrooms = req.query.bedrooms ? parseInt(req.query.bedrooms as string, 10) : null;
501+
const bathrooms = req.query.bathrooms ? parseInt(req.query.bathrooms as string, 10) : null;
502+
const size = req.query.size ? parseInt(req.query.size as string, 10) : null;
503+
const sortBy = req.query.sortBy || 'numReviews';
504+
505+
// Get all apartments from the application state
506+
const apts = req.app.get('apts');
507+
const aptsWithType: ApartmentWithId[] = apts;
508+
509+
// Start with text search if query is provided
510+
let filteredResults: ApartmentWithId[] = [];
511+
if (query && query.trim() !== '') {
512+
const options = {
513+
keys: ['name', 'address'],
514+
};
515+
const fuse = new Fuse(aptsWithType, options);
516+
const searchResults = fuse.search(query);
517+
filteredResults = searchResults.map((result) => result.item);
518+
} else {
519+
// If no query, start with all apartments
520+
filteredResults = aptsWithType;
521+
}
522+
523+
// Apply location filter if provided
524+
if (locations && locations.trim() !== '') {
525+
const locationArray = locations.split(',').map((loc) => loc.toUpperCase());
526+
filteredResults = filteredResults.filter((apt) =>
527+
locationArray.includes(apt.area ? apt.area.toUpperCase() : '')
528+
);
529+
}
530+
531+
// TODO: Right now we disable the price filter because of lack of data
532+
// Apply price range filters
533+
// if (minPrice !== null) {
534+
// filteredResults = filteredResults.filter((apt) => apt.price >= minPrice);
535+
// }
536+
537+
// if (maxPrice !== null) {
538+
// filteredResults = filteredResults.filter((apt) => apt.price <= maxPrice);
539+
// }
540+
541+
// Apply bedroom filter
542+
// TODO: Right now we disable the bedroom filter because of lack of data
543+
// if (bedrooms !== null && bedrooms > 0) {
544+
// filteredResults = filteredResults.filter(
545+
// (apt) => apt.numBeds !== null && apt.numBeds >= bedrooms
546+
// );
547+
// }
548+
549+
// // Apply bathroom filter
550+
// if (bathrooms !== null && bathrooms > 0) {
551+
// filteredResults = filteredResults.filter(
552+
// (apt) => apt.numBaths !== null && apt.numBaths >= bathrooms
553+
// );
554+
// }
555+
556+
// Process the filtered results through pageData to include reviews, ratings, etc.
557+
let enrichedResults = await pageData(filteredResults);
558+
559+
// If size is specified and less than available, slice the results
560+
if (size && size > 0 && enrichedResults.length > size) {
561+
console.log('endpoint sortBy', sortBy);
562+
switch (sortBy) {
563+
case 'numReviews':
564+
enrichedResults.sort((a, b) => b.numReviews - a.numReviews);
565+
break;
566+
case 'avgRating':
567+
enrichedResults.sort((a, b) => b.avgRating - a.avgRating);
568+
break;
569+
case 'distanceToCampus':
570+
enrichedResults.sort(
571+
(a, b) => a.buildingData.distanceToCampus - b.buildingData.distanceToCampus
572+
);
573+
break;
574+
default:
575+
break;
576+
}
577+
enrichedResults = enrichedResults.slice(0, size);
578+
}
579+
580+
res.status(200).send(JSON.stringify(enrichedResults));
581+
} catch (err) {
582+
console.error(err);
583+
res.status(400).send('Error');
584+
}
585+
});
586+
/**
587+
* Get Paginated Apartment Data – Retrieves paginated and sorted apartment listings.
588+
*
589+
* @remarks
590+
* Handles two different pagination strategies based on page type. For homepage, fetches all apartments and sorts them before paginating. For other pages, paginates directly from the database query. Enriches apartment data with reviews and ratings.
591+
*
592+
* @route GET /api/page-data/:page/:size/:sortBy?
593+
*
594+
* @input {string} req.params.page - Page type ('home' or other) determining pagination strategy
595+
* @input {string} req.params.size - Number of results to return per page
596+
* @input {string} req.params.sortBy - Optional sorting field ('numReviews', 'avgRating', 'distanceToCampus')
597+
*
598+
* @status
599+
* - 200: Successfully retrieved paginated apartment data
600+
* - 400: Error retrieving or processing apartment data
430601
*/
431-
app.get('/api/page-data/:page/:size', async (req, res) => {
432-
const { page, size } = req.params;
602+
app.get('/api/page-data/:page/:size/:sortBy?', async (req, res) => {
603+
const { page, size, sortBy = 'numReviews' } = req.params;
433604
let buildingDocs;
434605
let buildingData;
435606

@@ -445,9 +616,26 @@ app.get('/api/page-data/:page/:size', async (req, res) => {
445616
);
446617

447618
if (page === 'home') {
448-
const buildingReviewCounts = (await pageData(buildings)).map((elem) => [elem.numReviews, elem]);
449-
buildingReviewCounts.sort().reverse();
450-
buildingData = buildingReviewCounts.splice(0, Number(size)).map((elem) => elem[1]);
619+
// Get enriched data first
620+
const enrichedData = await pageData(buildings);
621+
622+
// Sort based on the specified field
623+
enrichedData.sort((a, b) => {
624+
// Handle fields that exist after pageData processing
625+
switch (sortBy) {
626+
case 'numReviews':
627+
return b.numReviews - a.numReviews;
628+
case 'avgRating':
629+
return b.avgRating - a.avgRating;
630+
case 'distanceToCampus':
631+
// Sort by distance to campus in ascending order (closer to campus comes first)
632+
return a.buildingData.distanceToCampus - b.buildingData.distanceToCampus;
633+
default:
634+
return 0;
635+
}
636+
});
637+
// Take only the requested size
638+
buildingData = enrichedData.slice(0, Number(size));
451639
} else {
452640
buildingData = await pageData(buildings);
453641
}
@@ -460,6 +648,20 @@ app.get('/api/page-data/:page/:size', async (req, res) => {
460648
res.status(200).send(returnData);
461649
});
462650

651+
/**
652+
* Get Apartments by Location - Retrieves all apartments from a specific location.
653+
*
654+
* @remarks
655+
* This endpoint fetches apartment data filtered by a single location parameter. The location is case-insensitive
656+
* and will be converted to uppercase before querying. Returns enriched apartment data including reviews and ratings.
657+
*
658+
* @route GET /api/location/:loc
659+
*
660+
* @input {string} req.params.loc - The location to filter apartments by (e.g. "Collegetown")
661+
*
662+
* @status
663+
* - 200: Successfully retrieved apartments for the specified location
664+
*/
463665
app.get('/api/location/:loc', async (req, res) => {
464666
const { loc } = req.params;
465667
const buildingDocs = (await buildingsCollection.where(`area`, '==', loc.toUpperCase()).get())
@@ -469,7 +671,51 @@ app.get('/api/location/:loc', async (req, res) => {
469671
);
470672

471673
const data = JSON.stringify(await pageData(buildings));
472-
res.status(200).send(data);
674+
return res.status(200).send(data);
675+
});
676+
677+
/**
678+
* Get Apartments by Multiple Locations – Retrieves apartments from multiple specified locations.
679+
*
680+
* @remarks
681+
* This endpoint allows fetching apartments from multiple locations in a single request. Locations are
682+
* passed as comma-separated values in the query string. The response includes apartment data with
683+
* reviews and ratings for each location.
684+
*
685+
* @route GET /api/locations?locations=[comma-separated locations]
686+
*
687+
* @input {string} req.query.locations - Comma-separated list of location names (e.g. "Collegetown,Downtown")
688+
*
689+
* @status
690+
* - 200: Successfully retrieved apartments for the specified locations
691+
* - 400: No locations specified in query parameter
692+
*/
693+
694+
app.get('/api/locations', async (req, res) => {
695+
const locations = req.query.locations as string;
696+
697+
if (!locations) {
698+
return res.status(400).send({ error: 'No locations specified' });
699+
}
700+
701+
const locationArray = locations.split(',');
702+
703+
// Create a query to fetch buildings from any of the specified locations
704+
let query = buildingsCollection.where('area', '==', locationArray[0].toUpperCase());
705+
706+
// If there are multiple locations, we need to use an 'in' query instead
707+
if (locationArray.length > 1) {
708+
const upperLocations = locationArray.map((loc) => loc.toUpperCase());
709+
query = buildingsCollection.where('area', 'in', upperLocations);
710+
}
711+
712+
const buildingDocs = (await query.get()).docs;
713+
const buildings: ApartmentWithId[] = buildingDocs.map(
714+
(doc) => ({ id: doc.id, ...doc.data() } as ApartmentWithId)
715+
);
716+
717+
const data = JSON.stringify(await pageData(buildings));
718+
return res.status(200).send(data);
473719
});
474720

475721
/**
@@ -763,7 +1009,7 @@ app.get('/api/saved-apartments', authenticate, async (req, res) => {
7631009
);
7641010

7651011
const data = JSON.stringify(await pageData(buildings)); // Preparing the data for response
766-
res.status(200).send(data); // Sending the data back to the client
1012+
return res.status(200).send(data);
7671013
});
7681014

7691015
// Endpoints for adding and removing saved landlords for a user
@@ -1445,5 +1691,46 @@ app.get('/api/travel-times-by-id/:buildingId', async (req, res) => {
14451691
return res.status(500).json({ error: 'Internal server error' });
14461692
}
14471693
});
1694+
/**
1695+
* Create Distance to Campus - Updates each building's distanceToCampus field with walking time to Ho Plaza.
1696+
*
1697+
* @remarks
1698+
* Queries the entire travel times collection and updates each building document in the buildings collection
1699+
* with its corresponding walking time to Ho Plaza. Returns an array of tuples containing the building ID
1700+
* and updated walking time.
1701+
*
1702+
* @route POST /api/create-distance-to-campus
1703+
*
1704+
* @returns {Array<[string, number]>} Array of [buildingId, walkingTime] tuples
1705+
*
1706+
* @status
1707+
* - 200: Successfully retrieved and updated walking times
1708+
* - 500: Server error occurred while retrieving times or updating buildings
1709+
*/
1710+
app.post('/api/create-distance-to-campus', async (req, res) => {
1711+
try {
1712+
const snapshot = await travelTimesCollection.get();
1713+
1714+
const walkingTimes = snapshot.docs.map((doc) => {
1715+
const data = doc.data() as LocationTravelTimes;
1716+
return [doc.id, data.hoPlazaWalking] as [string, number];
1717+
});
1718+
1719+
// Update each building's distanceToCampus field
1720+
await Promise.all(
1721+
walkingTimes.map(async ([buildingId, walkingTime]) => {
1722+
const buildingRef = buildingsCollection.doc(buildingId);
1723+
await buildingRef.update({
1724+
distanceToCampus: walkingTime,
1725+
});
1726+
})
1727+
);
1728+
1729+
return res.status(200).json(walkingTimes);
1730+
} catch (error) {
1731+
console.error('Error retrieving/updating Ho Plaza walking times:', error);
1732+
return res.status(500).json({ error: 'Internal server error' });
1733+
}
1734+
});
14481735

14491736
export default app;

common/types/db-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export type Apartment = {
6161
readonly area: 'COLLEGETOWN' | 'WEST' | 'NORTH' | 'DOWNTOWN' | 'OTHER';
6262
readonly latitude: number;
6363
readonly longitude: number;
64+
readonly price: number;
65+
readonly distanceToCampus: number; // walking distance to ho plaza in minutes
6466
};
6567

6668
export type LocationTravelTimes = {

0 commit comments

Comments
 (0)