Skip to content
Closed
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
4 changes: 4 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { MainLayout } from "./layout/MainLayout";
import { ConfigProvider } from "antd";
import { RegisterPage } from "./pages/RegisterPage";
import { ProtectedRoute } from "./components/ProtectedRoute";
import { ProductFormPage } from "./pages/ProductFormPage";
import { ProductShowPage } from "./pages/ProductShowPage";

function App() {
return (
Expand Down Expand Up @@ -38,6 +40,8 @@ function App() {
<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 />} />} />} />
<Route path="/inventory/new" element={<ProtectedRoute element={<MainLayout children={<ProductFormPage/>} />} />} />
<Route path="/inventory/:id" element={<ProtectedRoute element={<MainLayout children={<ProductShowPage />} />} />} />
</Routes>
</ConfigProvider>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Navigate } from "react-router";
import type { ReactNode } from "react";

interface ProtectedRouteProps {
element: ReactNode;
element?: ReactNode;
}

export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ element }) => {
Expand Down
3 changes: 2 additions & 1 deletion src/layout/MainFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const MainFooter: React.FC = () => {
display: "flex",
justifyContent: "space-around",
alignItems: "center",
backgroundColor: "white",
zIndex: 1000,
}}
>
Expand All @@ -39,7 +40,7 @@ export const MainFooter: React.FC = () => {
key={item.to}
to={item.to}
style={{
color: location.pathname === item.to ? "#000" : "#888",
color: location.pathname.startsWith(item.to) ? "#000" : "#888",
fontSize: "24px",
}}
>
Expand Down
61 changes: 55 additions & 6 deletions src/pages/InventoryPage.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,70 @@
import { Button, Divider } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import { useState, useEffect } from "react";
import { NavLink, useNavigate } from "react-router";
import { InventoryService } from "../services/InventoryService";
import { Image } from "antd";

export function InventoryPage() {
const [items, setItems] = useState<any[]>([]);
const navigate = useNavigate();

useEffect(() => {
(async () => {
try {
const { data } = await InventoryService.getMyItems();
setItems(data);
} catch (error) {
console.error("Failed to fetch items:", error);
}
})();
}, []);

return (
<div className="flex flex-col justify-between h-full w-full p-8">
<div className="flex flex-col justify-between h-full w-full p-8 gap-3">
<div className="flex flex-col w-full items-center">
<div className="flex justify-between w-full items-center">
<div></div>
<h3 className="font-bold text-xl">My Items</h3>
<Button type="primary" icon={<PlusOutlined />} />
<div></div>
</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>
{items.map((item, index) => (
<div
key={index}
className="flex justify-between gap-5 border border-gray-300 rounded p-4"
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 className="mb-16 flex flex-col w-full">
<Button type="primary" className="w-full">
Donate
</Button>
<NavLink to="/inventory/new">
<Button type="primary" className="w-full">
Donate
</Button>
</NavLink>
</div>
</div>
);
Expand Down
257 changes: 257 additions & 0 deletions src/pages/ProductFormPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import { Divider, Form, Input, InputNumber, Switch, Button, message, Upload } from "antd";
import { ArrowLeftOutlined, UploadOutlined } from "@ant-design/icons";
import { useState } from "react";
import { InventoryService } from "../services/InventoryService";
import { useNavigate } from "react-router";

export function ProductFormPage() {
const [form] = Form.useForm();
const [loading, setLoading] = useState<boolean>(false);
const [imageFiles, setImageFiles] = useState<Map<string, File>>(new Map());
const navigate = useNavigate();

const onSubmit = async (values: any) => {
try {
if (imageFiles.size === 0) {
message.error("Please uploat at least one image");
return;
}
if (loading) return;
setLoading(true);

// Create FormData
const formData = new FormData();

// Append form fields
formData.append(
"itemToBeAddedString",
JSON.stringify({
title: values.title,
description: values.description,
quantity: values.quantity,
location: values.location,
neverExpires: values.neverExpires,
expiresAfterDays: values.expiresAfterDays,
}),
);

formData.append("Content-Type", "application/json");

// Append image files
imageFiles.forEach((file) => {
formData.append("images", file);
});

const response = await InventoryService.donate(formData);
console.log(response);
if (response.status === 201) {
message.success("Successfully created!");
form.resetFields();
setImageFiles(new Map());
navigate("/inventory");
}
} catch (error: any) {
message.error(error?.response?.data?.message || error.toString());
console.log("ERROR:", error);
} finally {
setLoading(false);
}
};

const fillLocationAutomatically = () => {
if (!navigator.geolocation) {
message.error("Geolocation is not supported by your browser.");
return;
}

navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;

// Fill top-level coordinates (for validation) and nested location
// fields after reverse-geocoding.
// Try reverse geocoding to extract address components.
fetch(`https://nominatim.openstreetmap.org/reverse?lat=${latitude}&lon=${longitude}&format=json&addressdetails=1`)
.then((res) => res.json())
.then((data) => {
const display = data?.display_name || "";
const addr = data?.address || {};
const city = addr.city || addr.town || addr.village || "";
const state = addr.state || "";
const country = addr.country || "";

form.setFieldsValue({
// nested location object
location: {
address: display,
city,
state,
country,
latitude,
longitude,
},
});
message.success("Location auto-filled!");
})
.catch(() => {
// fallback: set coords only
form.setFieldsValue({
location: { address: "", city: "", state: "", country: "", latitude, longitude },
latitude,
longitude,
});
message.warning("Unable to auto-detect address components; coordinates were filled.");
});
},
() => {
message.error("Permission denied. Cannot fill location automatically.");
},
);
};

return (
<div className="w-full items-center flex flex-col p-8 gap-3 pb-20">
<div className="flex justify-between w-full items-center">
<div onClick={() => window.history.back()}>
<ArrowLeftOutlined />
</div>
<h3 className="font-bold text-xl">Add donation</h3>
<div></div>
</div>
<Divider />

<Form
form={form}
className="w-full"
layout="vertical"
onFinish={onSubmit}
initialValues={{
title: "",
description: "",
location: {
address: "Iowa, USA",
city: "Fairfield",
state: "IA",
country: "USA",
latitude: 41.7,
longitude: -93.0,
},
quantity: 1,
expiresAfterDays: 60,
neverExpires: false,
}}
>
{/* Name */}
<Form.Item name="title" label="Title" rules={[{ required: true, message: "Please input a title!" }]}>
<Input />
</Form.Item>

{/* Description */}
<Form.Item name="description" label="Description" rules={[{ required: true, message: "Please input a description!" }]}>
<Input.TextArea rows={3} />
</Form.Item>

<Form.Item name="quantity" label="Quantity" rules={[{ required: true, message: "Please input a name!" }]}>
<InputNumber />
</Form.Item>

{/* Images */}
<Form.Item label="Images" rules={[{ required: true, message: "Please upload at least one image!" }]}>
<Upload
multiple
accept="image/*"
maxCount={10}
listType="picture"
beforeUpload={(file) => {
setImageFiles((prev) => new Map(prev).set(file.name + file.size, file));
return false; // Prevent automatic upload
}}
onRemove={(file) => {
setImageFiles((prev) => {
const newMap = new Map(prev);
newMap.delete(file.name + file.size);
return newMap;
});
}}
>
<Button icon={<UploadOutlined />}>Upload Images (Max 10)</Button>
</Upload>
</Form.Item>

{/* Location fields */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<Form.Item name={["location", "address"]} label="Address" rules={[{ required: true, message: "Please input an address!" }]}>
<Input placeholder="Street address / display name" />
</Form.Item>

<Form.Item name={["location", "city"]} label="City" rules={[{ required: true, message: "Please input a city!" }]}>
<Input />
</Form.Item>

<Form.Item name={["location", "state"]} label="State">
<Input />
</Form.Item>

<Form.Item name={["location", "country"]} label="Country" rules={[{ required: true, message: "Please input a country!" }]}>
<Input />
</Form.Item>

<Form.Item
hidden
name={["location", "latitude"]}
label="Latitude"
rules={[{ required: true, message: "Please input a country!" }]}
>
<InputNumber />
</Form.Item>

<Form.Item
hidden
name={["location", "longitude"]}
label="Longitude"
rules={[{ required: true, message: "Please input a country!" }]}
>
<InputNumber />
</Form.Item>

<div className="col-span-1 md:col-span-2 flex items-end mb-4">
<Button onClick={fillLocationAutomatically}>Use my location to fill location</Button>
</div>
</div>

{/* Expiry */}
<Form.Item name="neverExpires" label="Never Expires" valuePropName="checked">
<Switch
onChange={(checked) => {
if (checked) {
form.setFieldsValue({ expiresAfterDays: null });
} else {
form.setFieldsValue({ expiresAfterDays: 60 });
}
}}
/>
</Form.Item>

<Form.Item shouldUpdate={(prev, curr) => prev.neverExpires !== curr.neverExpires} noStyle>
{({ getFieldValue }) =>
!getFieldValue("neverExpires") && (
<Form.Item
name="expiresAfterDays"
label="Expires After (Days)"
rules={[{ required: true, message: "Provide expiry days" }]}
>
<InputNumber min={1} className="w-full" />
</Form.Item>
)
}
</Form.Item>

<Form.Item>
<Button loading={loading} type="primary" htmlType="submit" className="w-full">
Submit
</Button>
</Form.Item>
</Form>
</div>
);
}
Loading