diff --git a/backend/internal/handlers/profiles/profiles.go b/backend/internal/handlers/profiles/profiles.go index d0aa4bc..d6eca92 100644 --- a/backend/internal/handlers/profiles/profiles.go +++ b/backend/internal/handlers/profiles/profiles.go @@ -287,3 +287,26 @@ func (s *Service) GetUserVisitedVenues(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(venues) } + +func (s *Service) GetUserLocation(c *fiber.Ctx) error { + userID := c.Params("userId") + if userID == "" { + return fiber.NewError(fiber.StatusBadRequest, "User ID is required") + } + + userUUID, err := uuid.Parse(userID) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid User ID format") + } + + // Fetch latitude and longitude from the store + location, err := s.store.GetUserLocation(c.Context(), userUUID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch user location") + } + + // Return the latitude and longitude + return c.Status(fiber.StatusOK).JSON(location) +} + + diff --git a/backend/internal/handlers/profiles/routes.go b/backend/internal/handlers/profiles/routes.go index e462180..addb6b2 100644 --- a/backend/internal/handlers/profiles/routes.go +++ b/backend/internal/handlers/profiles/routes.go @@ -27,5 +27,6 @@ func Routes(app *fiber.App, params types.Params) { protected.Get("/reviewed-venues/:userId", service.GetUserReviewsWithVenueData) protected.Get("/saved-venues/:userId", service.GetUserSavedVenues) protected.Get("/visited-venues/:userId", service.GetUserVisitedVenues) + protected.Get("/:userId/location", service.GetUserLocation) } diff --git a/backend/internal/handlers/venues/routes.go b/backend/internal/handlers/venues/routes.go index 2a88c8c..f929994 100644 --- a/backend/internal/handlers/venues/routes.go +++ b/backend/internal/handlers/venues/routes.go @@ -18,6 +18,7 @@ func Routes(app *fiber.App, params types.Params) { //Endpoints protected.Get("/", service.GetAllVenuesWithFilter) protected.Get("/batch", service.GetVenuesByIDs) + protected.Get("/location", service.GetVenuesByLocation) protected.Get("/persona/:venueId", service.GetVenuePersona) protected.Get("/search", service.GetVenuesFromName) diff --git a/backend/internal/handlers/venues/venues.go b/backend/internal/handlers/venues/venues.go index a5cf1cc..8c22b96 100644 --- a/backend/internal/handlers/venues/venues.go +++ b/backend/internal/handlers/venues/venues.go @@ -251,3 +251,37 @@ func (s *Service) GetVenuesByIDs(c *fiber.Ctx) error { // Return the list of venues return c.Status(fiber.StatusOK).JSON(venues) } + +func (s *Service) GetVenuesByLocation(c *fiber.Ctx) error { + // Parse latitude + latitude := c.QueryFloat("latitude") + fmt.Print(latitude) + if latitude == 0 { + log.Printf("Invalid or missing latitude parameter: %v", latitude) + return fiber.NewError(fiber.StatusBadRequest, "Invalid or missing latitude parameter") + } + + // Parse longitude + longitude := c.QueryFloat("longitude") + if longitude == 0 { + log.Printf("Invalid or missing longitude parameter: %v", longitude) + return fiber.NewError(fiber.StatusBadRequest, "Invalid or missing longitude parameter") + } + + // Parse radius with default value of 1000 + radius := c.QueryInt("radius", 1000) + if radius <= 0 { + log.Printf("Invalid radius parameter: %d", radius) + return fiber.NewError(fiber.StatusBadRequest, "Invalid radius parameter") + } + + // Fetch venues by location + venues, err := s.store.GetVenuesByLocation(c.Context(), latitude, longitude, radius) + if err != nil { + log.Printf("Error fetching venues by location: %v | Latitude: %f | Longitude: %f | Radius: %d", err, latitude, longitude, radius) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch venues by location") + } + + // Return the list of venues as JSON + return c.Status(fiber.StatusOK).JSON(venues) +} diff --git a/backend/internal/models/location.go b/backend/internal/models/location.go new file mode 100644 index 0000000..b709f50 --- /dev/null +++ b/backend/internal/models/location.go @@ -0,0 +1,6 @@ +package models + +type Location struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} diff --git a/backend/internal/storage/postgres/profiles.go b/backend/internal/storage/postgres/profiles.go index cc35f96..16ea5ca 100644 --- a/backend/internal/storage/postgres/profiles.go +++ b/backend/internal/storage/postgres/profiles.go @@ -440,3 +440,24 @@ func (db *DB) GetUserVisitedVenues(ctx context.Context, userID uuid.UUID) ([]mod return venues, rows.Err() } + +func (db *DB) GetUserLocation(ctx context.Context, userID uuid.UUID) (models.Location, error) { + query := ` + SELECT + ST_Y(location::geometry) AS latitude, + ST_X(location::geometry) AS longitude + FROM users + WHERE user_id = $1 + ` + + var location models.Location + row := db.conn.QueryRow(ctx, query, userID) + if err := row.Scan(&location.Latitude, &location.Longitude); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return models.Location{}, fmt.Errorf("no location found for user_id: %s", userID) + } + return models.Location{}, err + } + + return location, nil +} diff --git a/backend/internal/storage/postgres/venues.go b/backend/internal/storage/postgres/venues.go index ace2fba..8bb5ad4 100644 --- a/backend/internal/storage/postgres/venues.go +++ b/backend/internal/storage/postgres/venues.go @@ -110,6 +110,7 @@ func (db *DB) GetAllVenues(ctx context.Context) ([]models.Venue, error) { saturday_hours, sunday_hours, ST_Y(location::geometry) AS latitude, ST_X(location::geometry) AS longitude FROM venue` rows, err := db.conn.Query(ctx, query) if err != nil { + fmt.Print("hello") return []models.Venue{}, err } defer rows.Close() @@ -183,6 +184,49 @@ func (db *DB) GetVenuesByIDs(ctx context.Context, venueIDs []uuid.UUID) ([]model return venues, rows.Err() } +func (db *DB) GetVenuesByLocation(ctx context.Context, latitude float64, longitude float64, radiusInMeters int) ([]models.Venue, error) { + query := ` + SELECT + venue_id, + name, + address, + city, + state, + zip_code, + ST_Y(location::geometry) AS latitude, + ST_X(location::geometry) AS longitude, + venue_type, + total_rating, + price, + avg_mainstream, + avg_price, + avg_exclusive, + avg_energy, + monday_hours, + tuesday_hours, + wednesday_hours, + thursday_hours, + friday_hours, + saturday_hours, + sunday_hours, + created_at, + COALESCE(updated_at, '9999-12-31 23:59:59') AS updated_at + FROM venue + WHERE ST_DWithin( + location::geography, + ST_MakePoint($1, $2)::geography, $3) limit 20 + ` + + rows, err := db.conn.Query(ctx, query, latitude, longitude, radiusInMeters) + if err != nil { + log.Printf("Database query failed: %v | Query: %s | Params: longitude=%f, latitude=%f, radius=%d", err, query, longitude, latitude, radiusInMeters) + return nil, fmt.Errorf("database query error: %w", err) + } + defer rows.Close() + arr, err := pgx.CollectRows(rows, pgx.RowToStructByName[models.Venue]) + return arr, err +} + func (db *DB) GetAllVenuesWithFilter(ctx context.Context, where string, sort 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, diff --git a/backend/internal/storage/storage.go b/backend/internal/storage/storage.go index 89ccf8d..975b680 100644 --- a/backend/internal/storage/storage.go +++ b/backend/internal/storage/storage.go @@ -37,6 +37,7 @@ type Profile interface { GetUserReviewsWithVenueData(ctx context.Context, userID uuid.UUID) ([]models.ReviewWithVenue, error) GetUserSavedVenues(context.Context, uuid.UUID) ([]models.Venue, error) GetUserVisitedVenues(context.Context, uuid.UUID) ([]models.Venue, error) + GetUserLocation(ctx context.Context, userID uuid.UUID) (models.Location, error) } type UserRating interface { @@ -52,6 +53,7 @@ type Venues interface { GetVenuesFromName(context.Context, string) ([]models.Venue, error) GetAllVenues(ctx context.Context) ([]models.Venue, error) GetVenuesByIDs(ctx context.Context, ids []uuid.UUID) ([]models.Venue, error) + GetVenuesByLocation(ctx context.Context, latitude float64, longitude float64, radiusInMeters int) ([]models.Venue, error) } type VenueRatings interface { diff --git a/frontend/assets/custom-marker.png b/frontend/assets/custom-marker.png new file mode 100644 index 0000000..f668193 Binary files /dev/null and b/frontend/assets/custom-marker.png differ diff --git a/frontend/assets/location.svg b/frontend/assets/location.svg new file mode 100644 index 0000000..1ce6c94 --- /dev/null +++ b/frontend/assets/location.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/rating-star.png b/frontend/assets/rating-star.png new file mode 100644 index 0000000..44bea00 Binary files /dev/null and b/frontend/assets/rating-star.png differ diff --git a/frontend/components/Map/BottomModal.tsx b/frontend/components/Map/BottomModal.tsx index ab167f3..3776b22 100644 --- a/frontend/components/Map/BottomModal.tsx +++ b/frontend/components/Map/BottomModal.tsx @@ -1,31 +1,15 @@ import React from "react"; -import { - View, - Text, - StyleSheet, - Modal, - TouchableWithoutFeedback, - TouchableOpacity, - Dimensions, - FlatList, -} from "react-native"; -import { useNavigation } from "@react-navigation/native"; -import { BottomTabNavProps } from "@/types/NavigationTypes"; - -const { height: screenHeight } = Dimensions.get("window"); +import { View, Text, StyleSheet, Modal, TouchableWithoutFeedback } from "react-native"; +import { Venue } from "@/types/Venue"; interface BottomModalProps { visible: boolean; onClose: () => void; - venues: Array<{ venue_id: string; name: string; address: string }>; + venue: Venue | null; } -const BottomModal: React.FC = ({ - visible, - onClose, - venues, -}) => { - const navigation = useNavigation(); +const BottomModal: React.FC = ({ visible, onClose, venue }) => { + if (!venue) return null; return ( = ({ - Happening Today - - item.venue_id} - renderItem={({ item }) => ( - { - onClose(); - navigation.navigate("Venue", { venue: item }); - }} - > - - {item.name} - {item.address} - - - )} - showsVerticalScrollIndicator={false} - contentContainerStyle={styles.listContent} - /> + {venue.name} + + {venue.address} + + {/* ⭐ {venue.rating} | 💲{venue.price} */} + {/* + {venue.isOpen ? "Open Now" : "Closed"} + */} @@ -69,15 +40,14 @@ const BottomModal: React.FC = ({ const styles = StyleSheet.create({ overlay: { flex: 1, - backgroundColor: "rgba(0, 0, 0, 0.4)", + backgroundColor: "rgba(0, 0, 0, 0.5)", justifyContent: "flex-end", }, modalContainer: { backgroundColor: "#1c1c1e", - padding: 16, + padding: 20, borderTopLeftRadius: 16, borderTopRightRadius: 16, - height: screenHeight * 0.5, }, tabIndicator: { width: 40, @@ -87,32 +57,24 @@ const styles = StyleSheet.create({ marginBottom: 8, borderRadius: 2, }, - sectionTitle: { - color: "#fff", + venueName: { fontSize: 18, fontWeight: "bold", - marginBottom: 8, - fontFamily: "Archivo_700Bold" - }, - listContent: { - paddingBottom: 20, + color: "#fff", }, - venueItem: { - backgroundColor: "#333", - padding: 10, - borderRadius: 8, - marginBottom: 10, + venueDetails: { + fontSize: 14, + color: "#bbb", + marginVertical: 4, }, - venueName: { + rating: { + fontSize: 14, color: "#fff", - fontSize: 16, - fontWeight: "bold", - fontFamily: "Archivo_700Bold" + marginVertical: 4, }, - venueAddress: { - color: "#bbb", + status: { fontSize: 14, - fontFamily: "Archivo_500Medium" + // color: venue.isOpen ? "#4caf50" : "#f44336", }, }); diff --git a/frontend/components/Map/SearchBar.tsx b/frontend/components/Map/SearchBar.tsx index 072b864..0baaaff 100644 --- a/frontend/components/Map/SearchBar.tsx +++ b/frontend/components/Map/SearchBar.tsx @@ -35,28 +35,30 @@ const styles = StyleSheet.create({ searchContainer: { flexDirection: "row", alignItems: "center", - padding: 10, - backgroundColor: "#1c1c1c", - width: "100%", position: "absolute", - top: 0, + top: 20, + left: 20, + right: 20, zIndex: 1, - // borderBottomWidth: 1, - // borderBottomColor: "#555", + backgroundColor: "#333", + borderRadius: 20, + padding: 10, + shadowColor: "#000", + shadowOpacity: 0.2, + shadowOffset: { width: 0, height: 3 }, + shadowRadius: 5, + elevation: 5, }, searchIcon: { - paddingRight: 10, + marginRight: 10, }, searchInput: { flex: 1, height: 40, - borderColor: "#555", - borderWidth: 1, - borderRadius: 10, - paddingLeft: 10, color: "#fff", - backgroundColor: "#1e1e1e", - fontFamily: "Archivo_500Medium" + backgroundColor: "#444", + borderRadius: 10, + paddingHorizontal: 10, }, }); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1ee5fa5..e2c94d6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2720,126 +2720,6 @@ "darwin" ] }, - "node_modules/@expo/ngrok-bin-darwin-x64": { - "version": "2.3.41", - "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-darwin-x64/-/ngrok-bin-darwin-x64-2.3.41.tgz", - "integrity": "sha512-29QZHfX4Ec0p0pQF5UrqiP2/Qe7t2rI96o+5b8045VCEl9AEAKHceGuyo+jfUDR4FSQBGFLSDb06xy8ghL3ZYA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@expo/ngrok-bin-freebsd-ia32": { - "version": "2.3.41", - "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-freebsd-ia32/-/ngrok-bin-freebsd-ia32-2.3.41.tgz", - "integrity": "sha512-YYXgwNZ+p0aIrwgb+1/RxJbsWhGEzBDBhZulKg1VB7tKDAd2C8uGnbK1rOCuZy013iOUsJDXaj9U5QKc13iIXw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@expo/ngrok-bin-freebsd-x64": { - "version": "2.3.41", - "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-freebsd-x64/-/ngrok-bin-freebsd-x64-2.3.41.tgz", - "integrity": "sha512-1Ei6K8BB+3etmmBT0tXYC4dyVkJMigT4ELbRTF5jKfw1pblqeXM9Qpf3p8851PTlH142S3bockCeO39rSkOnkg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@expo/ngrok-bin-linux-arm": { - "version": "2.3.41", - "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-linux-arm/-/ngrok-bin-linux-arm-2.3.41.tgz", - "integrity": "sha512-B6+rW/+tEi7ZrKWQGkRzlwmKo7c1WJhNODFBSgkF/Sj9PmmNhBz67mer91S2+6nNt5pfcwLLd61CjtWfR1LUHQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@expo/ngrok-bin-linux-arm64": { - "version": "2.3.41", - "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-linux-arm64/-/ngrok-bin-linux-arm64-2.3.41.tgz", - "integrity": "sha512-eC8GA/xPcmQJy4h+g2FlkuQB3lf5DjITy8Y6GyydmPYMByjUYAGEXe0brOcP893aalAzRqbNOAjSuAw1lcCLSQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@expo/ngrok-bin-linux-ia32": { - "version": "2.3.41", - "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-linux-ia32/-/ngrok-bin-linux-ia32-2.3.41.tgz", - "integrity": "sha512-w5Cy31wSz4jYnygEHS7eRizR1yt8s9TX6kHlkjzayIiRTFRb2E1qD2l0/4T2w0LJpBjM5ZFPaaKqsNWgCUIEow==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@expo/ngrok-bin-linux-x64": { - "version": "2.3.41", - "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-linux-x64/-/ngrok-bin-linux-x64-2.3.41.tgz", - "integrity": "sha512-LcU3MbYHv7Sn2eFz8Yzo2rXduufOvX1/hILSirwCkH+9G8PYzpwp2TeGqVWuO+EmvtBe6NEYwgdQjJjN6I4L1A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@expo/ngrok-bin-sunos-x64": { - "version": "2.3.41", - "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-sunos-x64/-/ngrok-bin-sunos-x64-2.3.41.tgz", - "integrity": "sha512-bcOj45BLhiV2PayNmLmEVZlFMhEiiGpOr36BXC0XSL+cHUZHd6uNaS28AaZdz95lrRzGpeb0hAF8cuJjo6nq4g==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ] - }, - "node_modules/@expo/ngrok-bin-win32-ia32": { - "version": "2.3.41", - "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-win32-ia32/-/ngrok-bin-win32-ia32-2.3.41.tgz", - "integrity": "sha512-0+vPbKvUA+a9ERgiAknmZCiWA3AnM5c6beI+51LqmjKEM4iAAlDmfXNJ89aAbvZMUtBNwEPHzJHnaM4s2SeBhA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@expo/ngrok-bin-win32-x64": { - "version": "2.3.41", - "resolved": "https://registry.npmjs.org/@expo/ngrok-bin-win32-x64/-/ngrok-bin-win32-x64-2.3.41.tgz", - "integrity": "sha512-mncsPRaG462LiYrM8mQT8OYe3/i44m3N/NzUeieYpGi8+pCOo8TIC23kR9P93CVkbM9mmXsy3X6hq91a8FWBdA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@expo/osascript": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.1.4.tgz", @@ -2857,16 +2737,16 @@ "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.6.1.tgz", "integrity": "sha512-4rT46wP/94Ll+CWXtFKok1Lbo9XncSUtErFOo/9/3FVughGbIfdG4SKZOAWIpr9wxwEfkyhHfAP9q71ONlWODw==", "dependencies": { - "@expo/json-file": "^9.0.0", + "@expo/json-file": "^8.3.0", "@expo/spawn-async": "^1.7.2", "ansi-regex": "^5.0.0", "chalk": "^4.0.0", "find-up": "^5.0.0", + "find-yarn-workspace-root": "~2.0.0", "js-yaml": "^3.13.1", - "micromatch": "^4.0.8", - "npm-package-arg": "^11.0.0", + "micromatch": "^4.0.2", + "npm-package-arg": "^7.0.0", "ora": "^3.4.0", - "resolve-workspace-root": "^2.0.0", "split": "^1.0.1", "sudo-prompt": "9.1.1" } @@ -4570,9 +4450,6 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -4670,11 +4547,6 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } } }, "node_modules/@typescript-eslint/visitor-keys": { @@ -4693,6 +4565,17 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@urql/core": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/@urql/core/-/core-5.0.8.tgz", @@ -5120,7 +5003,7 @@ "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.3", + "@babel/helper-define-polyfill-provider": "^0.6.2", "semver": "^6.3.1" }, "peerDependencies": { @@ -5144,7 +5027,7 @@ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3" + "@babel/helper-define-polyfill-provider": "^0.6.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -6504,7 +6387,7 @@ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", "dependencies": { - "dotenv": "^16.4.5" + "dotenv": "^16.4.4" }, "engines": { "node": ">=12" @@ -6609,7 +6492,7 @@ "function.prototype.name": "^1.1.6", "get-intrinsic": "^1.2.4", "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.4", + "globalthis": "^1.0.3", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2", "has-proto": "^1.0.3", @@ -6625,10 +6508,10 @@ "is-string": "^1.0.7", "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.1", "object-keys": "^1.1.1", "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.2", "safe-array-concat": "^1.1.2", "safe-regex-test": "^1.0.3", "string.prototype.trim": "^1.2.9", @@ -6779,12 +6662,12 @@ "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.5", + "cross-spawn": "^7.0.2", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", @@ -6803,7 +6686,8 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3" + "optionator": "^0.9.3", + "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" @@ -10817,9 +10701,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.4.48", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.48.tgz", + "integrity": "sha512-GCRK8F6+Dl7xYniR5a4FYbpBzU8XnZVeowqsQFYdcXuSbChgiks7qybSkbvnaeqv0G0B+dd9/jJgH8kkLDQeEA==", "funding": [ { "type": "opencollective", @@ -11207,6 +11091,14 @@ } } }, + "node_modules/react-native-animatable": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.3.3.tgz", + "integrity": "sha512-2ckIxZQAsvWn25Ho+DK3d1mXIgj7tITkrS4pYDvx96WyOttSvzzFeQnM2od0+FUMzILbdHDsDEqZvnz1DYNQ1w==", + "dependencies": { + "prop-types": "^15.7.2" + } + }, "node_modules/react-native-dotenv": { "version": "3.4.11", "resolved": "https://registry.npmjs.org/react-native-dotenv/-/react-native-dotenv-3.4.11.tgz", @@ -11283,6 +11175,29 @@ } } }, + "node_modules/react-native-modal": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-13.0.1.tgz", + "integrity": "sha512-UB+mjmUtf+miaG/sDhOikRfBOv0gJdBU2ZE1HtFWp6UixW9jCk/bhGdHUgmZljbPpp0RaO/6YiMmQSSK3kkMaw==", + "dependencies": { + "prop-types": "^15.6.2", + "react-native-animatable": "1.3.3" + }, + "peerDependencies": { + "react": "*", + "react-native": ">=0.65.0" + } + }, + "node_modules/react-native-modalize": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/react-native-modalize/-/react-native-modalize-2.1.1.tgz", + "integrity": "sha512-4/7EZWsrUqAAkkAVEnOsSdpAPQaEBewX7TvwFuzgvGDzxKpq3O58I9SnSeU8QtG/r91XYHJNaU5dAuDrcLjUaQ==", + "peerDependencies": { + "react": "> 15.0.0", + "react-native": "> 0.50.0", + "react-native-gesture-handler": "> 1.0.0" + } + }, "node_modules/react-native-reanimated": { "version": "3.16.3", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.16.3.tgz", @@ -11692,7 +11607,7 @@ "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.0", "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", + "regjsparser": "^0.11.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" }, @@ -13203,8 +13118,7 @@ "for-each": "^0.3.3", "gopd": "^1.0.1", "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "reflect.getprototypeof": "^1.0.6" + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -13260,9 +13174,6 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true diff --git a/frontend/screens/HomeScreen.tsx b/frontend/screens/HomeScreen.tsx index c45b78a..0fdb17a 100644 --- a/frontend/screens/HomeScreen.tsx +++ b/frontend/screens/HomeScreen.tsx @@ -6,41 +6,44 @@ import EventsScrollable from "./explore/EventsScrollable"; import { API_DOMAIN } from "@env"; import { useNavigation } from "@react-navigation/native"; -const HomeScreen: React.FC = () => { +interface HomeScreenProps { + showSearchBar?: boolean; +} - const navigation = useNavigation(); +const HomeScreen: React.FC = ({ showSearchBar = true }) => { + 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 }); + } - 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 ( - - - - - - - - - - - - - + return ( + + {showSearchBar && ( + + - ); + )} + + + + + + + + + + + + + ); }; const styles = StyleSheet.create({ diff --git a/frontend/screens/MapScreen.tsx b/frontend/screens/MapScreen.tsx index 07ca513..200b571 100644 --- a/frontend/screens/MapScreen.tsx +++ b/frontend/screens/MapScreen.tsx @@ -1,99 +1,244 @@ import { useState, useEffect } from "react"; import { View, StyleSheet, TouchableOpacity, Text } from "react-native"; import MapView, { Marker } from "react-native-maps"; +import { Modalize } from "react-native-modalize"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; import SearchBar from "@/components/Map/SearchBar"; -import BottomModal from "@/components/Map/BottomModal"; +import { Image } from "react-native"; import { API_DOMAIN } from "@env"; import { Venue } from "@/types/Venue"; import { useAuth } from "@/context/AuthContext"; import React from "react"; +import HomeScreen from "./HomeScreen"; +import EventCard from "./explore/EventCard"; +import CustomMarkerImage from "@/assets/custom-marker.png"; +import RatingStarImage from "@/assets/rating-star.png"; const MapScreen: React.FC = () => { const [allVenues, setAllVenues] = useState([]); - const [isModalVisible, setModalVisible] = useState(false); - const { accessToken } = useAuth(); + const [selectedVenue, setSelectedVenue] = useState(null); + const [mapKey, setMapKey] = useState(0); + const { user, accessToken } = useAuth(); + const modalRef = React.useRef(null); - const mapCustomStyle = [ { "elementType": "geometry", "stylers": [ { "color": "#242f3e" } ] }, { "elementType": "labels.text.fill", "stylers": [ { "color": "#746855" } ] }, { "elementType": "labels.text.stroke", "stylers": [ { "color": "#242f3e" } ] }, { "featureType": "administrative.locality", "elementType": "labels.text.fill", "stylers": [ { "color": "#d59563" } ] }, { "featureType": "poi", "elementType": "labels.text.fill", "stylers": [ { "color": "#d59563" } ] }, { "featureType": "poi.park", "elementType": "geometry", "stylers": [ { "color": "#263c3f" } ] }, { "featureType": "poi.park", "elementType": "labels.text.fill", "stylers": [ { "color": "#6b9a76" } ] }, { "featureType": "road", "elementType": "geometry", "stylers": [ { "color": "#38414e" } ] }, { "featureType": "road", "elementType": "geometry.stroke", "stylers": [ { "color": "#212a37" } ] }, { "featureType": "road", "elementType": "labels.text.fill", "stylers": [ { "color": "#9ca5b3" } ] }, { "featureType": "road.highway", "elementType": "geometry", "stylers": [ { "color": "#746855" } ] }, { "featureType": "road.highway", "elementType": "geometry.stroke", "stylers": [ { "color": "#1f2835" } ] }, { "featureType": "road.highway", "elementType": "labels.text.fill", "stylers": [ { "color": "#f3d19c" } ] }, { "featureType": "transit", "elementType": "geometry", "stylers": [ { "color": "#2f3948" } ] }, { "featureType": "transit.station", "elementType": "labels.text.fill", "stylers": [ { "color": "#d59563" } ] }, { "featureType": "water", "elementType": "geometry", "stylers": [ { "color": "#17263c" } ] }, { "featureType": "water", "elementType": "labels.text.fill", "stylers": [ { "color": "#515c6d" } ] }, { "featureType": "water", "elementType": "labels.text.stroke", "stylers": [ { "color": "#17263c" } ] } ] - - const getAllVenues = async (): Promise => { - if (!accessToken) { - console.log("No access token available"); - return null; + const fetchVenues = async (): Promise => { + if (!accessToken || !user?.location) { + console.log("No access token available or user location available"); + return; } try { - const res = await fetch(`${API_DOMAIN}/venues`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - }); - - if (!res.ok) { - throw new Error(`Error fetching data: ${res.statusText}`); + const locationRes = await fetch( + `${API_DOMAIN}/profiles/${user.user_id}/location`, + { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + + if (!locationRes.ok) + throw new Error( + `Error fetching user location: ${locationRes.statusText}` + ); + const { latitude, longitude } = await locationRes.json(); + // Fetch venues by location + const radius = 80000; // 80,000 meters (80 km) + const venuesRes = await fetch( + `${API_DOMAIN}/venues/location?latitude=${latitude}&longitude=${longitude}&radius=${radius}`, + // `${API_DOMAIN}/venues`, + { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + }, + } + ); + + if (!venuesRes.ok) { + throw new Error(`Error fetching venues: ${venuesRes.statusText}`); } + const venues: Venue[] = await venuesRes.json(); + console.log(venues[1]); - const data: Venue[] = await res.json(); - return data; + setAllVenues(venues); + setMapKey((prevKey) => prevKey + 1); } catch (err) { - console.log("Could not connect to db or something", err); - return null; + console.error("Failed to fetch venues by location:", err); + } + }; + + const getPriceRepresentation = (price: number | undefined): string => { + // Fallback to 1 star if price is undefined or 0 + if (!price || price === 0) { + return "$"; } + // Return the correct number of dollar signs + return "$".repeat(price); }; useEffect(() => { - getAllVenues().then((venues) => { - if (venues) { - setAllVenues(venues); - } else { - console.log("Unable to get all venues"); - } - }); - }, [accessToken]); + fetchVenues(); + }, [accessToken, user?.location]); - const toggleModal = () => { - setModalVisible(!isModalVisible); + const handleMarkerPress = (venue: Venue) => { + setSelectedVenue(venue); + modalRef.current?.open(); + }; + + const handleToggleModal = () => { + setSelectedVenue(null); + modalRef.current?.open(); + }; + + // Get the current day of the week for displaying venue hours + const getCurrentDayHours = (venue: Venue): string | number => { + const days = [ + "monday_hours", + "tuesday_hours", + "wednesday_hours", + "thursday_hours", + "friday_hours", + "saturday_hours", + "sunday_hours", + ]; + const today = new Date().getDay(); + const dayKey = days[today - 1] as keyof Venue; + + if (venue[dayKey] == "NULL") { + return "Hours not available"; + } + + return venue[dayKey] || "Hours not available"; }; return ( - - setModalVisible(false)} - venues={allVenues} - /> - - - {allVenues.length > 0 && - allVenues.map((v) => ( + + + {/* Floating Search Bar */} + + + {/* Map */} + + {allVenues.map((venue) => ( + title={venue.name} + description={venue.address} + onPress={() => handleMarkerPress(venue)} + > + + + {/* */} + + ))} - - - {/* Circular button to toggle modal visibility */} - - + - - + + + {/** TODO: Fix button loading time */} + {/* Toggle Button */} + + + + + + {/* Bottom Modal */} + + {selectedVenue ? ( + // Selected Venue View + // TODO: Match fonts to figma + + {/* Venue Title with Glow */} + + {selectedVenue.name || "Unknown Venue"} + + + {/* Venue Details */} + + Venue type | {selectedVenue.city || "N/A"},{" "} + {selectedVenue.state || "N/A"} + + + {/* Rating Container */} + {/** TODO: Map venue data to star rating, money sign, open status. */} + + {/* Stand-in Rating */} + + {selectedVenue.total_rating} + + + {/* Star Icon */} + + + {/* Divider */} + | + + {/* Money Sign */} + + {getPriceRepresentation(selectedVenue?.price)} + + + {/* Divider */} + | + + {/* Hours for the Day */} + + {getCurrentDayHours(selectedVenue)} + + + + {/* Centered EventCard */} + + + + + ) : ( + + )} + + + ); }; @@ -102,10 +247,9 @@ const styles = StyleSheet.create({ flex: 1, }, map: { - width: "100%", - height: "100%", + flex: 1, }, - circularButton: { + toggleButton: { position: "absolute", bottom: 30, left: "50%", @@ -127,6 +271,114 @@ const styles = StyleSheet.create({ fontSize: 24, fontWeight: "bold", }, + modalBackground: { + backgroundColor: "#1E1E2C", + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + }, + handleStyle: { + backgroundColor: "#FFFFFF", + width: 50, + height: 5, + borderRadius: 2.5, + alignSelf: "center", + marginVertical: 10, + }, + modalContent: { + padding: 20, + paddingTop: 35, + }, + venueName: { + fontSize: 30, + fontWeight: "bold", + color: "#FFFFFF", + textShadowColor: "rgba(255, 255, 255, 0.8)", + textShadowOffset: { width: 0, height: 0 }, + textShadowRadius: 10, + marginBottom: 5, + }, + venueDetails: { + fontSize: 16, + color: "#CCCCCC", + marginBottom: 10, + }, + ratingGlow: { + fontSize: 24, + color: "#FFD700", + textShadowColor: "rgba(255, 215, 0, 0.8)", + textShadowOffset: { width: 0, height: 0 }, + textShadowRadius: 10, + marginRight: 10, + }, + priceText: { + fontSize: 18, + color: "#FFFFFF", + marginLeft: 10, + }, + listTitle: { + fontSize: 18, + fontWeight: "bold", + color: "#FFFFFF", + marginBottom: 10, + }, + venueItem: { + paddingVertical: 10, + borderBottomWidth: 1, + borderBottomColor: "#444", + }, + venueAddress: { + fontSize: 14, + color: "#BBBBBB", + }, + customMarker: { + width: 40, + height: 40, + justifyContent: "center", + alignItems: "center", + }, + markerImage: { + width: "100%", + height: "100%", + }, + ratingContainer: { + flexDirection: "row", + alignItems: "center", + justifyContent: "flex-start", + marginTop: 10, + }, + standInRating: { + fontSize: 18, + color: "#FFFFFF", + marginRight: 5, + }, + ratingStar: { + width: 20, + height: 20, + marginHorizontal: 5, + }, + divider: { + fontSize: 18, + color: "#FFFFFF", + marginHorizontal: 5, + }, + moneySign: { + fontSize: 18, + fontWeight: "bold", + color: "#FFFFFF", + marginHorizontal: 5, + }, + statusText: { + fontSize: 18, + color: "#FFFFFF", + marginLeft: 5, + }, + eventCardContainer: { + width: "100%", + justifyContent: "center", + alignItems: "center", + marginTop: 20, + paddingHorizontal: 10, + }, }); export default MapScreen; diff --git a/frontend/types/Venue.ts b/frontend/types/Venue.ts index 19f97b1..7ea4768 100644 --- a/frontend/types/Venue.ts +++ b/frontend/types/Venue.ts @@ -1,11 +1,21 @@ -export interface Venue { - venue_id: string; - name: string; - address: string; - city: string; - state: string; - zipcode: string; - longitude: number; - latitude: number; - created_at: string; +export interface Venue { + venue_id: string; + name: string; + address: string; + city: string; + state: string; + zipcode: string; + longitude: number; + latitude: number; + created_at: string; + total_rating: number; + price: number; + monday_hours: string; + tuesday_hours: string; + wednesday_hours: string; + thursday_horus: string; + friday_hours: string; + saturday_hours: string; + sunday_hours: string; + } \ No newline at end of file