Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
13 changes: 13 additions & 0 deletions frontend/app/profile/listings/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="container mx-auto w-full max-w-[96rem] space-y-6 px-4 pt-6 sm:px-12">
<h1 className="text-2xl font-bold">My Listings</h1>
<MyListingsContent listings={myListings.results} />
</div>
);
}
31 changes: 31 additions & 0 deletions frontend/app/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="container mx-auto w-full max-w-[96rem] space-y-8 px-4 pt-6 sm:px-12">
<ProfileHeader user={currentUser} />
<ListingSection
title="My Listings"
count={myListings.count}
listings={myListings.results}
seeAllHref="/profile/listings"
icon="listings"
/>
<ListingSection
title="Saved Posts"
count={savedPosts.count}
listings={savedPosts.results}
seeAllHref="/profile"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not have a separate page for saved posts?

icon="saved"
/>
</div>
);
}
16 changes: 6 additions & 10 deletions frontend/components/listings/detail/ListingDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,8 @@ export const ListingDetail = ({ listing, initialIsFavorited }: Props) => {
toggleFavoriteMutation.mutate(!isFavorited);
};

const subletCoords =
listingType === "sublet" ? listing.additional_data : null;
const hasLocation =
subletCoords?.latitude != null && subletCoords?.longitude != null;
const subletCoords = listingType === "sublet" ? listing.additional_data : null;
const hasLocation = subletCoords?.latitude != null && subletCoords?.longitude != null;

return (
<div className="mx-auto flex w-full max-w-[96rem] flex-col p-8 px-4 sm:px-12">
Expand Down Expand Up @@ -94,13 +92,11 @@ export const ListingDetail = ({ listing, initialIsFavorited }: Props) => {
<div>
<h2 className="text-lg font-semibold">{"Where you'll be living"}</h2>
<p className="text-sm text-gray-500">
Approximate location shown. The exact location will be shared once you connect with the owner.
Approximate location shown. The exact location will be shared once you connect
with the owner.
</p>
</div>
<SubletMap
latitude={subletCoords.latitude!}
longitude={subletCoords.longitude!}
/>
</div>
<SubletMap latitude={subletCoords.latitude!} longitude={subletCoords.longitude!} />
</div>
)}
<ListingActions
Expand Down
5 changes: 2 additions & 3 deletions frontend/components/listings/detail/SubletMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ interface Props {
}

const LazyMap = dynamic(
() =>
import("@/components/listings/detail/SubletMapContent").then((m) => m.SubletMapContent),
{ ssr: false },
() => import("@/components/listings/detail/SubletMapContent").then((m) => m.SubletMapContent),
{ ssr: false }
);

