Skip to content

Commit

Permalink
improved listing grid (#359)
Browse files Browse the repository at this point in the history
* improved listing grid

* format

* formatting

* dict -> model dump
  • Loading branch information
codekansas committed Sep 5, 2024
1 parent b750146 commit 8c19cb8
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 95 deletions.
31 changes: 24 additions & 7 deletions frontend/src/components/listings/ListingGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useEffect, useState } from "react";
import Masonry from "react-masonry-css";
import { Link } from "react-router-dom";

import { paths } from "gen/api";
import { useAlertQueue } from "hooks/useAlertQueue";
Expand Down Expand Up @@ -44,20 +46,35 @@ const ListingGrid = (props: ListingGridProps) => {
}
}, [listingIds]);

const breakpointColumnsObj = {
default: 4,
1536: 3,
1280: 3,
1024: 2,
768: 2,
640: 1,
};

return listingIds === null ? (
<div className="flex justify-center items-center h-64">
<Spinner />
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-8 mx-auto">
<Masonry
breakpointCols={breakpointColumnsObj}
className="flex w-auto -ml-4 sm:-ml-6"
columnClassName="pl-4 sm:pl-6 bg-clip-padding"
>
{listingIds.map((listingId) => (
<ListingGridCard
key={listingId}
listingId={listingId}
listing={listingInfo?.find((l) => l.id === listingId)}
/>
<Link key={listingId} to={`/item/${listingId}`}>
<ListingGridCard
listingId={listingId}
listing={listingInfo?.find((l) => l.id === listingId)}
showDescription={true}
/>
</Link>
))}
</div>
</Masonry>
);
};

Expand Down
118 changes: 40 additions & 78 deletions frontend/src/components/listings/ListingGridCard.tsx
Original file line number Diff line number Diff line change
@@ -1,94 +1,56 @@
import { useState } from "react";
import { FaEye } from "react-icons/fa";
import { useNavigate } from "react-router-dom";

import clsx from "clsx";
import { paths } from "gen/api";
import { formatNumber } from "utils/formatNumber";
import { formatTimeSince } from "utils/formatTimeSince";

import ImagePlaceholder from "components/ImagePlaceholder";
import ListingVoteButtons from "components/listing/ListingVoteButtons";
import { Card, CardFooter, CardHeader, CardTitle } from "components/ui/Card";

type ListingInfo =
paths["/listings/batch"]["get"]["responses"][200]["content"]["application/json"]["listings"][0];

