diff --git a/app.json b/app.json new file mode 100644 index 0000000..5839b55 --- /dev/null +++ b/app.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "expo-font" + ] +} diff --git a/backend/internal/handlers/venues/venues.go b/backend/internal/handlers/venues/venues.go index 8c22b96..d0e8a93 100644 --- a/backend/internal/handlers/venues/venues.go +++ b/backend/internal/handlers/venues/venues.go @@ -7,7 +7,7 @@ import ( "net/http" "strconv" "strings" - + "sort" "github.com/GenerateNU/nightlife/internal/errs" "github.com/GenerateNU/nightlife/internal/models" "github.com/GenerateNU/nightlife/internal/types" @@ -196,26 +196,54 @@ func (s *Service) GetVenuePersona(c *fiber.Ctx) error { fmt.Println("error: " + err.Error()) return fiber.NewError(fiber.StatusInternalServerError, "Could not get venue") } + + // Calculate weights total := v.AvgEnergy + v.AvgExclusive + v.AvgMainstream + v.AvgPrice priceWeight := v.AvgPrice / total mainstreamWeight := v.AvgMainstream / total energyWeight := v.AvgEnergy / total exclusiveWeight := v.AvgExclusive / total + temp := models.ByRecommendation{} - persona := `` - minDistance := math.Inf(1) + allDists := []struct { + Persona string + Distance float64 + }{} + + // Calculate distances and populate allDists for key, value := range temp.CharacterMap() { - // energy, exclusive, mainstream, price - distance := math.Abs(float64(energyWeight)-float64(value[0])) + math.Abs(float64(exclusiveWeight)-float64(value[1])) + math.Abs(float64(mainstreamWeight)-float64(value[2])) + math.Abs(float64(priceWeight)-float64(value[3])) - if distance < minDistance { - persona = key - minDistance = distance - } + distance := math.Abs(float64(energyWeight)-float64(value[0])) + + math.Abs(float64(exclusiveWeight)-float64(value[1])) + + math.Abs(float64(mainstreamWeight)-float64(value[2])) + + math.Abs(float64(priceWeight)-float64(value[3])) + + allDists = append(allDists, struct { + Persona string + Distance float64 + }{ + Persona: key, + Distance: distance, + }) } - if persona == `` { + + // If no personas are available + if len(allDists) == 0 { return c.Status(fiber.StatusOK).JSON("Not enough reviews to determine venue persona") } - return c.Status(fiber.StatusOK).JSON(persona) + + // Sort allDists by ascending distance + sort.Slice(allDists, func(i, j int) bool { + return allDists[i].Distance < allDists[j].Distance + }) + + // Collect the top 3 personas (or fewer if less than 3 exist) + top3Personas := []string{} + for i := 0; i < len(allDists) && i < 3; i++ { + top3Personas = append(top3Personas, allDists[i].Persona) + } + + // Return the top 3 personas + return c.Status(fiber.StatusOK).JSON(top3Personas) } func (s *Service) GetVenuesByIDs(c *fiber.Ctx) error { diff --git a/backend/internal/storage/postgres/userrating.go b/backend/internal/storage/postgres/userrating.go index ed1cecd..5f99615 100644 --- a/backend/internal/storage/postgres/userrating.go +++ b/backend/internal/storage/postgres/userrating.go @@ -38,11 +38,12 @@ WHERE func (db *DB) CreateReview(ctx context.Context, p models.Review) error { query := `INSERT INTO review (overall_rating, energy_rating, mainstream_rating, price_rating, crowd_rating, hype_rating, - exclusive_rating, review_text, venue_id, user_id) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` + exclusive_rating, review_text, venue_id, user_id) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` _, err := db.conn.Query(ctx, query, p.OverallRating, p.EnergyRating, - p.MainstreamRating, p.PriceRating, p.CrowdRating, - p.HypeRating, p.ExclusiveRating, p.ReviewText, p.VenueID, p.UserID) + p.MainstreamRating, p.PriceRating, p.CrowdRating, + p.HypeRating, p.ExclusiveRating, p.ReviewText, p.VenueID, p.UserID) + return err } diff --git a/backend/internal/storage/postgres/venues.go b/backend/internal/storage/postgres/venues.go index 8bb5ad4..b1e9d9d 100644 --- a/backend/internal/storage/postgres/venues.go +++ b/backend/internal/storage/postgres/venues.go @@ -110,7 +110,6 @@ 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() diff --git a/frontend/App.tsx b/frontend/App.tsx index 4294190..f428633 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -12,8 +12,9 @@ import MusicPreferences from './components/OnboardingCards/MusicPreference'; import PersonalityScreenReveal from './components/OnboardingCards/PersonalityScreenReveal' import PersonalityScreenReveal2 from './components/OnboardingCards/PersonalityScreenReveal2' import HowFarFromYou from './components/OnboardingCards/HowFarFromYou'; - +import { RootSiblingParent } from 'react-native-root-siblings'; import { Archivo_400Regular, Archivo_500Medium, Archivo_700Bold, useFonts } from "@expo-google-fonts/archivo"; +import { PlayfairDisplay_400Regular } from '@expo-google-fonts/playfair-display'; const Stack = createNativeStackNavigator(); @@ -140,15 +141,18 @@ export default function App() { Archivo_400Regular, Archivo_500Medium, Archivo_700Bold, + PlayfairDisplay_400Regular }); return ( fontsLoaded && ( - - - - - + + + + + + + ) ); } diff --git a/frontend/app.json b/frontend/app.json index 3959bb3..701095a 100644 --- a/frontend/app.json +++ b/frontend/app.json @@ -12,6 +12,20 @@ "resizeMode": "contain", "backgroundColor": "#ffffff" }, + "plugins": [ + [ + "expo-image-picker", + { + "photosPermission": "The app accesses your photos to let you share them with your friends." + } + ], + [ + "expo-font", + { + "fonts": ["./assets/fonts/DTNightingale-Light.ttf"] + } + ] + ], "ios": { "supportsTablet": true, "bundleIdentifier": "com.generate.nightlife" diff --git a/frontend/assets/blitz.png b/frontend/assets/blitz.png new file mode 100644 index 0000000..9d960a6 Binary files /dev/null and b/frontend/assets/blitz.png differ diff --git a/frontend/assets/buckley.png b/frontend/assets/buckley.png new file mode 100644 index 0000000..9da15a6 Binary files /dev/null and b/frontend/assets/buckley.png differ diff --git a/frontend/assets/lumi.png b/frontend/assets/lumi.png new file mode 100644 index 0000000..7cfc373 Binary files /dev/null and b/frontend/assets/lumi.png differ diff --git a/frontend/assets/mid_size_star.png b/frontend/assets/mid_size_star.png new file mode 100644 index 0000000..efb9d28 Binary files /dev/null and b/frontend/assets/mid_size_star.png differ diff --git a/frontend/assets/plumehart.png b/frontend/assets/plumehart.png new file mode 100644 index 0000000..d2d32d8 Binary files /dev/null and b/frontend/assets/plumehart.png differ diff --git a/frontend/assets/roux.png b/frontend/assets/roux.png new file mode 100644 index 0000000..516cf78 Binary files /dev/null and b/frontend/assets/roux.png differ diff --git a/frontend/assets/serafina.png b/frontend/assets/serafina.png new file mode 100644 index 0000000..4e729a6 Binary files /dev/null and b/frontend/assets/serafina.png differ diff --git a/frontend/assets/small_filled_star.png b/frontend/assets/small_filled_star.png new file mode 100644 index 0000000..d45a63a Binary files /dev/null and b/frontend/assets/small_filled_star.png differ diff --git a/frontend/assets/sprig.png b/frontend/assets/sprig.png new file mode 100644 index 0000000..37b53a6 Binary files /dev/null and b/frontend/assets/sprig.png differ diff --git a/frontend/components/Venue/BookmarkButton.tsx b/frontend/components/Venue/BookmarkButton.tsx index 456d4c0..a876a82 100644 --- a/frontend/components/Venue/BookmarkButton.tsx +++ b/frontend/components/Venue/BookmarkButton.tsx @@ -1,7 +1,7 @@ import React from "react"; import { View, TouchableOpacity, Image, StyleSheet } from "react-native"; import PropTypes from "prop-types"; -import Toast from "react-native-toast-message"; +import Toast from 'react-native-root-toast' const BookmarkButton = ({ venueID = "", userID = "" }) => { const bookmarkVenue = async () => { @@ -25,33 +25,25 @@ const BookmarkButton = ({ venueID = "", userID = "" }) => { if (response.ok) { const data = await response.json(); console.log("Bookmark submitted successfully:", data); - - Toast.show({ - type: "success", - text1: "Bookmark Saved!", - text2: "The venue has been successfully bookmarked.", - visibilityTime: 2000, + Toast.show("Venue Bookmarked!", { + duration: 800, + position: Toast.positions.BOTTOM, + backgroundColor: "#ffffff", + textColor: "#000000", + shadow: true, + animation: true, + hideOnPress: true, }); + + } else { const errorData = await response.json(); console.error("Error submitting review:", errorData); - Toast.show({ - type: "error", - text1: "Error Saving Bookmark", - text2: "Unable to save the venue. Please try again.", - visibilityTime: 2000, - }); } } catch (error) { console.error("Error:", error); - Toast.show({ - type: "error", - text1: "Network Error", - text2: "An error occurred. Please check your connection.", - visibilityTime: 2000, - }); } }; @@ -59,7 +51,8 @@ const BookmarkButton = ({ venueID = "", userID = "" }) => { diff --git a/frontend/components/Venue/Event.tsx b/frontend/components/Venue/Event.tsx index 6ec32db..b642caf 100644 --- a/frontend/components/Venue/Event.tsx +++ b/frontend/components/Venue/Event.tsx @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; const Event = ({ event = { create_at: "", event_date: "", event_id: 0, event_time: "", image_path: "", name: "", venue_id: "" } }) => { const date = parseISO(event.event_date); - const displayDate = format(date, "MMMM do, yyyy"); + const displayDate = format(date, "MMMM do"); const displayTime = format(date, 'h:mm a'); return ( @@ -17,8 +17,7 @@ const Event = ({ event = { create_at: "", event_date: "", event_id: 0, event_tim /> {event.name} - {displayDate} - {displayTime} + {displayDate} + {displayTime} @@ -63,6 +62,7 @@ const styles = StyleSheet.create({ dateText: { color: 'white', fontSize: 8, + paddingTop: 3 }, timeText: { color: 'white', diff --git a/frontend/components/Venue/PersonaIcons.tsx b/frontend/components/Venue/PersonaIcons.tsx new file mode 100644 index 0000000..f3537fa --- /dev/null +++ b/frontend/components/Venue/PersonaIcons.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { Image, View, Text, StyleSheet } from 'react-native'; +import PropTypes from 'prop-types'; + +const PersonaIcons = ({ personas = [] }) => { + const PersonaIconImages = { + // eslint-disable-next-line + roux: require('../../assets/roux.png'), + // eslint-disable-next-line + lumi: require('../../assets/lumi.png'), + // eslint-disable-next-line + sprig: require('../../assets/sprig.png'), + // eslint-disable-next-line + serafina: require('../../assets/serafina.png'), + // eslint-disable-next-line + buckley: require('../../assets/buckley.png'), + // eslint-disable-next-line + blitz: require('../../assets/blitz.png'), + // eslint-disable-next-line + plumehart: require('../../assets/plumehart.png'), + }; + + console.log("*************", personas); + return ( + + {personas.length > 0 ? ( + + {personas.map((persona, index) => ( + + ))} + + ) : ( + Not enough reviews + )} + + ); +}; + +PersonaIcons.propTypes = { + personas: PropTypes.arrayOf(PropTypes.oneOf(['roux', 'lumi', 'sprig', 'serafina', 'buckley', 'blitz', 'plumehart'])).isRequired +}; + +const styles = StyleSheet.create({ + container: { + padding: 10, + }, + iconContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'center', + }, + icon: { + width: 50, + height: 50, + marginLeft: 10 + }, + message: { + textAlign: 'center', + fontSize: 16, + color: '#666' + }, +}); + +export default PersonaIcons; diff --git a/frontend/components/Venue/RatingScrollBar.tsx b/frontend/components/Venue/RatingScrollBar.tsx index ce54bdf..5568037 100644 --- a/frontend/components/Venue/RatingScrollBar.tsx +++ b/frontend/components/Venue/RatingScrollBar.tsx @@ -1,7 +1,7 @@ import Slider from "@react-native-community/slider"; import {Text, View, StyleSheet} from "react-native"; import PropTypes from 'prop-types'; -import SoundWave from "./SoundWave"; +import SoundWave from "@/components/Venue/SoundWave"; /** * Allows a user to submit a rating from 0 - 10 on a specific category @@ -17,26 +17,28 @@ import SoundWave from "./SoundWave"; */ const RatingScrollBar = ({minTitle = "", maxTitle = "", value, onSliderChange, category, startColor, stopColor, avgValue}) => { + const adjustedRating = avgValue === 0 ? 1 : avgValue; + return( @@ -62,7 +64,8 @@ RatingScrollBar.propTypes = { const styles = StyleSheet.create({ container: { - alignItems: 'center' + alignItems: 'center', + backgroundColor: '#060019', }, title: { fontSize: 18, diff --git a/frontend/components/Venue/Review.tsx b/frontend/components/Venue/Review.tsx index a97e652..4c24699 100644 --- a/frontend/components/Venue/Review.tsx +++ b/frontend/components/Venue/Review.tsx @@ -27,7 +27,9 @@ const Review = ({ reviewDict = { const [username, setUserName] = useState(""); const [hasImage, setHasImage] = useState(false); const [hasReviewText, setHasReviewText] = useState(false); + const [randomPersona, setRandomPersona] = useState(null); + // eslint-disable-next-line const stars = { // eslint-disable-next-line "full": require("../../assets/filled_star.png"), @@ -35,6 +37,23 @@ const Review = ({ reviewDict = { "empty": require("../../assets/empty_star.png") } + const PersonaIconImages = { + // eslint-disable-next-line + roux: require('../../assets/roux.png'), + // eslint-disable-next-line + lumi: require('../../assets/lumi.png'), + // eslint-disable-next-line + sprig: require('../../assets/sprig.png'), + // eslint-disable-next-line + serafina: require('../../assets/serafina.png'), + // eslint-disable-next-line + buckley: require('../../assets/buckley.png'), + // eslint-disable-next-line + blitz: require('../../assets/blitz.png'), + // eslint-disable-next-line + plumehart: require('../../assets/plumehart.png'), + }; + useEffect(() => { if (reviewDict.image_path && reviewDict.image_path.trim() !== "") { @@ -53,6 +72,8 @@ const Review = ({ reviewDict = { .then(response => response.json()) .then(json => { setUserName(json.username); + const personas = Object.values(PersonaIconImages); + setRandomPersona(personas[Math.floor(Math.random() * personas.length)]); }) .catch(error => { console.error(error); @@ -79,6 +100,7 @@ const Review = ({ reviewDict = { }; const getTimeAgo = (timestamp) => { + console.log(timestamp) const now = new Date(); const past = new Date(timestamp); const diffInMs = now.getTime() - past.getTime(); @@ -101,9 +123,17 @@ const Review = ({ reviewDict = { return ( - _____________________________________________________________________ + ___________________________________________________ - {username} + + {randomPersona && ( + + )} + {username} + {starReview(Math.floor(reviewDict.overall_rating / 2))} {getTimeAgo(reviewDict.created_at)} @@ -142,7 +172,8 @@ Review.propTypes = { const styles = StyleSheet.create({ container: { - backgroundColor: "#121212", + backgroundColor: "#060019", + flex: 1 }, separator: { color: "white", @@ -153,23 +184,24 @@ const styles = StyleSheet.create({ fontSize: 10, color: "white", marginBottom: 5, - marginLeft: 20 + marginLeft: 30, + marginTop: 1 }, reviewText: { fontSize: 16, color: "white", - marginLeft: 20 + marginLeft: 30 }, eventImage: { height: 200, - width: 400, + width: 500, marginTop: 10, marginLeft: -10 }, starContainer: { flexDirection: "row", justifyContent: "flex-start", - marginLeft: 20, + marginLeft: 30, marginBottom: 5 }, star: { diff --git a/frontend/components/Venue/ScaledText.tsx b/frontend/components/Venue/ScaledText.tsx index 9f383a0..0a3018e 100644 --- a/frontend/components/Venue/ScaledText.tsx +++ b/frontend/components/Venue/ScaledText.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Text, StyleSheet } from 'react-native'; + type ScaledTextProps = { text: string; maxFontSize: number; @@ -42,10 +43,10 @@ const styles = StyleSheet.create({ text: { marginLeft: -4, color: 'white', - fontFamily: 'DTNightingale-Light', - textShadowColor: 'rgba(200, 150, 255, 0.75)', + fontFamily: 'PlayfairDisplay_400Regular', + textShadowColor: 'rgba(255, 255, 255, 0.9)', textShadowOffset: { width: 0, height: 0 }, - textShadowRadius: 5, + textShadowRadius: 5 }, }); diff --git a/frontend/components/Venue/SoundWave.tsx b/frontend/components/Venue/SoundWave.tsx index 8751a65..761266e 100644 --- a/frontend/components/Venue/SoundWave.tsx +++ b/frontend/components/Venue/SoundWave.tsx @@ -7,6 +7,7 @@ import Svg, { Defs, LinearGradient, Stop, Mask, Rect, Image } from 'react-native */ const SoundWave = ({ category = 1, rating = 1, startColor = "", stopColor = ""}) => { + // eslint-disable-next-line const soundWaveImages = { // eslint-disable-next-line 1: require("../../assets/sound_wave_1.png"), @@ -69,8 +70,6 @@ const SoundWave = ({ category = 1, rating = 1, startColor = "", stopColor = ""}) }; SoundWave.propTypes = { - minTitle: PropTypes.string.isRequired, - maxTitle: PropTypes.string.isRequired, rating: PropTypes.number.isRequired, startColor: PropTypes.string.isRequired, stopColor: PropTypes.string.isRequired, diff --git a/frontend/components/Venue/Stars.tsx b/frontend/components/Venue/Stars.tsx index 55a0e87..df1844b 100644 --- a/frontend/components/Venue/Stars.tsx +++ b/frontend/components/Venue/Stars.tsx @@ -1,4 +1,5 @@ // easy access to stars pngs +// eslint-disable-next-line const stars = { // eslint-disable-next-line full: require("../../assets/filled_star.png"), diff --git a/frontend/components/Venue/TimeCheck.tsx b/frontend/components/Venue/TimeCheck.tsx index c7baf66..2389361 100644 --- a/frontend/components/Venue/TimeCheck.tsx +++ b/frontend/components/Venue/TimeCheck.tsx @@ -5,19 +5,30 @@ * @returns boolean */ function isCurrentTimeInRange(startTimeStr, endTimeStr) { + if (startTimeStr === undefined || endTimeStr === undefined) { + return false; + } + const now = new Date(); - const targetDay = now.getDay(); - + const targetDay = now.getDay(); function parseTimeToDate(timeStr, targetDay) { const timeParts = timeStr.trim().match(/(\d{1,2}):(\d{2})\s?(AM|PM)/i); - + if (!timeParts) { - console.error("Invalid time format:", timeStr); - return new Date("invalid"); + if (timeStr.toLowerCase().includes("open")) { + const date = new Date(); + date.setDate(date.getDate() - date.getDay() + targetDay); + date.setHours(0, 0, 0, 0); + return date; + } else if (timeStr.toLowerCase().includes("closed")) { + return null; + } else { + return null; + } } - const [ , hoursStr, minutesStr, modifier] = timeParts; + const [, hoursStr, minutesStr, modifier] = timeParts; let hours = parseInt(hoursStr, 10); const minutes = parseInt(minutesStr, 10); @@ -29,18 +40,20 @@ function isCurrentTimeInRange(startTimeStr, endTimeStr) { } const date = new Date(); - date.setDate(date.getDate() - date.getDay() + targetDay); - date.setHours(hours); - date.setMinutes(minutes); - date.setSeconds(0); - date.setMilliseconds(0); + date.setDate(date.getDate() - date.getDay() + targetDay); + date.setHours(hours, minutes, 0, 0); + + if (isNaN(date.getTime())) { + return null; + } + return date; } const startTime = parseTimeToDate(startTimeStr, targetDay); const endTime = parseTimeToDate(endTimeStr, targetDay); - if (startTime.getTime() === new Date("invalid").getTime() || endTime.getTime() === new Date("invalid").getTime()) { + if (startTime === null || endTime === null) { return false; } @@ -48,10 +61,6 @@ function isCurrentTimeInRange(startTimeStr, endTimeStr) { endTime.setDate(endTime.getDate() + 1); } - if (now < startTime) { - return now <= endTime; - } - return now >= startTime && now <= endTime; } diff --git a/frontend/components/Venue/UpcomingEventScroll.tsx b/frontend/components/Venue/UpcomingEventScroll.tsx index 8da5374..8e5b0bd 100644 --- a/frontend/components/Venue/UpcomingEventScroll.tsx +++ b/frontend/components/Venue/UpcomingEventScroll.tsx @@ -19,7 +19,7 @@ const UpcomingEventScroll = ({ events = [] }) => { showsHorizontalScrollIndicator={false} contentContainerStyle={styles.scrollContent} > - {events && events.map((event, index) => ( + {events.map((event, index) => ( @@ -41,6 +41,7 @@ const styles = StyleSheet.create({ }, scrollContainer: { height: 148, + paddingLeft: 5 }, scrollContent: { flexDirection: 'row', @@ -54,8 +55,9 @@ const styles = StyleSheet.create({ justifyContent: 'center', alignItems: 'center', backgroundColor: '#f0f0f0', - marginRight: 16, + marginRight: 25, borderRadius: 10, + }, }); diff --git a/frontend/components/Venue/UploadImage.tsx b/frontend/components/Venue/UploadImage.tsx index a77f705..a2ee0a5 100644 --- a/frontend/components/Venue/UploadImage.tsx +++ b/frontend/components/Venue/UploadImage.tsx @@ -1,146 +1,65 @@ import React, { useState } from "react"; -import ImageUploading from "react-images-uploading"; +import { View, StyleSheet, TouchableOpacity, Text, Image } from "react-native"; +import * as ImagePicker from "expo-image-picker"; type UploadImageProps = { - onImageUpload: (uri: string | null) => void; + onImageUpload: (uri: string | null) => void; }; -/** - * Allows image uploading capabilities - */ -export const UploadImage: React.FC = ({ onImageUpload }) => { - const [images, setImages] = useState([]); - const maxNumber = 1; +export default function UploadImage({ onImageUpload }: UploadImageProps) { - const onChange = (imageList) => { - setImages(imageList); + const [image, setImage] = useState("") - if (imageList.length > 0) { - onImageUpload(imageList[0].data_url); - } else { - onImageUpload(null); + const pickImage = async () => { + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [4, 3], + quality: 1, + }); + + if (!result.canceled) { + const imageUri = result.assets[0].uri; + setImage(imageUri); + onImageUpload(imageUri); // Pass the image URI to the parent component } }; return ( -
- - {({ - imageList, - onImageUpload, - onImageUpdate, - onImageRemove, - isDragging, - dragProps, - }) => ( -
-
- -
- - {imageList.map((image, index) => ( -
- Uploaded -
- - -
-
- ))} -
- )} -
-
+ + + Upload Image + + {image && } + ); -}; +} -export default UploadImage; +const styles = StyleSheet.create({ + container: { + alignItems: "center", + justifyContent: "center", + }, + button: { + width: 350, + height: 40, + backgroundColor: "white", + justifyContent: "center", + alignItems: "center", + borderWidth: 1, + borderColor: "black", + borderRadius: 30, + marginTop: -10, + }, + buttonText: { + color: "black", + fontFamily: "Archivo", + fontSize: 16, + }, + image: { + width: 200, + height: 200, + marginTop: 10, + marginBottom: -40 + }, +}); diff --git a/frontend/components/Venue/VenueRatings.tsx b/frontend/components/Venue/VenueRatings.tsx index 5b714fb..f4fe9d2 100644 --- a/frontend/components/Venue/VenueRatings.tsx +++ b/frontend/components/Venue/VenueRatings.tsx @@ -16,64 +16,63 @@ const useVenueRatings = (venueID) => { useEffect(() => { fetch(`http://localhost:8080/venueratings/venue/${venueID}/ratings`) - .then((response) => response.json()) + .then((response) => { + if (!response.ok) { + throw new Error("Failed to fetch ratings"); + } + return response.json(); + }) .then((json) => { - const mainstream_rating = json.map((item) => item.mainstream_rating ?? 5); - const price_rating = json.map((item) => item.price_rating ?? 5); - const crowd_rating = json.map((item) => item.crowd_rating ?? 5); - const hype_rating = json.map((item) => item.hype_rating ?? 5); - const energy_rating = json.map((item) => item.energy_rating ?? 5); - const exclusive_rating = json.map((item) => item.exclusive_rating ?? 5); - const overall_rating = json.map((item) => item.overall_rating ?? 0); - - const mainstream_total = mainstream_rating.reduce( - (acc, curr) => acc + curr, - 0 - ); - const mainstream_average = - mainstream_rating.length > 0 - ? mainstream_total / mainstream_rating.length - : 5; // Default to 5 if no ratings exist - - const price_total = price_rating.reduce((acc, curr) => acc + curr, 0); - const price_average = - price_rating.length > 0 ? price_total / price_rating.length : 5; - - const crowd_total = crowd_rating.reduce((acc, curr) => acc + curr, 0); - const crowd_average = - crowd_rating.length > 0 ? crowd_total / crowd_rating.length : 5; - - const hype_total = hype_rating.reduce((acc, curr) => acc + curr, 0); - const hype_average = - hype_rating.length > 0 ? hype_total / hype_rating.length : 5; - - const energy_total = energy_rating.reduce((acc, curr) => acc + curr, 0); - const energy_average = - energy_rating.length > 0 ? energy_total / energy_rating.length : 5; - - const exclusive_total = exclusive_rating.reduce((acc, curr) => acc + curr, 0); - const exclusive_average = - exclusive_rating.length > 0 - ? exclusive_total / exclusive_rating.length - : 5; + console.log(venueID) + // If json is empty, set all ratings to default values (5 or 0 for overall) + if (!json || json.length === 0) { + setMainstreamRating(5); + setPriceRating(5); + setCrowdRating(5); + setHypeRating(5); + setEnergyRating(5); + setExclusiveRating(5); + setOverallRating(0); // Assuming 0 is the default for overall rating + } else { + // Calculate averages for each rating type + const mainstream_rating = json.map((item) => item.mainstream_rating ?? 5); + const price_rating = json.map((item) => item.price_rating ?? 5); + const crowd_rating = json.map((item) => item.crowd_rating ?? 5); + const hype_rating = json.map((item) => item.hype_rating ?? 5); + const energy_rating = json.map((item) => item.energy_rating ?? 5); + const exclusive_rating = json.map((item) => item.exclusive_rating ?? 5); + const overall_rating = json.map((item) => item.overall_rating ?? 0); - const overall_total = overall_rating.reduce((acc, curr) => acc + curr, 0); - const overall_average = - overall_rating.length > 0 ? overall_total / overall_rating.length : 0; + // Calculate average for each rating type + const mainstream_average = mainstream_rating.reduce((acc, curr) => acc + curr, 0) / mainstream_rating.length; + const price_average = price_rating.reduce((acc, curr) => acc + curr, 0) / price_rating.length; + const crowd_average = crowd_rating.reduce((acc, curr) => acc + curr, 0) / crowd_rating.length; + const hype_average = hype_rating.reduce((acc, curr) => acc + curr, 0) / hype_rating.length; + const energy_average = energy_rating.reduce((acc, curr) => acc + curr, 0) / energy_rating.length; + const exclusive_average = exclusive_rating.reduce((acc, curr) => acc + curr, 0) / exclusive_rating.length; + const overall_average = overall_rating.reduce((acc, curr) => acc + curr, 0) / overall_rating.length; - setMainstreamRating(Math.ceil(mainstream_average)); - setPriceRating(Math.ceil(price_average)); - setCrowdRating(Math.ceil(crowd_average)); - setHypeRating(Math.ceil(hype_average)); - setEnergyRating(Math.ceil(energy_average)); - setExclusiveRating(Math.ceil(exclusive_average)); - setOverallRating(overall_average); - + // Set state values + setMainstreamRating(Math.ceil(mainstream_average)); + setPriceRating(Math.ceil(price_average)); + setCrowdRating(Math.ceil(crowd_average)); + setHypeRating(Math.ceil(hype_average)); + setEnergyRating(Math.ceil(energy_average)); + setExclusiveRating(Math.ceil(exclusive_average)); + setOverallRating(overall_average); + } }) - .catch((error) => { - console.error(error); + .catch(() => { + // In case of error, use default values + setMainstreamRating(5); + setPriceRating(5); + setCrowdRating(5); + setHypeRating(5); + setEnergyRating(5); + setExclusiveRating(5); + setOverallRating(0); // Default value for overall rating }); - }, [venueID]); + }, []); return { mainstreamRating, diff --git a/frontend/components/Venue/VenueScreenHeader.tsx b/frontend/components/Venue/VenueScreenHeader.tsx index 986d56b..44c1f75 100644 --- a/frontend/components/Venue/VenueScreenHeader.tsx +++ b/frontend/components/Venue/VenueScreenHeader.tsx @@ -4,6 +4,7 @@ import ScaledText from "@/components/Venue/ScaledText"; import BookmarkButton from "@/components/Venue/BookmarkButton"; import StarReview from "@/components/Venue/StarReview"; + type VenueHeaderProps = { venueName: string; venueType: string; @@ -31,29 +32,49 @@ const VenueHeader: React.FC = ({ venueID, userID, }) => { + // Capitalize first letter of venueType and make the rest lowercase + const formattedVenueType = venueType.charAt(0).toUpperCase() + venueType.slice(1).toLowerCase(); + + const formatVenueDetails = (venueAddress, venueCity) => { + const combined = `${venueAddress}, ${venueCity}`; + return combined.length > 20 ? `${combined.slice(0, 20)}...` : combined; + }; + return ( - - - - {venueType} | {venueAddress}, {venueCity} + + + + + + {formattedVenueType} | {formatVenueDetails(venueAddress, venueCity)} + {/* eslint-disable-next-line */} - - + + + {/* eslint-disable-next-line */} + + + + + + - {parseFloat(overallRating.toFixed(1))} - - - {" | " + "$".repeat(Math.round(priceRating / 10))} + + {parseFloat(overallRating.toFixed(1))} + + + {priceRating > 0 ? "| " + "$".repeat(priceRating) : "| No Price Rating"} + - {" | " + (isOpen ? "Open" : "Closed")} + {" | " + (isOpen ? "Open" : "Closed")} @@ -69,11 +90,23 @@ const styles = StyleSheet.create({ review: { flexDirection: "row", marginTop: 10, + marginLeft: 4 }, buttonImage: { width: 30, height: 30, }, + overallRating: { + textShadowColor: 'rgba(255, 255, 255, 1)', + textShadowOffset: { width: 0, height: 0 }, + textShadowRadius: 5, + fontSize: 18, + color: "white", + marginTop: -4, + marginLeft: -4, + marginRight: 3, + fontFamily: 'PlayfairDisplay_400Regular' + } }); export default VenueHeader; diff --git a/frontend/components/Venue/VibeScrollBar.tsx b/frontend/components/Venue/VibeScrollBar.tsx index 14aa978..6e8d521 100644 --- a/frontend/components/Venue/VibeScrollBar.tsx +++ b/frontend/components/Venue/VibeScrollBar.tsx @@ -1,17 +1,19 @@ import { Text, View, StyleSheet } from "react-native"; import PropTypes from "prop-types"; -import SoundWave from "./SoundWave"; +import SoundWave from "@/components/Venue/SoundWave" /** * Combines sound wave image with respective labels into one component */ const VibeScrollBar = ({ category = 1, rating = 1, startColor = "", stopColor = "", minTitle = "", maxTitle = "" }) => { + const adjustedRating = rating === 0 ? 1 : rating; + return ( diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 92cb5f6..2e92818 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15088,4 +15088,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 5379cce..f7589b1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "dependencies": { "@eslint/js": "^9.10.0", "@expo-google-fonts/archivo": "^0.2.3", + "@expo-google-fonts/playfair-display": "^0.2.3", "@expo/metro-runtime": "~4.0.0", "@expo/ngrok": "^4.1.3", "@react-native-async-storage/async-storage": "1.23.1", @@ -21,6 +22,8 @@ "date-fns": "^4.1.0", "eslint-plugin-react": "^7.35.2", "expo": "^52.0.11", + "expo-font": "~13.0.1", + "expo-image-picker": "~16.0.3", "expo-status-bar": "~2.0.0", "react": "18.3.1", "react-dom": "18.3.1", @@ -35,6 +38,7 @@ "react-native-maps": "1.18.0", "react-native-modalize": "^2.1.1", "react-native-reanimated": "^3.16.1", + "react-native-root-toast": "^3.6.0", "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.1.0", "react-native-svg-transformer": "^1.5.0", diff --git a/frontend/screens/HomeScreen.tsx b/frontend/screens/HomeScreen.tsx index 2b83ba5..c6a84ad 100644 --- a/frontend/screens/HomeScreen.tsx +++ b/frontend/screens/HomeScreen.tsx @@ -6,9 +6,10 @@ import EventsScrollable from "./explore/EventsScrollable"; import { API_DOMAIN } from "@env"; import { useNavigation } from "@react-navigation/native"; -interface HomeScreenProps { - showSearchBar?: boolean; -} +type HomeScreenProps = { + navigation: StackNavigationProp; + showSearchBar?: boolean; // Make showSearchBar optional +}; const HomeScreen: React.FC = ({ showSearchBar = true }) => { const navigation = useNavigation(); diff --git a/frontend/screens/venue/PhotosScreen.tsx b/frontend/screens/venue/PhotosScreen.tsx index fcea662..991be1c 100644 --- a/frontend/screens/venue/PhotosScreen.tsx +++ b/frontend/screens/venue/PhotosScreen.tsx @@ -1,16 +1,15 @@ import React from "react"; -import { View, StyleSheet, SafeAreaView, Image } from "react-native"; +import { View, StyleSheet, SafeAreaView, Image, ScrollView, Text } from "react-native"; import { useEffect, useState } from "react"; -import { ScrollView } from "react-native-gesture-handler"; /** - * Screen to display all photos that have been included in any reviews of current venue + * Screen to display all photos that have been included in any reviews of the current venue * @param venueID venue ID of the venue being explored * @returns a scrollable list of photos */ const PhotosScreen: React.FC = ({ venueID }) => { - console.log(venueID) + console.log(venueID); const [reviewDictList, setReviewDictList] = useState([]); // get all reviews from a venue @@ -25,22 +24,25 @@ const PhotosScreen: React.FC = ({ venueID }) => { }); }, []); - console.log(reviewDictList) + console.log(reviewDictList); + + const photos = reviewDictList.filter(review => review.image_path && review.image_path.trim() !== ""); return ( - {/* Only collect & show images in reviews */} - {reviewDictList.map((review, index) => ( - review.image_path && review.image_path.trim() !== "" && ( + {photos.length === 0 ? ( + No Photos Yet! + ) : ( + photos.map((review, index) => ( - ) - ))} + )) + )} @@ -51,7 +53,7 @@ const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#060019", - marginLeft:-20 + marginLeft: -20, }, text: { color: "#fff", @@ -69,6 +71,17 @@ const styles = StyleSheet.create({ height: 200, marginTop: 10, }, + noPhotosText: { + color: "white", + textAlign: "center", + fontSize: 24, + marginLeft: 130, + marginTop: 140, + fontFamily: 'PlayfairDisplay_400Regular', + textShadowColor: 'rgba(255, 255, 255, 1)', + textShadowOffset: { width: 0, height: 0 }, + textShadowRadius: 5, + }, }); export default PhotosScreen; diff --git a/frontend/screens/venue/RateReviewScreen.tsx b/frontend/screens/venue/RateReviewScreen.tsx index af3ac44..9e1d343 100644 --- a/frontend/screens/venue/RateReviewScreen.tsx +++ b/frontend/screens/venue/RateReviewScreen.tsx @@ -1,11 +1,13 @@ import React, { useState } from "react"; import { Text, View, StyleSheet, SafeAreaView, Image, TextInput, TouchableOpacity } from "react-native"; import { RouteProp } from "@react-navigation/native"; -import { UploadImage } from "@/components/Venue/UploadImage"; +import UploadImage from "@/components/Venue/UploadImage"; import Slider from '@react-native-community/slider'; import { Feather } from "@expo/vector-icons"; import PropTypes from "prop-types"; import stars from "@/components/Venue/Stars"; +import Toast from 'react-native-root-toast' + type RootStackParamList = { Home: undefined; RateReviews: { @@ -14,6 +16,7 @@ type RootStackParamList = { venueType: string; venueCity: string; username: string; + venueID: string; }; }; @@ -21,28 +24,20 @@ type ReviewScreenProps = { route: RouteProp; }; -/** - * Allows the user to submit a *generic* review (i.e. overall rating, image, summary review) - * @param route venue- & user- specific info - * @param navigation project navigation - * @returns screen for reviewing - */ - const RateReviewScreen: React.FC = ({ route, navigation }) => { - const { venueName, venueAddress, venueType, venueCity, username } = route.params; + const { venueID, venueName, venueAddress, venueType, venueCity, username } = route.params; const [userInput, setUserInput] = useState(""); const [rating, setRating] = useState(0); const [imageURI, setImageURI] = useState(null); const [sliderValue, setSliderValue] = useState(1); - + + console.log(venueID) const labels = ['$', '$$', '$$$', '$$$$', '$$$$$']; - // on-click handler for star review const handleStarPress = (index: number) => { setRating(index + 1); }; - // star review const starReview = () => { return ( @@ -60,18 +55,16 @@ const RateReviewScreen: React.FC = ({ route, navigation }) => ); }; - // collect user input for review text const handleInputChange = (text: string) => { setUserInput(text); }; - // collect user input for image const handleImageUpload = (uri: string | null) => { + console.log("-------------------") setImageURI(uri); }; const saveReviews = async () => { - // collect all info to post to db const reviewData = { overall_rating: rating, energy_rating: null, @@ -81,7 +74,7 @@ const RateReviewScreen: React.FC = ({ route, navigation }) => hype_rating: null, exclusive_rating: null, review_text: userInput, - venue_id: "0006b62a-21bd-4e48-8fc7-e3bcca66d0d0", + venue_id: venueID, user_id: "26d636d9-f8b0-4ad7-be9c-9b98c4c8a0c4", image_path: imageURI }; @@ -101,34 +94,64 @@ const RateReviewScreen: React.FC = ({ route, navigation }) => if (response.ok) { const data = await response.json(); console.log("Review submitted successfully:", data); + Toast.show("Review Submitted!", { + duration: 800, + position: Toast.positions.BOTTOM, + backgroundColor: "white", + textColor: "black", + shadow: true, + animation: true, + hideOnPress: true, + }); } else { const errorData = await response.json(); console.error("Error submitting review:", errorData); + Toast.show("Error Submitting Review", { + duration: 800, + position: Toast.positions.BOTTOM, + backgroundColor: "#ffffff", + textColor: "#000000", + shadow: true, + animation: true, + hideOnPress: true, + }); } } catch (error) { console.error("Error:", error); } }; + const formattedVenueType = venueType.charAt(0).toUpperCase() + venueType.slice(1).toLowerCase(); + + return ( - - navigation.navigate('Venue')}> - Back + + navigation.navigate('Venue', { + venue_id: venueID + })}> + Back - {venueName} - {venueType} | {venueAddress}, {venueCity} - - {username} - - + {venueName} + + {formattedVenueType} | {venueAddress}, {venueCity} + + + + + {username} + + - + {starReview()} - Label + Label = ({ route, navigation }) => placeholder="Enter your review here..." placeholderTextColor="gray" /> - - - - setSliderValue(value)} - minimumTrackTintColor="#FFFFFF" - maximumTrackTintColor="#CCCCCC" - thumbTintColor="#FFFFFF" - /> - - - {labels.map((label, index) => ( - - {label} - - ))} + + + + + setSliderValue(value)} + minimumTrackTintColor="#FFFFFF" + maximumTrackTintColor="#CCCCCC" + thumbTintColor="#FFFFFF" + /> + + {labels.map((label, index) => ( + + {label} + + ))} + + + + + Submit - ); }; @@ -177,6 +204,7 @@ RateReviewScreen.propTypes = { venueType: PropTypes.string.isRequired, venueCity: PropTypes.string.isRequired, username: PropTypes.string.isRequired, + venueID: PropTypes.string.isRequired }).isRequired, }).isRequired, navigation: PropTypes.object.isRequired, @@ -185,18 +213,18 @@ RateReviewScreen.propTypes = { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: "#060019" + backgroundColor: "#060019", + flexDirection: 'column' }, - text: { - color: "white", - fontSize: 24, + mainContent: { + paddingHorizontal: 20, + paddingVertical: 10, + flexDirection: 'column', }, starContainer: { flexDirection: "row", justifyContent: "center", marginVertical: 10, - alignContent: 'center', - marginLeft: -15 }, star: { width: 60, @@ -213,37 +241,55 @@ const styles = StyleSheet.create({ marginBottom: 10, fontSize: 16, padding: 15, - textAlignVertical: 'top', // Ensures the text starts at the top + textAlignVertical: 'top', }, - saveButton: { - color: "black", - fontSize: 18, - textAlign: "center", - backgroundColor: "white", - padding: 10, - borderRadius: 20, - width: 350 + uploadImageContainer: { + alignItems: 'center', + marginVertical: 20, }, - label: { - fontSize: 24, - color: '#FFFFFF', - marginBottom: 10, + imagePreview: { + width: 200, + height: 200, + marginVertical: 10, + }, + sliderContainer: { + paddingTop: 20, + }, + sliderContainerWithImage: { + marginTop: 200, }, slider: { width: 350, height: 40, }, - markers: { flexDirection: 'row', justifyContent: 'space-between', width: 340, - marginLeft: 10 + marginLeft: 10, }, marker: { fontSize: 14, color: '#FFFFFF', }, + saveButton: { + color: "black", + fontSize: 18, + textAlign: "center", + backgroundColor: "white", + padding: 10, + borderRadius: 20, + width: 350, + marginLeft: 25 + }, + title: { + fontSize: 36, + color: 'white', + textShadowColor: 'rgba(255, 255, 255, 0.9)', + textShadowOffset: { width: 0, height: 0 }, + textShadowRadius: 5, + fontFamily: 'PlayfairDisplay_400Regular', + } }); export default RateReviewScreen; diff --git a/frontend/screens/venue/RatingScreen.tsx b/frontend/screens/venue/RatingScreen.tsx index 80c0b05..3aefbe9 100644 --- a/frontend/screens/venue/RatingScreen.tsx +++ b/frontend/screens/venue/RatingScreen.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import {Text, View, TouchableOpacity, ScrollView, StyleSheet} from "react-native"; import RatingScrollBar from "@/components/Venue/RatingScrollBar"; - +import Toast from "react-native-root-toast"; /** * Allows a user to submit a rating score for various specific categories * @param venueId venueId of the venue currently being explored @@ -13,9 +13,8 @@ import RatingScrollBar from "@/components/Venue/RatingScrollBar"; * @param exclusive average exclusive score for the current venue (*see VenueOverviewScreen) * @returns a rating page for users */ -const RatingScreen: React.FC<{ venueId: string, hype: number, mainstream: number, price: number, crowd: number, energy: number, exclusive: number }> = ({ venueId, hype, mainstream, price, crowd, energy, exclusive }) => { +const RatingScreen: React.FC<{ venueId: string, personas: object, hype: number, mainstream: number, price: number, crowd: number, energy: number, exclusive: number }> = ({ venueId, hype, mainstream, price, crowd, energy, exclusive }) => { const userID = "26d636d9-f8b0-4ad7-be9c-9b98c4c8a0c4"; - // initial state of ratings const [ratings, setRatings] = useState({ hype: 0, @@ -69,9 +68,28 @@ const RatingScreen: React.FC<{ venueId: string, hype: number, mainstream: number if (response.ok) { const data = await response.json(); console.log('Review submitted successfully:', data); + Toast.show("Rating Submitted!", { + duration: 800, + position: Toast.positions.CENTER, + backgroundColor: "#ffffff", + textColor: "#000000", + shadow: true, + animation: true, + hideOnPress: true, + }); + } else { const errorData = await response.json(); console.error('Error submitting review:', errorData); + Toast.show("Error Submitting Rating", { + duration: 800, + position: Toast.positions.BOTTOM, + backgroundColor: "#ffffff", + textColor: "#000000", + shadow: true, + animation: true, + hideOnPress: true, + }); } } catch (error) { console.error('Error:', error); @@ -79,9 +97,8 @@ const RatingScreen: React.FC<{ venueId: string, hype: number, mainstream: number }; return ( - - - + + + + + Save + - - - Save - - ); }; @@ -167,11 +183,11 @@ const styles = StyleSheet.create({ flex: 1, display: 'flex', flexDirection: 'column', - alignItems: 'flex-start', paddingLeft: 10, paddingRight: 10, paddingTop: 10, - marginLeft: 0 + marginLeft: 0, + backgroundColor: '#060019' }, buttonText: { fontSize: 20, @@ -187,6 +203,7 @@ const styles = StyleSheet.create({ alignSelf: 'center', width: 300, marginTop: 10, + }, }); diff --git a/frontend/screens/venue/VenueOverviewScreen.tsx b/frontend/screens/venue/VenueOverviewScreen.tsx index bc85885..fc9e4c5 100644 --- a/frontend/screens/venue/VenueOverviewScreen.tsx +++ b/frontend/screens/venue/VenueOverviewScreen.tsx @@ -17,7 +17,8 @@ import VibeScrollBar from '@/components/Venue/VibeScrollBar'; * @returns sound wave list */ -const OverviewScreen = ({ navigation, eventDictList, hype, mainstream, price, crowd, energy, exclusive }) => { +const OverviewScreen = ({ navigation, venueID, eventDictList, hype, mainstream, price, crowd, energy, exclusive }) => { + return ( @@ -32,7 +33,7 @@ const OverviewScreen = ({ navigation, eventDictList, hype, mainstream, price, cr - ______________________________________________________________ + ________________________________________________ @@ -43,10 +44,11 @@ const OverviewScreen = ({ navigation, eventDictList, hype, mainstream, price, cr onPress={() => navigation.navigate('Venue', { defaultTab: 'Rating', + venue_id: venueID }) } style={styles.buttonWrapper}> - + {/* eslint-disable-next-line */} { +const VenueReviews: React.FC = ({ navigation, venueID, venueName, venueAddress, venueType, venueCity }) => { const [reviewDictList, setReviewDictList] = useState([]); - + + // eslint-disable-next-line const stars = { - // eslint-disable-next-line + // eslint-disable-next-line empty: require("../../assets/empty_star.png"), }; useEffect(() => { - fetch("http://localhost:8080/venueratings/venue/0006b62a-21bd-4e48-8fc7-e3bcca66d0d0/ratings") + fetch(`http://localhost:8080/venueratings/venue/${venueID}/ratings`) .then((response) => response.json()) .then((json) => { setReviewDictList(json); @@ -52,6 +52,7 @@ const VenueReviews: React.FC = ({ navigation, venueName, venueAddress, venueType onPress={() => navigation.navigate("RatingReview", { navigation: navigation, + venueID: venueID, venueName: venueName, venueAddress: venueAddress, venueType: venueType, @@ -66,14 +67,17 @@ const VenueReviews: React.FC = ({ navigation, venueName, venueAddress, venueType - {/* Scrollable content */} - - - {reviewDictList.map((review, index) => ( - - - - ))} + + + {reviewDictList.length === 0 ? ( + No Reviews Yet! + ) : ( + reviewDictList.map((review, index) => ( + + + + )) + )} @@ -84,30 +88,41 @@ const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#060019", - marginLeft: -25 + width: 500, }, rateReviewContainer: { - paddingLeft: 20 + backgroundColor: "#060019", }, rateReviewText: { color: "white", - paddingBottom: 5 + paddingBottom: 5, }, scrollView: { - + backgroundColor: "#060019", }, scrollContent: { - paddingBottom: 20, + paddingBottom: 20, }, starContainer: { flexDirection: "row", - flexGrow: 1 + flexGrow: 1, }, star: { width: 20, height: 20, marginRight: 3, }, + noReviewsText: { + color: "white", + textAlign: "center", + fontSize: 24, + marginLeft: -100, + marginTop: 100, + fontFamily: 'PlayfairDisplay_400Regular', + textShadowColor: 'rgba(255, 255, 255, 1)', + textShadowOffset: { width: 0, height: 0 }, + textShadowRadius: 5, + }, }); export default VenueReviews; diff --git a/frontend/screens/venue/VenueScreen.tsx b/frontend/screens/venue/VenueScreen.tsx index fbe523c..b7cd432 100644 --- a/frontend/screens/venue/VenueScreen.tsx +++ b/frontend/screens/venue/VenueScreen.tsx @@ -7,8 +7,7 @@ import useVenueRatings from "@/components/Venue/VenueRatings"; import isCurrentTimeInRange from "@/components/Venue/TimeCheck"; import VenueHeader from "@/components/Venue/VenueScreenHeader"; import RatingScreen from "./RatingScreen"; -import { useRoute } from "@react-navigation/native"; -import { useAuth } from "@/context/AuthContext"; +import PersonaIcons from "@/components/Venue/PersonaIcons"; enum VenueTabs { Overview = "Overview", @@ -24,21 +23,15 @@ enum VenueTabs { * @returns overall venue screen */ -const VenueScreen: React.FC = ({ navigation }) => { - - const defaultTab = VenueTabs.Overview - - const { user } = useAuth(); - - const route = useRoute(); - +const VenueScreen: React.FC = ({ navigation, route }) => { const [selectedTab, setSelectedTab] = useState(VenueTabs.Overview); - const { venue_id } = route.params as { venue_id: string }; - - console.log(route.params); + const { defaultTab = VenueTabs.Overview, venue_id: venueID } = route.params || {}; - const venueID = venue_id; - const userID = user?.user_id; + const [currentID, setCurrentID] = useState(venueID) + console.log("--------------------------VENUEID", venueID) + const [personas, setPersonas] = useState([]) + //const venueID = "0006b62a-21bd-4e48-8fc7-e3bcca66d0d0"; + const userID = "26d636d9-f8b0-4ad7-be9c-9b98c4c8a0c4"; const day = new Date().getDay(); const dayNames = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]; const dayName = dayNames[day] + "_hours"; @@ -50,7 +43,7 @@ const VenueScreen: React.FC = ({ navigation }) => { const [venueCity, setVenueCity] = useState(""); const [venueAddress, setVenueAddress] = useState(""); const [venueType, setVenueType] = useState(""); - + const [venuePrice, setVenuePrice] = useState(1); // venue-specific event info const [eventDictList, setEventDictList] = useState([]); @@ -80,10 +73,16 @@ const VenueScreen: React.FC = ({ navigation }) => { setVenueAddress(json.address.split(',')[0]); setVenueCity(json.city); setVenueType(json.venue_type); - const times = json[dayName].split(": ")[1]; - const [start, stop] = times.split(" – ").map(time => time.trim()); - setCurrentStartHours(start); - setCurrentStopHours(stop); + const times = json[dayName]?.split(": ")[1]; // Use optional chaining for times + if (times) { + const [start, stop] = times.split(" – ").map(time => time.trim()); + setCurrentStartHours(start); + setCurrentStopHours(stop); + } else { + setCurrentStartHours(undefined); + setCurrentStopHours(undefined); + } + setVenuePrice(json.price) }) .catch(error => { console.error(error); @@ -102,16 +101,45 @@ const VenueScreen: React.FC = ({ navigation }) => { }); }, []); + useEffect(() => { + fetch(`http://localhost:8080/venues/persona/${venueID}`) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to fetch personas'); + } + return response.json(); + }) + .then((json) => { + console.log(personas.length === 0) + setPersonas(json); + }) + .catch((error) => { + console.error('Error fetching personas:', error); + }); + }, []); + return ( - navigation.goBack()}> - Go Back - {selectedTab === VenueTabs.Rating && ( setSelectedTab(VenueTabs.Overview)}> - Back + onPress={() => { + setCurrentID(venueID) + setSelectedTab(VenueTabs.Overview); + navigation.setParams({ + defaultTab: VenueTabs.Overview + }); + }} + > + Back + )} + + {selectedTab !== VenueTabs.Rating && ( + { + navigation.navigate("Home"); + }}> + Back )} { venueAddress={venueAddress} venueCity={venueCity} overallRating={overallRating} - priceRating={priceRating} + priceRating={venuePrice} isOpen={isCurrentTimeInRange(currentStartHours, currentStopHours)} venueID={venueID} userID={userID} @@ -128,18 +156,32 @@ const VenueScreen: React.FC = ({ navigation }) => { {selectedTab === VenueTabs.Rating && ( - )} + + {/* Render the text and PersonaIcons */} + + Recommended for ... + {personas.length > 0 && } + {personas.length === 0 && Not enough reviews} + + + {/* Render the RatingScreen component */} + + + )} + {/* TAB NAVIGATION */} {selectedTab !== VenueTabs.Rating && ( - + setSelectedTab(VenueTabs.Overview)}> Overview @@ -151,20 +193,31 @@ const VenueScreen: React.FC = ({ navigation }) => { )} - - + + + {selectedTab === VenueTabs.Overview && ( - )} - {selectedTab === VenueTabs.Reviews && } + + + Recommended for ... + {personas.length > 0 && } + {personas.length === 0 && Not enough reviews} + + + + )} + + {selectedTab === VenueTabs.Reviews && } {selectedTab === VenueTabs.Photos && } @@ -180,7 +233,7 @@ const styles = StyleSheet.create({ paddingLeft: 10, paddingRight: 10, paddingTop: 10, - backgroundColor: '#060019', + backgroundColor: '#060019' }, review: { display: 'flex', @@ -190,6 +243,7 @@ const styles = StyleSheet.create({ header: { display: 'flex', flexDirection: 'column', + marginBottom: -10 }, bookmark: { paddingTop: 5, diff --git a/frontend/types/NavigationTypes.ts b/frontend/types/NavigationTypes.ts index 2a16d4f..c8c1c77 100644 --- a/frontend/types/NavigationTypes.ts +++ b/frontend/types/NavigationTypes.ts @@ -15,7 +15,7 @@ export type BottomTabParamList = { }; Rating: undefined; VenueReviews: undefined; - RateReviews: { venueName: string, venueAddress: string, venueType: string, venueCity: string, username: string }; + RateReviews: { venueName: string, venueID: string, venueAddress: string, venueType: string, venueCity: string, username: string }; }; diff --git a/react-native.config.js b/react-native.config.js new file mode 100644 index 0000000..b7440da --- /dev/null +++ b/react-native.config.js @@ -0,0 +1,4 @@ +module.exports = { + assets: ['./assets/fonts/'], + }; + \ No newline at end of file