From 79c3f22fd16f3defa3f71e72ca12f094550001e7 Mon Sep 17 00:00:00 2001 From: Badri Paudel Date: Tue, 9 Dec 2025 17:44:55 -0600 Subject: [PATCH 1/4] Navigation to detains page on notification item selection, style adjusted a bit --- src/pages/NotificationPage.tsx | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/pages/NotificationPage.tsx b/src/pages/NotificationPage.tsx index 5e239ba..95fabcd 100644 --- a/src/pages/NotificationPage.tsx +++ b/src/pages/NotificationPage.tsx @@ -1,5 +1,7 @@ -import { List, Avatar, Divider, Spin } from "antd"; +import { List, Avatar, Divider, Spin, Button, Tooltip } from "antd"; +import { ArrowRightOutlined } from "@ant-design/icons"; import { useEffect, useState } from "react"; +import { useNavigate } from "react-router"; import { NotificationService } from "../services/NotificationService"; interface Notification { @@ -37,9 +39,16 @@ export function NotificationPage() { const [loading, setLoading] = useState(true); const [selectedType, setSelectedType] = useState("ALL"); + const navigate = useNavigate(); + const handleOnItemSelect = (itemId: number) => { - // navigate(`/items/${itemId}`); - alert(`(TODO) Navigate to item details page with ID: ${itemId}`); + // navigate to item details + navigate(`/inventory/${itemId}`); + }; + + const handleOpenIcon = (e: React.MouseEvent, itemId: number) => { + e.stopPropagation(); + handleOnItemSelect(itemId); }; useEffect(() => { @@ -153,7 +162,18 @@ export function NotificationPage() { onMouseLeave={(e) => { if (item.isRead) e.currentTarget.style.backgroundColor = "white"; }} -> + actions={[ + + diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index bbacae5..2064c55 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -50,7 +50,13 @@ export function LoginPage() { - or Sign up + + Don't have an account?{" "} + Sign Up + + + By clicking continue, you agree to our Terms of Service and Privacy Policy + ); diff --git a/src/pages/NotificationPage.tsx b/src/pages/NotificationPage.tsx index 95fabcd..b5973de 100644 --- a/src/pages/NotificationPage.tsx +++ b/src/pages/NotificationPage.tsx @@ -131,6 +131,7 @@ export function NotificationPage() { px-4 py-1 rounded-full font-medium transition-colors duration-200 whitespace-nowrap + cursor-pointer ${isSelected ? "bg-blue-500 text-white shadow-md" : "bg-gray-200 text-gray-700 hover:bg-gray-300"} `}> {labels[type]} diff --git a/src/pages/ProductFormPage.tsx b/src/pages/ProductFormPage.tsx index 8e532c7..62b9945 100644 --- a/src/pages/ProductFormPage.tsx +++ b/src/pages/ProductFormPage.tsx @@ -44,7 +44,6 @@ export function ProductFormPage() { }); const response = await InventoryService.donate(formData); - console.log(response); if (response.status === 201) { message.success("Successfully created!"); form.resetFields(); diff --git a/src/pages/ProductShowPage.tsx b/src/pages/ProductShowPage.tsx index 058961d..13b1be1 100644 --- a/src/pages/ProductShowPage.tsx +++ b/src/pages/ProductShowPage.tsx @@ -1,150 +1,444 @@ -import React, { useEffect, useState } from "react"; +import React, { use, useEffect, useState } from "react"; import { useParams } from "react-router"; import { InventoryService } from "../services/InventoryService"; -import { Card, Alert, Row, Col, Image, Descriptions, Tag, Space, Typography, Divider } from "antd"; -import { ArrowLeftOutlined } from "@ant-design/icons"; +import { Card, Alert, Row, Col, Image, Descriptions, Tag, Space, Typography, Divider, Button, Modal, List } from "antd"; +import { ArrowLeftOutlined, InfoCircleOutlined, CloseCircleFilled } from "@ant-design/icons"; +import { useNavigate } from "react-router"; const { Title, Text } = Typography; -type Location = { - address?: string; - city?: string; - state?: string; - country?: string; - latitude?: number; - longitude?: number; -}; - -type Owner = { - id?: number; - fullName?: string; - email?: string; -}; +type Location = { address?: string; city?: string; state?: string; country?: string; latitude?: number; longitude?: number }; +type Owner = { id?: number; fullName?: string; email?: string }; +type Claim = { id: number; userId: number; userName: string; status: string; message?: string }; type Item = { - id?: number; - name?: string; - description?: string; - quantity?: number; - location?: Location; - imageUrls?: string[]; - status?: string; - owner?: Owner; - receiver?: any; - expiresAfterDays?: number; - neverExpires?: boolean; - claims?: any[]; - [key: string]: any; + id?: number; + name?: string; + description?: string; + quantity?: number; + location?: Location; + imageUrls?: string[]; + status?: string; + owner?: Owner; + receiver?: any; + expiresAfterDays?: number; + neverExpires?: boolean; + claims?: Claim[]; + [key: string]: any; }; export const ProductShowPage: React.FC = () => { - const { id } = useParams<{ id: string }>(); - const [item, setItem] = useState(null); - const [loading, setLoading] = useState(false); - - useEffect(() => { - if (!id) return; - (async () => { - if (loading) return; - setLoading(true); - try { - const { data } = await InventoryService.getItem(id); - setItem(data); - } catch (error) { - console.log("ERROR:", error); - } - })(); - }); - - if (!item) - return ( -
-
-
-
window.history.back()}> - -
-

Show item

-
-
- -
- -
- ); + const { id } = useParams<{ id: string }>(); + const [item, setItem] = useState(null); + const [loading, setLoading] = useState(false); + + const [error, setError] = useState(""); + const [showErrorDetails, setShowErrorDetails] = useState(false); + const navigate = useNavigate(); + + const loggedInUser = JSON.parse(localStorage.getItem("user") || "{}"); + + const fetchItem = async () => { + if (!id) return; + setLoading(true); + try { + const { data } = await InventoryService.getItem(id); + console.log("Fetched item data:", data); + setItem(data); + } + catch (error) { + console.log("ERROR:", error); + setError("Failed to fetch item details."); + } + finally { + setLoading(false); + } + }; - const imageSrc = item.imageUrls && item.imageUrls.length > 0 ? item.imageUrls[0] : undefined; - const expiryText = item.neverExpires ? "Never expires" : item.expiresAfterDays ? `Expires after ${item.expiresAfterDays} days` : "No expiry info"; + useEffect(() => { + fetchItem(); + }, [id]); + if (!item) return ( -
-
-
-
window.history.back()}> - -
-

Show item

-
+
+
+
+
window.history.back()}> + +
+

