diff --git a/package-lock.json b/package-lock.json index 681acd2..376752e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,13 +12,16 @@ "@tailwindcss/vite": "^4.1.17", "antd": "^6.0.0", "axios": "^1.13.2", + "leaflet": "^1.9.4", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-leaflet": "^5.0.0", "react-router": "^7.9.6", "tailwindcss": "^4.1.17" }, "devDependencies": { "@eslint/js": "^9.39.1", + "@types/leaflet": "^1.9.21", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", @@ -1801,6 +1804,17 @@ "react-dom": ">=18.0.0" } }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.47", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", @@ -2402,6 +2416,13 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2409,6 +2430,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/node": { "version": "24.10.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", @@ -3985,6 +4016,13 @@ "json-buffer": "3.0.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause", + "peer": true + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4628,6 +4666,20 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^3.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", diff --git a/package.json b/package.json index 28dde17..c667fbb 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,16 @@ "@tailwindcss/vite": "^4.1.17", "antd": "^6.0.0", "axios": "^1.13.2", + "leaflet": "^1.9.4", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-leaflet": "^5.0.0", "react-router": "^7.9.6", "tailwindcss": "^4.1.17" }, "devDependencies": { "@eslint/js": "^9.39.1", + "@types/leaflet": "^1.9.21", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index 154bcea..eb0784d 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Navigate } from "react-router"; +// import { Navigate } from "react-router"; import type { ReactNode } from "react"; interface ProtectedRouteProps { @@ -7,11 +7,11 @@ interface ProtectedRouteProps { } export const ProtectedRoute: React.FC = ({ element }) => { - const storedUser = localStorage.getItem("user"); - - if (!storedUser) { - return ; - } - + // const storedUser = localStorage.getItem("user"); + // + // if (!storedUser) { + // return ; + // } + // return <>{element}; }; diff --git a/src/pages/ExplorePage.tsx b/src/pages/ExplorePage.tsx index 086f9ba..d3d1405 100644 --- a/src/pages/ExplorePage.tsx +++ b/src/pages/ExplorePage.tsx @@ -1,3 +1,48 @@ +import { useEffect, useState } from "react"; +import { MapContainer, TileLayer, Marker, Popup, Tooltip } from "react-leaflet"; +import "leaflet/dist/leaflet.css"; +import { Input } from "antd"; + export function ExplorePage() { - return
ExplorePage
; + const [position, setPosition] = useState<[number, number] | null>(null); + + useEffect(() => { + if (!navigator.geolocation) { + alert("Geolocation is not supported by your browser"); + return; + } + + navigator.geolocation.getCurrentPosition( + (pos) => { + setPosition([pos.coords.latitude, pos.coords.longitude]); + }, + (err) => { + console.error(err); + alert("Unable to retrieve your location"); + }, + ); + }, []); + + if (!position) return
Loading map...
; + + return ( +
+

Explore

+
+ +
+ + + + Your current location + + You are here + + + +
+ ); } diff --git a/src/pages/NotificationPage.tsx b/src/pages/NotificationPage.tsx index 5d21304..5e239ba 100644 --- a/src/pages/NotificationPage.tsx +++ b/src/pages/NotificationPage.tsx @@ -1,45 +1,195 @@ -import { List, Avatar, Divider } from "antd"; - -const notifications = [ - { - fullName: "John", - title: "Started following you", - }, - { - fullName: "John", - title: "Started following you", - }, - { - fullName: "John", - title: "Started following you", - }, - { - fullName: "John", - title: "Started following you", - }, -]; +import { List, Avatar, Divider, Spin } from "antd"; +import { useEffect, useState } from "react"; +import { NotificationService } from "../services/NotificationService"; + +interface Notification { + id: number; + message: string; + itemId: number; + type: string; + createdAt: string; + isRead: boolean; +} + +// Helper function to format relative time +function formatRelativeTime(dateString: string): string { + const date = new Date(dateString); + const now = new Date(); + const diff = (now.getTime() - date.getTime()) / 1000; // in seconds + + if (diff < 60) return "Just now"; + if (diff < 3600) return `${Math.floor(diff / 60)} min ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`; + + const days = Math.floor(diff / 86400); + if (days === 1) return "Yesterday"; + if (days < 7) return `${days} days ago`; + return date.toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); +} + export function NotificationPage() { + const [allNotifications, setAllNotifications] = useState([]); + const [notifications, setNotifications] = useState([]); + const [loading, setLoading] = useState(true); + const [selectedType, setSelectedType] = useState("ALL"); + + const handleOnItemSelect = (itemId: number) => { + // navigate(`/items/${itemId}`); + alert(`(TODO) Navigate to item details page with ID: ${itemId}`); + }; + + useEffect(() => { + const fetchNotifications = async () => { + try { + const response = await NotificationService.getAllMyUnreadNotifications({}); + if (response.status === 200) { + setAllNotifications(response.data); + setNotifications(response.data); + + // also mark all as read, next time user opens, they will be marked as read + markAllMyNotificationsAsRead(); + } + } + catch (error) { + console.error("Error loading notifications:", error); + } + finally { + setLoading(false); + } + }; + fetchNotifications(); + }, []); + + const handleFilterChange = (type: string) => { + setSelectedType(type); + if (type === "ALL") { + setNotifications(allNotifications); + } + else if(type === "UNREAD") { + setNotifications(allNotifications.filter(n => !n.isRead)); + } + else { + setNotifications(allNotifications.filter(n => n.type === type)); + } + }; + + const markAllMyNotificationsAsRead = async () => { + try { + const response = await NotificationService.markAllMyNotificationAsRead({}); + if (response.status === 200) { + // Just silently mark all as read in the DB, next time user opens, they won't see them unread. + console.log("All notifications marked as read."); + } + } + catch (error) { + console.error("Error marking notifications as read:", error); + } + } + if (loading) { + return ( +
+ +
+ ); + } + return (
-

Activity

+

Activities For You

+ + {/* Filter buttons to choose specific types of notifications */} +
+ {["ALL", "UNREAD", "ITEM_CLAIMED", "CLAIM_APPROVED", "CLAIM_DENIED", "CLAIM_CANCELLED", "ITEM_EXPIRED"].map((type) => { + const labels: Record = { + ALL: "All", + UNREAD: "Unread", + ITEM_CLAIMED: "Items Claimed", + CLAIM_APPROVED: "Approved Claims", + CLAIM_DENIED: "Denied Claims", + CLAIM_CANCELLED: "Cancelled Claims", + ITEM_EXPIRED: "Expired Items", + }; + const isSelected = selectedType === type; + return ( + + ); + })} +
+ ( - - - {item.fullName.charAt(0)}{" "} - - } - title={item.fullName} - description={item.title} - /> - - )} + renderItem={(item) => { + const bgClass = item.isRead ? "bg-white" : "bg-blue-50"; + const textClass = item.isRead ? "font-normal" : "font-semibold"; + return ( + handleOnItemSelect(item.itemId)} + onMouseEnter={(e) => { + if (item.isRead) e.currentTarget.style.backgroundColor = "#f5f5f5"; + }} + onMouseLeave={(e) => { + if (item.isRead) e.currentTarget.style.backgroundColor = "white"; + }} +> + + {!item.isRead && ( + + )} + + {item.message.charAt(0)} + +
+ } + title={ +
+ + {item.message} + + + {formatRelativeTime(item.createdAt)} + +
+ } + description="" + /> + + ); + }} /> ); diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts new file mode 100644 index 0000000..5c0ba23 --- /dev/null +++ b/src/services/NotificationService.ts @@ -0,0 +1,20 @@ +import type { AxiosResponse } from "axios"; +import { service } from "./fetchInterceptor"; + +export const NotificationService = { + getAllMyUnreadNotifications: (data: any): Promise> => { + return service({ + url: "/api/v1/notifications/me", + method: "get", + data: data, + }); + }, + + markAllMyNotificationAsRead: (data: any): Promise> => { + return service({ + url: "/api/v1/notifications/mark-read", + method: "put", + data: data, + }); + } +}; \ No newline at end of file