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/App.jsx b/src/App.jsx index f300761..49e5fbc 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,194 +1,20 @@ -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"; +import ReviewsPage from "./pages/ReviewsPage"; +import CafePage from "./pages/CafePage"; +const App = () => { return ( -
-
-

CafeWay

- {user ? ( - <> - -

Welcome, {user.email}

- - ) : ( - - )} -
- -
-
-

Share a Review

- setNewReview(e.target.value)} - /> - - -
    - {reviews.map((review) => ( -
  • -

    setSelectedReview(review)}>{review.text}

    -
      - {review.replies.map((reply, index) => ( -
    • - Reply: {reply} -
    • - ))} -
    - {selectedReview && selectedReview.id === review.id && ( -
    - setReplyText(e.target.value)} - /> - -
    - )} -
  • - ))} -
- -

Update Cafe Availability

- setCafeStatus({ ...cafeStatus, seating: e.target.value })} - /> - setCafeStatus({ ...cafeStatus, outlets: e.target.value })} - /> - - -

Availability History

- {availability['cafe-123']?.history?.map((update, index) => ( -
-

- {update.timestamp}: Seating: {update.seating}, Outlets: {update.outlets} -

-
- ))} -
- -
-

Cafes Near Me

- setAddress(e.target.value)} - /> - setDist(e.target.value)} - /> - -

Cafes Near Me

-
    - {cafes.map((cafe, index) => ( -
  • {cafe}
  • - ))} -
- - -
    - {cafes.map((cafesMap, index) => ( -
  • - {cafe.name} (Lat: {cafe.lat}, Lng: {cafe.lng}) -
  • - ))} -
- - {/* Integrate MapComponent with hardcoded data */} -
- -
-
-
-
- ); + + + }/> + }/> + }/> + }/> + + + ) } export default App; diff --git a/src/components/Banner.css b/src/components/Banner.css index 89c1df9..670039d 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 { @@ -16,7 +16,7 @@ } .logo-div { - height:100%; + height: 100%; display: flex; align-items: center; margin-right: 3rem; @@ -31,11 +31,14 @@ .logo-search-div form { display: flex; align-items: center; + position: relative; /* Allow positioning of the autocomplete dropdown */ } .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 +49,7 @@ } .search-btn { - height: 2.6rem; + height: 2.3rem; width: 3rem; background-color: #8A3323; border: none; @@ -54,10 +57,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.25rem; } .banner-buttons { @@ -68,7 +74,7 @@ .banner-btn { text-align: center; - height:2.45rem; + height: 2.45rem; width: 8rem; background-color: #8A3323; color: white; @@ -83,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 2642603..b5a7a2f 100644 --- a/src/components/Banner.jsx +++ b/src/components/Banner.jsx @@ -1,38 +1,105 @@ import './Banner.css'; import Icon from '../Icon.svg'; import Search from '../Search.svg'; +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); + + 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.'); + } + } + }; -const Banner = () => { - // const navigate = useNavigate(); - // const navigateToReviews = () => { - // navigate('/reviews') - // } return (
- -

- CafeWay -

+ CafeWay Logo +

CafeWay

-
- - + {filteredCafes.length > 0 && ( +
    + {filteredCafes.map((cafe) => ( +
  • handleSelectCafe(cafe)} + className="autocomplete-item" + > + {cafe.name} - {cafe.vicinity} +
  • + ))} +
+ )}
+
- + {user ? ( + + ) : ( + + )}
- ) -} + ); +}; -export default Banner \ No newline at end of file +export default Banner; 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..c0c16e0 100644 --- a/src/components/CafeList.jsx +++ b/src/components/CafeList.jsx @@ -1,43 +1,16 @@ import './CafeList.css'; +import { useNavigate } from 'react-router-dom'; -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 = ({ cafes }) => { + const navigate = useNavigate(); + const handleClick = (placeId) => { + navigate(`/cafe/${placeId}`); + }; -const CafeList = () => { return (
- {/* Filter section */}

Filter Options

- {/* Example filter options */} - {/* Add more filters here as needed */}
- - {/* Review cards */}
- {reviews.map((review) => ( -
-

{review.author}

-

{review.location}

-
- {Array.from({ length: review.rating }).map((_, index) => ( - - ))} -
-

{review.content}

+ {cafes.map((cafe) => ( +
handleClick(cafe.placeId)} + > +

{cafe.name}

+

{cafe.vicinity}

))}
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.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 new file mode 100644 index 0000000..8a2fd53 --- /dev/null +++ b/src/pages/CafePage.jsx @@ -0,0 +1,158 @@ +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, addReplyToPost } from '../utilities/posts'; +import Banner from '../components/Banner'; + +const CafePage = () => { + const { place_id } = useParams(); + const [user] = useAuthState(); + const [zipcode, setZipcode] = useState(null); + const [cafes, setCafes] = useState([]); + const [cafe, setCafe] = useState(null); + const [replyMessages, setReplyMessages] = useState({}); + + 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]); + + const [posts, postError] = findCafePosts(cafe?.placeId); + + if (!cafe) { + return ( +
+

Cafe not found

+

Sorry, we couldn't find a cafe with the specified ID.

+
+ ); + } + + const postItems = posts ? Object.entries(posts) : []; + const sortedPosts = postItems.sort((a, b) => new Date(b[1].date) - new Date(a[1].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 ( +
+ +
+
+

{cafe.name}

+

Location: {cafe.vicinity}

+
+ + {sortedPosts && sortedPosts.length > 0 ? ( +
+

Posts

+
    + {sortedPosts.map(([postKey, post], index) => ( +
  • +
    +

    Category: {post.category}

    + {new Date(post.date).toLocaleString()} +
    +
    +

    {post.content}

    +
    + + {post.replies && Object.entries(post.replies).length > 0 ? ( +
    +

    Replies:

    +
      + {Object.entries(post.replies).map(([replyId, message], replyIndex) => ( +
    • +

      {message}

      +
    • + ))} +
    +
    + ) : ( +

    No replies for this post.

    + )} + + {user ? ( +
    +