From c8b5cb93bab6238869de96322ff121461fb36619 Mon Sep 17 00:00:00 2001 From: fkvivid Date: Mon, 8 Dec 2025 11:45:14 -0600 Subject: [PATCH 1/3] add product show product --- src/App.tsx | 4 + src/components/ProtectedRoute.tsx | 2 +- src/layout/MainFooter.tsx | 3 +- src/pages/InventoryPage.tsx | 61 ++++++- src/pages/ProductFormPage.tsx | 257 ++++++++++++++++++++++++++++++ src/pages/ProductShowPage.tsx | 152 ++++++++++++++++++ src/services/InventoryService.ts | 25 +++ src/services/fetchInterceptor.ts | 3 +- 8 files changed, 498 insertions(+), 9 deletions(-) create mode 100644 src/pages/ProductFormPage.tsx create mode 100644 src/pages/ProductShowPage.tsx create mode 100644 src/services/InventoryService.ts diff --git a/src/App.tsx b/src/App.tsx index 6f6c406..0c9889f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( @@ -38,6 +40,8 @@ function App() { } />} />} /> } />} />} /> } />} />} /> + } />} />} /> + } />} />} /> ); diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index 7f8490a..154bcea 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -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 = ({ element }) => { diff --git a/src/layout/MainFooter.tsx b/src/layout/MainFooter.tsx index a75fb5d..ff9c217 100644 --- a/src/layout/MainFooter.tsx +++ b/src/layout/MainFooter.tsx @@ -30,6 +30,7 @@ export const MainFooter: React.FC = () => { display: "flex", justifyContent: "space-around", alignItems: "center", + backgroundColor: "white", zIndex: 1000, }} > @@ -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", }} > diff --git a/src/pages/InventoryPage.tsx b/src/pages/InventoryPage.tsx index 4f15104..8a063a3 100644 --- a/src/pages/InventoryPage.tsx +++ b/src/pages/InventoryPage.tsx @@ -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([]); + const navigate = useNavigate(); + + useEffect(() => { + (async () => { + try { + const { data } = await InventoryService.getMyItems(); + setItems(data); + } catch (error) { + console.error("Failed to fetch items:", error); + } + })(); + }, []); + return ( -
+

My Items

-
+
+
    +
  • Items
  • +
  • Description
  • +
  • Quantity
  • +