Show item

+
+
+ +
+ +
+ ); + + const expiryText = item.neverExpires + ? "Never expires" + : item.expiresAfterDays + ? `Expires after ${item.expiresAfterDays} days` + : "No expiry info"; + + const handleClaim = async () => { + if (!item?.id) return; + setError(""); + try { + await InventoryService.claimItem(item.id); + fetchItem(); + } + catch (error) { + console.log("Claim error:", error); + setError("Failed to claim the item"); + } + }; + const isOwner = item.owner?.id === loggedInUser.id; + + // Find a pending claim for the logged-in user (only if they are NOT the owner) + const myClaim = !isOwner + ? item.claims?.find(c => c.userId === loggedInUser.id && c.status === "PENDING") + : undefined; + + const handleCancel = async () => { + if (!myClaim) return; + setError(""); + try { + await InventoryService.cancelClaim(myClaim.id); + fetchItem(); + } + catch (error) { + console.log("Cancel claim error:", error); + setError("Failed to cancel the claim."); + } + }; + + const handleApprove = async (claimId: number) => { + setError(""); + try { + await InventoryService.approveClaim(claimId); + fetchItem(); + } + catch (error) { + console.log("Approve claim error:", error); + setError("Failed to approve the claim."); + } + }; + + const handleDelete = async () => { + if (!item?.id) return; + setError(""); + try { + await InventoryService.deleteItem(item.id); + navigate('/inventory'); + } + catch (error) { + console.log("Delete error:", error); + setError("Failed to delete the item."); + } + }; + + return ( +
+
+
+
window.history.back()}> + +
+

Details of the selected item.

