Skip to content
Open
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
47 changes: 47 additions & 0 deletions app/home/comments-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client'

import { Col } from '@/components/layout/col'
import { Comment } from '@/components/comment'
import { FullComment } from '@/db/comment'
import { Row } from '@/components/layout/row'

export function CommentsSection({
comments,
userId,
}: {
comments: FullComment[]
userId?: string
}) {
return (
<section className="space-y-4">
<div className="my-4 flex items-center justify-center">
<div className="h-px flex-1 bg-gray-300"></div>
<h2 className="px-6 font-serif text-2xl tracking-wide text-gray-700">
Comments
</h2>
<div className="h-px flex-1 bg-gray-300"></div>
</div>
<Row className="justify-end">
<a
href="/projects?tab=comments"
className="font-serif text-sm italic text-gray-600 hover:text-gray-900"
>
View all →
</a>
</Row>
<Col className="gap-6">
{comments.map((comment) => (
<Comment
key={comment.id}
comment={comment}
commenter={comment.profiles}
userId={userId}
rxns={comment.comment_rxns}
commentHref={`/projects/${comment.projects.slug}?tab=comments#${comment.id}`}
projectTitle={comment.projects.title}
/>
))}
</Col>
</section>
)
}
82 changes: 82 additions & 0 deletions app/home/donations-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Col } from '@/components/layout/col'
import { Card } from '@/components/layout/card'
import { Row } from '@/components/layout/row'
import { Tag } from '@/components/tags'
import { UserAvatarAndBadge } from '@/components/user-link'
import { HomeHeader } from '@/components/home-header'
import { FullTxn } from '@/db/txn'
import { FullBid } from '@/db/bid'
import { formatDistanceToNow } from 'date-fns'
import Link from 'next/link'

export function DonationsSection({
donations,
bids,
}: {
donations: FullTxn[]
bids: FullBid[]
}) {
// Combine and sort donations and bids by date
const combinedItems = [
...donations.map((txn) => ({
type: 'donation' as const,
item: txn,
})),
...bids.map((bid) => ({ type: 'bid' as const, item: bid })),
].sort((a, b) => {
return (
new Date(b.item.created_at).getTime() -
new Date(a.item.created_at).getTime()
)
})

return (
<section className="space-y-4">
<HomeHeader title="Donations" viewAllLink="/projects?tab=donations" />

<Col className="gap-4">
{combinedItems.map(({ type, item }) => (
<Col key={`${type}-${item.id}`}>
<Link href={`/projects/${item.projects?.slug}`} className="w-fit">
<Tag
text={item.projects?.title ?? ''}
className="hover:bg-orange-200"
/>
</Link>
<Card className="rounded-tl-sm !p-1">
<DonationItem type={type} item={item} />
</Card>
</Col>
))}
</Col>
</section>
)
}

function DonationItem(props: {
type: 'donation' | 'bid'
item: FullTxn | FullBid
}) {
const { type, item } = props
return (
<div className="grid w-full grid-cols-3 items-center gap-3 rounded p-3 text-sm">
<Row className="justify-start">
{item.profiles && <UserAvatarAndBadge profile={item.profiles} />}
</Row>
<Row className="items-center justify-end">
<div className={type === 'bid' ? 'text-gray-500' : ''}>
<span title={type === 'bid' ? 'pending donation' : undefined}>
${Math.round(item.amount)}
</span>
</div>
</Row>
<Row className="items-center justify-end gap-2">
<span className="hidden text-right text-gray-500 sm:block">
{formatDistanceToNow(new Date(item.created_at), {
addSuffix: true,
})}
</span>
</Row>
</div>
)
}
29 changes: 29 additions & 0 deletions app/home/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Col } from '@/components/layout/col'

export default function HomeLoading() {
return (
<Col className="gap-8 px-3 py-5 sm:px-6">
<div className="text-center">
<div className="mx-auto h-8 w-64 animate-pulse rounded bg-gray-200" />
<div className="mx-auto mt-2 h-4 w-96 animate-pulse rounded bg-gray-200" />
</div>

{[...Array(3)].map((_, i) => (
<div key={i} className="space-y-4">
<div className="flex items-center justify-between">
<div className="h-8 w-32 animate-pulse rounded bg-gray-200" />
<div className="h-4 w-16 animate-pulse rounded bg-gray-200" />
</div>
<div className="rounded-lg border bg-white p-4">
<div className="animate-pulse space-y-4">
<div className="h-4 w-1/3 rounded bg-gray-200" />
{[...Array(3)].map((_, j) => (
<div key={j} className="h-20 rounded bg-gray-200" />
))}
</div>
</div>
</div>
))}
</Col>
)
}
84 changes: 84 additions & 0 deletions app/home/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Suspense } from 'react'
import { Metadata } from 'next'
import { createServerSupabaseClient } from '@/db/supabase-server'
import { getUser } from '@/db/profile'
import { Col } from '@/components/layout/col'
import { ProjectsSection } from './projects-section'
import { CommentsSection } from './comments-section'
import { DonationsSection } from './donations-section'
import {
getCachedHotProjects,
getCachedRecentComments,
getCachedRecentDonations,
getCachedRecentBids,
getCachedCauses,
} from '@/db/home-cached'