+ {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/ProductFormPage.tsx b/src/pages/ProductFormPage.tsx new file mode 100644 index 0000000..df95c22 --- /dev/null +++ b/src/pages/ProductFormPage.tsx @@ -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(false); + const [imageFiles, setImageFiles] = useState>(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 ( +
+
+
window.history.back()}> + +
+

Add donation

+
+
+ + +
+ {/* Name */} + + + + + {/* Description */} + + + + + + + + + {/* Images */} + + { + 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; + }); + }} + > + + + + + {/* Location fields */} +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + {/* Expiry */} + + { + if (checked) { + form.setFieldsValue({ expiresAfterDays: null }); + } else { + form.setFieldsValue({ expiresAfterDays: 60 }); + } + }} + /> + + + prev.neverExpires !== curr.neverExpires} noStyle> + {({ getFieldValue }) => + !getFieldValue("neverExpires") && ( + + + + ) + } + + + + + +
+
+ ); +} diff --git a/src/pages/ProductShowPage.tsx b/src/pages/ProductShowPage.tsx new file mode 100644 index 0000000..618e3da --- /dev/null +++ b/src/pages/ProductShowPage.tsx @@ -0,0 +1,152 @@ +import React, { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router"; +import { InventoryService } from "../services/InventoryService"; +import { Button, Card, Alert, Row, Col, Image, Descriptions, Tag, Space, Typography, Divider } from "antd"; +import { ArrowLeftOutlined } from "@ant-design/icons"; + +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 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; +}; + +export const ProductShowPage: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const [item, setItem] = useState(null); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + + 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 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"; + + return ( +
+
+
+
window.history.back()}> + +
+

Show item

+
+
+ +
+ + {item.name} + + } + > + + + {imageSrc ? ( + {item.name} + ) : ( +
+ No image +
+ )} + + {`Quantity: ${item.quantity ?? 0}`} + {expiryText} + + + + + + {item.description || "-"} + + {item.location ? ( +
+
{item.location.address}
+
{[item.location.city, item.location.state, item.location.country].filter(Boolean).join(", ")}
+
+ ) : ( + "-" + )} +
+ {item.claims ? item.claims.length : 0} +
+ + +
+
+
+ ); +}; + +export default ProductShowPage; diff --git a/src/services/InventoryService.ts b/src/services/InventoryService.ts new file mode 100644 index 0000000..d68e73c --- /dev/null +++ b/src/services/InventoryService.ts @@ -0,0 +1,25 @@ +import type { AxiosResponse } from "axios"; +import { service } from "./fetchInterceptor"; + +export const InventoryService = { + donate: (data: any): Promise> => { + return service({ + url: "/api/v1/items", + method: "post", + data: data, + headers: data instanceof FormData ? { "Content-Type": "multipart/form-data" } : {}, + }); + }, + getMyItems: (): Promise> => { + return service({ + url: "/api/v1/items/my-items", + method: "get", + }); + }, + getItem: (id: string): Promise> => { + return service({ + url: `/api/v1/items/${id}`, + method: "get", + }); + }, +}; diff --git a/src/services/fetchInterceptor.ts b/src/services/fetchInterceptor.ts index 99ab4a8..4e4721f 100644 --- a/src/services/fetchInterceptor.ts +++ b/src/services/fetchInterceptor.ts @@ -2,7 +2,8 @@ import axios from "axios"; const service = axios.create({ // baseURL: "http://localhost:8080", - baseURL: "https://testapi.xcodestest.net", + // baseURL: import.meta.env.VITE_API_BASE_URL, + baseURL: import.meta.env.DEV ? "http://localhost:8080" : "https://testapi.xcodestest.net", timeout: 60000, maxBodyLength: Infinity, }); From 537e23c8adb709df745567137b8735e571b11b89 Mon Sep 17 00:00:00 2001 From: fkvivid Date: Mon, 8 Dec 2025 11:47:19 -0600 Subject: [PATCH 2/3] lint fix --- src/pages/ProductShowPage.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/ProductShowPage.tsx b/src/pages/ProductShowPage.tsx index 618e3da..058961d 100644 --- a/src/pages/ProductShowPage.tsx +++ b/src/pages/ProductShowPage.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; -import { useParams, useNavigate } from "react-router"; +import { useParams } from "react-router"; import { InventoryService } from "../services/InventoryService"; -import { Button, Card, Alert, Row, Col, Image, Descriptions, Tag, Space, Typography, Divider } from "antd"; +import { Card, Alert, Row, Col, Image, Descriptions, Tag, Space, Typography, Divider } from "antd"; import { ArrowLeftOutlined } from "@ant-design/icons"; const { Title, Text } = Typography; @@ -41,7 +41,6 @@ export const ProductShowPage: React.FC = () => { const { id } = useParams<{ id: string }>(); const [item, setItem] = useState(null); const [loading, setLoading] = useState(false); - const navigate = useNavigate(); useEffect(() => { if (!id) return; @@ -141,7 +140,6 @@ export const ProductShowPage: React.FC = () => { {item.claims ? item.claims.length : 0} - From 75039c37ca6b513517d908cc2d9f58f447b8808f Mon Sep 17 00:00:00 2001 From: fkvivid Date: Mon, 8 Dec 2025 11:58:33 -0600 Subject: [PATCH 3/3] routing error on vercel --- vercel.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 vercel.json diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..b4bb135 --- /dev/null +++ b/vercel.json @@ -0,0 +1,6 @@ +{ + "rewrites": [ + { "source": "/(.*)", "destination": "/" } + ] +} +