Skip to content

Commit 30614e8

Browse files
committed
My listings page implementation
1 parent b4b82bf commit 30614e8

7 files changed

Lines changed: 144 additions & 17 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { getMyListings } from "@/lib/actions";
2+
import { MyListingsContent } from "@/components/profile/MyListingsContent";
3+
4+
export default async function MyListingsPage() {
5+
const myListings = await getMyListings();
6+
7+
return (
8+
<div className="container mx-auto w-full max-w-[96rem] space-y-6 px-4 pt-6 sm:px-12">
9+
<h1 className="text-2xl font-bold">My Listings</h1>
10+
<MyListingsContent listings={myListings.results} />
11+
</div>
12+
);
13+
}

frontend/app/profile/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default async function ProfilePage() {
1616
title="My Listings"
1717
count={myListings.count}
1818
listings={myListings.results}
19-
seeAllHref="/profile"
19+
seeAllHref="/profile/listings"
2020
icon="listings"
2121
/>
2222
<ListingSection

frontend/components/listings/detail/ListingDetail.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,8 @@ export const ListingDetail = ({ listing, initialIsFavorited }: Props) => {
5656
toggleFavoriteMutation.mutate(!isFavorited);
5757
};
5858

59-
const subletCoords =
60-
listingType === "sublet" ? listing.additional_data : null;
61-
const hasLocation =
62-
subletCoords?.latitude != null && subletCoords?.longitude != null;
59+
const subletCoords = listingType === "sublet" ? listing.additional_data : null;
60+
const hasLocation = subletCoords?.latitude != null && subletCoords?.longitude != null;
6361

6462
return (
6563
<div className="mx-auto flex w-full max-w-[96rem] flex-col p-8 px-4 sm:px-12">
@@ -94,13 +92,11 @@ export const ListingDetail = ({ listing, initialIsFavorited }: Props) => {
9492
<div>
9593
<h2 className="text-lg font-semibold">{"Where you'll be living"}</h2>
9694
<p className="text-sm text-gray-500">
97-
Approximate location shown. The exact location will be shared once you connect with the owner.
95+
Approximate location shown. The exact location will be shared once you connect
96+
with the owner.
9897
</p>
99-
</div>
100-
<SubletMap
101-
latitude={subletCoords.latitude!}
102-
longitude={subletCoords.longitude!}
103-
/>
98+
</div>
99+
<SubletMap latitude={subletCoords.latitude!} longitude={subletCoords.longitude!} />
104100
</div>
105101
)}
106102
<ListingActions

frontend/components/listings/detail/SubletMap.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ interface Props {
88
}
99

1010
const LazyMap = dynamic(
11-
() =>
12-
import("@/components/listings/detail/SubletMapContent").then((m) => m.SubletMapContent),
13-
{ ssr: false },
11+
() => import("@/components/listings/detail/SubletMapContent").then((m) => m.SubletMapContent),
12+
{ ssr: false }
1413
);
1514

1615
export const SubletMap = ({ latitude, longitude }: Props) => {

frontend/components/navbar/UserProfileDropdown.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function UserProfileDropdown({ isOpen, onClose }: UserProfileDropdownProp
2323
role="menu"
2424
>
2525
<Link
26-
href="/"
26+
href="/profile"
2727
onClick={onClose}
2828
className={cn(
2929
"flex items-center gap-3 rounded-t-md px-3 py-3 text-sm",
@@ -37,7 +37,7 @@ export function UserProfileDropdown({ isOpen, onClose }: UserProfileDropdownProp
3737

3838
<div className="text-foreground mt-1 px-3 py-1 text-xs font-bold">Selling</div>
3939
<Link
40-
href="/"
40+
href="/profile/listings"
4141
onClick={onClose}
4242
className={cn(
4343
"flex items-center gap-3 px-3 py-3 text-sm",
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"use client";
2+
3+
import { useState, useMemo } from "react";
4+
import { SearchInput } from "@/components/filters/SearchInput";
5+
import { ListingsCard } from "@/components/listings/ListingsCard";
6+
import { Listing } from "@/lib/types";
7+
import { cn } from "@/lib/utils";
8+
import {
9+
Select,
10+
SelectContent,
11+
SelectItem,
12+
SelectTrigger,
13+
SelectValue,
14+
} from "@/components/ui/select";
15+
16+
const TABS = ["All Listings", "Active Listings", "Completed"] as const;
17+
type Tab = (typeof TABS)[number];
18+
19+
interface Props {
20+
listings: Listing[];
21+
}
22+
23+
function isActive(listing: Listing): boolean {
24+
if (!listing.expires_at) return true;
25+
return new Date(listing.expires_at) > new Date();
26+
}
27+
28+
export const MyListingsContent = ({ listings }: Props) => {
29+
const [activeTab, setActiveTab] = useState<Tab>("All Listings");
30+
const [search, setSearch] = useState("");
31+
const [category, setCategory] = useState<string>("all");
32+
33+
const categories = useMemo(() => {
34+
const cats = new Set<string>();
35+
listings.forEach((l) => {
36+
if (l.listing_type === "item") cats.add(l.additional_data.category);
37+
if (l.listing_type === "sublet") cats.add("Sublet");
38+
});
39+
return Array.from(cats).sort();
40+
}, [listings]);
41+
42+
const filtered = useMemo(() => {
43+
let result = listings;
44+
45+
if (activeTab === "Active Listings") {
46+
result = result.filter(isActive);
47+
} else if (activeTab === "Completed") {
48+
result = result.filter((l) => !isActive(l));
49+
}
50+
51+
if (search.trim()) {
52+
const q = search.trim().toLowerCase();
53+
result = result.filter((l) => l.title.toLowerCase().includes(q));
54+
}
55+
56+
if (category !== "all") {
57+
result = result.filter((l) => {
58+
if (category === "Sublet") return l.listing_type === "sublet";
59+
return l.listing_type === "item" && l.additional_data.category === category;
60+
});
61+
}
62+
63+
return result;
64+
}, [listings, activeTab, search, category]);
65+
66+
return (
67+
<div className="space-y-6">
68+
<div className="flex items-center justify-between gap-4">
69+
<div className="flex gap-6">
70+
{TABS.map((tab) => (
71+
<button
72+
key={tab}
73+
onClick={() => setActiveTab(tab)}
74+
className={cn(
75+
"pb-1 text-sm font-medium transition-colors",
76+
activeTab === tab
77+
? "text-brand border-brand border-b-2"
78+
: "text-gray-500 hover:text-gray-700"
79+
)}
80+
>
81+
{tab}
82+
</button>
83+
))}
84+
</div>
85+
86+
<div className="flex items-center gap-3">
87+
<SearchInput placeholder="Search Listings" value={search} onChange={setSearch} />
88+
<Select value={category} onValueChange={setCategory}>
89+
<SelectTrigger>
90+
<SelectValue placeholder="Category" />
91+
</SelectTrigger>
92+
<SelectContent>
93+
<SelectItem value="all">All Categories</SelectItem>
94+
{categories.map((cat) => (
95+
<SelectItem key={cat} value={cat}>
96+
{cat}
97+
</SelectItem>
98+
))}
99+
</SelectContent>
100+
</Select>
101+
</div>
102+
</div>
103+
104+
{filtered.length === 0 ? (
105+
<p className="py-12 text-center text-sm text-gray-500">No listings found.</p>
106+
) : (
107+
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
108+
{filtered.map((listing) => (
109+
<ListingsCard
110+
key={listing.id}
111+
listing={listing}
112+
previewImageUrl={listing.images[0]}
113+
href={`/${listing.listing_type === "item" ? "items" : "sublets"}/${listing.id}`}
114+
/>
115+
))}
116+
</div>
117+
)}
118+
</div>
119+
);
120+
};

frontend/lib/constants.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export const BASE_URL =
1111

1212
export const API_BASE_URL =
1313
process.env.NODE_ENV === "production" ? "REPLACE WITH PROD API URL" : "http://backend:8000"; // can't be localhost because server fetch happens in container
14-
1514

1615
export const PLATFORM_URL = process.env.PLATFORM_URL;
1716
export const CLIENT_ID = process.env.CLIENT_ID;

0 commit comments

Comments
 (0)