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}
+
+ {/* TODO: replace with actual user rating */}
+
+
+
+
+
+ 5.0
+ (10 reviews)
+
+
+
+
+
+
+
+
+
+
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;
};