export const SubletMap = ({ latitude, longitude }: Props) => {
Expand Down
4 changes: 2 additions & 2 deletions frontend/components/navbar/UserProfileDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function UserProfileDropdown({ isOpen, onClose }: UserProfileDropdownProp
role="menu"
>
<Link
href="/"
href="/profile"
onClick={onClose}
className={cn(
"flex items-center gap-3 rounded-t-md px-3 py-3 text-sm",
Expand All @@ -37,7 +37,7 @@ export function UserProfileDropdown({ isOpen, onClose }: UserProfileDropdownProp

<div className="text-foreground mt-1 px-3 py-1 text-xs font-bold">Selling</div>
<Link
href="/"
href="/profile/listings"
Comment thread
i30101 marked this conversation as resolved.
onClick={onClose}
className={cn(
"flex items-center gap-3 px-3 py-3 text-sm",
Expand Down
48 changes: 48 additions & 0 deletions frontend/components/profile/ListingSection.tsx
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this component used anywhere?

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Link from "next/link";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding bg to this component to distinguish itself from the background. It doesn't look bad right now, but just something I'd think about

import { ShoppingBag, Bookmark } from "lucide-react";
import { ListingsCard } from "@/components/listings/ListingsCard";
import { Listing } from "@/lib/types";

interface Props {
title: string;
count: number;
listings: Listing[];
seeAllHref: string;
icon: "listings" | "saved";
}

export const ListingSection = ({ title, count, listings, seeAllHref, icon }: Props) => {
const Icon = icon === "listings" ? ShoppingBag : Bookmark;

return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Icon className="h-5 w-5" />
<h2 className="text-lg font-bold">
{title} ({count})
</h2>
</div>
<Link href={seeAllHref} className="text-brand text-sm font-medium hover:underline">
See All {title}
</Link>
</div>

{listings.length === 0 ? (
<p className="text-sm text-gray-500">No {title.toLowerCase()} yet.</p>
) : (
<div className="flex gap-4 overflow-x-auto pb-2">
{listings.map((listing) => (
<div key={listing.id} className="w-48 flex-shrink-0">
<ListingsCard
listing={listing}
previewImageUrl={listing.images[0]}
href={`/${listing.listing_type === "item" ? "items" : "sublets"}/${listing.id}`}
/>
</div>
))}
</div>
)}
</div>
);
};
120 changes: 120 additions & 0 deletions frontend/components/profile/MyListingsContent.tsx
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now, the listings are horizontally scrollable - im wondering if this is necessary if we have "Show all my listings" button. What do you think?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would say the same, I think it would be simpler if clicking on "My Listings" or "Saved Posts" sent you to their respective pages instead of what we have now

Original file line number Diff line number Diff line change
@@ -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) => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine for now how you are showing all my listings at once, but better practice is using paginated listings. It's unlikely that a single user has that many listings that fetching all at once is gonna cause any issues, but we never know. Take a look at how this is handled in the main grid view. If you want feel free to take a stab at it here as well, but its not necessary at this pt imo

const [activeTab, setActiveTab] = useState<Tab>("All Listings");
const [search, setSearch] = useState("");
const [category, setCategory] = useState<string>("all");

const categories = useMemo(() => {
const cats = new Set<string>();
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 (
<div className="space-y-6">
<div className="flex items-center justify-between gap-4">
<div className="flex gap-6">
{TABS.map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={cn(
"pb-1 text-sm font-medium transition-colors",
activeTab === tab
? "text-brand border-brand border-b-2"
: "text-gray-500 hover:text-gray-700"
)}
>
{tab}
</button>
))}
</div>

<div className="flex items-center gap-3">
<SearchInput placeholder="Search Listings" value={search} onChange={setSearch} />
<Select value={category} onValueChange={setCategory}>
<SelectTrigger>
<SelectValue placeholder="Category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
{categories.map((cat) => (
<SelectItem key={cat} value={cat}>
{cat}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>

{filtered.length === 0 ? (
<p className="py-12 text-center text-sm text-gray-500">No listings found.</p>
) : (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
{filtered.map((listing) => (
<ListingsCard
key={listing.id}
listing={listing}
previewImageUrl={listing.images[0]}
href={`/${listing.listing_type === "item" ? "items" : "sublets"}/${listing.id}`}
Comment on lines +109 to +113
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image I think the card ratio looks kinda weird here? Maybe make it bigger?

/>
))}
</div>
)}
</div>
);
};
66 changes: 66 additions & 0 deletions frontend/components/profile/ProfileHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Image from "next/image";
import { Star, Mail, Phone } from "lucide-react";

Check warning on line 2 in frontend/components/profile/ProfileHeader.tsx

View workflow job for this annotation

GitHub Actions / Frontend Checks

'Phone' is defined but never used

Check warning on line 2 in frontend/components/profile/ProfileHeader.tsx

View workflow job for this annotation

GitHub Actions / Frontend Checks

'Mail' is defined but never used

Check warning on line 2 in frontend/components/profile/ProfileHeader.tsx

View workflow job for this annotation

GitHub Actions / Frontend Checks

'Phone' is defined but never used

Check warning on line 2 in frontend/components/profile/ProfileHeader.tsx

View workflow job for this annotation

GitHub Actions / Frontend Checks

'Mail' is defined but never used
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove unused imports, this is another thing I'd check in my files before committing

import { Button } from "@/components/ui/button";
import { User } from "@/lib/types";

interface Props {
user: User;
}

function maskPhoneNumber(phone: string): string {
const digits = phone.replace(/\D/g, "");
if (digits.length < 4) return phone;
const areaCode = digits.slice(digits.length - 10, digits.length - 7);
const prefix = digits.slice(digits.length - 7, digits.length - 4);
return `(${areaCode}) ${prefix}-XXXX`;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should we mask phone numbers for user profile page?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, when viewing other peoples' profiles we might want masking but it's not needed here


export const ProfileHeader = ({ user }: Props) => {
const fullName = `${user.first_name} ${user.last_name}`;

return (
<div className="rounded-xl border border-gray-200 bg-white p-6 sm:p-8">
<div className="flex items-start justify-between">
<div className="flex items-center gap-4 sm:gap-6">
<Image
src="/images/default-avatar.png"
alt={fullName}
width={80}
height={80}
className="rounded-full"
/>
<div className="space-y-1">
<h1 className="text-xl font-bold sm:text-2xl">{fullName}</h1>
<div className="flex items-center gap-1.5 text-sm text-gray-600">
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
Comment thread
i30101 marked this conversation as resolved.
<span className="ml-1 font-medium">5.0</span>
<span className="text-gray-400">(10 reviews)</span>
</div>
</div>
</div>
<Button variant="outline">Edit Info</Button>
</div>

<div className="mt-6 flex flex-wrap gap-x-12 gap-y-3 text-sm">
<div>
<p className="text-brand font-medium">email</p>
<p className="text-gray-700">{user.email}</p>
</div>
<div>
<p className="text-brand font-medium">phone number</p>
<p className="text-gray-700">
{user.phone_number ? maskPhoneNumber(user.phone_number) : "Not set"}
</p>
</div>
<div>
<p className="text-brand font-medium">notification method</p>
<p className="text-gray-700">Email | SMS</p>
</div>
</div>
Comment thread
i30101 marked this conversation as resolved.
</div>
);
};
16 changes: 16 additions & 0 deletions frontend/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,19 @@ export async function createListing(payload: CreateListingPayload): Promise<List
body: JSON.stringify(payload),
});
}

// ------------------------------------------------------------
// user's own listings
// ------------------------------------------------------------

export async function getMyListings({ pageParam = 1 }: { pageParam?: unknown } = {}) {
const page = typeof pageParam === "number" ? pageParam : 1;
const offset = (page - 1) * FETCH_LISTINGS_LIMIT;

const params = new URLSearchParams();
params.append("seller", "true");
params.append("limit", FETCH_LISTINGS_LIMIT.toString());
params.append("offset", offset.toString());

return await serverFetch<PaginatedResponse<Listing>>(`/market/listings/?${params.toString()}`);
}
1 change: 0 additions & 1 deletion frontend/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export const BASE_URL =

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


export const PLATFORM_URL = process.env.PLATFORM_URL;
export const CLIENT_ID = process.env.CLIENT_ID;
Expand Down
3 changes: 3 additions & 0 deletions frontend/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export type User = {
first_name: string;
last_name: string;
email: string;
phone_number: string | null;
phone_verified: boolean;
};

// ------------------------------------------------------------
Expand Down Expand Up @@ -124,6 +126,7 @@ type BaseListing = {
images: string[];
tags: string[];
favorite_count: number;
offers_count?: number;
is_favorited?: boolean;
seller: User;
};
Expand Down
Loading