diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6d0d0e6..3f07f6d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -40,6 +40,7 @@ model User { cashouts CashoutRequest[] halalBookmarks HalalBookmark[] halalUsage HalalUsage? + userPlaces UserPlace[] ownedGroups Group[] @relation("GroupOwner") groupMemberships GroupMember[] forumThreads ForumThread[] @@ -241,6 +242,23 @@ model HalalUsage { periodEnd DateTime? } +model UserPlace { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id]) + name String + address String + lat Float + lng Float + type String // "mosque" | "restaurant" | "cafe" + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId]) + @@index([lat, lng]) +} + model Notification { id String @id @default(cuid()) userId String diff --git a/src/app/api/halal/places/route.ts b/src/app/api/halal/places/route.ts index d4ef36d..fb81a0c 100644 --- a/src/app/api/halal/places/route.ts +++ b/src/app/api/halal/places/route.ts @@ -1,98 +1,114 @@ import { NextRequest, NextResponse } from "next/server"; -import { haversineDistance } from "@/lib/geo"; +import { fetchNearbyPlaces, fetchCityPlaces } from "@/lib/halal-places"; -interface Place { - id: string; - name: string; - address: string; - lat: number; - lng: number; - rating: number; - type: "mosque" | "restaurant" | "cafe"; - halal_certified: boolean; - distance?: number; -} - -const MOCK_PLACES: Place[] = [ - // ── Mosques ─────────────────────────────────────────── - { id: "mosque-1", name: "Masjid Negara", address: "Jalan Perdana, Tasik Perdana, 50480 Kuala Lumpur", lat: 3.1422, lng: 101.6929, rating: 4.7, type: "mosque", halal_certified: true }, - { id: "mosque-2", name: "Masjid Jamek Sultan Abdul Samad", address: "Jalan Tun Perak, 50050 Kuala Lumpur", lat: 3.1492, lng: 101.6958, rating: 4.5, type: "mosque", halal_certified: true }, - { id: "mosque-3", name: "Masjid Wilayah Persekutuan", address: "Jalan Masjid, 50546 Kuala Lumpur", lat: 3.1723, lng: 101.6887, rating: 4.8, type: "mosque", halal_certified: true }, - { id: "mosque-4", name: "Masjid Putra", address: "Persiaran Persekutuan, Presint 1, 62502 Putrajaya", lat: 2.9374, lng: 101.6888, rating: 4.9, type: "mosque", halal_certified: true }, - { id: "mosque-5", name: "Masjid Zahir", address: "Jalan Sultan Badlishah, 05400 Alor Setar, Kedah", lat: 6.1227, lng: 100.3663, rating: 4.6, type: "mosque", halal_certified: true }, - { id: "mosque-6", name: "Masjid India", address: "Jalan Masjid India, 50100 Kuala Lumpur", lat: 3.1462, lng: 101.6962, rating: 4.4, type: "mosque", halal_certified: true }, - { id: "mosque-7", name: "Masjid Saidina Abu Bakar As Siddiq", address: "Jalan Bangsar, 59100 Kuala Lumpur", lat: 3.1277, lng: 101.6778, rating: 4.6, type: "mosque", halal_certified: true }, - { id: "mosque-8", name: "Masjid Al-Akram", address: "Kompleks PKNS, Jalan Bukit Bintang, 55100 Kuala Lumpur", lat: 3.1112, lng: 101.6668, rating: 4.3, type: "mosque", halal_certified: true }, - { id: "mosque-9", name: "Masjid Jamek Pantai Dalam", address: "Lorong Bukit Pantai, 59100 Kuala Lumpur", lat: 3.1033, lng: 101.6688, rating: 4.2, type: "mosque", halal_certified: true }, - { id: "mosque-10", name: "Masjid Al-Hasanah", address: "Jalan Perak, 50450 Kuala Lumpur", lat: 3.1649, lng: 101.7088, rating: 4.5, type: "mosque", halal_certified: true }, - - // ── Restaurants ─────────────────────────────────────── - { id: "rest-1", name: "Nasi Kandar Pelita", address: "No. 17, Jalan Telawi 3, Bangsar Baru, 59100 Kuala Lumpur", lat: 3.1295, lng: 101.6708, rating: 4.2, type: "restaurant", halal_certified: true }, - { id: "rest-2", name: "Restoran Ana Ikan Bakar Petai", address: "Jalan Cempaka, Kampung Datuk Keramat, 54000 Kuala Lumpur", lat: 3.1625, lng: 101.7319, rating: 4.3, type: "restaurant", halal_certified: true }, - { id: "rest-3", name: "Satey Zainab", address: "Jalan Tun Razak, 50400 Kuala Lumpur", lat: 3.1547, lng: 101.7112, rating: 4.1, type: "restaurant", halal_certified: true }, - { id: "rest-4", name: "Murni Discovery", address: "No. 8, Jalan SS2/75, SS2, 47300 Petaling Jaya", lat: 3.1187, lng: 101.6263, rating: 4.0, type: "restaurant", halal_certified: true }, - { id: "rest-5", name: "Nazeer's Banana Leaf", address: "No. 6, Jalan Kamuning, Off Jalan Imbi, 55100 Kuala Lumpur", lat: 3.1432, lng: 101.7115, rating: 4.4, type: "restaurant", halal_certified: true }, - { id: "rest-6", name: "Sultan Restaurant", address: "No. 88, Jalan Masjid India, 50100 Kuala Lumpur", lat: 3.1458, lng: 101.7138, rating: 4.3, type: "restaurant", halal_certified: true }, - { id: "rest-7", name: "Din Tai Fung (Pavilion)", address: "Lot 6.01, Pavilion KL, 168 Jalan Bukit Bintang, 55100 Kuala Lumpur", lat: 3.1562, lng: 101.7133, rating: 4.5, type: "restaurant", halal_certified: true }, - { id: "rest-8", name: "Beirut Grill", address: "G-01, Wisma Limi, Jalan Bukit Bintang, 55100 Kuala Lumpur", lat: 3.1552, lng: 101.7139, rating: 4.4, type: "restaurant", halal_certified: true }, - { id: "rest-9", name: "Rebung (Istana Budaya)", address: "Jalan Tembi, 50650 Kuala Lumpur", lat: 3.1519, lng: 101.7145, rating: 4.6, type: "restaurant", halal_certified: true }, - { id: "rest-10", name: "Nasi Lemak Bumbung", address: "Jalan Telawi 2, Bangsar Baru, 59100 Kuala Lumpur", lat: 3.1347, lng: 101.6789, rating: 4.3, type: "restaurant", halal_certified: true }, - { id: "rest-11", name: "Yut Kee", address: "35, Jalan Dang Wangi, 50100 Kuala Lumpur", lat: 3.1507, lng: 101.6977, rating: 4.5, type: "restaurant", halal_certified: true }, - - // ── Cafes ───────────────────────────────────────────── - { id: "cafe-1", name: "Brew & Bread", address: "Lot 6.12, Pavilion KL, 168 Jalan Bukit Bintang, 55100 Kuala Lumpur", lat: 3.1559, lng: 101.7149, rating: 4.2, type: "cafe", halal_certified: true }, - { id: "cafe-2", name: "Artisan Coffee", address: "2, Jalan Telawi 5, Bangsar Baru, 59100 Kuala Lumpur", lat: 3.1333, lng: 101.6797, rating: 4.3, type: "cafe", halal_certified: true }, - { id: "cafe-3", name: "The Gajah Coffee", address: "32, Jalan Telawi 4, Bangsar Baru, 59100 Kuala Lumpur", lat: 3.1299, lng: 101.6709, rating: 4.4, type: "cafe", halal_certified: true }, - { id: "cafe-4", name: "VCR Cafe", address: "26, Jalan Kamunting, 50300 Kuala Lumpur", lat: 3.1534, lng: 101.7129, rating: 4.5, type: "cafe", halal_certified: true }, - { id: "cafe-5", name: "Butter + Beans", address: "43, Jalan Medan Setia 1, Bukit Damansara, 50490 Kuala Lumpur", lat: 3.1416, lng: 101.6995, rating: 4.3, type: "cafe", halal_certified: true }, -]; +export const dynamic = "force-dynamic"; +export const maxDuration = 30; // Overpass can be slow export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); - const q = searchParams.get("q")?.toLowerCase(); - const type = searchParams.get("type")?.toLowerCase(); + const q = searchParams.get("q")?.trim(); const latParam = searchParams.get("lat"); const lngParam = searchParams.get("lng"); + const city = searchParams.get("city")?.trim(); + const country = searchParams.get("country")?.trim(); + const type = searchParams.get("type")?.toLowerCase(); + const radiusParam = searchParams.get("radius"); - let filtered = [...MOCK_PLACES]; + const radius = radiusParam ? parseInt(radiusParam, 10) : 5000; - // Filter by type if provided and valid - if (type && ["mosque", "restaurant", "cafe"].includes(type)) { - filtered = filtered.filter((p) => p.type === type); + let places; + let searchLocation: string | null = null; + + // Priority 1: City search (with optional country) + if (city) { + const result = await fetchCityPlaces(city, country || undefined, radius); + places = result.places; + searchLocation = result.location; + } + // Priority 2: Lat/lng nearby search + else if (latParam && lngParam) { + const lat = parseFloat(latParam); + const lng = parseFloat(lngParam); + if (!isNaN(lat) && !isNaN(lng)) { + places = await fetchNearbyPlaces(lat, lng, radius); + searchLocation = `${lat.toFixed(4)}, ${lng.toFixed(4)}`; + } else { + places = []; + } + } + // Priority 3: Text search only (no location) — return empty, need a location + else if (q) { + // Text search without location — return empty with a helpful message + return NextResponse.json({ + places: [], + error: "Please enable location or specify a city", + searchLocation: null, + }); + } + // No params at all — default to Kuala Lumpur as fallback + else { + const result = await fetchCityPlaces("Kuala Lumpur", "Malaysia", radius); + places = result.places; + searchLocation = result.location; + } + + // Apply post-fetch filters + if (type && ["mosque", "restaurant", "cafe"].includes(type)) { + places = places.filter((p) => p.type === type); } - // Filter by search query (name or address) if (q) { - filtered = filtered.filter( + const lowerQ = q.toLowerCase(); + places = places.filter( (p) => - p.name.toLowerCase().includes(q) || - p.address.toLowerCase().includes(q), + p.name.toLowerCase().includes(lowerQ) || + p.address.toLowerCase().includes(lowerQ) ); } - // If lat/lng provided, calculate distance and sort by nearest + // Sort by distance if lat/lng provided if (latParam && lngParam) { const userLat = parseFloat(latParam); const userLng = parseFloat(lngParam); - if (!isNaN(userLat) && !isNaN(userLng)) { - filtered = filtered.map((p) => ({ - ...p, - distance: haversineDistance(userLat, userLng, p.lat, p.lng), - })); - - // Sort by distance ascending (nearest first) - filtered.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0)); + places = places + .map((p) => ({ + ...p, + distance: haversineDistance(userLat, userLng, p.lat, p.lng), + })) + .sort((a, b) => (a.distance ?? Infinity) - (b.distance ?? Infinity)); } } - return NextResponse.json({ places: filtered }); + return NextResponse.json({ + places, + searchLocation, + total: places.length, + }); } catch (error) { console.error("GET /api/halal/places error:", error); return NextResponse.json( - { error: "Internal server error" }, - { status: 500 }, + { places: [], error: "Unable to fetch halal places right now" }, + { status: 200 } // Return 200 with empty results — graceful degradation ); } } + +// ─── Client-side haversine (duplicated for SSR-safety) ───── +function haversineDistance( + lat1: number, + lng1: number, + lat2: number, + lng2: number +): number { + const R = 6371; + 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 Math.round(R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * 100) / 100; +} diff --git a/src/app/api/halal/user-places/route.ts b/src/app/api/halal/user-places/route.ts new file mode 100644 index 0000000..9641f4c --- /dev/null +++ b/src/app/api/halal/user-places/route.ts @@ -0,0 +1,122 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { requireAuth } from "@/lib/auth"; + +export const dynamic = "force-dynamic"; + +// ─── GET /api/halal/user-places ───────────────────────────── +// Returns all user-contributed places, optionally within a bounding box +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const userId = searchParams.get("userId"); // optional filter + const swLat = searchParams.get("swLat"); + const swLng = searchParams.get("swLng"); + const neLat = searchParams.get("neLat"); + const neLng = searchParams.get("neLng"); + + const where: Record = {}; + + if (userId) where.userId = userId; + + if (swLat && swLng && neLat && neLng) { + where.lat = { gte: parseFloat(swLat), lte: parseFloat(neLat) }; + where.lng = { gte: parseFloat(swLng), lte: parseFloat(neLng) }; + } + + const places = await prisma.userPlace.findMany({ + where, + orderBy: { createdAt: "desc" }, + include: { + user: { select: { id: true, name: true } }, + }, + }); + + return NextResponse.json({ places }); + } catch (error) { + console.error("GET /api/halal/user-places error:", error); + return NextResponse.json( + { places: [], error: "Failed to fetch user places" }, + { status: 200 } + ); + } +} + +// ─── POST /api/halal/user-places ──────────────────────────── +export async function POST(request: NextRequest) { + try { + const auth = await requireAuth(request); + if (!auth) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const body = await request.json(); + const { name, address, lat, lng, type, notes } = body; + + if (!name || !name.trim()) { + return NextResponse.json({ error: "Name is required" }, { status: 400 }); + } + if (lat === undefined || lng === undefined || isNaN(lat) || isNaN(lng)) { + return NextResponse.json({ error: "Valid coordinates are required" }, { status: 400 }); + } + if (!["mosque", "restaurant", "cafe"].includes(type)) { + return NextResponse.json({ error: "Type must be mosque, restaurant, or cafe" }, { status: 400 }); + } + + const place = await prisma.userPlace.create({ + data: { + userId: auth.userId, + name: name.trim(), + address: address?.trim() || "", + lat, + lng, + type, + notes: notes?.trim() || null, + }, + include: { + user: { select: { id: true, name: true } }, + }, + }); + + return NextResponse.json({ place }, { status: 201 }); + } catch (error) { + console.error("POST /api/halal/user-places error:", error); + return NextResponse.json( + { error: "Failed to create place" }, + { status: 500 } + ); + } +} + +// ─── DELETE /api/halal/user-places?id=xxx ─────────────────── +export async function DELETE(request: NextRequest) { + try { + const auth = await requireAuth(request); + if (!auth) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const id = searchParams.get("id"); + if (!id) { + return NextResponse.json({ error: "Place ID required" }, { status: 400 }); + } + + const place = await prisma.userPlace.findUnique({ where: { id } }); + if (!place) { + return NextResponse.json({ error: "Place not found" }, { status: 404 }); + } + if (place.userId !== auth.userId) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + await prisma.userPlace.delete({ where: { id } }); + return NextResponse.json({ success: true }); + } catch (error) { + console.error("DELETE /api/halal/user-places error:", error); + return NextResponse.json( + { error: "Failed to delete place" }, + { status: 500 } + ); + } +} diff --git a/src/app/dhikr/page.tsx b/src/app/dhikr/page.tsx index 66c21a1..99e34a8 100644 --- a/src/app/dhikr/page.tsx +++ b/src/app/dhikr/page.tsx @@ -248,7 +248,7 @@ export default function DhikrPage() { ${active ? `bg-gradient-to-br ${c.from} ${c.to} border-${c.accent}-500/50` : "bg-[#111118] border-gray-800/60"}`}>

