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
4 changes: 1 addition & 3 deletions app/[communitySlug]/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@ import { queryKeys } from "@/lib/query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { MembershipExpiryBadge } from "@/components/ui/membership-expiry-badge";
import { MembershipCardSkeleton } from "@/components/dashboard/membership-card-skeleton";
import Link from "next/link";
import { buttonVariants } from "@/components/ui/button";
import {
LoadingState,
ErrorState,
EmptyState,
DeniedState,
Expand Down Expand Up @@ -465,5 +463,5 @@ export default function DashboardPage() {
</Section>
</div>
</div>
);
)
}
42 changes: 42 additions & 0 deletions components/dashboard/community-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* components/dashboard/community-section.tsx
*
* Server Component that fetches and renders community information.
* Streamed via Suspense — the page shell renders immediately while
* this data loads server-side.
*/

import { fetchCommunity } from '@/lib/api/fetch-server'
import { CommunityCardSkeleton } from './skeletons'

// Re-export the skeleton so the parent can use it as a Suspense fallback
export { CommunityCardSkeleton }

export default async function CommunitySection() {
let community
try {
community = await fetchCommunity()
} catch {
return (
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 space-y-2">
<p className="text-sm font-medium text-destructive">Could not load community</p>
<p className="text-xs text-muted-foreground">
Community information is temporarily unavailable.
</p>
</div>
)
}

return (
<div className="rounded-lg border border-border bg-muted/20 p-4 space-y-2">
<h3 className="text-sm font-medium text-muted-foreground">Community</h3>
<p className="text-lg font-semibold">{community.name}</p>
{community.description && (
<p className="text-sm text-muted-foreground">{community.description}</p>
)}
<div className="text-xs text-muted-foreground pt-1">
Tiers: {community.tiers.map((t) => t.charAt(0).toUpperCase() + t.slice(1)).join(', ')}
</div>
</div>
)
}
70 changes: 70 additions & 0 deletions components/dashboard/resources-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* components/dashboard/resources-section.tsx
*
* Server Component that fetches and renders the public resource listing.
* Streamed via Suspense — the page shell renders immediately while
* this data loads server-side.
*
* Note: Access gating (which resources the user can actually view) is
* still determined client-side based on the connected wallet's membership.
* This component renders the full list; the gating logic runs in the
* client-side WalletDashboardSections component.
*/

import { fetchResources } from '@/lib/api/fetch-server'
import { ResourcesCardSkeleton } from './skeletons'

// Re-export the skeleton so the parent can use it as a Suspense fallback
export { ResourcesCardSkeleton }

export default async function ResourcesSection() {
let resources
try {
resources = await fetchResources()
} catch {
return (
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-4 space-y-2">
<p className="text-sm font-medium text-destructive">Could not load resources</p>
<p className="text-xs text-muted-foreground">
Resource listing is temporarily unavailable.
</p>
</div>
)
}

if (!resources || resources.length === 0) {
return (
<div className="rounded-lg border border-dashed border-border bg-muted/20 p-4 space-y-2">
<h3 className="text-sm font-medium text-muted-foreground">Gated Resources</h3>
<p className="text-sm text-muted-foreground">
No resources have been configured for this community yet.
</p>
</div>
)
}

return (
<div className="rounded-lg border border-border bg-muted/20 p-4 space-y-3">
<h3 className="text-sm font-medium text-muted-foreground">
Gated Resources ({resources.length})
</h3>
<p className="text-xs text-muted-foreground">
Resources are available based on your membership tier. Connect your
wallet to see which ones you can access.
</p>
<ul className="space-y-1">
{resources.map((r) => (
<li key={r.id} className="flex items-center gap-2 text-sm">
<span className="h-1.5 w-1.5 rounded-full bg-muted-foreground/40 shrink-0" />
<span>{r.title}</span>
{r.minTier && (
<span className="text-xs text-muted-foreground ml-auto">
{r.minTier.charAt(0).toUpperCase() + r.minTier.slice(1)}+
</span>
)}
</li>
))}
</ul>
</div>
)
}
58 changes: 58 additions & 0 deletions components/dashboard/skeletons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* components/dashboard/skeletons.tsx
*
* Loading-placeholder components used as Suspense fallbacks during
* SSR streaming of the member dashboard. Each skeleton mirrors the
* layout of its corresponding data section.
*/

export function CommunityCardSkeleton() {
return (
<div className="rounded-lg border border-border bg-muted/20 p-4 space-y-3 animate-pulse">
<div className="h-4 w-24 bg-muted rounded" />
<div className="h-6 w-40 bg-muted rounded" />
<div className="space-y-2 pt-2">
<div className="h-4 w-28 bg-muted rounded" />
<div className="h-4 w-20 bg-muted rounded" />
<div className="h-4 w-32 bg-muted rounded" />
</div>
</div>
)
}

export function ResourcesCardSkeleton() {
return (
<div className="rounded-lg border border-border bg-muted/20 p-4 space-y-3 animate-pulse">
<div className="h-4 w-28 bg-muted rounded" />
<div className="h-4 w-full bg-muted rounded" />
<div className="flex flex-wrap gap-2 pt-1">
<div className="h-8 w-20 bg-muted rounded-md" />
<div className="h-8 w-24 bg-muted rounded-md" />
<div className="h-8 w-16 bg-muted rounded-md" />
</div>
</div>
)
}

export function ProfileCardSkeleton() {
return (
<div className="rounded-lg border border-border bg-muted/20 p-4 space-y-3 animate-pulse">
<div className="h-4 w-24 bg-muted rounded" />
<div className="h-4 w-36 bg-muted rounded" />
<div className="h-4 w-48 bg-muted rounded" />
</div>
)
}

export function BadgesCardSkeleton() {
return (
<div className="rounded-lg border border-border bg-muted/20 p-4 space-y-3 animate-pulse">
<div className="h-4 w-16 bg-muted rounded" />
<div className="flex flex-wrap gap-2">
<div className="h-6 w-16 bg-muted rounded-full" />
<div className="h-6 w-20 bg-muted rounded-full" />
<div className="h-6 w-14 bg-muted rounded-full" />
</div>
</div>
)
}
Loading