interface Props {
interface ListingGridCardProps {
listingId: string;
listing: ListingInfo | undefined;
listing?: ListingInfo;
showDescription?: boolean;
}

const ListingGridCard = ({ listingId, listing }: Props) => {
const navigate = useNavigate();
const [hovering, setHovering] = useState(false);
const ListingGridCard = ({
listing,
showDescription,
}: ListingGridCardProps) => {
const getFirstLine = (text: string | null) => {
if (!text) return null;
const firstLine = text.split("\n")[0].trim();
return firstLine.length > 100 ? `${firstLine.slice(0, 97)}...` : firstLine;
};

return (
<Card
className={clsx(
"transition-all duration-100 ease-in-out cursor-pointer",
"flex flex-col rounded material-card bg-white justify-between",
"dark:bg-gray-900",
"relative overflow-hidden",
)}
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
onClick={() => navigate(`/item/${listingId}`)}
>
{/* Hover overlay */}
<div
className={clsx(
"absolute inset-0 transition-opacity duration-100 ease-in-out",
"bg-black dark:bg-white",
hovering ? "opacity-10" : "opacity-0",
)}
/>

{listing?.image_url ? (
<div className="w-full aspect-square bg-white">
<img
src={listing.image_url}
alt={listing.name}
className="w-full h-full object-cover"
/>
</div>
) : (
<ImagePlaceholder />
)}
<div className="flex flex-col flex-grow p-4">
<CardHeader className="p-0 mb-2">
<CardTitle className="text-gray-800 dark:text-gray-200 text-lg font-semibold truncate">
{listing ? (
listing.name
) : (
<div className="animate-pulse bg-gray-200 dark:bg-gray-700 h-6 w-1/2 mb-2"></div>
)}
</CardTitle>
</CardHeader>
<CardFooter className="flex flex-col items-start p-0 mt-auto">
{listing && (
<>
<div className="flex items-center text-sm text-gray-400 mb-1">
<FaEye className="mr-1" />
<span>{formatNumber(listing.views || 0)}</span>
</div>
<div className="text-xs text-gray-500">
{formatTimeSince(new Date(listing.created_at * 1000))}
</div>
</>
<div className="mb-4 sm:mb-6 bg-white rounded-lg shadow-md overflow-hidden transition-transform duration-300 ease-in-out hover:shadow-lg hover:-translate-y-1">
{listing ? (
<>
{listing.image_url && (
<div className="relative pb-[100%]">
<img
src={listing.image_url}
alt={listing.name}
className="absolute top-0 left-0 w-full h-full object-cover"
/>
</div>
)}
</CardFooter>
</div>
{listing && (
<div className="absolute top-2 left-2 z-10">
<ListingVoteButtons
listingId={listingId}
initialScore={listing.score ?? 0}
initialUserVote={listing.user_vote ?? null}
/>
<div className="p-4">
<h3 className="text-lg font-semibold mb-2 text-gray-800">
{listing.name}
</h3>
{showDescription && listing.description !== null && (
<p className="text-sm text-gray-600">
{getFirstLine(listing.description)}
</p>
)}
</div>
</>
) : (
<div className="animate-pulse p-4">
<div className="h-6 bg-gray-300 rounded w-3/4 mb-3"></div>
<div className="h-4 bg-gray-300 rounded w-full mb-2"></div>
<div className="h-4 bg-gray-300 rounded w-5/6"></div>
</div>
)}
</Card>
</div>
);
};

Expand Down
8 changes: 0 additions & 8 deletions store/app/crud/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,17 @@ async def get_user(self, id: str, throw_if_missing: bool = False) -> User | None
async def get_user(self, id: str, throw_if_missing: bool = False) -> User | None:
return await self._get_item(id, User, throw_if_missing=throw_if_missing)

"""For safely retrieving public user data for display on profile pages"""

async def get_user_public(self, id: str, throw_if_missing: bool = False) -> UserPublic | None:
user = await self.get_user(id, throw_if_missing=throw_if_missing)
if user is None:
return None
return UserPublic(**user.model_dump())

"""Standard sign up with email and password, leaves oauth providers empty"""

async def _create_user_from_email(self, email: str, password: str) -> User:
user = User.create(email=email, password=password)
await self._add_item(user, unique_fields=["email"])
return user

"""OAuth sign up, creates user and links OAuthKey"""

async def _create_user_from_oauth(self, email: str, provider: str, user_token: str) -> User:
user = await self.get_user_from_email(email)
if user is None:
Expand Down Expand Up @@ -195,9 +189,7 @@ async def update_user(self, user_id: str, updates: dict[str, Any]) -> User:
async def test_adhoc() -> None:
async with UserCrud() as crud:
await crud._create_user_from_email(email="[email protected]", password="examplepas$w0rd")

await crud.get_user_from_github_token(token="gh_token_example", email="[email protected]")

await crud.get_user_from_google_token(email="[email protected]")


Expand Down
2 changes: 1 addition & 1 deletion store/app/routers/listings.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ class GetListingResponse(BaseModel):
views: int
score: int
user_vote: bool | None
creator_id: str # Add this line
creator_id: str
creator_name: str | None


Expand Down
2 changes: 1 addition & 1 deletion store/app/routers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ async def update_profile(
crud: Annotated[Crud, Depends(Crud.get)],
) -> UserPublic:
try:
update_dict = updates.dict(exclude_unset=True, exclude_none=True)
update_dict = updates.model_dump(exclude_unset=True, exclude_none=True)
updated_user = await crud.update_user(user.id, update_dict)
return UserPublic(**updated_user.model_dump())
except ValueError as e:
Expand Down

0 comments on commit 8c19cb8

Please sign in to comment.