{p.label}

- {active ? `0/${p.target}` : `/${p.target}`} + {active ? `${count}/${p.target}` : `/${p.target}`}

); diff --git a/src/app/halal-monitor/page.tsx b/src/app/halal-monitor/page.tsx index b1dc173..bc401eb 100644 --- a/src/app/halal-monitor/page.tsx +++ b/src/app/halal-monitor/page.tsx @@ -14,6 +14,10 @@ import { Sparkles, Crown, Loader2, + Plus, + Trash2, + Send, + Navigation, } from "lucide-react"; // Leaflet CSS — safe in client component @@ -37,6 +41,17 @@ const Popup = dynamic( { ssr: false } ); +// ─── Map click handler (for adding places) ───────────────── +function MapClickHandler({ onMapClick }: { onMapClick: (lat: number, lng: number) => void }) { + const { useMapEvents } = require("react-leaflet"); + useMapEvents({ + click: (e: any) => { + onMapClick(e.latlng.lat, e.latlng.lng); + }, + }); + return null; +} + // ─── Types ────────────────────────────────────────────────── interface Place { id: string; @@ -48,6 +63,7 @@ interface Place { type: "mosque" | "restaurant" | "cafe"; halal_certified: boolean; distance?: number; + source?: string; } interface UsageInfo { @@ -116,7 +132,13 @@ export default function HalalMonitorPage() { const [usageLoading, setUsageLoading] = useState(false); const [userLocation, setUserLocation] = useState(null); const [locationError, setLocationError] = useState(null); + const [searchLocation, setSearchLocation] = useState(null); const [icons, setIcons] = useState | null>(null); + const [userPlaces, setUserPlaces] = useState([]); + const [showAddPlace, setShowAddPlace] = useState(false); + const [addForm, setAddForm] = useState({ name: "", address: "", type: "restaurant", notes: "" }); + const [mapClickPos, setMapClickPos] = useState<[number, number] | null>(null); + const [submitting, setSubmitting] = useState(false); const isPremium = user?.isPremium || user?.isPro || false; const isFree = !isPremium; @@ -164,6 +186,13 @@ export default function HalalMonitorPage() { iconSize: [20, 20], iconAnchor: [10, 10], }), + userPlace: L.divIcon({ + html: `
`, + className: "", + iconSize: [36, 36], + iconAnchor: [18, 36], + popupAnchor: [0, -36], + }), }); })(); }, []); @@ -200,16 +229,21 @@ export default function HalalMonitorPage() { const params = new URLSearchParams(); if (q) params.set("q", q); if (type && type !== "all") params.set("type", type); + if (userLocation) { + params.set("lat", userLocation.lat.toString()); + params.set("lng", userLocation.lng.toString()); + } const res = await fetch(`/mobile/api/halal/places?${params.toString()}`); if (res.ok) { const data = await res.json(); setPlaces(data.places); + if (data.searchLocation) setSearchLocation(data.searchLocation); } } catch { // ignore } - }, [search, typeFilter]); + }, [search, typeFilter, userLocation]); // ── Fetch usage ─────────────────────────────────────────── const fetchUsage = useCallback(async () => { @@ -243,6 +277,81 @@ export default function HalalMonitorPage() { } }, [token]); + // ── Fetch user-contributed places ───────────────────────── + const fetchUserPlaces = useCallback(async () => { + try { + const res = await fetch("/mobile/api/halal/user-places"); + if (res.ok) { + const data = await res.json(); + setUserPlaces(data.places.map((p: any) => ({ + id: `user-${p.id}`, + name: p.name, + address: p.address, + lat: p.lat, + lng: p.lng, + rating: 4.0, + type: p.type, + halal_certified: true, + source: "user", + notes: p.notes, + userId: p.user?.id, + userName: p.user?.name, + createdAt: p.createdAt, + }))); + } + } catch { + // ignore + } + }, []); + + // ── Submit a new user place ───────────────────────────── + const handleAddPlace = useCallback(async () => { + if (!token || !addForm.name.trim() || !mapClickPos) return; + setSubmitting(true); + try { + const res = await fetch("/mobile/api/halal/user-places", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + name: addForm.name, + address: addForm.address, + lat: mapClickPos[0], + lng: mapClickPos[1], + type: addForm.type, + notes: addForm.notes, + }), + }); + if (res.ok) { + setShowAddPlace(false); + setAddForm({ name: "", address: "", type: "restaurant", notes: "" }); + setMapClickPos(null); + await fetchUserPlaces(); + } + } catch { + // ignore + } finally { + setSubmitting(false); + } + }, [token, addForm, mapClickPos, fetchUserPlaces]); + + // ── Delete a user place ───────────────────────────────── + const handleDeletePlace = useCallback(async (placeId: string) => { + if (!token) return; + const realId = placeId.replace("user-", ""); + try { + await fetch(`/mobile/api/halal/user-places?id=${realId}`, { + method: "DELETE", + headers: { Authorization: `Bearer ${token}` }, + }); + await fetchUserPlaces(); + } catch { + // ignore + } + }, [token, fetchUserPlaces]); + // ── Track usage query ───────────────────────────────────── const trackQuery = useCallback(async () => { if (!token) return; @@ -273,16 +382,34 @@ export default function HalalMonitorPage() { if (loading || !token) return; const init = async () => { setPageLoading(true); - await Promise.all([fetchPlaces(), fetchUsage(), fetchBookmarks()]); + await Promise.all([fetchPlaces(), fetchUsage(), fetchBookmarks(), fetchUserPlaces()]); setPageLoading(false); }; init(); }, [loading, token, fetchPlaces, fetchUsage, fetchBookmarks]); + // ── Re-fetch when geolocation arrives (if initial load beat it) ── + useEffect(() => { + if (userLocation && !pageLoading) { + fetchPlaces(); + } + // Only fire when userLocation transitions from null → value + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [userLocation]); + + // ── Handle map click for adding places ──────────────────── + const handleMapClick = useCallback((lat: number, lng: number) => { + setMapClickPos([lat, lng]); + if (!showAddPlace) { + setShowAddPlace(true); + } + }, [showAddPlace]); + // ── Compute distances & sort by distance ────────────────── const sortedPlaces = useMemo(() => { - if (!places.length) return []; - const withDistances = places.map((p) => { + const allPlaces = [...places, ...userPlaces]; + if (!allPlaces.length) return []; + const withDistances = allPlaces.map((p) => { if (userLocation) { const dist = haversine( userLocation.lat, @@ -300,7 +427,7 @@ export default function HalalMonitorPage() { ); } return withDistances; - }, [places, userLocation]); + }, [places, userPlaces, userLocation]); // Free tier shows ALL place types (rate-limiting stays on query tracking) const visiblePlaces = sortedPlaces; @@ -417,7 +544,15 @@ export default function HalalMonitorPage() { Halal Monitor - {usage && ( +
+ + {usage && (

Today

@@ -442,6 +577,7 @@ export default function HalalMonitorPage() {
)} +

Discover halal-certified places around you @@ -540,6 +676,7 @@ export default function HalalMonitorPage() { className="w-full h-full" scrollWheelZoom={false} > + setSelectedPlace(place), }} @@ -579,11 +716,18 @@ export default function HalalMonitorPage() { {/* ── Results List ─────────────────────────────────── */}

-

- {visiblePlaces.length}{" "} - {visiblePlaces.length === 1 ? "place" : "places"} - {userLocation && " · Nearest first"} -

+
+

+ {visiblePlaces.length}{" "} + {visiblePlaces.length === 1 ? "place" : "places"} + {userLocation && " · Nearest first"} +

+ {searchLocation && ( +

+ {searchLocation} +

+ )} +
{usageLoading && ( )} @@ -593,12 +737,20 @@ export default function HalalMonitorPage() {
- ) : visiblePlaces.length === 0 ? ( + ) : visiblePlaces.length === 0 && locationError ? (
-

No places found

+

Location unavailable

- Try adjusting your search or filters. + Enable location or type a city name above. +

+
+ ) : visiblePlaces.length === 0 && !pageLoading ? ( +
+ +

No halal places found nearby

+

+ Try a different area or search for a city.

) : ( @@ -821,6 +973,162 @@ export default function HalalMonitorPage() { ? "Remove Bookmark" : "Add to Bookmarks"} + + {/* Delete button for user-contributed places */} + {selectedPlace.source === "user" && ( + + )} +
+
+ + )} + + {/* ── Add Place Modal ────────────────────────────────── */} + {showAddPlace && ( +
setShowAddPlace(false)} + > +
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" + > +
+
+
+ +
+ {/* Close button */} +
+

+ + Add a Place +

+ +
+ +

+ Share a halal place you know. Yellow markers help the community. +

+ + {/* Map click hint */} + {!mapClickPos && ( +
+

+ 👆 Tap on the map to set a location, then fill in the details +

+
+ )} + {mapClickPos && ( +
+
+
+

📍 Location set

+

+ {mapClickPos[0].toFixed(4)}, {mapClickPos[1].toFixed(4)} +

+
+ +
+
+ )} + + {/* Name */} + + + {/* Type selector */} + + + {/* Address */} + + + {/* Notes */} +