From 5b8b6a209dbab0011692ec2d1dfd688078838cbf Mon Sep 17 00:00:00 2001 From: angelp03 <angelpimentel2025@u.northwestern.edu> Date: Fri, 8 Nov 2024 10:24:55 -0600 Subject: [PATCH 1/7] refactor existing frontend code --- src/App.jsx | 200 ++--------------------------------- src/components/Banner.css | 13 ++- src/components/Banner.jsx | 7 +- src/pages/LandingPage.jsx | 25 +++++ src/pages/ReviewPostPage.jsx | 157 +++++++++++++++++++++++++++ src/utilities/firebase.js | 46 ++++---- 6 files changed, 234 insertions(+), 214 deletions(-) create mode 100644 src/pages/LandingPage.jsx create mode 100644 src/pages/ReviewPostPage.jsx diff --git a/src/App.jsx b/src/App.jsx index f300761..1dc6065 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,194 +1,16 @@ -import React, { useState, useEffect } from 'react'; -import './App.css'; -import { useAuthState, signInWithGoogle, firebaseSignOut } from './utilities/firebase'; -import { findCafes } from './utilities/findCafes'; -import MapComponent from "./MapComponent"; - -function App() { - const [reviews, setReviews] = useState([]); - const [availability, setAvailability] = useState({}); - const [newReview, setNewReview] = useState(''); - const [selectedReview, setSelectedReview] = useState(null); - const [replyText, setReplyText] = useState(''); - const [cafeStatus, setCafeStatus] = useState({ seating: '', outlets: '' }); - const [address, setAddress] = useState(''); - const [dist, setDist] = useState(''); - const [cafes, setCafes] = useState([]); - - const [user] = useAuthState(); - - const handleSignIn = async () => { - try { - await signInWithGoogle(); - } catch (error) { - console.error('Error signing in:', error); - } - }; - - const handleLogout = () => firebaseSignOut(); - - useEffect(() => {}, []); - - const findCafesWrapper = () => { - findCafes(dist, address, setCafes); - }; - - const handleReviewSubmit = () => { - if (newReview.trim()) { - const review = { id: Date.now(), text: newReview, replies: [] }; - setReviews((prevReviews) => [...prevReviews, review]); - setNewReview(''); - } - }; - - const handleReplySubmit = (reviewId) => { - if (replyText.trim()) { - setReviews((prevReviews) => - prevReviews.map((review) => - review.id === reviewId - ? { ...review, replies: [...review.replies, replyText] } - : review - ) - ); - setReplyText(''); - setSelectedReview(null); - } - }; - - const handleAvailabilityUpdate = (cafeId) => { - const timestamp = new Date().toLocaleString(); - const newUpdate = { timestamp, seating: cafeStatus.seating, outlets: cafeStatus.outlets }; - - setAvailability((prevAvailability) => ({ - ...prevAvailability, - [cafeId]: { - history: [...(prevAvailability[cafeId]?.history || []), newUpdate], - }, - })); - setCafeStatus({ seating: '', outlets: '' }); - }; - - const cafesMap = [ - { name: "Cafe Blue", lat: 42.046, lng: -87.688 }, - { name: "Green Bean Cafe", lat: 42.048, lng: -87.684 }, - { name: "Java Lounge", lat: 42.044, lng: -87.690 }, - ]; +import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; +import LandingPage from './pages/LandingPage'; +import ReviewPostPage from "./pages/ReviewPostPage"; +const App = () => { return ( - <div className="App"> - <header className="App-header"> - <h1>CafeWay</h1> - {user ? ( - <> - <button className="my-post-button" onClick={handleLogout}>Sign Out</button> - <p>Welcome, {user.email}</p> - </> - ) : ( - <button className="my-post-button" onClick={handleSignIn}>Sign In</button> - )} - </header> - - <div className="listings-container"> - <div className="listings-column"> - <h2>Share a Review</h2> - <input - type="text" - placeholder="Share a quick review..." - value={newReview} - onChange={(e) => setNewReview(e.target.value)} - /> - <button onClick={handleReviewSubmit}>Submit</button> - - <ul className="review-list"> - {reviews.map((review) => ( - <li key={review.id} className="review-item"> - <p onClick={() => setSelectedReview(review)}>{review.text}</p> - <ul className="reply-list"> - {review.replies.map((reply, index) => ( - <li key={index} className="reply-item"> - <span className="reply-label">Reply:</span> {reply} - </li> - ))} - </ul> - {selectedReview && selectedReview.id === review.id && ( - <div className="reply-input"> - <input - type="text" - placeholder="Write a reply..." - value={replyText} - onChange={(e) => setReplyText(e.target.value)} - /> - <button onClick={() => handleReplySubmit(review.id)}>Send</button> - </div> - )} - </li> - ))} - </ul> - - <h2>Update Cafe Availability</h2> - <input - type="text" - placeholder="Seating status (e.g., Few seats left)" - value={cafeStatus.seating} - onChange={(e) => setCafeStatus({ ...cafeStatus, seating: e.target.value })} - /> - <input - type="text" - placeholder="Outlet status (e.g., All taken)" - value={cafeStatus.outlets} - onChange={(e) => setCafeStatus({ ...cafeStatus, outlets: e.target.value })} - /> - <button onClick={() => handleAvailabilityUpdate('cafe-123')}>Update Availability</button> - - <h2>Availability History</h2> - {availability['cafe-123']?.history?.map((update, index) => ( - <div key={index} className="availability-item"> - <p> - <strong>{update.timestamp}:</strong> Seating: {update.seating}, Outlets: {update.outlets} - </p> - </div> - ))} - </div> - - <div className="map-column"> - <h2>Cafes Near Me</h2> - <input - type="text" - placeholder="Enter zip code" - value={address} - onChange={(e) => setAddress(e.target.value)} - /> - <input - type="number" - placeholder="Enter radius (in miles)" - value={dist} - onChange={(e) => setDist(e.target.value)} - /> - <button onClick={findCafesWrapper}>Search</button> - <h2>Cafes Near Me</h2> - <ul> - {cafes.map((cafe, index) => ( - <li className="cafe-card" key={index}>{cafe}</li> - ))} - </ul> - - - <ul className="cafe-list"> - {cafes.map((cafesMap, index) => ( - <li key={index} className="cafe-item"> - {cafe.name} (Lat: {cafe.lat}, Lng: {cafe.lng}) - </li> - ))} - </ul> - - {/* Integrate MapComponent with hardcoded data */} - <div style={{ height: '400px', width: '100%', marginTop: '20px' }}> - <MapComponent cafes={cafesMap} /> - </div> - </div> - </div> - </div> - ); + <Router> + <Routes> + <Route path="/" element={ <LandingPage/> }/> + <Route path="/post" element={ <ReviewPostPage/> }/> + </Routes> + </Router> + ) } export default App; diff --git a/src/components/Banner.css b/src/components/Banner.css index 89c1df9..f3016a4 100644 --- a/src/components/Banner.css +++ b/src/components/Banner.css @@ -7,7 +7,7 @@ font-family: 'Hind Vadodara', sans-serif; font-weight: bolder; color: #8A3323; - border-bottom: 1px solid #8A3323; + border-bottom: 2px solid #8A3323; } .logo-search-div { @@ -35,7 +35,9 @@ .text-input { width: 22rem; - height: 1.5rem; + height: 2rem; + border-top: none; + border-left: none; border-bottom: 1px solid black; border-radius: 0; padding-left: 0.5rem; @@ -46,7 +48,7 @@ } .search-btn { - height: 2.6rem; + height: 2.3rem; width: 3rem; background-color: #8A3323; border: none; @@ -54,10 +56,13 @@ display: flex; justify-content: center; align-items: center; - margin-bottom: 0.65rem; + border-top: none; + border-left:none; + border-right:none; border-top-left-radius: 0; border-bottom-left-radius: 0; border-bottom: 1px solid black; + border-top-right-radius: 0.25; } .banner-buttons { diff --git a/src/components/Banner.jsx b/src/components/Banner.jsx index 2642603..1633ca0 100644 --- a/src/components/Banner.jsx +++ b/src/components/Banner.jsx @@ -1,8 +1,9 @@ import './Banner.css'; import Icon from '../Icon.svg'; import Search from '../Search.svg'; +import { handleSignIn } from '../utilities/firebase'; -const Banner = () => { +export const Banner = () => { // const navigate = useNavigate(); // const navigateToReviews = () => { // navigate('/reviews') @@ -27,7 +28,7 @@ const Banner = () => { <button className="banner-btn"> Review </button> - <button className="banner-btn"> + <button className="banner-btn" onClick={handleSignIn}> Login </button> </div> @@ -35,4 +36,4 @@ const Banner = () => { ) } -export default Banner \ No newline at end of file +export default Banner; \ No newline at end of file diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx new file mode 100644 index 0000000..a198607 --- /dev/null +++ b/src/pages/LandingPage.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import '../App.css'; +import MapComponent from "../MapComponent"; +import Banner from '../components/Banner'; + +const LandingPage = () => { + + const cafesMap = [ + { name: "Cafe Blue", lat: 42.046, lng: -87.688 }, + { name: "Green Bean Cafe", lat: 42.048, lng: -87.684 }, + { name: "Java Lounge", lat: 42.044, lng: -87.690 }, + ]; + + return ( + <div className="App"> + <Banner /> + {/* Integrate MapComponent with hardcoded data */} + <div style={{ height: '600px', width: '100%'}}> + <MapComponent cafes={cafesMap} /> + </div> + </div> + ); +} + +export default LandingPage; \ No newline at end of file diff --git a/src/pages/ReviewPostPage.jsx b/src/pages/ReviewPostPage.jsx new file mode 100644 index 0000000..861853d --- /dev/null +++ b/src/pages/ReviewPostPage.jsx @@ -0,0 +1,157 @@ +import React, { useState, useEffect } from 'react'; +import '../App.css'; +import { useAuthState } from '../utilities/firebase'; +import { findCafes } from '../utilities/findCafes'; + +const ReviewPostPage = () => { + const [reviews, setReviews] = useState([]); + const [availability, setAvailability] = useState({}); + const [newReview, setNewReview] = useState(''); + const [selectedReview, setSelectedReview] = useState(null); + const [replyText, setReplyText] = useState(''); + const [cafeStatus, setCafeStatus] = useState({ seating: '', outlets: '' }); + const [address, setAddress] = useState(''); + const [dist, setDist] = useState(''); + const [cafes, setCafes] = useState([]); + useEffect(() => { }, []); + + const findCafesWrapper = () => { + findCafes(dist, address, setCafes); + }; + + const handleReviewSubmit = () => { + if (newReview.trim()) { + const review = { id: Date.now(), text: newReview, replies: [] }; + setReviews((prevReviews) => [...prevReviews, review]); + setNewReview(''); + } + }; + + const handleReplySubmit = (reviewId) => { + if (replyText.trim()) { + setReviews((prevReviews) => + prevReviews.map((review) => + review.id === reviewId + ? { ...review, replies: [...review.replies, replyText] } + : review + ) + ); + setReplyText(''); + setSelectedReview(null); + } + }; + + const handleAvailabilityUpdate = (cafeId) => { + const timestamp = new Date().toLocaleString(); + const newUpdate = { timestamp, seating: cafeStatus.seating, outlets: cafeStatus.outlets }; + + setAvailability((prevAvailability) => ({ + ...prevAvailability, + [cafeId]: { + history: [...(prevAvailability[cafeId]?.history || []), newUpdate], + }, + })); + setCafeStatus({ seating: '', outlets: '' }); + }; + return ( + <div> + <div className="listings-container"> + <div className="listings-column"> + <h2>Share a Review</h2> + <input + type="text" + placeholder="Share a quick review..." + value={newReview} + onChange={(e) => setNewReview(e.target.value)} + /> + <button onClick={handleReviewSubmit}>Submit</button> + + <ul className="review-list"> + {reviews.map((review) => ( + <li key={review.id} className="review-item"> + <p onClick={() => setSelectedReview(review)}>{review.text}</p> + <ul className="reply-list"> + {review.replies.map((reply, index) => ( + <li key={index} className="reply-item"> + <span className="reply-label">Reply:</span> {reply} + </li> + ))} + </ul> + {selectedReview && selectedReview.id === review.id && ( + <div className="reply-input"> + <input + type="text" + placeholder="Write a reply..." + value={replyText} + onChange={(e) => setReplyText(e.target.value)} + /> + <button onClick={() => handleReplySubmit(review.id)}>Send</button> + </div> + )} + </li> + ))} + </ul> + + <h2>Update Cafe Availability</h2> + <input + type="text" + placeholder="Seating status (e.g., Few seats left)" + value={cafeStatus.seating} + onChange={(e) => setCafeStatus({ ...cafeStatus, seating: e.target.value })} + /> + <input + type="text" + placeholder="Outlet status (e.g., All taken)" + value={cafeStatus.outlets} + onChange={(e) => setCafeStatus({ ...cafeStatus, outlets: e.target.value })} + /> + <button onClick={() => handleAvailabilityUpdate('cafe-123')}>Update Availability</button> + + <h2>Availability History</h2> + {availability['cafe-123']?.history?.map((update, index) => ( + <div key={index} className="availability-item"> + <p> + <strong>{update.timestamp}:</strong> Seating: {update.seating}, Outlets: {update.outlets} + </p> + </div> + ))} + </div> + + <div className="map-column"> + <h2>Cafes Near Me</h2> + <input + type="text" + placeholder="Enter zip code" + value={address} + onChange={(e) => setAddress(e.target.value)} + /> + <input + type="number" + placeholder="Enter radius (in miles)" + value={dist} + onChange={(e) => setDist(e.target.value)} + /> + <button onClick={findCafesWrapper}>Search</button> + <h2>Cafes Near Me</h2> + <ul> + {cafes.map((cafe, index) => ( + <li className="cafe-card" key={index}>{cafe}</li> + ))} + </ul> + + + <ul className="cafe-list"> + {cafes.map((cafesMap, index) => ( + <li key={index} className="cafe-item"> + {cafe.name} (Lat: {cafe.lat}, Lng: {cafe.lng}) + </li> + ))} + </ul> + + </div> + </div> + </div> + ) +} + +export default ReviewPostPage; \ No newline at end of file diff --git a/src/utilities/firebase.js b/src/utilities/firebase.js index 4390349..ea1d27d 100644 --- a/src/utilities/firebase.js +++ b/src/utilities/firebase.js @@ -27,16 +27,16 @@ const database = getDatabase(app); export const signInWithGoogle = () => { signInWithPopup(auth, provider) - .then(async (result) => { - console.log('User signed in:', result.user); - var zipcode = await findZipcode(result.user.email); - console.log("Zipcode: ", zipcode); - }) - .catch((error) => { - console.error('Error signing in with Google:', error); - }); - }; - + .then(async (result) => { + console.log('User signed in:', result.user); + var zipcode = await findZipcode(result.user.email); + console.log("Zipcode: ", zipcode); + }) + .catch((error) => { + console.error('Error signing in with Google:', error); + }); +}; + export const firebaseSignOut = () => signOut(auth); export const useAuthState = () => { @@ -53,13 +53,13 @@ export const useAuthState = () => { export const useDbData = (path) => { const [data, setData] = useState(); const [error, setError] = useState(null); - + useEffect(() => { const dbRef = ref(database, path); const unsubscribe = onValue( - dbRef, - (snapshot) => setData(snapshot.val()), - (error) => setError(error) + dbRef, + (snapshot) => setData(snapshot.val()), + (error) => setError(error) ); return () => unsubscribe(); }, [path]); @@ -72,9 +72,9 @@ export const useDbUpdate = (path) => { const updateData = useCallback( (value) => { - update(ref(database, path), value) - .then(() => setResult({ message: "Update successful", timestamp: Date.now() })) - .catch((error) => setResult({ error, message: error.message })); + update(ref(database, path), value) + .then(() => setResult({ message: "Update successful", timestamp: Date.now() })) + .catch((error) => setResult({ error, message: error.message })); }, [path] ); @@ -82,4 +82,14 @@ export const useDbUpdate = (path) => { return [updateData, result]; }; -export {database}; \ No newline at end of file +export const handleSignIn = async () => { + try { + await signInWithGoogle(); + } catch (error) { + console.error('Error signing in:', error); + } +}; + +export const handleLogout = () => firebaseSignOut(); + +export { database }; \ No newline at end of file From 06eb086a235c427e7e0eb1010009eed17df65f91 Mon Sep 17 00:00:00 2001 From: angelp03 <angelpimentel2025@u.northwestern.edu> Date: Fri, 8 Nov 2024 12:40:47 -0600 Subject: [PATCH 2/7] populate cafes with cafes near authenticated email --- src/App.jsx | 2 ++ src/components/Banner.jsx | 11 ++++---- src/components/CafeList.css | 5 +++- src/components/CafeList.jsx | 52 ++++--------------------------------- src/pages/LandingPage.jsx | 4 ++- src/pages/ReviewsPage.jsx | 42 ++++++++++++++++++++++++++---- src/utilities/findCafes.jsx | 6 ++++- 7 files changed, 62 insertions(+), 60 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 1dc6065..05dee09 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,7 @@ import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import LandingPage from './pages/LandingPage'; import ReviewPostPage from "./pages/ReviewPostPage"; +import ReviewsPage from "./pages/ReviewsPage"; const App = () => { return ( @@ -8,6 +9,7 @@ const App = () => { <Routes> <Route path="/" element={ <LandingPage/> }/> <Route path="/post" element={ <ReviewPostPage/> }/> + <Route path="/cafes" element={ <ReviewsPage/> }/> </Routes> </Router> ) diff --git a/src/components/Banner.jsx b/src/components/Banner.jsx index 1633ca0..308290c 100644 --- a/src/components/Banner.jsx +++ b/src/components/Banner.jsx @@ -2,12 +2,13 @@ import './Banner.css'; import Icon from '../Icon.svg'; import Search from '../Search.svg'; import { handleSignIn } from '../utilities/firebase'; +import { useNavigate } from 'react-router-dom'; export const Banner = () => { - // const navigate = useNavigate(); - // const navigateToReviews = () => { - // navigate('/reviews') - // } + const navigate = useNavigate(); + const navigateToReviews = () => { + navigate('/cafes') + } return ( <div className='banner-div'> <div className="logo-search-div"> @@ -19,7 +20,7 @@ export const Banner = () => { </div> <form> <input className="text-input" type="text" placeholder="Search for cafes near you" /> - <button className="search-btn"> + <button className="search-btn" onClick={navigateToReviews}> <img src={Search}/> </button> </form> diff --git a/src/components/CafeList.css b/src/components/CafeList.css index 18ba163..661a09d 100644 --- a/src/components/CafeList.css +++ b/src/components/CafeList.css @@ -1,6 +1,8 @@ .cafe-list-container { display: flex; gap: 2rem; + width: 100vw; + height: 100vh; } .filter { @@ -9,6 +11,7 @@ background-color: #f5f5f5; border-radius: 5px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + height: 100%; } .cafe-list { @@ -17,7 +20,7 @@ } .cafe-card { - width: 100%; + width: 95%; margin-bottom: 1rem; padding: 1rem; border: 1px solid #ccc; diff --git a/src/components/CafeList.jsx b/src/components/CafeList.jsx index e29411c..8a7d1bc 100644 --- a/src/components/CafeList.jsx +++ b/src/components/CafeList.jsx @@ -1,43 +1,10 @@ import './CafeList.css'; -const reviews = [ - { - id: 1, - author: 'John Doe', - rating: 4, - content: 'Currently busy.', - location: 'Starbucks', - }, - { - id: 2, - author: 'Jane Smith', - rating: 5, - content: 'Somewhat busy, some seats remain.', - location: 'Colectivo', - }, - { - id: 3, - author: 'Alice Johnson', - rating: 3, - content: 'Very empty, plenty of seats.', - location: "Phil's", - }, - { - id: 4, - author: 'Bob Brown', - rating: 5, - content: 'Very quiet and great coffee!', - location: 'Coffee Lab', - }, -]; - -const CafeList = () => { +const CafeList = ({ cafes }) => { return ( <div className="cafe-list-container"> - {/* Filter section */} <div className="filter"> <h4>Filter Options</h4> - {/* Example filter options */} <label> Rating: <select> @@ -53,21 +20,12 @@ const CafeList = () => { Location: <input type="text" placeholder="Search by location" /> </label> - {/* Add more filters here as needed */} </div> - - {/* Review cards */} <div className="cafe-list"> - {reviews.map((review) => ( - <div key={review.id} className="cafe-card"> - <h3>{review.author}</h3> - <p>{review.location}</p> - <div> - {Array.from({ length: review.rating }).map((_, index) => ( - <span key={index}>★</span> - ))} - </div> - <p>{review.content}</p> + {cafes.map((cafe, index) => ( + <div key={index} className="cafe-card"> + <h3>{cafe.name}</h3> + <p>{cafe.vicinity}</p> </div> ))} </div> diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index a198607..b38d666 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -2,9 +2,11 @@ import React from 'react'; import '../App.css'; import MapComponent from "../MapComponent"; import Banner from '../components/Banner'; +import { useAuthState } from '../utilities/firebase'; const LandingPage = () => { - + const [user] = useAuthState(); + console.log(user) const cafesMap = [ { name: "Cafe Blue", lat: 42.046, lng: -87.688 }, { name: "Green Bean Cafe", lat: 42.048, lng: -87.684 }, diff --git a/src/pages/ReviewsPage.jsx b/src/pages/ReviewsPage.jsx index c79993c..6fbc6fa 100644 --- a/src/pages/ReviewsPage.jsx +++ b/src/pages/ReviewsPage.jsx @@ -1,13 +1,45 @@ +import { useAuthState } from "../utilities/firebase"; +import { useState, useEffect } from "react"; +import { findZipcode } from "../utilities/findZipcode"; +import { findCafes } from "../utilities/findCafes"; import Banner from "../components/Banner"; import CafeList from "../components/CafeList"; const ReviewsPage = () => { + const [user] = useAuthState(); + const [zipcode, setZipcode] = useState(null); + const [cafes, setCafes] = useState([]); + + useEffect(() => { + const fetchData = async () => { + if (user && user.email) { + try { + const userZipcode = await findZipcode(user.email); + setZipcode(userZipcode); + + if (userZipcode) { + await findCafes(2, userZipcode, setCafes); + } + } catch (error) { + console.error("Error fetching zipcode or cafes:", error); + setZipcode(null); + setCafes([]); + } + } + }; + fetchData(); + }, [user]); + + useEffect(() => { + console.log(cafes); + }, [cafes]); + return ( <div> - <Banner/> - <CafeList/> + <Banner /> + <CafeList cafes={cafes} /> </div> - ) -} + ); +}; -export default ReviewsPage; \ No newline at end of file +export default ReviewsPage; diff --git a/src/utilities/findCafes.jsx b/src/utilities/findCafes.jsx index 98dea2a..7160390 100644 --- a/src/utilities/findCafes.jsx +++ b/src/utilities/findCafes.jsx @@ -15,7 +15,11 @@ export const findCafes = (dist, address, setCafes) => { }, (results, status) => { if (status === window.google.maps.places.PlacesServiceStatus.OK) { - setCafes(results.map((place) => place.name)); + setCafes(results.map((place) => ({ + name: place.name, + placeId: place.place_id, + vicinity: place.vicinity + }))); } else { alert("No cafes found within the specified radius."); setCafes([]); From 244bed195cd2e3351eeb3a718feece3b0d8c44bd Mon Sep 17 00:00:00 2001 From: angelp03 <angelpimentel2025@u.northwestern.edu> Date: Fri, 8 Nov 2024 13:08:22 -0600 Subject: [PATCH 3/7] navigation to cafe pages based on place id --- src/App.jsx | 2 + src/components/Banner.css | 39 +++++++++++++++--- src/components/Banner.jsx | 82 +++++++++++++++++++++++++++++++------ src/components/CafeList.jsx | 14 ++++++- src/pages/CafePage.jsx | 62 ++++++++++++++++++++++++++++ src/pages/LandingPage.jsx | 33 ++++++++++++++- src/pages/ReviewsPage.jsx | 2 +- 7 files changed, 211 insertions(+), 23 deletions(-) create mode 100644 src/pages/CafePage.jsx diff --git a/src/App.jsx b/src/App.jsx index 05dee09..49e5fbc 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,6 +2,7 @@ import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import LandingPage from './pages/LandingPage'; import ReviewPostPage from "./pages/ReviewPostPage"; import ReviewsPage from "./pages/ReviewsPage"; +import CafePage from "./pages/CafePage"; const App = () => { return ( @@ -10,6 +11,7 @@ const App = () => { <Route path="/" element={ <LandingPage/> }/> <Route path="/post" element={ <ReviewPostPage/> }/> <Route path="/cafes" element={ <ReviewsPage/> }/> + <Route path="/cafe/:place_id" element={<CafePage/>}/> </Routes> </Router> ) diff --git a/src/components/Banner.css b/src/components/Banner.css index f3016a4..670039d 100644 --- a/src/components/Banner.css +++ b/src/components/Banner.css @@ -16,7 +16,7 @@ } .logo-div { - height:100%; + height: 100%; display: flex; align-items: center; margin-right: 3rem; @@ -31,6 +31,7 @@ .logo-search-div form { display: flex; align-items: center; + position: relative; /* Allow positioning of the autocomplete dropdown */ } .text-input { @@ -57,12 +58,12 @@ justify-content: center; align-items: center; border-top: none; - border-left:none; - border-right:none; + border-left: none; + border-right: none; border-top-left-radius: 0; border-bottom-left-radius: 0; border-bottom: 1px solid black; - border-top-right-radius: 0.25; + border-top-right-radius: 0.25rem; } .banner-buttons { @@ -73,7 +74,7 @@ .banner-btn { text-align: center; - height:2.45rem; + height: 2.45rem; width: 8rem; background-color: #8A3323; color: white; @@ -88,4 +89,30 @@ .banner-btn:hover { background-color: #a85c43; -} \ No newline at end of file +} + +/* Autocomplete Dropdown */ +.autocomplete-dropdown { + position: absolute; + top: 100%; /* Align directly below the input field */ + left: 0; + right: 0; + background-color: white; + border: 1px solid #ddd; + border-radius: 0.25rem; + max-height: 200px; + overflow-y: auto; + z-index: 10; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + width: 22rem; /* Match the width of the input field */ +} + +.autocomplete-item { + padding: 10px; + cursor: pointer; + font-family: 'Hind Vadodara', sans-serif; +} + +.autocomplete-item:hover { + background-color: #f0f0f0; +} diff --git a/src/components/Banner.jsx b/src/components/Banner.jsx index 308290c..99609df 100644 --- a/src/components/Banner.jsx +++ b/src/components/Banner.jsx @@ -3,26 +3,84 @@ import Icon from '../Icon.svg'; import Search from '../Search.svg'; import { handleSignIn } from '../utilities/firebase'; import { useNavigate } from 'react-router-dom'; +import { useState } from 'react'; -export const Banner = () => { +export const Banner = ({ cafes }) => { const navigate = useNavigate(); - const navigateToReviews = () => { - navigate('/cafes') - } + const [searchText, setSearchText] = useState(''); + const [filteredCafes, setFilteredCafes] = useState([]); + const handleInputChange = (e) => { + const text = e.target.value; + setSearchText(text); + + if (text) { + const matches = cafes.filter(cafe => + cafe.name.toLowerCase().includes(text.toLowerCase()) + ); + setFilteredCafes(matches); + } else { + setFilteredCafes([]); + } + }; + + const handleSelectCafe = (cafe) => { + setSearchText(cafe.name); + setFilteredCafes([]); + navigate(`/cafe/${cafe.placeId}`); + }; + + + const handleSearch = (e) => { + e.preventDefault(); + + if (searchText.trim() === '') { + navigate('/cafes'); + } else { + const selectedCafe = cafes.find(cafe => + cafe.name.toLowerCase() === searchText.toLowerCase() + ); + + if (selectedCafe) { + navigate(`/cafe/${selectedCafe.placeId}`); + } else { + alert('Cafe not found.'); + } + } + }; + return ( <div className='banner-div'> <div className="logo-search-div"> <div className='logo-div'> - <img src={Icon}/> + <img src={Icon} alt="CafeWay Logo" /> <h1> CafeWay </h1> </div> - <form> - <input className="text-input" type="text" placeholder="Search for cafes near you" /> - <button className="search-btn" onClick={navigateToReviews}> - <img src={Search}/> + <form onSubmit={handleSearch}> + <input + className="text-input" + type="text" + placeholder="Search for cafes near you" + value={searchText} + onChange={handleInputChange} + /> + <button className="search-btn" type="submit"> + <img src={Search} alt="Search Icon" /> </button> + {filteredCafes.length > 0 && ( + <ul className="autocomplete-dropdown"> + {filteredCafes.map((cafe) => ( + <li + key={cafe.placeId} + onClick={() => handleSelectCafe(cafe)} + className="autocomplete-item" + > + {cafe.name} - {cafe.vicinity} + </li> + ))} + </ul> + )} </form> </div> <div className="banner-buttons"> @@ -34,7 +92,7 @@ export const Banner = () => { </button> </div> </div> - ) -} + ); +}; -export default Banner; \ No newline at end of file +export default Banner; diff --git a/src/components/CafeList.jsx b/src/components/CafeList.jsx index 8a7d1bc..c0c16e0 100644 --- a/src/components/CafeList.jsx +++ b/src/components/CafeList.jsx @@ -1,6 +1,12 @@ import './CafeList.css'; +import { useNavigate } from 'react-router-dom'; const CafeList = ({ cafes }) => { + const navigate = useNavigate(); + const handleClick = (placeId) => { + navigate(`/cafe/${placeId}`); + }; + return ( <div className="cafe-list-container"> <div className="filter"> @@ -22,8 +28,12 @@ const CafeList = ({ cafes }) => { </label> </div> <div className="cafe-list"> - {cafes.map((cafe, index) => ( - <div key={index} className="cafe-card"> + {cafes.map((cafe) => ( + <div + key={cafe.placeId} + className="cafe-card" + onClick={() => handleClick(cafe.placeId)} + > <h3>{cafe.name}</h3> <p>{cafe.vicinity}</p> </div> diff --git a/src/pages/CafePage.jsx b/src/pages/CafePage.jsx new file mode 100644 index 0000000..c29b1c2 --- /dev/null +++ b/src/pages/CafePage.jsx @@ -0,0 +1,62 @@ +import { useParams } from 'react-router-dom'; +import { useState, useEffect } from 'react'; +import { findZipcode } from '../utilities/findZipcode'; +import { findCafes } from '../utilities/findCafes'; +import { useAuthState } from '../utilities/firebase'; + +const CafePage = () => { + const { place_id } = useParams(); + const [user] = useAuthState(); + const [zipcode, setZipcode] = useState(null); + const [cafes, setCafes] = useState([]); + const [cafe, setCafe] = useState(null); + + useEffect(() => { + if (user && user.email) { + const fetchZipCode = async () => { + const userZipcode = await findZipcode(user.email); + setZipcode(userZipcode); + }; + fetchZipCode(); + } + }, [user]); + + useEffect(() => { + if (zipcode) { + const fetchCafes = async () => { + try { + await findCafes(5, zipcode, setCafes); + } catch (error) { + console.error('Error fetching cafes:', error); + setCafes([]); + } + }; + fetchCafes(); + } + }, [zipcode]); + + useEffect(() => { + if (cafes.length > 0) { + const foundCafe = cafes.find((cafe) => cafe.placeId === place_id); + setCafe(foundCafe); + } + }, [cafes, place_id]); + + if (!cafe) { + return ( + <div> + <h1>Cafe not found</h1> + <p>Sorry, we couldn't find a cafe with the specified ID.</p> + </div> + ); + } + + return ( + <div> + <h1>{cafe.name}</h1> + <p><strong>Location:</strong> {cafe.vicinity}</p> + </div> + ); +} + +export default CafePage; diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index b38d666..0ed5ef1 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -3,10 +3,39 @@ import '../App.css'; import MapComponent from "../MapComponent"; import Banner from '../components/Banner'; import { useAuthState } from '../utilities/firebase'; +import { useState, useEffect } from "react"; +import { findZipcode } from "../utilities/findZipcode"; +import { findCafes } from "../utilities/findCafes"; const LandingPage = () => { const [user] = useAuthState(); - console.log(user) + const [zipcode, setZipcode] = useState(null); + const [cafes, setCafes] = useState([]); + + useEffect(() => { + const fetchData = async () => { + if (user && user.email) { + try { + const userZipcode = await findZipcode(user.email); + setZipcode(userZipcode); + + if (userZipcode) { + await findCafes(2, userZipcode, setCafes); + } + } catch (error) { + console.error("Error fetching zipcode or cafes:", error); + setZipcode(null); + setCafes([]); + } + } + }; + fetchData(); + }, [user]); + + useEffect(() => { + console.log(cafes); + }, [cafes]); + const cafesMap = [ { name: "Cafe Blue", lat: 42.046, lng: -87.688 }, { name: "Green Bean Cafe", lat: 42.048, lng: -87.684 }, @@ -15,7 +44,7 @@ const LandingPage = () => { return ( <div className="App"> - <Banner /> + <Banner cafes={cafes} /> {/* Integrate MapComponent with hardcoded data */} <div style={{ height: '600px', width: '100%'}}> <MapComponent cafes={cafesMap} /> diff --git a/src/pages/ReviewsPage.jsx b/src/pages/ReviewsPage.jsx index 6fbc6fa..f7c8750 100644 --- a/src/pages/ReviewsPage.jsx +++ b/src/pages/ReviewsPage.jsx @@ -36,7 +36,7 @@ const ReviewsPage = () => { return ( <div> - <Banner /> + <Banner cafes={cafes}/> <CafeList cafes={cafes} /> </div> ); From fac5ddcd02a64fdfc09e2913fde9524bd6a21db8 Mon Sep 17 00:00:00 2001 From: angelp03 <angelpimentel2025@u.northwestern.edu> Date: Fri, 8 Nov 2024 13:30:16 -0600 Subject: [PATCH 4/7] populate cafe page with posts and responses --- database.rules.json | 4 +-- src/dummy.json | 31 +++++++++++++++++ src/pages/CafePage.jsx | 50 +++++++++++++++++++++++++++ src/utilities/{posts.js => posts.jsx} | 3 +- 4 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 src/dummy.json rename src/utilities/{posts.js => posts.jsx} (94%) diff --git a/database.rules.json b/database.rules.json index f54493d..f122d7b 100644 --- a/database.rules.json +++ b/database.rules.json @@ -1,7 +1,7 @@ { /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */ "rules": { - ".read": false, - ".write": false + ".read": true, + ".write": true } } \ No newline at end of file diff --git a/src/dummy.json b/src/dummy.json new file mode 100644 index 0000000..bd2b4d8 --- /dev/null +++ b/src/dummy.json @@ -0,0 +1,31 @@ +{ + "test_id": { + "cafeId": "ChIJ60vPsg_QD4gRE61MVG65nSs", + "content": "Great place for coffee and work!", + "category": "Review", + "date": "2024-11-07T14:30:00Z", + "replies": { + "replyId_1": "I agree, the coffee is fantastic!", + "replyId_2": "The WiFi is also great for working!" + }, + "uid": "user_1" + }, + "test_id_2": { + "cafeId": "ChIJ60vPsg_QD4gRE61MVG65nSs", + "content": "Could use more seating during peak hours.", + "category": "Suggestion", + "date": "2024-11-08T09:00:00Z", + "replies": {}, + "uid": "user_2" + }, + "test_id_3": { + "cafeId": "ChIJ60vPsg_QD4gRE61MVG65nSs", + "content": "Is the cafe open late on weekends?", + "category": "Question", + "date": "2024-11-09T11:45:00Z", + "replies": { + "replyId_1": "Yes, they stay open until 10 PM on Saturdays and Sundays!" + }, + "uid": "user_3" + } +} \ No newline at end of file diff --git a/src/pages/CafePage.jsx b/src/pages/CafePage.jsx index c29b1c2..e5f722e 100644 --- a/src/pages/CafePage.jsx +++ b/src/pages/CafePage.jsx @@ -3,6 +3,7 @@ import { useState, useEffect } from 'react'; import { findZipcode } from '../utilities/findZipcode'; import { findCafes } from '../utilities/findCafes'; import { useAuthState } from '../utilities/firebase'; +import { findCafePosts } from '../utilities/posts'; // Import the hook const CafePage = () => { const { place_id } = useParams(); @@ -42,6 +43,10 @@ const CafePage = () => { } }, [cafes, place_id]); + // Use the findCafePosts hook correctly here + const [posts, postError] = findCafePosts(cafe?.placeId); // Using the hook directly + console.log(posts); // Check the format of posts + if (!cafe) { return ( <div> @@ -51,10 +56,55 @@ const CafePage = () => { ); } + // Assuming posts are in the form of an object with keys like test_id + const postItems = posts ? Object.values(posts) : []; // Convert the object into an array if needed + return ( <div> <h1>{cafe.name}</h1> <p><strong>Location:</strong> {cafe.vicinity}</p> + + {/* Render posts if they exist */} + {postItems && postItems.length > 0 ? ( + <div> + <h2>Posts</h2> + <ul> + {postItems.map((post, index) => ( + <li key={index}> + <p><strong>Category:</strong> {post.category}</p> + <p><strong>Content:</strong> {post.content}</p> + <small>{new Date(post.date).toLocaleString()}</small> + + {/* Render replies if they exist */} + {post.replies && Object.entries(post.replies).length > 0 ? ( + <div style={{ marginLeft: '20px', marginTop: '10px' }}> + <h3>Replies:</h3> + <ul> + {Object.entries(post.replies).map(([replyId, message], replyIndex) => ( + <li key={replyIndex}> + <p>{message}</p> + </li> + ))} + </ul> + </div> + ) : ( + <p>No replies for this post.</p> + )} + </li> + ))} + </ul> + </div> + ) : ( + <p>No posts available for this cafe.</p> + )} + + {/* Handle errors in fetching posts */} + {postError && ( + <div> + <h3>Error loading posts</h3> + <p>{postError.message}</p> + </div> + )} </div> ); } diff --git a/src/utilities/posts.js b/src/utilities/posts.jsx similarity index 94% rename from src/utilities/posts.js rename to src/utilities/posts.jsx index d1f47f1..050ff36 100644 --- a/src/utilities/posts.js +++ b/src/utilities/posts.jsx @@ -11,11 +11,11 @@ export const findCafePosts = (cafeId) => { const postsRef = ref(database, `/posts`); const postsQuery = query(postsRef, orderByChild('cafeId'), equalTo(cafeId)); - const unsubscribe = onValue( postsQuery, (snapshot) => { if (snapshot.exists()) { + console.log(snapshot.val()); setData(snapshot.val()); } else { setData([]); @@ -28,6 +28,5 @@ export const findCafePosts = (cafeId) => { return () => unsubscribe(); }, [cafeId]); - return [data, error]; }; \ No newline at end of file From 0db40fdbab72ab97907cd43b66e9278a9b3dbdc4 Mon Sep 17 00:00:00 2001 From: angelp03 <angelpimentel2025@u.northwestern.edu> Date: Fri, 8 Nov 2024 13:36:10 -0600 Subject: [PATCH 5/7] improve frontend of cafe page --- src/pages/CafePage.css | 90 ++++++++++++++++++++++++++++++++++++ src/pages/CafePage.jsx | 102 +++++++++++++++++++++-------------------- 2 files changed, 143 insertions(+), 49 deletions(-) create mode 100644 src/pages/CafePage.css diff --git a/src/pages/CafePage.css b/src/pages/CafePage.css new file mode 100644 index 0000000..5aa8304 --- /dev/null +++ b/src/pages/CafePage.css @@ -0,0 +1,90 @@ +.cafe-page-container { + padding: 2rem; + font-family: 'Hind Vadodara', sans-serif; +} + +.cafe-header { + margin-bottom: 2rem; + border-bottom: 1px solid #ddd; + padding-bottom: 1rem; + text-align: center; +} + +.cafe-header h1 { + font-size: 2.5rem; + color: #8A3323; + margin-bottom: 0.5rem; +} + +.cafe-header p { + font-size: 1.2rem; +} + +.posts-section { + margin-top: 2rem; +} + +.posts-section h2 { + font-size: 1.8rem; + margin-bottom: 1rem; + color: #8A3323; +} +.posts-list { + list-style-type: none; + padding: 0; +} + +.post-item { + padding: 1rem; + margin-bottom: 1rem; + border: 1px solid #ddd; + border-radius: 8px; + background-color: #fafafa; +} + +.post-header { + margin-bottom: 1rem; + font-size: 1rem; + color: #555; +} + +.post-header strong { + color: #8A3323; +} + +.post-content p { + font-size: 1.2rem; + color: #333; +} + +.replies-section { + margin-top: 1rem; + padding-left: 20px; + background-color: #f9f9f9; + border-left: 3px solid #8A3323; +} + +.replies-section h3 { + font-size: 1.5rem; + margin-bottom: 1rem; +} + +.reply-item { + margin-bottom: 0.8rem; + font-size: 1.1rem; + color: #333; +} + +.error-message { + text-align: center; + padding: 2rem; + background-color: #ffebee; + border: 1px solid #f44336; + border-radius: 8px; + color: #d32f2f; +} + +.error-message h1, +.error-message h3 { + color: #d32f2f; +} diff --git a/src/pages/CafePage.jsx b/src/pages/CafePage.jsx index e5f722e..939e376 100644 --- a/src/pages/CafePage.jsx +++ b/src/pages/CafePage.jsx @@ -1,9 +1,11 @@ +import './CafePage.css'; import { useParams } from 'react-router-dom'; import { useState, useEffect } from 'react'; import { findZipcode } from '../utilities/findZipcode'; import { findCafes } from '../utilities/findCafes'; import { useAuthState } from '../utilities/firebase'; -import { findCafePosts } from '../utilities/posts'; // Import the hook +import { findCafePosts } from '../utilities/posts'; +import Banner from '../components/Banner'; const CafePage = () => { const { place_id } = useParams(); @@ -43,68 +45,70 @@ const CafePage = () => { } }, [cafes, place_id]); - // Use the findCafePosts hook correctly here - const [posts, postError] = findCafePosts(cafe?.placeId); // Using the hook directly - console.log(posts); // Check the format of posts + const [posts, postError] = findCafePosts(cafe?.placeId); + console.log(posts); if (!cafe) { return ( - <div> + <div className="error-message"> <h1>Cafe not found</h1> <p>Sorry, we couldn't find a cafe with the specified ID.</p> </div> ); } - // Assuming posts are in the form of an object with keys like test_id - const postItems = posts ? Object.values(posts) : []; // Convert the object into an array if needed + const postItems = posts ? Object.values(posts) : []; return ( <div> - <h1>{cafe.name}</h1> - <p><strong>Location:</strong> {cafe.vicinity}</p> - - {/* Render posts if they exist */} - {postItems && postItems.length > 0 ? ( - <div> - <h2>Posts</h2> - <ul> - {postItems.map((post, index) => ( - <li key={index}> - <p><strong>Category:</strong> {post.category}</p> - <p><strong>Content:</strong> {post.content}</p> - <small>{new Date(post.date).toLocaleString()}</small> - - {/* Render replies if they exist */} - {post.replies && Object.entries(post.replies).length > 0 ? ( - <div style={{ marginLeft: '20px', marginTop: '10px' }}> - <h3>Replies:</h3> - <ul> - {Object.entries(post.replies).map(([replyId, message], replyIndex) => ( - <li key={replyIndex}> - <p>{message}</p> - </li> - ))} - </ul> - </div> - ) : ( - <p>No replies for this post.</p> - )} - </li> - ))} - </ul> + <Banner cafes={cafes}/> + <div className="cafe-page-container"> + <div className="cafe-header"> + <h1>{cafe.name}</h1> + <p><strong>Location:</strong> {cafe.vicinity}</p> </div> - ) : ( - <p>No posts available for this cafe.</p> - )} - {/* Handle errors in fetching posts */} - {postError && ( - <div> - <h3>Error loading posts</h3> - <p>{postError.message}</p> - </div> - )} + {postItems && postItems.length > 0 ? ( + <div className="posts-section"> + <h2>Posts</h2> + <ul className="posts-list"> + {postItems.map((post, index) => ( + <li key={index} className="post-item"> + <div className="post-header"> + <p><strong>Category:</strong> {post.category}</p> + <small>{new Date(post.date).toLocaleString()}</small> + </div> + <div className="post-content"> + <p>{post.content}</p> + </div> + {post.replies && Object.entries(post.replies).length > 0 ? ( + <div className="replies-section"> + <h3>Replies:</h3> + <ul> + {Object.entries(post.replies).map(([replyId, message], replyIndex) => ( + <li key={replyIndex} className="reply-item"> + <p>{message}</p> + </li> + ))} + </ul> + </div> + ) : ( + <p>No replies for this post.</p> + )} + </li> + ))} + </ul> + </div> + ) : ( + <p>No posts available for this cafe.</p> + )} + {postError && ( + <div className="error-message"> + <h3>Error loading posts</h3> + <p>{postError.message}</p> + </div> + )} + </div> </div> ); } From 13f6919cb23b3ab8d03bf8fa508125575c4e4fef Mon Sep 17 00:00:00 2001 From: angelp03 <angelpimentel2025@u.northwestern.edu> Date: Fri, 8 Nov 2024 13:41:07 -0600 Subject: [PATCH 6/7] update login/logout button based on auth state --- src/components/Banner.jsx | 23 +++++++++++++++-------- src/pages/CafePage.jsx | 10 ++++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/components/Banner.jsx b/src/components/Banner.jsx index 99609df..b5a7a2f 100644 --- a/src/components/Banner.jsx +++ b/src/components/Banner.jsx @@ -1,14 +1,17 @@ import './Banner.css'; import Icon from '../Icon.svg'; import Search from '../Search.svg'; -import { handleSignIn } from '../utilities/firebase'; import { useNavigate } from 'react-router-dom'; import { useState } from 'react'; +import { handleSignIn, handleLogout, useAuthState } from '../utilities/firebase'; export const Banner = ({ cafes }) => { const navigate = useNavigate(); const [searchText, setSearchText] = useState(''); const [filteredCafes, setFilteredCafes] = useState([]); + + const [user] = useAuthState(); + const handleInputChange = (e) => { const text = e.target.value; setSearchText(text); @@ -29,7 +32,6 @@ export const Banner = ({ cafes }) => { navigate(`/cafe/${cafe.placeId}`); }; - const handleSearch = (e) => { e.preventDefault(); @@ -53,9 +55,7 @@ export const Banner = ({ cafes }) => { <div className="logo-search-div"> <div className='logo-div'> <img src={Icon} alt="CafeWay Logo" /> - <h1> - CafeWay - </h1> + <h1>CafeWay</h1> </div> <form onSubmit={handleSearch}> <input @@ -83,13 +83,20 @@ export const Banner = ({ cafes }) => { )} </form> </div> + <div className="banner-buttons"> <button className="banner-btn"> Review </button> - <button className="banner-btn" onClick={handleSignIn}> - Login - </button> + {user ? ( + <button className="banner-btn" onClick={handleLogout}> + Logout + </button> + ) : ( + <button className="banner-btn" onClick={handleSignIn}> + Login + </button> + )} </div> </div> ); diff --git a/src/pages/CafePage.jsx b/src/pages/CafePage.jsx index 939e376..08b1485 100644 --- a/src/pages/CafePage.jsx +++ b/src/pages/CafePage.jsx @@ -57,22 +57,24 @@ const CafePage = () => { ); } - const postItems = posts ? Object.values(posts) : []; + const postItems = posts ? Object.values(posts) : []; + + const sortedPosts = postItems.sort((a, b) => new Date(b.date) - new Date(a.date)); return ( <div> - <Banner cafes={cafes}/> + <Banner cafes={cafes} /> <div className="cafe-page-container"> <div className="cafe-header"> <h1>{cafe.name}</h1> <p><strong>Location:</strong> {cafe.vicinity}</p> </div> - {postItems && postItems.length > 0 ? ( + {sortedPosts && sortedPosts.length > 0 ? ( <div className="posts-section"> <h2>Posts</h2> <ul className="posts-list"> - {postItems.map((post, index) => ( + {sortedPosts.map((post, index) => ( <li key={index} className="post-item"> <div className="post-header"> <p><strong>Category:</strong> {post.category}</p> From cd0b6028a785ca830035694228220fb4587e8880 Mon Sep 17 00:00:00 2001 From: angelp03 <angelpimentel2025@u.northwestern.edu> Date: Fri, 8 Nov 2024 14:00:50 -0600 Subject: [PATCH 7/7] allow replies and fix zipcode lookup --- src/pages/CafePage.jsx | 52 +++++++++++++++++++++++++++++++---- src/pages/ReviewsPage.jsx | 1 + src/utilities/findZipcode.jsx | 3 +- src/utilities/posts.jsx | 34 +++++++++++++++++++++-- 4 files changed, 81 insertions(+), 9 deletions(-) diff --git a/src/pages/CafePage.jsx b/src/pages/CafePage.jsx index 08b1485..8a2fd53 100644 --- a/src/pages/CafePage.jsx +++ b/src/pages/CafePage.jsx @@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'; import { findZipcode } from '../utilities/findZipcode'; import { findCafes } from '../utilities/findCafes'; import { useAuthState } from '../utilities/firebase'; -import { findCafePosts } from '../utilities/posts'; +import { findCafePosts, addReplyToPost } from '../utilities/posts'; import Banner from '../components/Banner'; const CafePage = () => { @@ -13,6 +13,7 @@ const CafePage = () => { const [zipcode, setZipcode] = useState(null); const [cafes, setCafes] = useState([]); const [cafe, setCafe] = useState(null); + const [replyMessages, setReplyMessages] = useState({}); useEffect(() => { if (user && user.email) { @@ -46,7 +47,6 @@ const CafePage = () => { }, [cafes, place_id]); const [posts, postError] = findCafePosts(cafe?.placeId); - console.log(posts); if (!cafe) { return ( @@ -57,9 +57,30 @@ const CafePage = () => { ); } - const postItems = posts ? Object.values(posts) : []; + const postItems = posts ? Object.entries(posts) : []; + const sortedPosts = postItems.sort((a, b) => new Date(b[1].date) - new Date(a[1].date)); - const sortedPosts = postItems.sort((a, b) => new Date(b.date) - new Date(a.date)); + const handleReplyChange = (e, postId) => { + setReplyMessages((prev) => ({ + ...prev, + [postId]: e.target.value, + })); + }; + + const handleAddReply = async (postId) => { + const replyMessage = replyMessages[postId]; + if (replyMessage && replyMessage.trim()) { + const error = await addReplyToPost(postId, user?.uid, replyMessage); + if (error) { + console.error('Error adding reply:', error); + } else { + setReplyMessages((prev) => ({ + ...prev, + [postId]: '', + })); + } + } + }; return ( <div> @@ -74,7 +95,7 @@ const CafePage = () => { <div className="posts-section"> <h2>Posts</h2> <ul className="posts-list"> - {sortedPosts.map((post, index) => ( + {sortedPosts.map(([postKey, post], index) => ( <li key={index} className="post-item"> <div className="post-header"> <p><strong>Category:</strong> {post.category}</p> @@ -83,6 +104,7 @@ const CafePage = () => { <div className="post-content"> <p>{post.content}</p> </div> + {post.replies && Object.entries(post.replies).length > 0 ? ( <div className="replies-section"> <h3>Replies:</h3> @@ -97,6 +119,24 @@ const CafePage = () => { ) : ( <p>No replies for this post.</p> )} + + {user ? ( + <div className="reply-section"> + <textarea + value={replyMessages[postKey] || ''} + onChange={(e) => handleReplyChange(e, postKey)} + placeholder="Write your reply..." + /> + <button + onClick={() => handleAddReply(postKey)} + className="reply-btn" + > + Reply + </button> + </div> + ) : ( + <p>Please log in to reply.</p> + )} </li> ))} </ul> @@ -113,6 +153,6 @@ const CafePage = () => { </div> </div> ); -} +}; export default CafePage; diff --git a/src/pages/ReviewsPage.jsx b/src/pages/ReviewsPage.jsx index f7c8750..f184840 100644 --- a/src/pages/ReviewsPage.jsx +++ b/src/pages/ReviewsPage.jsx @@ -9,6 +9,7 @@ const ReviewsPage = () => { const [user] = useAuthState(); const [zipcode, setZipcode] = useState(null); const [cafes, setCafes] = useState([]); + console.log(user); useEffect(() => { const fetchData = async () => { diff --git a/src/utilities/findZipcode.jsx b/src/utilities/findZipcode.jsx index 740ced2..f16b393 100644 --- a/src/utilities/findZipcode.jsx +++ b/src/utilities/findZipcode.jsx @@ -41,7 +41,8 @@ const getZipCode = async (placeId) => { const getUniversity = (email) => { const domain = email.split('@')[1]; - const university = domain.split('.')[0]; + console.log(domain); + const university = domain.split('.')[1]; return university; } diff --git a/src/utilities/posts.jsx b/src/utilities/posts.jsx index 050ff36..8bd36fe 100644 --- a/src/utilities/posts.jsx +++ b/src/utilities/posts.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { onValue, query, orderByChild, equalTo, ref } from 'firebase/database'; +import { onValue, query, orderByChild, equalTo, ref, get, update } from 'firebase/database'; import { database } from './firebase'; export const findCafePosts = (cafeId) => { @@ -29,4 +29,34 @@ export const findCafePosts = (cafeId) => { return () => unsubscribe(); }, [cafeId]); return [data, error]; -}; \ No newline at end of file +}; + +export const addReplyToPost = async (postId, userId, replyMessage) => { + try { + const postRef = ref(database, `/posts/${postId}`); + const postSnapshot = await get(postRef); + if (!postSnapshot.exists()) { + return 'Post not found'; + } + + const postData = postSnapshot.val(); + const replies = postData.replies || {}; + const replyId = `replyId_${Object.keys(replies).length + 1}`; + + const newReply = { + [replyId]: replyMessage + }; + + await update(postRef, { + replies: { + ...replies, + ...newReply + } + }); + + return null; + } catch (error) { + console.error('Error adding reply:', error); + return error.message || 'Error adding reply'; + } +};