export const metadata: Metadata = {
title: 'Home - Manifund',
description: 'Discover and support impactful projects on Manifund',
}

export default async function HomePage() {
const supabase = await createServerSupabaseClient()
const user = await getUser(supabase)

return (
<Col className="gap-8 px-3 py-5 sm:px-6">
{/* Use Suspense boundaries for progressive loading */}
<Suspense fallback={<SectionSkeleton title="Hot Projects" />}>
<AsyncProjectsSection />
</Suspense>

<Suspense fallback={<SectionSkeleton title="Recent Comments" />}>
<AsyncCommentsSection userId={user?.id} />
</Suspense>

<Suspense fallback={<SectionSkeleton title="Recent Activity" />}>
<AsyncDonationsSection />
</Suspense>
</Col>
)
}

// Async components for each section
async function AsyncProjectsSection() {
const [projects, causesList] = await Promise.all([
getCachedHotProjects(),
getCachedCauses(),
])

return <ProjectsSection projects={projects} causesList={causesList} />
}

async function AsyncCommentsSection({ userId }: { userId?: string }) {
const comments = await getCachedRecentComments()
return <CommentsSection comments={comments} userId={userId} />
}

async function AsyncDonationsSection() {
const [donations, bids] = await Promise.all([
getCachedRecentDonations(),
getCachedRecentBids(),
])

return <DonationsSection donations={donations} bids={bids} />
}

function SectionSkeleton({ title }: { title: string }) {
return (
<section className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">{title}</h2>
</div>
<div className="rounded-lg border bg-white p-4">
<div className="animate-pulse space-y-4">
<div className="h-4 w-1/3 rounded bg-gray-200" />
{[...Array(3)].map((_, i) => (
<div key={i} className="h-20 rounded bg-gray-200" />
))}
</div>
</div>
</section>
)
}
23 changes: 23 additions & 0 deletions app/home/projects-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ProjectsDisplay } from '@/components/projects-display'
import { FullProject } from '@/db/project'
import { SimpleCause } from '@/db/cause'
import { HomeHeader } from '@/components/home-header'

export function ProjectsSection({
projects,
causesList,
}: {
projects: FullProject[]
causesList: SimpleCause[]
}) {
return (
<section className="space-y-4">
<HomeHeader title="Projects" viewAllLink="/projects" />
<ProjectsDisplay
projects={projects}
defaultSort={'hot'}
causesList={causesList}
/>
</section>
)
}
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions components/home-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Row } from '@/components/layout/row'

interface HomeHeaderProps {
title: string
viewAllLink: string
}

export function HomeHeader({ title, viewAllLink }: HomeHeaderProps) {
return (
<>
<div className="my-4 flex items-center justify-center">
<div className="h-px flex-1 bg-gray-300"></div>
<h2 className="px-6 font-serif text-2xl tracking-wide text-gray-700">
{title}
</h2>
<div className="h-px flex-1 bg-gray-300"></div>
</div>
<Row className="justify-end">
<a
href={viewAllLink}
className="font-serif text-sm italic text-gray-600 hover:text-gray-900"
>
View all →
</a>
</Row>
</>
)
}
21 changes: 14 additions & 7 deletions components/project-group.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
'use client'
import { FullProject } from '@/db/project'
import { ProjectCard } from '@/components/project-card'
import { Masonry } from 'react-plock'

export function ProjectGroup(props: {
projects: FullProject[]
prices?: { [k: string]: number }
}) {
const { projects, prices } = props
return (
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{projects.map((project) => (
<Masonry
items={projects}
config={{
columns: [1, 2],
gap: [16, 16],
media: [640, 1024],
}}
render={(item, idx) => (
<ProjectCard
key={project.id}
project={project}
valuation={prices ? prices[project.id] : undefined}
key={idx}
project={item}
valuation={prices ? prices[item.id] : undefined}
/>
))}
</div>
)}
/>
)
}
Loading