-
Notifications
You must be signed in to change notification settings - Fork 0
User profile #62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
User profile #62
Changes from all commits
ca6eca7
b4b82bf
30614e8
dc48e38
e01fbd0
c48aaf8
83d5e91
f0a4f20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||
| ); | ||
| } |
| 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/saved" | ||
| icon="saved" | ||
| /> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <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">Saved Listings</h1> | ||
| <MyListingsContent listings={savedListings.results} /> | ||
| </div> | ||
| ); | ||
| } |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 rounded-xl border border-gray-200 bg-white p-6"> | ||
| <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> | ||
| ); | ||
| }; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"> | ||
| {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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| /> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <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"> | ||
| {/* TODO: replace with actual user rating */} | ||
| <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" /> | ||
|
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 || "Not set"}</p> | ||
| </div> | ||
| <div> | ||
| <p className="text-brand font-medium">Notification method</p> | ||
| {/* TODO: replace with actual notification preference */} | ||
| <p className="text-gray-700">Email</p> | ||
| </div> | ||
| </div> | ||
|
i30101 marked this conversation as resolved.
|
||
| </div> | ||
| ); | ||
| }; | ||

Uh oh!
There was an error while loading. Please reload this page.