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:
+
+ {suggestions.map((suggestion, idx) => (
+ - {suggestion}
+ ))}
+
+
+ )}
+
+ {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
-
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) => )}
+
+
+
+
+ >
+ )}
+
+