diff --git a/frontend/app/profile/listings/page.tsx b/frontend/app/profile/listings/page.tsx new file mode 100644 index 0000000..8676e25 --- /dev/null +++ b/frontend/app/profile/listings/page.tsx @@ -0,0 +1,13 @@ +import { getMyListings } from "@/lib/actions"; +import { MyListingsContent } from "@/components/profile/MyListingsContent"; + +export default async function MyListingsPage() { + const myListings = await getMyListings(); + + return ( +
+

My Listings

+ +
+ ); +} diff --git a/frontend/app/profile/page.tsx b/frontend/app/profile/page.tsx new file mode 100644 index 0000000..b4b5c02 --- /dev/null +++ b/frontend/app/profile/page.tsx @@ -0,0 +1,31 @@ +import { getCurrentUser, getMyListings, getUsersFavorites } from "@/lib/actions"; +import { ProfileHeader } from "@/components/profile/ProfileHeader"; +import { ListingSection } from "@/components/profile/ListingSection"; + +export default async function ProfilePage() { + const [currentUser, myListings, savedPosts] = await Promise.all([ + getCurrentUser(), + getMyListings(), + getUsersFavorites(), + ]); + + return ( +
+ + + +
+ ); +} diff --git a/frontend/app/profile/saved/page.tsx b/frontend/app/profile/saved/page.tsx new file mode 100644 index 0000000..b709317 --- /dev/null +++ b/frontend/app/profile/saved/page.tsx @@ -0,0 +1,13 @@ +import { getUsersFavorites } from "@/lib/actions"; +import { MyListingsContent } from "@/components/profile/MyListingsContent"; + +export default async function SavedListingsPage() { + const savedListings = await getUsersFavorites(); + + return ( +
+

Saved Listings

+ +
+ ); +} diff --git a/frontend/components/navbar/UserProfileDropdown.tsx b/frontend/components/navbar/UserProfileDropdown.tsx index 9ecf097..66f95a7 100644 --- a/frontend/components/navbar/UserProfileDropdown.tsx +++ b/frontend/components/navbar/UserProfileDropdown.tsx @@ -23,7 +23,7 @@ export function UserProfileDropdown({ isOpen, onClose }: UserProfileDropdownProp role="menu" > Selling Buying { + const Icon = icon === "listings" ? ShoppingBag : Bookmark; + + return ( +
+
+
+ +

+ {title} ({count}) +

+
+ + See All {title} + +
+ + {listings.length === 0 ? ( +

No {title.toLowerCase()} yet.

+ ) : ( +
+ {listings.map((listing) => ( +
+ +
+ ))} +
+ )} +
+ ); +}; diff --git a/frontend/components/profile/MyListingsContent.tsx b/frontend/components/profile/MyListingsContent.tsx new file mode 100644 index 0000000..25eeb8f --- /dev/null +++ b/frontend/components/profile/MyListingsContent.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { SearchInput } from "@/components/filters/SearchInput"; +import { ListingsCard } from "@/components/listings/ListingsCard"; +import { Listing } from "@/lib/types"; +import { cn } from "@/lib/utils"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +const TABS = ["All Listings", "Active Listings", "Completed"] as const; +type Tab = (typeof TABS)[number]; + +interface Props { + listings: Listing[]; +} + +function isActive(listing: Listing): boolean { + if (!listing.expires_at) return true; + return new Date(listing.expires_at) > new Date(); +} + +export const MyListingsContent = ({ listings }: Props) => { + const [activeTab, setActiveTab] = useState("All Listings"); + const [search, setSearch] = useState(""); + const [category, setCategory] = useState("all"); + + const categories = useMemo(() => { + const cats = new Set(); + listings.forEach((l) => { + if (l.listing_type === "item") cats.add(l.additional_data.category); + if (l.listing_type === "sublet") cats.add("Sublet"); + }); + return Array.from(cats).sort(); + }, [listings]); + + const filtered = useMemo(() => { + let result = listings; + + if (activeTab === "Active Listings") { + result = result.filter(isActive); + } else if (activeTab === "Completed") { + result = result.filter((l) => !isActive(l)); + } + + if (search.trim()) { + const q = search.trim().toLowerCase(); + result = result.filter((l) => l.title.toLowerCase().includes(q)); + } + + if (category !== "all") { + result = result.filter((l) => { + if (category === "Sublet") return l.listing_type === "sublet"; + return l.listing_type === "item" && l.additional_data.category === category; + }); + } + + return result; + }, [listings, activeTab, search, category]); + + return ( +
+
+
+ {TABS.map((tab) => ( + + ))} +
+ +
+ + +
+
+ + {filtered.length === 0 ? ( +

No listings found.

+ ) : ( +
+ {filtered.map((listing) => ( + + ))} +
+ )} +
+ ); +}; diff --git a/frontend/components/profile/ProfileHeader.tsx b/frontend/components/profile/ProfileHeader.tsx new file mode 100644 index 0000000..ec920fd --- /dev/null +++ b/frontend/components/profile/ProfileHeader.tsx @@ -0,0 +1,58 @@ +import Image from "next/image"; +import { Star } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { User } from "@/lib/types"; + +interface Props { + user: User; +} + +export const ProfileHeader = ({ user }: Props) => { + const fullName = `${user.first_name} ${user.last_name}`; + + return ( +
+
+
+ {fullName} +
+

{fullName}

+
+ {/* TODO: replace with actual user rating */} + + + + + + 5.0 + (10 reviews) +
+
+
+ +
+ +
+
+

Email

+

{user.email}

+
+
+

Phone number

+

{user.phone_number || "Not set"}

+
+
+

Notification method

+ {/* TODO: replace with actual notification preference */} +

Email

+
+
+
+ ); +}; diff --git a/frontend/lib/actions.ts b/frontend/lib/actions.ts index 7061b7e..fbf3981 100644 --- a/frontend/lib/actions.ts +++ b/frontend/lib/actions.ts @@ -270,3 +270,19 @@ export async function createListing(payload: CreateListingPayload): Promise>(`/market/listings/?${params.toString()}`); +} diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index c21cf62..1980bee 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -14,6 +14,8 @@ export type User = { first_name: string; last_name: string; email: string; + phone_number: string | null; + phone_verified: boolean; }; // ------------------------------------------------------------ @@ -124,6 +126,7 @@ type BaseListing = { images: string[]; tags: string[]; favorite_count: number; + offers_count?: number; is_favorited?: boolean; seller: User; };