diff --git a/package-lock.json b/package-lock.json index bf631b2..946ae7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1387,6 +1388,7 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1428,6 +1430,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1533,6 +1536,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -1767,6 +1771,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2453,6 +2458,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2514,6 +2520,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2523,6 +2530,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -2788,6 +2796,7 @@ "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -2909,6 +2918,7 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/App.jsx b/src/App.jsx index 4706134..ef08f54 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -19,40 +19,48 @@ import NotFound from "./pages/NotFound"; import { RestaurantsProvider } from "./context/RestaurantsContext"; import { AuthProvider } from "./context/AuthContext"; +import { ToastProvider } from "./context/ToastContext"; +import ToastContainer from "./components/common/ToastContainer"; +import { useToastContext } from "./context/ToastContext"; import "./styles/main.css"; +function AppContent() { + const { toasts, removeToast } = useToastContext(); + return ( +
+ +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
- - - +
+ ); +} function App() { return ( -
- -
- - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - -
- -
-
+ + +
diff --git a/src/components/common/EmptyState.jsx b/src/components/common/EmptyState.jsx new file mode 100644 index 0000000..c4907d9 --- /dev/null +++ b/src/components/common/EmptyState.jsx @@ -0,0 +1,37 @@ +import React from "react"; +import { Link } from "react-router-dom"; + +function EmptyState({ title, message, suggestions = [], actionLabel, actionLink }) { + return ( +
+
+ + + + +
+

{title}

+

{message}

+ + {suggestions.length > 0 && ( +
+

Suggestions:

+ +
+ )} + + {actionLabel && actionLink && ( + + {actionLabel} + + )} +
+ ); +} + +export default EmptyState; + diff --git a/src/components/common/FilterChips.jsx b/src/components/common/FilterChips.jsx new file mode 100644 index 0000000..c57eff4 --- /dev/null +++ b/src/components/common/FilterChips.jsx @@ -0,0 +1,47 @@ +import React from "react"; + +function FilterChips({ filters, onRemoveFilter, onClearAll }) { + const activeFilters = []; + + if (filters.category) { + activeFilters.push({ key: "category", label: `Category: ${filters.category}`, value: filters.category }); + } + if (filters.minRating > 0) { + activeFilters.push({ key: "minRating", label: `Rating: ${filters.minRating.toFixed(1)}+`, value: filters.minRating }); + } + if (filters.price) { + activeFilters.push({ key: "price", label: `Price: ${filters.price}`, value: filters.price }); + } + + if (activeFilters.length === 0) return null; + + return ( +
+
Active filters:
+
+ {activeFilters.map((filter) => ( + + ))} + {activeFilters.length > 1 && ( + + )} +
+
+ ); +} + +export default FilterChips; + diff --git a/src/components/common/ShareButton.jsx b/src/components/common/ShareButton.jsx new file mode 100644 index 0000000..a28a833 --- /dev/null +++ b/src/components/common/ShareButton.jsx @@ -0,0 +1,50 @@ +import React, { useState } from "react"; + +function ShareButton({ url, title, text }) { + const [copied, setCopied] = useState(false); + + async function handleShare() { + const shareUrl = url || window.location.href; + const shareText = text || title || "Check out this restaurant on Find Addis!"; + + if (navigator.share) { + try { + await navigator.share({ + title: title || "Find Addis", + text: shareText, + url: shareUrl, + }); + } catch (err) { + // User cancelled or error occurred + if (err.name !== "AbortError") { + copyToClipboard(shareUrl); + } + } + } else { + copyToClipboard(shareUrl); + } + } + + function copyToClipboard(text) { + navigator.clipboard.writeText(text).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + } + + return ( + + ); +} + +export default ShareButton; + diff --git a/src/components/common/SortDropdown.jsx b/src/components/common/SortDropdown.jsx new file mode 100644 index 0000000..fa19bd2 --- /dev/null +++ b/src/components/common/SortDropdown.jsx @@ -0,0 +1,55 @@ +import React from "react"; +import Dropdown from "./Dropdown"; + +const SORT_OPTIONS = [ + { value: "rating-desc", label: "Rating: High to Low" }, + { value: "rating-asc", label: "Rating: Low to High" }, + { value: "name-asc", label: "Name: A to Z" }, + { value: "name-desc", label: "Name: Z to A" }, + { value: "price-asc", label: "Price: Low to High" }, + { value: "price-desc", label: "Price: High to Low" }, +]; + +function SortDropdown({ value, onChange, className = "" }) { + return ( +
+ + onChange(e.target.value)} + options={SORT_OPTIONS} + className={className} + /> +
+ ); +} + +export function sortRestaurants(restaurants, sortBy) { + const sorted = [...restaurants]; + + switch (sortBy) { + case "rating-desc": + return sorted.sort((a, b) => (b.rating || 0) - (a.rating || 0)); + case "rating-asc": + return sorted.sort((a, b) => (a.rating || 0) - (b.rating || 0)); + case "name-asc": + return sorted.sort((a, b) => a.name.localeCompare(b.name)); + case "name-desc": + return sorted.sort((a, b) => b.name.localeCompare(a.name)); + case "price-asc": + return sorted.sort((a, b) => { + const priceOrder = { "$": 1, "$$": 2, "$$$": 3, "$$$$": 4 }; + return (priceOrder[a.price] || 0) - (priceOrder[b.price] || 0); + }); + case "price-desc": + return sorted.sort((a, b) => { + const priceOrder = { "$": 1, "$$": 2, "$$$": 3, "$$$$": 4 }; + return (priceOrder[b.price] || 0) - (priceOrder[a.price] || 0); + }); + default: + return sorted; + } +} + +export default SortDropdown; + diff --git a/src/components/common/Toast.jsx b/src/components/common/Toast.jsx new file mode 100644 index 0000000..06862b8 --- /dev/null +++ b/src/components/common/Toast.jsx @@ -0,0 +1,24 @@ +import React, { useEffect } from "react"; + +function Toast({ message, type = "success", onClose, duration = 3000 }) { + useEffect(() => { + if (duration > 0) { + const timer = setTimeout(() => { + onClose(); + }, duration); + return () => clearTimeout(timer); + } + }, [duration, onClose]); + + return ( +
+ {message} + +
+ ); +} + +export default Toast; + diff --git a/src/components/common/ToastContainer.jsx b/src/components/common/ToastContainer.jsx new file mode 100644 index 0000000..1933f79 --- /dev/null +++ b/src/components/common/ToastContainer.jsx @@ -0,0 +1,21 @@ +import React from "react"; +import Toast from "./Toast"; + +function ToastContainer({ toasts, removeToast }) { + return ( +
+ {toasts.map((toast) => ( + removeToast(toast.id)} + duration={toast.duration} + /> + ))} +
+ ); +} + +export default ToastContainer; + diff --git a/src/components/home/RecentlyViewed.jsx b/src/components/home/RecentlyViewed.jsx new file mode 100644 index 0000000..e284c1b --- /dev/null +++ b/src/components/home/RecentlyViewed.jsx @@ -0,0 +1,51 @@ +import React, { useMemo, useState, useEffect } from "react"; +import RestaurantsContext from "../../context/RestaurantsContext"; +import { useContext } from "react"; +import FeaturedCard from "./FeaturedCard"; + +function RecentlyViewed() { + const { restaurants } = useContext(RestaurantsContext); + const [recentIds, setRecentIds] = useState([]); + + useEffect(() => { + function loadRecent() { + try { + const recent = JSON.parse(localStorage.getItem("fa_recently_viewed") || "[]"); + setRecentIds(recent); + } catch { + setRecentIds([]); + } + } + + loadRecent(); + + // Check periodically for updates (since storage events only fire in other tabs) + const interval = setInterval(loadRecent, 500); + + return () => clearInterval(interval); + }, []); + + const recentRestaurants = useMemo(() => { + if (!recentIds || recentIds.length === 0) return []; + return recentIds + .map((id) => restaurants.find((r) => r.id === id)) + .filter(Boolean) + .slice(0, 6); + }, [recentIds, restaurants]); + + if (recentRestaurants.length === 0) return null; + + return ( +
+

Recently viewed

+
+ {recentRestaurants.map((r) => ( + + ))} +
+
+ ); +} + +export default RecentlyViewed; + diff --git a/src/components/layout/Navbar.jsx b/src/components/layout/Navbar.jsx index 8dd68e7..96db750 100644 --- a/src/components/layout/Navbar.jsx +++ b/src/components/layout/Navbar.jsx @@ -1,41 +1,136 @@ -import React from "react"; +import React, { useState, useRef, useEffect } from "react"; import { Link, useNavigate, useLocation } from "react-router-dom"; import InputField from "../common/InputField"; - - +import { useDebounce } from "../../hooks/useDebounce"; +import { useLocalStorage } from "../../hooks/useLocalStorage"; function Navbar({ onSearch }) { const navigate = useNavigate(); - const [q, setQ] = React.useState(""); + const [q, setQ] = useState(""); + const [suggestions, setSuggestions] = useState([]); + const [showSuggestions, setShowSuggestions] = useState(false); + const [recentSearches] = useLocalStorage("fa_recent_searches", []); const location = useLocation(); + const searchRef = useRef(null); + const debouncedQuery = useDebounce(q, 300); + + // Get search suggestions from context or restaurants + useEffect(() => { + if (!debouncedQuery || debouncedQuery.length < 2) { + setSuggestions([]); + return; + } + + // This would ideally come from RestaurantsContext + // For now, we'll create basic suggestions + const lower = debouncedQuery.toLowerCase(); + const categories = ["Ethiopian", "Italian", "Cafe", "Fast food"]; + const categoryMatches = categories.filter(cat => cat.toLowerCase().includes(lower)); + + setSuggestions(categoryMatches.slice(0, 5)); + }, [debouncedQuery]); function submitSearch(e) { e.preventDefault(); const qs = q.trim(); if (!qs) return; + + // Save to recent searches + const updated = [qs, ...recentSearches.filter(s => s !== qs)].slice(0, 5); + localStorage.setItem("fa_recent_searches", JSON.stringify(updated)); + navigate(`/search?q=${encodeURIComponent(qs)}`); + setShowSuggestions(false); if (onSearch) onSearch(qs); } + function handleSuggestionClick(suggestion) { + setQ(suggestion); + navigate(`/search?q=${encodeURIComponent(suggestion)}`); + setShowSuggestions(false); + } + + function handleRecentSearchClick(search) { + setQ(search); + navigate(`/search?q=${encodeURIComponent(search)}`); + setShowSuggestions(false); + } + + useEffect(() => { + function handleClickOutside(event) { + if (searchRef.current && !searchRef.current.contains(event.target)) { + setShowSuggestions(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + return (
Find Addis
-
+
setQ(e.target.value)} + onChange={(e) => { + setQ(e.target.value); + setShowSuggestions(true); + }} + onFocus={() => setShowSuggestions(true)} />
+ + {(showSuggestions && (suggestions.length > 0 || recentSearches.length > 0)) && ( +
+ {suggestions.length > 0 && ( +
+
Suggestions
+ {suggestions.map((suggestion, idx) => ( + + ))} +
+ )} + {recentSearches.length > 0 && ( +
+
Recent searches
+ {recentSearches.slice(0, 5).map((search, idx) => ( + + ))} +
+ )} +
+ )}
diff --git a/src/components/layout/Sidebar.jsx b/src/components/layout/Sidebar.jsx index b0aebf6..78d5b90 100644 --- a/src/components/layout/Sidebar.jsx +++ b/src/components/layout/Sidebar.jsx @@ -7,13 +7,29 @@ function Sidebar({ filters, setFilters }) {

Category

setFilters({ ...filters, category: e.target.value })} + value={filters.category || ""} + onChange={(e) => setFilters({ ...filters, category: e.target.value || null })} options={[ - { value: "", label: "All" }, + { value: "", label: "All categories" }, { value: "Ethiopian", label: "Ethiopian" }, { value: "Italian", label: "Italian" }, { value: "Cafe", label: "Cafe" }, + { value: "Fast food", label: "Fast food" }, + ]} + /> +
+ +
+

Price range

+ setFilters({ ...filters, price: e.target.value || null })} + options={[ + { value: "", label: "All prices" }, + { value: "$", label: "$ - Budget friendly" }, + { value: "$$", label: "$$ - Moderate" }, + { value: "$$$", label: "$$$ - Expensive" }, + { value: "$$$$", label: "$$$$ - Very expensive" }, ]} />
@@ -24,12 +40,12 @@ function Sidebar({ filters, setFilters }) { type="range" min="0" max="5" - step="0.1" - value={filters.minRating} + step="0.5" + value={filters.minRating || 0} onChange={(e) => setFilters({ ...filters, minRating: Number(e.target.value) })} className="filter-range" /> -
{filters.minRating.toFixed(1)}+
+
{filters.minRating?.toFixed(1) || "0.0"}+
); diff --git a/src/components/restaurant/RestaurantCard.jsx b/src/components/restaurant/RestaurantCard.jsx index 08e659d..997b27e 100644 --- a/src/components/restaurant/RestaurantCard.jsx +++ b/src/components/restaurant/RestaurantCard.jsx @@ -2,7 +2,7 @@ import React, { useContext } from "react"; import { Link } from "react-router-dom"; import StarRating from "../common/StarRating"; import RestaurantsContext from "../../context/RestaurantsContext"; - +import Placeholder from "../../assets/addis-cafe.jpg"; function RestaurantCard({ restaurant }) { const { toggleFavorite, isFavorite } = useContext(RestaurantsContext); diff --git a/src/components/restaurant/RestaurantList.jsx b/src/components/restaurant/RestaurantList.jsx index e005977..4e74649 100644 --- a/src/components/restaurant/RestaurantList.jsx +++ b/src/components/restaurant/RestaurantList.jsx @@ -1,9 +1,22 @@ import React from "react"; import RestaurantCard from "./RestaurantCard"; +import EmptyState from "../common/EmptyState"; function RestaurantList({ restaurants }) { if (!restaurants || restaurants.length === 0) { - return
No restaurants found.
; + return ( + + ); } return ( diff --git a/src/components/restaurant/SimilarRestaurants.jsx b/src/components/restaurant/SimilarRestaurants.jsx new file mode 100644 index 0000000..d050bd8 --- /dev/null +++ b/src/components/restaurant/SimilarRestaurants.jsx @@ -0,0 +1,39 @@ +import React, { useMemo } from "react"; +import { Link } from "react-router-dom"; +import StarRating from "../common/StarRating"; + +function SimilarRestaurants({ restaurant, allRestaurants, limit = 3 }) { + const similar = useMemo(() => { + if (!restaurant || !allRestaurants) return []; + + return allRestaurants + .filter((r) => r.id !== restaurant.id && r.category === restaurant.category) + .sort((a, b) => (b.rating || 0) - (a.rating || 0)) + .slice(0, limit); + }, [restaurant, allRestaurants, limit]); + + if (similar.length === 0) return null; + + return ( +
+

Similar restaurants

+
+ {similar.map((r) => ( + +
+
+
{r.name}
+
+ + {r.price} +
+
+ + ))} +
+
+ ); +} + +export default SimilarRestaurants; + diff --git a/src/context/ToastContext.jsx b/src/context/ToastContext.jsx new file mode 100644 index 0000000..a9d6847 --- /dev/null +++ b/src/context/ToastContext.jsx @@ -0,0 +1,23 @@ +import React, { createContext, useContext } from "react"; +import { useToast } from "../hooks/useToast"; + +const ToastContext = createContext(); + +export function ToastProvider({ children }) { + const toast = useToast(); + return {children}; +} + +export function useToastContext() { + const context = useContext(ToastContext); + if (!context) { + // Return a default implementation if not in provider (for graceful degradation) + return { + toasts: [], + showToast: () => {}, + removeToast: () => {}, + }; + } + return context; +} + diff --git a/src/hooks/useDebounce.js b/src/hooks/useDebounce.js new file mode 100644 index 0000000..d208b0c --- /dev/null +++ b/src/hooks/useDebounce.js @@ -0,0 +1,18 @@ +import { useState, useEffect } from "react"; + +export function useDebounce(value, delay = 300) { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} + diff --git a/src/hooks/useLocalStorage.js b/src/hooks/useLocalStorage.js new file mode 100644 index 0000000..80e4cfb --- /dev/null +++ b/src/hooks/useLocalStorage.js @@ -0,0 +1,24 @@ +import { useState, useEffect } from "react"; + +export function useLocalStorage(key, initialValue) { + const [storedValue, setStoredValue] = useState(() => { + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch (error) { + return initialValue; + } + }); + + const setValue = (value) => { + try { + setStoredValue(value); + window.localStorage.setItem(key, JSON.stringify(value)); + } catch (error) { + console.error(`Error saving to localStorage:`, error); + } + }; + + return [storedValue, setValue]; +} + diff --git a/src/hooks/useToast.js b/src/hooks/useToast.js new file mode 100644 index 0000000..c202207 --- /dev/null +++ b/src/hooks/useToast.js @@ -0,0 +1,20 @@ +import { useState, useCallback } from "react"; + +let toastId = 0; + +export function useToast() { + const [toasts, setToasts] = useState([]); + + const showToast = useCallback((message, type = "success", duration = 3000) => { + const id = ++toastId; + setToasts((prev) => [...prev, { id, message, type, duration }]); + return id; + }, []); + + const removeToast = useCallback((id) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }, []); + + return { toasts, showToast, removeToast }; +} + diff --git a/src/pages/Favorites.jsx b/src/pages/Favorites.jsx index 6c0d32f..99cb165 100644 --- a/src/pages/Favorites.jsx +++ b/src/pages/Favorites.jsx @@ -1,18 +1,46 @@ -import React, { useContext } from "react"; +import React, { useContext, useState, useMemo } from "react"; import RestaurantsContext from "../context/RestaurantsContext"; import RestaurantList from "../components/restaurant/RestaurantList"; +import SortDropdown, { sortRestaurants } from "../components/common/SortDropdown"; +import EmptyState from "../components/common/EmptyState"; function Favorites() { const { restaurants, favorites } = useContext(RestaurantsContext); - const favRestaurants = restaurants.filter((r) => favorites.includes(r.id)); + const [sortBy, setSortBy] = useState("rating-desc"); + + const favRestaurants = useMemo(() => { + return restaurants.filter((r) => favorites.includes(r.id)); + }, [restaurants, favorites]); + + const sorted = useMemo(() => { + return sortRestaurants(favRestaurants, sortBy); + }, [favRestaurants, sortBy]); return (
-

Favorites

+
+

Favorites

+ {favRestaurants.length > 0 && } +
{favRestaurants.length === 0 ? ( -

You haven't saved any restaurants yet.

+ ) : ( - + <> +
+ {sorted.length} {sorted.length === 1 ? "favorite" : "favorites"} +
+ + )}
); diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index dcbbd0a..1aebb71 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -3,9 +3,10 @@ import HeroSection from "../components/home/HeroSection"; import CategoryCards from "../components/home/CategoryCards"; import RestaurantList from "../components/restaurant/RestaurantList"; import FeaturedCard from "../components/home/FeaturedCard"; +import RecentlyViewed from "../components/home/RecentlyViewed"; import { useContext } from "react"; import RestaurantsContext from "../context/RestaurantsContext"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, Link } from "react-router-dom"; function Home() { const { restaurants } = useContext(RestaurantsContext); @@ -25,13 +26,18 @@ function Home() {
-

Featured restaurants

+
+

Featured restaurants

+ View all → +
{restaurants.slice(0, 6).map((r) => ( ))}
+ + ); } diff --git a/src/pages/RestaurantDetailsPage.jsx b/src/pages/RestaurantDetailsPage.jsx index 515c3dc..8ade398 100644 --- a/src/pages/RestaurantDetailsPage.jsx +++ b/src/pages/RestaurantDetailsPage.jsx @@ -1,19 +1,48 @@ -import React from "react"; +import React, { useEffect, useState, useMemo } from "react"; import { useParams, useNavigate } from "react-router-dom"; import RestaurantsContext from "../context/RestaurantsContext"; import { useContext } from "react"; import MenuSection from "../components/restaurant/MenuSection"; import ReviewCard from "../components/restaurant/Reviewcard"; import StarRating from "../components/common/StarRating"; - - +import SimilarRestaurants from "../components/restaurant/SimilarRestaurants"; +import ShareButton from "../components/common/ShareButton"; +import Dropdown from "../components/common/Dropdown"; function RestaurantDetailsPage() { const { id } = useParams(); const navigate = useNavigate(); const { restaurants } = useContext(RestaurantsContext); + const [reviewSort, setReviewSort] = useState("newest"); const r = restaurants.find((x) => x.id === id); + // Track recently viewed + useEffect(() => { + if (r) { + const recent = JSON.parse(localStorage.getItem("fa_recently_viewed") || "[]"); + const updated = [r.id, ...recent.filter((id) => id !== r.id)].slice(0, 10); + localStorage.setItem("fa_recently_viewed", JSON.stringify(updated)); + } + }, [r]); + + const sortedReviews = useMemo(() => { + if (!r || !r.reviews) return []; + const reviews = [...r.reviews]; + + switch (reviewSort) { + case "newest": + return reviews.sort((a, b) => new Date(b.date) - new Date(a.date)); + case "oldest": + return reviews.sort((a, b) => new Date(a.date) - new Date(b.date)); + case "highest": + return reviews.sort((a, b) => b.rating - a.rating); + case "lowest": + return reviews.sort((a, b) => a.rating - b.rating); + default: + return reviews; + } + }, [r, reviewSort]); + if (!r) { return

Restaurant not found

; } @@ -23,10 +52,18 @@ function RestaurantDetailsPage() {
-

{r.name}

-
-
{r.category} • {r.price}
- +
+
+

{r.name}

+
+
{r.category} • {r.price}
+ + {r.reviews && r.reviews.length > 0 && ( + ({r.reviews.length} {r.reviews.length === 1 ? "review" : "reviews"}) + )} +
+
+

{r.description}

@@ -34,15 +71,41 @@ function RestaurantDetailsPage() {
-

Reviews

- {r.reviews && r.reviews.length === 0 &&
No reviews yet—be the first.
} -
- {r.reviews.map((rev) => )} -
-
- +
+

Reviews

+ {r.reviews && r.reviews.length > 0 && ( + setReviewSort(e.target.value)} + options={[ + { value: "newest", label: "Newest first" }, + { value: "oldest", label: "Oldest first" }, + { value: "highest", label: "Highest rated" }, + { value: "lowest", label: "Lowest rated" }, + ]} + /> + )}
+ {r.reviews && r.reviews.length === 0 ? ( +
+

No reviews yet—be the first to share your experience!

+ +
+ ) : ( + <> +
+ {sortedReviews.map((rev) => )} +
+
+ +
+ + )}
+ +