Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
78 changes: 78 additions & 0 deletions src/components/CommunityProjectCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Link } from '@tanstack/react-router'
import { ArrowSquareOutIcon, CodeIcon } from '@phosphor-icons/react'
import type { PublicShowcase } from '~/db/types'
import { libraries } from '~/libraries'
import { showcaseLinkRel } from '~/utils/showcase.shared'

export function CommunityProjectCard({
showcase,
}: {
showcase: PublicShowcase
}) {
return (
<article className="flex items-start gap-3 border-b border-gray-200 py-5 dark:border-gray-800">
<Link
to="/showcase/$id"
params={{ id: showcase.id }}
className="group flex min-w-0 flex-1 items-start gap-3 rounded-md focus-visible:outline-2 focus-visible:outline-blue-600"
>
<div className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-gray-100 dark:bg-gray-800">
{showcase.logoUrl ? (
<img
src={showcase.logoUrl}
alt=""
loading="lazy"
decoding="async"
className="size-full object-contain"
/>
) : (
<CodeIcon className="size-5 text-gray-400" />
)}
</div>
<div className="min-w-0">
<h2 className="font-semibold text-gray-900 group-hover:text-blue-600 dark:text-white dark:group-hover:text-blue-400">
{showcase.name}
</h2>
<p className="mt-1 line-clamp-2 text-sm text-gray-600 dark:text-gray-400">
{showcase.tagline}
</p>
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
{showcase.libraries
.slice(0, 3)
.map(
(id) =>
libraries
.find((library) => library.id === id)
?.name.replace('TanStack ', '') ?? id,
)
.join(' · ')}
{showcase.libraries.length > 3 &&
` +${showcase.libraries.length - 3}`}
</p>
</div>
</Link>
<div className="flex shrink-0 items-center">
{showcase.sourceUrl && (
<a
href={showcase.sourceUrl}
target="_blank"
rel={showcaseLinkRel(showcase)}
aria-label={`View source for ${showcase.name}`}
className="rounded-md p-2 text-gray-500 hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-white"
>
<CodeIcon className="size-4" />
</a>
)}
<a
href={showcase.url}
target="_blank"
rel={showcaseLinkRel(showcase)}
aria-label={`Visit ${showcase.name}`}
className="rounded-md p-2 text-gray-500 hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-white"
>
<ArrowSquareOutIcon className="size-4" />
</a>
</div>
</article>
)
}
3 changes: 2 additions & 1 deletion src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { ShoppingBagIcon } from '@phosphor-icons/react/ShoppingBag'
import { SignInIcon } from '@phosphor-icons/react/SignIn'
import { SparkleIcon } from '@phosphor-icons/react/Sparkle'
import { TrendUpIcon } from '@phosphor-icons/react/TrendUp'
import { UsersThreeIcon } from '@phosphor-icons/react/UsersThree'
import { UsersIcon } from '@phosphor-icons/react/Users'
import { XIcon } from '@phosphor-icons/react/X'
import { ThemeToggle } from './ThemeToggle'
Expand Down Expand Up @@ -221,7 +222,7 @@ const NAV_GROUPS = [
{
label: 'Community projects',
to: '/community-projects',
icon: CodeIcon,
icon: UsersThreeIcon,
},
],
},
Expand Down
69 changes: 47 additions & 22 deletions src/components/ShowcaseGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '~/queries/showcases'
import { voteShowcase } from '~/utils/showcase.functions'
import { ShowcaseCard, ShowcaseCardSkeleton } from './ShowcaseCard'
import { CommunityProjectCard } from './CommunityProjectCard'
import { SubmitShowcasePlaceholder } from './ShowcaseSection'
import { PaginationControls } from './PaginationControls'
import { ShowcaseTopBarFilters } from './ShowcaseTopBarFilters'
Expand All @@ -23,6 +24,9 @@ export function ShowcaseGallery({
}: {
community?: boolean
}) {
const gridClassName = community
? 'grid grid-cols-1 md:grid-cols-2 gap-x-10'
: 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6'
const from = community ? '/community-projects' : '/showcase/'
const placement = community ? 'community' : 'showcase'
const navigate = useNavigate({ from })
Expand Down Expand Up @@ -56,7 +60,7 @@ export function ShowcaseGallery({

const { data: votesData } = useQuery({
...getMyShowcaseVotesQueryOptions(showcaseIds),
enabled: !!currentUser && showcaseIds.length > 0,
enabled: !community && !!currentUser && showcaseIds.length > 0,
})

const votesMap = React.useMemo(() => {
Expand Down Expand Up @@ -308,11 +312,21 @@ export function ShowcaseGallery({
return (
<div className="min-h-screen">
{/* Header */}
<div className="bg-linear-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-950">
<div className="max-w-7xl mx-auto px-4 py-12 sm:py-16">
<div
className={
community
? 'bg-white dark:bg-gray-950'
: 'bg-linear-to-b from-gray-50 to-white dark:from-gray-900 dark:to-gray-950'
}
>
<div
className={`max-w-7xl mx-auto px-4 ${community ? 'py-8' : 'py-12 sm:py-16'}`}
>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-4xl font-bold text-gray-900 dark:text-white">
<h1
className={`${community ? 'text-3xl' : 'text-4xl'} font-bold text-gray-900 dark:text-white`}
>
{community ? 'Community projects' : 'Showcase'}
</h1>
{!community && (
Expand Down Expand Up @@ -366,27 +380,38 @@ export function ShowcaseGallery({
{/* Grid */}
<div className="max-w-7xl mx-auto px-4 py-8">
{isLoading ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<ShowcaseCardSkeleton key={i} />
))}
<div className={gridClassName}>
{Array.from({ length: 6 }).map((_, i) =>
community ? (
<div
key={i}
className="h-32 animate-pulse border-b border-gray-200 dark:border-gray-800"
/>
) : (
<ShowcaseCardSkeleton key={i} />
),
)}
</div>
) : data?.showcases && data.showcases.length > 0 ? (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{data.showcases.map(({ showcase, user }) => (
<ShowcaseCard
key={showcase.id}
showcase={showcase}
user={user}
currentUserVote={votesMap.get(showcase.id)}
onVote={(value) => handleVote(showcase.id, value)}
isVoting={
voteMutation.isPending &&
voteMutation.variables?.showcaseId === showcase.id
}
/>
))}
<div className={gridClassName}>
{data.showcases.map(({ showcase, user }) =>
community ? (
<CommunityProjectCard key={showcase.id} showcase={showcase} />
) : (
<ShowcaseCard
key={showcase.id}
showcase={showcase}
user={user}
currentUserVote={votesMap.get(showcase.id)}
onVote={(value) => handleVote(showcase.id, value)}
isVoting={
voteMutation.isPending &&
voteMutation.variables?.showcaseId === showcase.id
}
/>
),
)}
</div>

{data.pagination.totalPages > 1 && (
Expand Down
Loading