+
+
+ +
+ + {item.name} + + } + extra={ + isOwner && ( + + ) + } + > + + +
+ {item.imageUrls && item.imageUrls.length > 0 ? ( +
+
+ {item.imageUrls.map((imageUrl, index) => ( +
+ {`${item.name} +
+ ))} +
+
+ ) : ( +
+ 🖼️ + No image available
- + )} + + 📦 Quantity: {item.quantity ?? 0} + ⏰ {expiryText} +
- - {item.name} - - } + + + + + {item.description || "-"} + + {item.location ? ( + + {/* Address line */} +
+ 🏠 + {item.location.address} +
+ + {/* City, State, Country */} +
+ 📍 + + {[item.location.city, item.location.state, item.location.country].filter(Boolean).join(", ")} + +
+ + {/* See on Map link */} + {item.location.latitude != null && item.location.longitude != null && ( + - ); + )} +
+ ) : ( + "-" + )} +
+ + {item.owner && ( + + + + 👤 Owner / Donator + {item.owner.fullName} + {item.owner.email} + + Contact the owner for any inquiries regarding this donation + + + + + )} + { item.receiver && ( + + + + ✓ Allocated Recipient + {item.receiver.fullName} + {item.receiver.email} + + ⚠ This item has already been donated and is not available + + + + + )} + + + {item.claims ? item.claims.length : 0} + {item.claims && item.claims.length > 0 && ( + + {item.claims.filter(c => c.status === "PENDING").length} total pending claims + + )} + + + {error && ( + + +
+
+
+ } + style={{ marginTop: 4 }} + /> + + )} + + {/* Error Details Modal - Only shows when info icon is clicked */} + {showErrorDetails && ( + setShowErrorDetails(false)} + closable={true} + footer={[ + , + ]} + > +
+ Possible reasons: +
+ {[ + "✓ You have already claimed this item", + "⏰ Your session has expired, please log in again", + "📋 Your previous claim was cancelled - you cannot claim it again", + "✓ Someone else has already received this item", + "📦 The item is no longer available", + "⏰ This item has expired", + "🌐 Network connectivity issue", + "🔒 You don't have permission to claim this item", + ].map((reason: string, idx: number) => ( +
+ {reason} +
+ ))} +
+
+ + 💬 If you believe this is an error, please contact the item owner or support team. + +
+
+
+ )} + + + {/* Claim button, */} + {!isOwner && item.status !== "DONATED" && !myClaim && ( + + )} + {/* Cancel claim button */} + {myClaim && ( + + )} + {/* Approve buttons for owner */} + {isOwner && (item.claims ?? []).filter((c) => c.status === "PENDING").length > 0 && ( +
+ Pending claims to approve: +
+ {(item.claims ?? []).filter((c) => c.status === "PENDING").map((claim) => ( + +
+ {claim.userName} + {claim.message && {claim.message}} +
+ +
+ ))} +
+
+ )} +
+
+ + + + + + {/* Error Details Modal - Only shows when info icon is clicked */} + {showErrorDetails && ( + setShowErrorDetails(false)} + closable={true} + footer={[ + , + ]} + > +
+ Possible reasons: +
+ {[ + "1. You have already claimed this item", + "2. Your previous claim was cancelled - you cannot claim it again", + "3. Someone else has already received this item", + "4. The item is no longer available", + "5. This item has expired", + "6. Network connectivity issue", + "7. You don't have permission to claim this item", + ].map((reason: string, idx: number) => ( +
+ {reason} +
+ ))} +
+
+ + 💬 If you believe this is an error, please contact the item owner or support team. + +
+
+
+ )} +
+ ); }; export default ProductShowPage; diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index e06f12c..10cf2ce 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -9,12 +9,6 @@ export function ProfilePage() { return stored ? JSON.parse(stored) : null; }); - useEffect(() => { - if (!user) { - navigate("/signin"); - } - }, [user, navigate]); - const handleSignOut = () => { localStorage.removeItem("user"); localStorage.removeItem("auth_token"); @@ -42,42 +36,30 @@ export function ProfilePage() { {user?.email}
- -
-
-
- {/* placeholder */}0 -
-
- Donations -
-
-
-
- {/* placeholder */}0 +
+
+
+ {/* placeholder */}0 +
+
+ Donations +
-
- Claims +
+
+ {/* placeholder */}0 +
+
+ Claims +
-
-
- {/* placeholder */}0 -
-
- Friends -
-
-
-
diff --git a/src/pages/RegisterPage.tsx b/src/pages/RegisterPage.tsx index d5bd615..799e879 100644 --- a/src/pages/RegisterPage.tsx +++ b/src/pages/RegisterPage.tsx @@ -2,7 +2,7 @@ import { Button, Form, Input, message } from "antd"; import { NavLink } from "react-router"; import { AuthService } from "../services/AuthService"; import { useNavigate } from "react-router"; -import { useState } from "react"; +import { useEffect, useState } from "react"; type RegisterType = { email?: string; @@ -14,6 +14,14 @@ export function RegisterPage() { const navigate = useNavigate(); const [loading, setLoading] = useState(false); + useEffect(() => { + const stored = localStorage.getItem("auth_token"); + if (stored) { + navigate("/profile"); + return; + } + }, []); + const onSubmit = async (values: RegisterType) => { try { if (loading) return; @@ -48,10 +56,14 @@ export function RegisterPage() { + + Already have an account?{" "} + Sign in + By clicking continue, you agree to our Terms of Service and Privacy Policy diff --git a/src/services/InventoryService.ts b/src/services/InventoryService.ts index d68e73c..c5171e1 100644 --- a/src/services/InventoryService.ts +++ b/src/services/InventoryService.ts @@ -22,4 +22,28 @@ export const InventoryService = { method: "get", }); }, + claimItem: (itemId: number): Promise> => { + return service({ + url: `/api/v1/items/${itemId}/claim`, + method: "post", + }); + }, + cancelClaim: (claimId: number): Promise> => { + return service({ + url: `/api/v1/items/cancel/claims/${claimId}`, + method: "post", + }); + }, + approveClaim: (claimId: number): Promise> => { + return service({ + url: `/api/v1/items/approve/claims/${claimId}`, + method: "post", + }); + }, + deleteItem: (itemId: number): Promise> => { + return service({ + url: `/api/v1/items/${itemId}`, + method: "delete", + }); + } }; From 2deea96c53e5ed88da1c0973d5c4f79a969961c2 Mon Sep 17 00:00:00 2001 From: Badri Paudel Date: Wed, 10 Dec 2025 23:55:44 -0600 Subject: [PATCH 3/4] es-lint failures fixed --- src/pages/ProductShowPage.tsx | 7 ++++--- src/pages/ProfilePage.tsx | 2 +- src/pages/RegisterPage.tsx | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/ProductShowPage.tsx b/src/pages/ProductShowPage.tsx index 13b1be1..c0de4b6 100644 --- a/src/pages/ProductShowPage.tsx +++ b/src/pages/ProductShowPage.tsx @@ -1,7 +1,7 @@ -import React, { use, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { useParams } from "react-router"; import { InventoryService } from "../services/InventoryService"; -import { Card, Alert, Row, Col, Image, Descriptions, Tag, Space, Typography, Divider, Button, Modal, List } from "antd"; +import { Card, Alert, Row, Col, Image, Descriptions, Tag, Space, Typography, Divider, Button, Modal } from "antd"; import { ArrowLeftOutlined, InfoCircleOutlined, CloseCircleFilled } from "@ant-design/icons"; import { useNavigate } from "react-router"; @@ -30,7 +30,7 @@ type Item = { export const ProductShowPage: React.FC = () => { const { id } = useParams<{ id: string }>(); const [item, setItem] = useState(null); - const [loading, setLoading] = useState(false); + const [_, setLoading] = useState(false); const [error, setError] = useState(""); const [showErrorDetails, setShowErrorDetails] = useState(false); @@ -57,6 +57,7 @@ export const ProductShowPage: React.FC = () => { useEffect(() => { fetchItem(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [id]); if (!item) diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index 10cf2ce..f89f781 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -1,5 +1,5 @@ import { Avatar, Button, Divider } from "antd"; -import { useState, useEffect } from "react"; +import { useState } from "react"; import { useNavigate } from "react-router"; export function ProfilePage() { diff --git a/src/pages/RegisterPage.tsx b/src/pages/RegisterPage.tsx index 799e879..c4a732a 100644 --- a/src/pages/RegisterPage.tsx +++ b/src/pages/RegisterPage.tsx @@ -20,6 +20,7 @@ export function RegisterPage() { navigate("/profile"); return; } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const onSubmit = async (values: RegisterType) => { From 75b51b474b006d3695f952fbe18d8a8f091c180b Mon Sep 17 00:00:00 2001 From: fkvivid Date: Thu, 11 Dec 2025 10:29:54 -0600 Subject: [PATCH 4/4] update --- src/pages/ExplorePage.tsx | 176 ++++++++++++++++++++++++++++--- src/pages/InventoryPage.tsx | 2 +- src/services/InventoryService.ts | 52 +++++---- 3 files changed, 189 insertions(+), 41 deletions(-) diff --git a/src/pages/ExplorePage.tsx b/src/pages/ExplorePage.tsx index e011265..0cf2d2d 100644 --- a/src/pages/ExplorePage.tsx +++ b/src/pages/ExplorePage.tsx @@ -1,10 +1,30 @@ import { useEffect, useState } from "react"; import { MapContainer, TileLayer, Marker, Popup, Tooltip } from "react-leaflet"; import "leaflet/dist/leaflet.css"; -import { Input, Spin } from "antd"; +import { Input, Radio, Spin, Image } from "antd"; +import { InventoryService } from "../services/InventoryService"; +import { useNavigate } from "react-router"; +import L from "leaflet"; + +// Fix Leaflet marker icons in production +const defaultIcon = L.icon({ + 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", + iconSize: [25, 41], + iconAnchor: [12, 41], + popupAnchor: [1, -34], + shadowSize: [41, 41], +}); + +L.Marker.prototype.setIcon(defaultIcon); export function ExplorePage() { const [position, setPosition] = useState<[number, number] | null>(null); + const [menu, setMenu] = useState("map"); + const [items, setItems] = useState([]); + const navigate = useNavigate(); useEffect(() => { if (!navigator.geolocation) { @@ -19,30 +39,152 @@ export function ExplorePage() { (err) => { console.error(err); alert("Unable to retrieve your location"); - }, + } ); + + // eslint-disable-next-line react-hooks/immutability + getItems(); }, []); - if (!position) return
; + const getItems = async () => { + try { + const { data } = await InventoryService.getAvailableItems(); + setItems(data); + } catch (error) { + console.log("ERROR:", error); + } + }; + + if (!position) + return ( +
+ +
+ ); return (
-

Explore

-
- +

+ Explore +

+
+
- - + setMenu(e.target.value)} + defaultValue={menu} + optionType="button" + buttonStyle="solid" /> - - Your current location - - You are here - - - +
+ {menu === "map" ? ( + + + + + You are here + + + {items.map((item) => { + if (!item.location.latitude || !item.location.longitude) + return null; + const itemPosition: [number, number] = [ + item.location.latitude, + item.location.longitude, + ]; + return ( + + // navigate(`/inventory/${item.id}`), + // }} + > + +
+

+ {item.name} +

+

+ {item.description} +

+ +
+
+ + + {item.name} + + +
+ ); + })} +
+ ) : ( +
+ {items.map((item, index) => ( +
navigate(`/inventory/${item.id}`)} + > +
+ basic image 0 + ? item.imageUrls[0] + : undefined + } + preview={false} + fallback="https://plus.unsplash.com/premium_vector-1731660866636-d6ee4dcd6806?q=80&w=1480&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" + /> +
+
+

+ {item.name} +

+

+ {item.description} +

+
+
+ {item.quantity} +
+
+ ))} +
+ )}
); } diff --git a/src/pages/InventoryPage.tsx b/src/pages/InventoryPage.tsx index 4d379a0..6fcdf06 100644 --- a/src/pages/InventoryPage.tsx +++ b/src/pages/InventoryPage.tsx @@ -20,7 +20,7 @@ export function InventoryPage() { }, []); return ( -
+
{/* Header */}
diff --git a/src/services/InventoryService.ts b/src/services/InventoryService.ts index c5171e1..e688845 100644 --- a/src/services/InventoryService.ts +++ b/src/services/InventoryService.ts @@ -10,6 +10,12 @@ export const InventoryService = { headers: data instanceof FormData ? { "Content-Type": "multipart/form-data" } : {}, }); }, + getAvailableItems: (): Promise> => { + return service({ + url: "/api/v1/items/available", + method: "get", + }); + }, getMyItems: (): Promise> => { return service({ url: "/api/v1/items/my-items", @@ -23,27 +29,27 @@ export const InventoryService = { }); }, claimItem: (itemId: number): Promise> => { - return service({ - url: `/api/v1/items/${itemId}/claim`, - method: "post", - }); - }, - cancelClaim: (claimId: number): Promise> => { - return service({ - url: `/api/v1/items/cancel/claims/${claimId}`, - method: "post", - }); - }, - approveClaim: (claimId: number): Promise> => { - return service({ - url: `/api/v1/items/approve/claims/${claimId}`, - method: "post", - }); - }, - deleteItem: (itemId: number): Promise> => { - return service({ - url: `/api/v1/items/${itemId}`, - method: "delete", - }); - } + return service({ + url: `/api/v1/items/${itemId}/claim`, + method: "post", + }); + }, + cancelClaim: (claimId: number): Promise> => { + return service({ + url: `/api/v1/items/cancel/claims/${claimId}`, + method: "post", + }); + }, + approveClaim: (claimId: number): Promise> => { + return service({ + url: `/api/v1/items/approve/claims/${claimId}`, + method: "post", + }); + }, + deleteItem: (itemId: number): Promise> => { + return service({ + url: `/api/v1/items/${itemId}`, + method: "delete", + }); + }, };