From 3585c0b59b24a201277f23afd79a797934bf3471 Mon Sep 17 00:00:00 2001 From: Ben Petrillo Date: Tue, 3 Dec 2024 21:09:34 -0500 Subject: [PATCH] feat: venue search using wildcards (#84) --- backend/internal/handlers/venues/routes.go | 2 +- backend/internal/handlers/venues/venues.go | 4 +- backend/internal/storage/postgres/venues.go | 22 +++++-- backend/internal/storage/storage.go | 2 +- frontend/components/Map/SearchBar.tsx | 4 +- frontend/navigation/BottomNavigator.tsx | 3 + frontend/screens/HomeScreen.tsx | 23 ++++++- frontend/screens/explore/VenueCard.tsx | 73 +++++++++++++++++++++ frontend/screens/explore/VenueCardPage.tsx | 57 ++++++++++++++++ 9 files changed, 177 insertions(+), 13 deletions(-) create mode 100644 frontend/screens/explore/VenueCard.tsx create mode 100644 frontend/screens/explore/VenueCardPage.tsx diff --git a/backend/internal/handlers/venues/routes.go b/backend/internal/handlers/venues/routes.go index 9481b59..2a88c8c 100644 --- a/backend/internal/handlers/venues/routes.go +++ b/backend/internal/handlers/venues/routes.go @@ -20,7 +20,7 @@ func Routes(app *fiber.App, params types.Params) { protected.Get("/batch", service.GetVenuesByIDs) protected.Get("/persona/:venueId", service.GetVenuePersona) - protected.Get("/search", service.GetVenueFromName) + protected.Get("/search", service.GetVenuesFromName) protected.Get("/:venueId", service.GetVenueFromID) protected.Delete("/:venueId", service.DeleteVenue) diff --git a/backend/internal/handlers/venues/venues.go b/backend/internal/handlers/venues/venues.go index 4714140..a5cf1cc 100644 --- a/backend/internal/handlers/venues/venues.go +++ b/backend/internal/handlers/venues/venues.go @@ -130,12 +130,12 @@ func (s *Service) GetVenueFromID(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(venue) } -func (s *Service) GetVenueFromName(c *fiber.Ctx) error { +func (s *Service) GetVenuesFromName(c *fiber.Ctx) error { name := c.Query("q") if name == "" { return fiber.NewError(fiber.StatusBadRequest, "Venue name is required") } - venue, err := s.store.GetVenueFromName(c.Context(), name) + venue, err := s.store.GetVenuesFromName(c.Context(), name) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Could not get venue") } diff --git a/backend/internal/storage/postgres/venues.go b/backend/internal/storage/postgres/venues.go index 14f23ca..ace2fba 100644 --- a/backend/internal/storage/postgres/venues.go +++ b/backend/internal/storage/postgres/venues.go @@ -82,20 +82,28 @@ func (db *DB) GetVenueFromID(ctx context.Context, id uuid.UUID) (models.Venue, e return arr[0], err } -func (db *DB) GetVenueFromName(ctx context.Context, name string) (models.Venue, error) { +func (db *DB) GetVenuesFromName(ctx context.Context, name string) ([]models.Venue, error) { query := `SELECT venue_id, name, address, city, state, zip_code, created_at, venue_type, updated_at, price, total_rating, - avg_energy, avg_mainstream, avg_exclusive, avg_price, monday_hours, tuesday_hours, wednesday_hours, thursday_hours, friday_hours, - saturday_hours, sunday_hours, ST_Y(location::geometry) AS latitude, ST_X(location::geometry) AS longitude FROM Venue WHERE name ilike $1` + avg_energy, avg_mainstream, avg_exclusive, avg_price, monday_hours, tuesday_hours, wednesday_hours, thursday_hours, friday_hours, + saturday_hours, sunday_hours, ST_Y(location::geometry) AS latitude, ST_X(location::geometry) AS longitude + FROM Venue + WHERE name ILIKE $1 || '%';` + rows, err := db.conn.Query(ctx, query, name) if err != nil { - fmt.Println("HALLO " + err.Error()) - return models.Venue{}, err + fmt.Println("Error: " + err.Error()) + return nil, err } defer rows.Close() - arr, err := pgx.CollectRows(rows, pgx.RowToStructByName[models.Venue]) - return arr[0], err + venues, err := pgx.CollectRows(rows, pgx.RowToStructByName[models.Venue]) + if err != nil { + fmt.Println("Error collecting rows: " + err.Error()) + return nil, err + } + return venues, nil } + func (db *DB) GetAllVenues(ctx context.Context) ([]models.Venue, error) { query := `SELECT venue_id, name, address, city, state, zip_code, created_at, venue_type, updated_at, price, total_rating, avg_energy, avg_mainstream, avg_exclusive, avg_price, monday_hours, tuesday_hours, wednesday_hours, thursday_hours, friday_hours, diff --git a/backend/internal/storage/storage.go b/backend/internal/storage/storage.go index 1981d93..89ccf8d 100644 --- a/backend/internal/storage/storage.go +++ b/backend/internal/storage/storage.go @@ -49,7 +49,7 @@ type Venues interface { DeleteReviewForVenue(context.Context, int8) error GetAllVenueRatings(context.Context, uuid.UUID) ([]models.VenueRatings, error) GetVenueFromID(context.Context, uuid.UUID) (models.Venue, error) - GetVenueFromName(context.Context, string) (models.Venue, error) + GetVenuesFromName(context.Context, string) ([]models.Venue, error) GetAllVenues(ctx context.Context) ([]models.Venue, error) GetVenuesByIDs(ctx context.Context, ids []uuid.UUID) ([]models.Venue, error) } diff --git a/frontend/components/Map/SearchBar.tsx b/frontend/components/Map/SearchBar.tsx index 007eaf7..072b864 100644 --- a/frontend/components/Map/SearchBar.tsx +++ b/frontend/components/Map/SearchBar.tsx @@ -5,9 +5,10 @@ import { MaterialCommunityIcons } from "@expo/vector-icons"; type SearchBarProps = { placeholderText: string; icon?: boolean + onSubmitEditing?: (text: string) => void; } -const SearchBar = ({ placeholderText, icon }: SearchBarProps) => { +const SearchBar = ({ placeholderText, icon, onSubmitEditing }: SearchBarProps) => { const [searchText, setSearchText] = React.useState(""); return ( @@ -24,6 +25,7 @@ const SearchBar = ({ placeholderText, icon }: SearchBarProps) => { placeholderTextColor="#aaa" value={searchText} onChangeText={setSearchText} + onSubmitEditing={() => onSubmitEditing && onSubmitEditing(searchText)} /> ); diff --git a/frontend/navigation/BottomNavigator.tsx b/frontend/navigation/BottomNavigator.tsx index 132082c..794fb70 100644 --- a/frontend/navigation/BottomNavigator.tsx +++ b/frontend/navigation/BottomNavigator.tsx @@ -16,6 +16,7 @@ import RatingScreen from "@/screens/venue/RatingScreen"; import VenueReviews from "@/screens/venue/VenueReviews"; import RateReviewScreen from "@/screens/venue/RateReviewScreen"; import MapScreen from "@/screens/MapScreen"; +import VenueCardPage from "@/screens/explore/VenueCardPage"; const Tab = createBottomTabNavigator(); @@ -53,6 +54,7 @@ type RootStackParamList = { EditProfile: undefined; EditProfileAttribute: { field: string; existing: string }; Venue: undefined; + VenueCards: undefined; }; const createScreenOptions = ( @@ -79,6 +81,7 @@ const HomeStackNavigator = () => ( + ); diff --git a/frontend/screens/HomeScreen.tsx b/frontend/screens/HomeScreen.tsx index 403f2a5..c45b78a 100644 --- a/frontend/screens/HomeScreen.tsx +++ b/frontend/screens/HomeScreen.tsx @@ -3,10 +3,31 @@ import { ScrollView, View, StyleSheet } from "react-native"; import SearchBar from "@/components/Map/SearchBar"; import EventsScrollable from "./explore/EventsScrollable"; +import { API_DOMAIN } from "@env"; +import { useNavigation } from "@react-navigation/native"; + const HomeScreen: React.FC = () => { + + const navigation = useNavigation(); + + const handleSearch = async (text: string) => { + + + const req = await fetch(`${API_DOMAIN}/venues/search?q=${encodeURIComponent(text)}`); + + if (!req.ok) { + console.error("Failed to search for venues"); + return; + } + + const res = await req.json(); + + navigation.navigate("VenueCards", { venues: res }); + } + return ( - + diff --git a/frontend/screens/explore/VenueCard.tsx b/frontend/screens/explore/VenueCard.tsx new file mode 100644 index 0000000..ffa9605 --- /dev/null +++ b/frontend/screens/explore/VenueCard.tsx @@ -0,0 +1,73 @@ +import { useNavigation } from "@react-navigation/native"; +import React from "react"; +import { TouchableOpacity } from "react-native"; +import { View, Text, Image, StyleSheet } from "react-native"; + +export type VenuePreview = { + venue_id: string; + name: string; + address: string; + city: string; + state: string; + zip_code: string; + venue_type: string; + total_rating: number; + price: number; +} + +type EventCardProps = { + venue_preview: VenuePreview; +}; + +export const VenueCard = ({ venue_preview }: EventCardProps) => { + + const navigation = useNavigation(); + const venue_id = venue_preview.venue_id; + + return ( + navigation.navigate("Venue", { venue_id })}> + + + + {venue_preview.name} + {venue_preview.address} + {venue_preview.city}, {venue_preview.state} {venue_preview.zip_code} + {venue_preview.venue_type} Rating: {venue_preview.total_rating} Price: {venue_preview.price} + + + + ); +}; + +const styles = StyleSheet.create({ + card: { + backgroundColor: "#2d2d44", + borderRadius: 10, + overflow: "hidden", + marginRight: 10, + width: "auto", + height: "auto", + borderWidth: 2, + borderColor: "#735ad1", + marginVertical: 6, + padding: 0 + }, + image: { + width: "100%", + aspectRatio: 16 / 9, + }, + cardContent: { + padding: 10, + height: "auto" + }, + eventTitle: { + fontSize: 14, + fontWeight: "bold", + color: "#fff", + }, + eventDateTime: { + fontSize: 14, + color: "#ffffff", + marginTop: 5, + }, +}); diff --git a/frontend/screens/explore/VenueCardPage.tsx b/frontend/screens/explore/VenueCardPage.tsx new file mode 100644 index 0000000..1922c02 --- /dev/null +++ b/frontend/screens/explore/VenueCardPage.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { FlatList, StyleSheet, View, Text, TouchableOpacity } from "react-native"; +import { VenueCard, VenuePreview } from "@/screens/explore/VenueCard"; +import { useNavigation, useRoute } from "@react-navigation/native"; + +type VenueCardPageProps = { + venues: VenuePreview[]; +}; + +const VenueCardPage: React.FC = () => { + + const route = useRoute(); + + const venues: VenuePreview[] = route.params?.venues || []; + + console.log(venues); + + const navigation = useNavigation(); + + return ( + + navigation.goBack()} style={{ display: "flex", borderColor: "gray", borderWidth: 2, borderRadius: 6, marginBottom: 4, marginHorizontal: 12}}> + Back + + } + keyExtractor={(item) => item.venue_id} + horizontal={false} + showsHorizontalScrollIndicator={false} + contentContainerStyle={styles.listContent} + style={{ flex: 1, marginTop: 10 }} + /> + + ); +}; + +const styles = StyleSheet.create({ + title: { + fontSize: 24, + color: "#fff", + fontFamily: "Archivo_700Bold", + marginLeft: 10, + marginBottom: 10, + padding: 10 + }, + container: { + flex: 1, + backgroundColor: "#1c1c1e", + paddingVertical: 10, + }, + listContent: { + paddingHorizontal: 10, + }, +}); + +export default VenueCardPage;