@@ -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+ */
243257app . 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+ */
379408app . 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+ */
405449app . 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+ */
463665app . 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
14491736export default app ;
0 commit comments