Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ function App() {
<Routes>
{/* Public routes */}
<Route index path="/" element={<MainLayout children={<HomePage />} />} />
<Route path="/profile" element={<MainLayout children={<ProfilePage />} />} />
<Route path="/signin" element={<MainLayout children={<LoginPage />} />} />
<Route path="/signup" element={<MainLayout children={<RegisterPage />} />} />

{/* Protected routes */}
<Route path="/profile" element={<ProtectedRoute element={<MainLayout children={<ProfilePage />} />} />} />
<Route path="/explore" element={<ProtectedRoute element={<MainLayout children={<ExplorePage />} />} />} />
<Route path="/inventory" element={<ProtectedRoute element={<MainLayout children={<InventoryPage />} />} />} />
<Route path="/notifications" element={<ProtectedRoute element={<MainLayout children={<NotificationPage />} />} />} />
Expand Down
13 changes: 6 additions & 7 deletions src/components/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import React from "react";
// import { Navigate } from "react-router";
import type { ReactNode } from "react";
import { Navigate } from "react-router";

interface ProtectedRouteProps {
element?: ReactNode;
}

export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ element }) => {
// const storedUser = localStorage.getItem("user");
//
// if (!storedUser) {
// return <Navigate to="/signin" replace />;
// }
//
const storedUser = localStorage.getItem("user");

if (!storedUser) {
return <Navigate to="/signin" replace />;
}
return <>{element}</>;
};
176 changes: 159 additions & 17 deletions src/pages/ExplorePage.tsx
Original file line number Diff line number Diff line change
@@ -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<string>("map");
const [items, setItems] = useState<any[]>([]);
const navigate = useNavigate();

