Files
falah-mobile/src/app/halal-monitor/page.tsx
T

831 lines
31 KiB
TypeScript
Raw Normal View History

"use client";
import { useState, useEffect, useCallback, useMemo } from "react";
import { useAuth } from "@/lib/AuthContext";
import { useRouter } from "next/navigation";
import dynamic from "next/dynamic";
import {
MapPin,
Search,
Heart,
X,
Star,
ChevronRight,
Sparkles,
Crown,
Loader2,
} from "lucide-react";
// Leaflet CSS — safe in client component
import "leaflet/dist/leaflet.css";
// ─── Dynamic react-leaflet imports (SSR safe) ──────────────
const LeafletMapContainer = dynamic(
() => import("react-leaflet").then((m) => m.MapContainer),
{ ssr: false }
);
const TileLayer = dynamic(
() => import("react-leaflet").then((m) => m.TileLayer),
{ ssr: false }
);
const Marker = dynamic(
() => import("react-leaflet").then((m) => m.Marker),
{ ssr: false }
);
const Popup = dynamic(
() => import("react-leaflet").then((m) => m.Popup),
{ ssr: false }
);
// ─── Types ──────────────────────────────────────────────────
interface Place {
id: string;
name: string;
address: string;
lat: number;
lng: number;
rating: number;
type: "mosque" | "restaurant" | "cafe";
halal_certified: boolean;
distance?: number;
}
interface UsageInfo {
queriesUsed: number;
queriesLimit: number;
remaining: number;
tier: string;
scope: string;
}
interface Bookmark {
id: string;
itemId: string;
itemType: string;
label: string | null;
createdAt: string;
place: Place | null;
}
interface UserLocation {
lat: number;
lng: number;
}
// ─── Haversine distance (km) ────────────────────────────────
function haversine(
lat1: number,
lng1: number,
lat2: number,
lng2: number
): number {
const R = 6371; // Earth's radius in km
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLng = ((lng2 - lng1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLng / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
// ─── Constants ──────────────────────────────────────────────
const TYPES = ["all", "mosque", "restaurant", "cafe"] as const;
const TYPE_LABELS: Record<string, string> = {
all: "All",
mosque: "Mosques",
restaurant: "Restaurants",
cafe: "Cafes",
};
const DEFAULT_CENTER: [number, number] = [3.139, 101.6869]; // Kuala Lumpur
// ─── Page ───────────────────────────────────────────────────
export default function HalalMonitorPage() {
const { user, token, loading } = useAuth();
const router = useRouter();
const [places, setPlaces] = useState<Place[]>([]);
const [usage, setUsage] = useState<UsageInfo | null>(null);
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
const [search, setSearch] = useState("");
const [typeFilter, setTypeFilter] = useState<string>("all");
const [selectedPlace, setSelectedPlace] = useState<Place | null>(null);
const [pageLoading, setPageLoading] = useState(true);
const [usageLoading, setUsageLoading] = useState(false);
const [userLocation, setUserLocation] = useState<UserLocation | null>(null);
const [locationError, setLocationError] = useState<string | null>(null);
const [icons, setIcons] = useState<Record<string, any> | null>(null);
const isPremium = user?.isPremium || user?.isPro || false;
const isFree = !isPremium;
// ── Initialize Leaflet custom icons ───────────────────────
useEffect(() => {
(async () => {
const L = await import("leaflet");
// Fix default icon paths for bundlers
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl:
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon-2x.png",
iconUrl:
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png",
shadowUrl:
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png",
});
setIcons({
mosque: L.divIcon({
html: `<div style="display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:50%;background:rgba(16,185,129,0.9);border:2px solid rgba(110,231,183,0.8);box-shadow:0 2px 8px rgba(0,0,0,0.3);font-size:18px">🕌</div>`,
className: "",
iconSize: [36, 36],
iconAnchor: [18, 36],
popupAnchor: [0, -36],
}),
restaurant: L.divIcon({
html: `<div style="display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:50%;background:rgba(245,158,11,0.9);border:2px solid rgba(252,211,77,0.8);box-shadow:0 2px 8px rgba(0,0,0,0.3);font-size:18px">🍽️</div>`,
className: "",
iconSize: [36, 36],
iconAnchor: [18, 36],
popupAnchor: [0, -36],
}),
cafe: L.divIcon({
html: `<div style="display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:50%;background:rgba(245,158,11,0.9);border:2px solid rgba(252,211,77,0.8);box-shadow:0 2px 8px rgba(0,0,0,0.3);font-size:18px">☕</div>`,
className: "",
iconSize: [36, 36],
iconAnchor: [18, 36],
popupAnchor: [0, -36],
}),
user: L.divIcon({
html: `<div style="width:20px;height:20px;border-radius:50%;background:#3b82f6;border:3px solid white;box-shadow:0 0 0 3px rgba(59,130,246,0.4)"></div>`,
className: "",
iconSize: [20, 20],
iconAnchor: [10, 10],
}),
});
})();
}, []);
// ── GeoLocation ───────────────────────────────────────────
useEffect(() => {
if (!navigator.geolocation) {
setLocationError("Geolocation is not supported by your browser");
return;
}
navigator.geolocation.getCurrentPosition(
(pos) => {
setUserLocation({
lat: pos.coords.latitude,
lng: pos.coords.longitude,
});
setLocationError(null);
},
(err) => {
setLocationError(err.message);
},
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 300000 }
);
}, []);
// ── Redirect if not logged in ─────────────────────────────
useEffect(() => {
if (!loading && !token) router.push("/auth");
}, [loading, token, router]);
// ── Fetch places ──────────────────────────────────────────
const fetchPlaces = useCallback(async (q = search, type = typeFilter) => {
try {
const params = new URLSearchParams();
if (q) params.set("q", q);
if (type && type !== "all") params.set("type", type);
const res = await fetch(`/mobile/api/halal/places?${params.toString()}`);
if (res.ok) {
const data = await res.json();
setPlaces(data.places);
}
} catch {
// ignore
}
}, [search, typeFilter]);
// ── Fetch usage ───────────────────────────────────────────
const fetchUsage = useCallback(async () => {
if (!token) return;
try {
const res = await fetch("/mobile/api/halal/usage", {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const data = await res.json();
setUsage(data);
}
} catch {
// ignore
}
}, [token]);
// ── Fetch bookmarks ───────────────────────────────────────
const fetchBookmarks = useCallback(async () => {
if (!token) return;
try {
const res = await fetch("/mobile/api/halal/bookmarks", {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
const data = await res.json();
setBookmarks(data.bookmarks);
}
} catch {
// ignore
}
}, [token]);
// ── Track usage query ─────────────────────────────────────
const trackQuery = useCallback(async () => {
if (!token) return;
setUsageLoading(true);
try {
const res = await fetch("/mobile/api/halal/usage", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
if (res.ok) {
const data = await res.json();
setUsage((prev) => (prev ? { ...prev, ...data } : data));
} else if (res.status === 429) {
setUsage((prev) => (prev ? { ...prev, remaining: 0 } : prev));
}
} catch {
// ignore
} finally {
setUsageLoading(false);
}
}, [token]);
// ── Initial load ──────────────────────────────────────────
useEffect(() => {
if (loading || !token) return;
const init = async () => {
setPageLoading(true);
await Promise.all([fetchPlaces(), fetchUsage(), fetchBookmarks()]);
setPageLoading(false);
};
init();
}, [loading, token, fetchPlaces, fetchUsage, fetchBookmarks]);
// ── Compute distances & sort by distance ──────────────────
const sortedPlaces = useMemo(() => {
if (!places.length) return [];
const withDistances = places.map((p) => {
if (userLocation) {
const dist = haversine(
userLocation.lat,
userLocation.lng,
p.lat,
p.lng
);
return { ...p, distance: Math.round(dist * 100) / 100 };
}
return p;
});
if (userLocation) {
return [...withDistances].sort(
(a, b) => (a.distance ?? Infinity) - (b.distance ?? Infinity)
);
}
return withDistances;
}, [places, userLocation]);
// Free tier shows ALL place types (rate-limiting stays on query tracking)
const visiblePlaces = sortedPlaces;
// ── Search / filter triggers ──────────────────────────────
const handleSearch = useCallback(() => {
fetchPlaces(search, typeFilter);
if (!isFree) trackQuery();
}, [search, typeFilter, fetchPlaces, isFree, trackQuery]);
const handleTypeFilter = useCallback(
(type: string) => {
setTypeFilter(type);
fetchPlaces(search, type);
if (!isFree) trackQuery();
},
[search, fetchPlaces, isFree, trackQuery]
);
// ── Bookmark toggle ───────────────────────────────────────
const toggleBookmark = useCallback(
async (place: Place) => {
if (!token) return;
const existing = bookmarks.find((b) => b.itemId === place.id);
if (existing) {
const res = await fetch(
`/mobile/api/halal/bookmarks?id=${existing.id}`,
{
method: "DELETE",
headers: { Authorization: `Bearer ${token}` },
}
);
if (res.ok) fetchBookmarks();
} else {
const res = await fetch("/mobile/api/halal/bookmarks", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
itemId: place.id,
itemType: place.type,
label: place.name,
}),
});
if (res.ok) fetchBookmarks();
}
},
[token, bookmarks, fetchBookmarks]
);
const isBookmarked = useCallback(
(placeId: string) => bookmarks.some((b) => b.itemId === placeId),
[bookmarks]
);
// ── Render stars ──────────────────────────────────────────
const renderStars = (rating: number) => {
const full = Math.floor(rating);
const half = rating - full >= 0.5;
const empty = 5 - full - (half ? 1 : 0);
return (
<span className="inline-flex items-center gap-0.5">
{Array.from({ length: full }, (_, i) => (
<Star
key={`full-${i}`}
size={12}
className="fill-amber-400 text-amber-400"
/>
))}
{half && (
<div className="relative">
<Star size={12} className="text-gray-600" />
<Star
size={12}
className="absolute inset-0 fill-amber-400 text-amber-400"
style={{ clipPath: "inset(0 50% 0 0)" }}
/>
</div>
)}
{Array.from({ length: empty }, (_, i) => (
<Star key={`empty-${i}`} size={12} className="text-gray-600" />
))}
<span className="text-xs text-gray-400 ml-1">
{rating.toFixed(1)}
</span>
</span>
);
};
// ── Map center ────────────────────────────────────────────
const mapCenter: [number, number] = userLocation
? [userLocation.lat, userLocation.lng]
: DEFAULT_CENTER;
// ── Premium Gating Overlay ─────────────────────────────────
if (loading) {
return (
<div className="min-h-dvh bg-[#0a0a0f] flex items-center justify-center">
<div className="w-8 h-8 rounded-full border-2 border-emerald-500/30 border-t-emerald-500 animate-spin" />
</div>
);
}
if (!user) return null;
return (
<div className="min-h-dvh bg-[#0a0a0f] pb-24">
{/* ── Header ───────────────────────────────────────── */}
<div className="px-4 pt-6 pb-4">
<div className="flex items-center justify-between mb-2">
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<MapPin size={20} className="text-emerald-400" />
Halal Monitor
</h1>
{usage && (
<div className="flex items-center gap-2">
<div className="text-right">
<p className="text-xs text-gray-500">Today</p>
<p className="text-xs font-semibold text-gray-300">
{usage.remaining}/{usage.queriesLimit}
</p>
</div>
<div className="w-16 h-1.5 bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-500"
style={{
width: `${Math.min(
100,
((usage?.queriesUsed ?? 0) /
(usage?.queriesLimit ?? 10)) *
100
)}%`,
backgroundColor:
(usage?.remaining ?? 0) <= 2 ? "#ef4444" : "#10b981",
}}
/>
</div>
</div>
)}
</div>
<p className="text-xs text-gray-500">
Discover halal-certified places around you
</p>
{/* ── Premium Upgrade Banner ─────────────────────── */}
{isFree && (
<button
onClick={() => router.push("/upgrade")}
className="mt-3 flex items-center justify-between glass-gold rounded-2xl px-4 py-3 w-full active:opacity-80 transition"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-emerald-500/20 flex items-center justify-center">
<Sparkles size={16} className="text-emerald-400" />
</div>
<div className="text-left">
<p className="text-sm font-semibold text-emerald-400">
Upgrade for Unlimited
</p>
<p className="text-xs text-gray-500">
Unlock unlimited queries
</p>
</div>
</div>
<ChevronRight size={16} className="text-emerald-400" />
</button>
)}
{/* Free user scope notice */}
{isFree && (
<div className="mt-2 flex items-center gap-1.5 text-xs text-amber-400/70">
<Crown size={12} />
Free tier: 10 queries/day
</div>
)}
</div>
{/* ── Search Bar ───────────────────────────────────── */}
<div className="px-4 mb-3">
<div className="flex items-center gap-2 bg-[#111118] border border-gray-800/60 rounded-2xl px-4 py-3">
<Search size={16} className="text-gray-500 shrink-0" />
<input
type="text"
placeholder="Search halal places..."
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
className="flex-1 bg-transparent text-sm text-white placeholder-gray-600 outline-none"
/>
{search && (
<button
onClick={() => {
setSearch("");
fetchPlaces("", typeFilter);
}}
>
<X size={14} className="text-gray-500" />
</button>
)}
</div>
</div>
{/* ── Category Filter ──────────────────────────────── */}
<div className="px-4 mb-4 overflow-x-auto">
<div className="flex gap-2 min-w-max pb-1">
{TYPES.map((t) => {
const active = typeFilter === t;
// All filter buttons enabled for free tier (rate limiting stays)
const isDisabled = false;
return (
<button
key={t}
disabled={isDisabled}
onClick={() => handleTypeFilter(t)}
className={`px-3.5 py-1.5 rounded-full text-xs font-medium transition-all shrink-0 ${
active
? "bg-emerald-500/20 text-emerald-300 border border-emerald-500/40"
: isDisabled
? "bg-gray-900 text-gray-600 border border-gray-800/40 cursor-not-allowed opacity-50"
: "bg-[#111118] text-gray-400 border border-gray-800/60 hover:border-gray-700"
}`}
>
{TYPE_LABELS[t]}
</button>
);
})}
</div>
</div>
{/* ── Leaflet Map ──────────────────────────────────── */}
<div className="mx-4 mb-4">
<div className="w-full h-48 rounded-2xl overflow-hidden border border-gray-800/60">
<LeafletMapContainer
center={mapCenter}
zoom={13}
className="w-full h-full"
scrollWheelZoom={false}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{/* User location blue dot */}
{icons && userLocation && (
<Marker
position={[userLocation.lat, userLocation.lng]}
icon={icons.user}
>
<Popup>You are here</Popup>
</Marker>
)}
{/* Place markers */}
{icons &&
visiblePlaces.map((place) => (
<Marker
key={place.id}
position={[place.lat, place.lng]}
icon={icons[place.type]}
eventHandlers={{
click: () => setSelectedPlace(place),
}}
>
<Popup>
<div className="text-sm font-semibold">{place.name}</div>
<div className="text-xs text-gray-500">
{place.address}
</div>
</Popup>
</Marker>
))}
</LeafletMapContainer>
</div>
</div>
{/* ── Results List ─────────────────────────────────── */}
<div className="px-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wider">
{visiblePlaces.length}{" "}
{visiblePlaces.length === 1 ? "place" : "places"}
{userLocation && " · Nearest first"}
</h2>
{usageLoading && (
<Loader2 size={12} className="animate-spin text-gray-600" />
)}
</div>
{pageLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 size={24} className="animate-spin text-emerald-500/50" />
</div>
) : visiblePlaces.length === 0 ? (
<div className="bg-[#111118] border border-gray-800/60 rounded-2xl p-8 text-center">
<MapPin size={32} className="mx-auto text-gray-700 mb-2" />
<p className="text-sm text-gray-500">No places found</p>
<p className="text-xs text-gray-700 mt-1">
Try adjusting your search or filters.
</p>
</div>
) : (
<div className="space-y-3">
{visiblePlaces.map((place) => {
const bookmarked = isBookmarked(place.id);
return (
<div
key={place.id}
onClick={() => setSelectedPlace(place)}
className="bg-[#111118] border border-gray-800/60 rounded-2xl p-4 active:scale-[0.99] transition cursor-pointer"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
{/* Place name + type badge */}
<div className="flex items-center gap-2 mb-1">
<h3 className="text-sm font-semibold text-white truncate">
{place.name}
</h3>
<span
className={`text-xs font-medium px-2 py-0.5 rounded-full shrink-0 ${
place.type === "mosque"
? "bg-emerald-500/15 text-emerald-300 border border-emerald-500/30"
: "bg-amber-500/15 text-amber-300 border border-amber-500/30"
}`}
>
{place.type === "mosque"
? "Mosque"
: place.type === "restaurant"
? "Restaurant"
: "Cafe"}
</span>
</div>
{/* Address */}
<p className="text-xs text-gray-500 mb-1.5 truncate">
{place.address}
</p>
{/* Rating */}
<div className="mb-1.5">{renderStars(place.rating)}</div>
{/* Distance & Halal badge */}
<div className="flex items-center gap-2 flex-wrap">
{place.distance !== undefined && (
<span className="text-xs text-blue-400/80">
{place.distance < 1
? `${(place.distance * 1000).toFixed(0)} m`
: `${place.distance.toFixed(1)} km`}
</span>
)}
{place.halal_certified && (
<span className="inline-flex items-center gap-1 text-xs font-medium text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
Halal Certified
</span>
)}
</div>
</div>
{/* Bookmark heart */}
<button
onClick={(e) => {
e.stopPropagation();
toggleBookmark(place);
}}
className={`shrink-0 w-8 h-8 rounded-full flex items-center justify-center transition ${
bookmarked
? "bg-emerald-500/20 text-emerald-400"
: "bg-gray-800/60 text-gray-600 hover:text-gray-400"
}`}
>
<Heart
size={14}
className={bookmarked ? "fill-emerald-400" : ""}
/>
</button>
</div>
</div>
);
})}
</div>
)}
</div>
{/* ── Detail Modal ──────────────────────────────────── */}
{selectedPlace && (
<div
className="fixed inset-0 z-[60] bg-black/70 flex items-end sm:items-center justify-center"
onClick={() => setSelectedPlace(null)}
>
<div
onClick={(e) => e.stopPropagation()}
className="bg-[#111118] border border-gray-800/60 rounded-t-3xl sm:rounded-3xl w-full max-w-md max-h-[85vh] overflow-y-auto animate-fade-in"
>
{/* Handle */}
<div className="pt-3 pb-1 flex justify-center sm:hidden">
<div className="w-10 h-1 rounded-full bg-gray-700" />
</div>
<div className="p-5">
{/* Close button */}
<div className="flex justify-end mb-2">
<button
onClick={() => setSelectedPlace(null)}
className="w-8 h-8 rounded-full bg-gray-800/80 flex items-center justify-center"
>
<X size={14} className="text-gray-400" />
</button>
</div>
{/* Type badge */}
<div className="mb-3">
<span
className={`text-xs font-medium px-3 py-1 rounded-full ${
selectedPlace.type === "mosque"
? "bg-emerald-500/15 text-emerald-300 border border-emerald-500/30"
: "bg-amber-500/15 text-amber-300 border border-amber-500/30"
}`}
>
{selectedPlace.type === "mosque"
? "🕌 Mosque"
: selectedPlace.type === "restaurant"
? "🍽️ Restaurant"
: "☕ Cafe"}
</span>
</div>
{/* Name */}
<h2 className="text-lg font-bold text-white mb-1">
{selectedPlace.name}
</h2>
{/* Halal certified */}
{selectedPlace.halal_certified && (
<div className="flex items-center gap-1.5 mb-3">
<span className="w-2 h-2 rounded-full bg-emerald-400" />
<span className="text-xs font-medium text-emerald-400">
Halal Certified
</span>
</div>
)}
{/* Address */}
<div className="flex items-start gap-2 mb-3">
<MapPin size={14} className="text-gray-500 mt-0.5 shrink-0" />
<p className="text-sm text-gray-400">
{selectedPlace.address}
</p>
</div>
{/* Rating */}
<div className="flex items-center gap-2 mb-4">
<Star size={14} className="fill-amber-400 text-amber-400" />
<span className="text-sm font-semibold text-white">
{selectedPlace.rating.toFixed(1)}
</span>
<span className="text-xs text-gray-600">/ 5.0</span>
</div>
{/* Distance (if available) */}
{selectedPlace.distance !== undefined && (
<div className="flex items-center gap-2 mb-4 text-sm text-blue-400">
<MapPin size={14} />
<span>
{selectedPlace.distance < 1
? `${(selectedPlace.distance * 1000).toFixed(0)} m`
: `${selectedPlace.distance.toFixed(1)} km`}{" "}
from your location
</span>
</div>
)}
{/* Coords */}
<div className="grid grid-cols-2 gap-3 mb-5">
<div className="bg-[#0a0a0f] rounded-xl p-3 text-center">
<p className="text-xs text-gray-600">Latitude</p>
<p className="text-xs text-gray-300 font-mono">
{selectedPlace.lat.toFixed(4)}
</p>
</div>
<div className="bg-[#0a0a0f] rounded-xl p-3 text-center">
<p className="text-xs text-gray-600">Longitude</p>
<p className="text-xs text-gray-300 font-mono">
{selectedPlace.lng.toFixed(4)}
</p>
</div>
</div>
{/* Open in Google Maps */}
<a
href={`https://www.google.com/maps/dir/?api=1&destination=${selectedPlace.lat},${selectedPlace.lng}`}
target="_blank"
rel="noopener noreferrer"
className="w-full py-3 rounded-2xl text-sm font-semibold transition flex items-center justify-center gap-2 mb-3 bg-blue-500/20 text-blue-300 border border-blue-500/40 hover:bg-blue-500/30"
>
<MapPin size={16} />
Open in Google Maps
</a>
{/* Bookmark button */}
<button
onClick={() => {
toggleBookmark(selectedPlace);
setSelectedPlace(null);
}}
className={`w-full py-3 rounded-2xl text-sm font-semibold transition flex items-center justify-center gap-2 ${
isBookmarked(selectedPlace.id)
? "bg-gray-800 text-gray-400 border border-gray-700"
: "bg-emerald-500/20 text-emerald-300 border border-emerald-500/40"
}`}
>
<Heart
size={16}
className={
isBookmarked(selectedPlace.id) ? "fill-emerald-400" : ""
}
/>
{isBookmarked(selectedPlace.id)
? "Remove Bookmark"
: "Add to Bookmarks"}
</button>
</div>
</div>
</div>
)}
</div>
);
}