useEffect(() => {
if (!navigator.geolocation) {
Expand All @@ -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 <div className="flex w-full h-full items-center justify-center"><Spin /></div>;
const getItems = async () => {
try {
const { data } = await InventoryService.getAvailableItems();
setItems(data);
} catch (error) {
console.log("ERROR:", error);
}
};

if (!position)
return (
<div className="flex w-full h-full items-center justify-center">
<Spin />
</div>
);

return (
<div className="flex h-full flex-col gap-3 pt-8">
<h4 className="text-center font-bold text-lg border-b border-gray-300 pb-4">Explore</h4>
<div className="px-4 py-4">
<Input.Search placeholder="Search item" className="h-full"/>
<h4 className="text-center font-bold text-lg border-b border-gray-300 pb-4">
Explore
</h4>
<div className="px-4 py-2">
<Input.Search placeholder="Search item" className="h-full" />
</div>
<MapContainer center={position} zoom={13} style={{ height: "100vh", width: "100%" }}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
<div className="px-4 mb-2">
<Radio.Group
block
options={[
{ label: "Map", value: "map" },
{ label: "List", value: "list" },
]}
onChange={(e) => setMenu(e.target.value)}
defaultValue={menu}
optionType="button"
buttonStyle="solid"
/>
<Marker position={position}>
<Popup>Your current location</Popup>
<Tooltip direction="top" offset={[-15, -10]} permanent>
You are here
</Tooltip>
</Marker>
</MapContainer>
</div>
{menu === "map" ? (
<MapContainer
center={position}
zoom={13}
style={{ height: "100vh", width: "100%" }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
<Marker position={position} icon={defaultIcon}>
<Tooltip direction="top" offset={[0, -30]} permanent>
You are here
</Tooltip>
</Marker>
{items.map((item) => {
if (!item.location.latitude || !item.location.longitude)
return null;
const itemPosition: [number, number] = [
item.location.latitude,
item.location.longitude,
];
return (
<Marker
key={item.id}
position={itemPosition}
icon={defaultIcon}
// eventHandlers={{
// click: () =>
// navigate(`/inventory/${item.id}`),
// }}
>
<Popup>
<div className="flex flex-col gap-2">
<h4 className="font-semibold">
{item.name}
</h4>
<p className="text-sm text-gray-600 max-w-xs">
{item.description}
</p>
<button
className="bg-blue-500 text-white px-3 py-1 rounded text-sm hover:bg-blue-600"
onClick={() =>
navigate(
`/inventory/${item.id}`
)
}
>
View Details
</button>
</div>
</Popup>
<Tooltip
direction="top"
offset={[0, -35]}
permanent
>
<span className="text-xs font-semibold bg-white px-2 py-1 rounded">
{item.name}
</span>
</Tooltip>
</Marker>
);
})}
</MapContainer>
) : (
<div className="flex-1 overflow-auto flex flex-col gap-3 mb-4">
{items.map((item, index) => (
<div
key={index}
className="flex justify-between gap-5 border border-gray-300 rounded p-4 cursor-pointer hover:shadow-md transition"
onClick={() => navigate(`/inventory/${item.id}`)}
>
<div>
<Image
alt="basic image"
width={80}
height={80}
src={
item.imageUrls.length > 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"
/>
</div>
<div className="w-full">
<h4 className="font-semibold text-lg">
{item.name}
</h4>
<p className="text-sm text-gray-600 line-clamp-3">
{item.description}
</p>
</div>
<div className="flex items-center">
{item.quantity}
</div>
</div>
))}
</div>
)}
</div>
);
}
2 changes: 1 addition & 1 deletion src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function HomePage() {
<div className="w-full max-w-4xl">
<div className="bg-linear-to-r from-indigo-50 to-white rounded-xl p-6 sm:p-8 shadow-sm">
<div className="flex flex-col items-start gap-4">
<h1 className="text-3xl sm:text-4xl font-extrabold text-gray-900">FairFree — Fair. Free. For Everyone.</h1>
<h1 className="text-3xl sm:text-4xl font-extrabold text-gray-900">FairFree — Fair and Free For Everyone.</h1>
<p className="text-gray-600 leading-relaxed">
A community-driven marketplace for sharing items and services with fairness and trust. Discover listings near you, give
away what you no longer need, and connect with neighbors.
Expand Down
18 changes: 8 additions & 10 deletions src/pages/InventoryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export function InventoryPage() {
}, []);

return (
<div className="flex flex-col justify-between h-full w-full p-8 gap-3">
<div className="flex flex-col h-screen w-full p-8 gap-3 pb-24">
{/* Header */}
<div className="flex flex-col w-full items-center">
<div className="flex justify-between w-full items-center">
<div></div>
Expand All @@ -29,16 +30,13 @@ export function InventoryPage() {
</div>
<Divider />
</div>
<div className="h-full flex flex-col gap-3">
<ul className="flex justify-between text-gray-500">
<li>Items</li>
<li>Description</li>
<li>Quantity</li>
</ul>

{/* Scrollable content */}
<div className="flex-1 overflow-auto flex flex-col gap-3 mb-4">
{items.map((item, index) => (
<div
key={index}
className="flex justify-between gap-5 border border-gray-300 rounded p-4"
className="flex justify-between gap-5 border border-gray-300 rounded p-4 cursor-pointer hover:shadow-md transition"
onClick={() => navigate(`/inventory/${item.id}`)}
>
<div>
Expand All @@ -59,9 +57,9 @@ export function InventoryPage() {
</div>
))}
</div>
<div className="mb-16 flex flex-col w-full">
<div className="pb-auto" >
<NavLink to="/inventory/new">
<Button type="primary" className="w-full">
<Button type="primary" className="w-full mb-4">
Donate
</Button>
</NavLink>
Expand Down
8 changes: 7 additions & 1 deletion src/pages/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,13 @@ export function LoginPage() {
</Form.Item>
</Form>
<Divider />
<NavLink to="/signup">or Sign up</NavLink>
<span className="text-sm text-center text-gray-500">
Don't have an account?{" "}
<NavLink to="/signup" className="text-blue-600 hover:underline">Sign Up</NavLink>
</span>
<span className="text-sm text-center text-gray-500">
By clicking continue, you agree to our <NavLink to="#">Terms of Service</NavLink> and <NavLink to="#">Privacy Policy </NavLink>
</span>
</div>
</div>
);
Expand Down
33 changes: 28 additions & 5 deletions src/pages/NotificationPage.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -37,9 +39,16 @@ export function NotificationPage() {
const [loading, setLoading] = useState(true);
const [selectedType, setSelectedType] = useState<string>("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(() => {
Expand Down Expand Up @@ -122,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]}
Expand Down Expand Up @@ -153,7 +163,18 @@ export function NotificationPage() {
onMouseLeave={(e) => {
if (item.isRead) e.currentTarget.style.backgroundColor = "white";
}}
>
actions={[
<Tooltip key="open" title="Open details" placement="top">
<Button
type="text"
onClick={(e) => handleOpenIcon(e, item.itemId)}
icon={<ArrowRightOutlined />}
aria-label="Open notification details"
style={{ minWidth: 44, minHeight: 44, padding: 8, display: "inline-flex", alignItems: "center", justifyContent: "center" }}
/>
</Tooltip>,
]}
>
<List.Item.Meta
avatar={
<div className="relative">
Expand All @@ -166,7 +187,9 @@ export function NotificationPage() {
</div>
}
title={
<div className="flex justify-between items-start w-full gap-4">
<div className="flex justify-between items-start w-full gap-4"
style={{ marginTop: 10, display: "inline-flex"}}
>
<span
className={`
${textClass}
Expand Down
1 change: 0 additions & 1 deletion src/pages/ProductFormPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading