Skip to content

Commit 6235210

Browse files
committed
feat: Complete Phase 2 - Data Integration
Integrate all pages with ServiceStack backend API: **Server-Side Data Fetching:** - ✅ Created server-side query functions for RSC - ✅ Created client-side React Query hooks - ✅ Integrated with existing ServiceStack DTOs - ✅ Server-client authentication via cookies **Pages Connected to API:** - ✅ /tech - Browse technologies with search - ✅ /tech/[slug] - Technology details with related stacks - ✅ /stacks - Browse technology stacks with search - ✅ /stacks/[slug] - Stack details with technologies list - ✅ /top - Top ranked techs and stacks by favorites **Shared Components:** - ✅ Loading skeletons for async boundaries - ✅ Error state and empty state components - ✅ Search forms with URL params - ✅ Suspense boundaries for streaming **Features:** - Real data from ServiceStack AutoQuery - Server-side rendering with Next.js RSC - Search functionality with query params - Dynamic routing for detail pages - Loading and error states - Stats and metadata display - Related content sections **Build Stats:** - 8 pages total (3 static, 5 dynamic) - First Load JS: 102-105 kB - Successfully compiling with warnings only - All API calls properly typed with DTOs Ready for Phase 3: Authentication
1 parent 1d6a07d commit 6235210

File tree

9 files changed

+745
-226
lines changed

9 files changed

+745
-226
lines changed

app-next/src/app/(browse)/stacks/[slug]/page.tsx

Lines changed: 100 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,144 @@
1+
import { notFound } from 'next/navigation'
2+
import Link from 'next/link'
3+
import { getTechnologyStack } from '@/lib/api/queries-server'
14
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
25
import { Button } from '@/components/ui/button'
36

4-
export default async function StackDetailPage({ params }: { params: Promise<{ slug: string }> }) {
7+
export const dynamic = 'force-dynamic'
8+
9+
export default async function StackDetailPage({
10+
params,
11+
}: {
12+
params: Promise<{ slug: string }>
13+
}) {
514
const { slug } = await params
6-
// Mock data for demonstration
7-
const stack = {
8-
name: 'Modern Web Stack',
9-
description: 'A comprehensive full-stack web development setup with React and Node.js',
10-
creator: 'TechCorp',
11-
favCount: 42,
12-
technologies: [
13-
{ id: 1, name: 'React', tier: 'Frontend Library' },
14-
{ id: 2, name: 'Next.js', tier: 'Frontend Framework' },
15-
{ id: 3, name: 'TypeScript', tier: 'Programming Language' },
16-
{ id: 4, name: 'Node.js', tier: 'Runtime' },
17-
{ id: 5, name: 'PostgreSQL', tier: 'Database' },
18-
{ id: 6, name: 'Tailwind CSS', tier: 'CSS Framework' },
19-
{ id: 7, name: 'Vercel', tier: 'Hosting' },
20-
{ id: 8, name: 'GitHub Actions', tier: 'CI/CD' },
21-
],
15+
16+
let stackResponse
17+
try {
18+
stackResponse = await getTechnologyStack(slug)
19+
} catch (error) {
20+
notFound()
21+
}
22+
23+
const stack = stackResponse.result
24+
25+
if (!stack) {
26+
notFound()
2227
}
2328

29+
const technologies = stack.technologyChoices || []
30+
2431
return (
2532
<div className="container py-8">
2633
<div className="mb-8">
2734
<div className="flex items-start justify-between">
2835
<div>
2936
<h1 className="text-4xl font-bold tracking-tight">{stack.name}</h1>
3037
<p className="text-lg text-muted-foreground mt-2">
31-
by {stack.creator}
38+
{stack.vendorName && `by ${stack.vendorName}`}
3239
</p>
3340
</div>
3441
<Button size="lg">❤️ Favorite</Button>
3542
</div>
3643
</div>
3744

3845
<div className="grid gap-8 lg:grid-cols-3">
39-
<div className="lg:col-span-2">
40-
<Card className="mb-8">
46+
<div className="lg:col-span-2 space-y-8">
47+
<Card>
4148
<CardHeader>
4249
<CardTitle>About This Stack</CardTitle>
4350
</CardHeader>
4451
<CardContent>
45-
<p className="text-muted-foreground">{stack.description}</p>
52+
{stack.description ? (
53+
<p className="text-muted-foreground">{stack.description}</p>
54+
) : (
55+
<p className="text-muted-foreground italic">No description available.</p>
56+
)}
57+
{stack.detailsHtml && (
58+
<div
59+
className="prose prose-slate max-w-none mt-4"
60+
dangerouslySetInnerHTML={{ __html: stack.detailsHtml }}
61+
/>
62+
)}
4663
</CardContent>
4764
</Card>
4865

49-
<div>
50-
<h2 className="text-2xl font-bold mb-4">Technology Stack</h2>
51-
<div className="grid gap-4 sm:grid-cols-2">
52-
{stack.technologies.map((tech) => (
53-
<Card key={tech.id} className="hover:shadow-md transition-shadow">
54-
<CardHeader>
55-
<CardTitle className="text-lg">{tech.name}</CardTitle>
56-
<CardDescription>{tech.tier}</CardDescription>
57-
</CardHeader>
58-
</Card>
59-
))}
66+
{technologies.length > 0 && (
67+
<div>
68+
<h2 className="text-2xl font-bold mb-4">
69+
Technology Stack ({technologies.length} technologies)
70+
</h2>
71+
<div className="grid gap-4 sm:grid-cols-2">
72+
{technologies.map((tech) => (
73+
<Link key={tech.technologyId} href={`/tech/${tech.slug}`}>
74+
<Card className="h-full hover:shadow-md transition-shadow cursor-pointer">
75+
<CardHeader>
76+
<CardTitle className="text-lg">{tech.name || 'Unknown'}</CardTitle>
77+
<CardDescription>{tech.tier}</CardDescription>
78+
</CardHeader>
79+
</Card>
80+
</Link>
81+
))}
82+
</div>
6083
</div>
61-
</div>
84+
)}
6285
</div>
6386

64-
<div>
87+
<div className="space-y-6">
6588
<Card>
6689
<CardHeader>
6790
<CardTitle>Stats</CardTitle>
6891
</CardHeader>
6992
<CardContent className="space-y-4">
7093
<div>
71-
<div className="text-2xl font-bold">{stack.favCount}</div>
94+
<div className="text-2xl font-bold">{stack.favCount || 0}</div>
7295
<div className="text-sm text-muted-foreground">Favorites</div>
7396
</div>
7497
<div>
75-
<div className="text-2xl font-bold">{stack.technologies.length}</div>
98+
<div className="text-2xl font-bold">{technologies.length}</div>
7699
<div className="text-sm text-muted-foreground">Technologies</div>
77100
</div>
101+
<div>
102+
<div className="text-2xl font-bold">{stack.viewCount || 0}</div>
103+
<div className="text-sm text-muted-foreground">Page Views</div>
104+
</div>
105+
</CardContent>
106+
</Card>
107+
108+
{stack.appUrl && (
109+
<Card>
110+
<CardHeader>
111+
<CardTitle>Links</CardTitle>
112+
</CardHeader>
113+
<CardContent>
114+
<Button variant="outline" className="w-full" asChild>
115+
<a href={stack.appUrl} target="_blank" rel="noopener noreferrer">
116+
Visit Application →
117+
</a>
118+
</Button>
119+
</CardContent>
120+
</Card>
121+
)}
122+
123+
<Card>
124+
<CardHeader>
125+
<CardTitle>Info</CardTitle>
126+
</CardHeader>
127+
<CardContent className="space-y-3 text-sm">
128+
{stack.createdBy && (
129+
<div>
130+
<div className="font-medium">Created by</div>
131+
<div className="text-muted-foreground">{stack.createdBy}</div>
132+
</div>
133+
)}
134+
{stack.created && (
135+
<div>
136+
<div className="font-medium">Added</div>
137+
<div className="text-muted-foreground">
138+
{new Date(stack.created).toLocaleDateString()}
139+
</div>
140+
</div>
141+
)}
78142
</CardContent>
79143
</Card>
80144
</div>
Lines changed: 82 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,19 @@
1+
import { Suspense } from 'react'
2+
import Link from 'next/link'
3+
import { getTechnologyStacks } from '@/lib/api/queries-server'
14
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
5+
import { GridSkeleton } from '@/components/shared/loading-skeleton'
6+
import { EmptyState } from '@/components/shared/error-state'
27

3-
export default function StacksPage() {
4-
// Temporary mock data for layout demonstration
5-
const mockStacks = [
6-
{
7-
id: 1,
8-
name: 'Modern Web Stack',
9-
description: 'Full-stack web development with React and Node.js',
10-
techCount: 8,
11-
favCount: 42,
12-
},
13-
{
14-
id: 2,
15-
name: 'Serverless Stack',
16-
description: 'Cloud-native serverless architecture',
17-
techCount: 6,
18-
favCount: 31,
19-
},
20-
{
21-
id: 3,
22-
name: 'E-commerce Platform',
23-
description: 'Scalable e-commerce technology stack',
24-
techCount: 12,
25-
favCount: 58,
26-
},
27-
{
28-
id: 4,
29-
name: 'Mobile-First Stack',
30-
description: 'Cross-platform mobile development',
31-
techCount: 7,
32-
favCount: 26,
33-
},
34-
{
35-
id: 5,
36-
name: 'Data Science Stack',
37-
description: 'Machine learning and data analysis',
38-
techCount: 10,
39-
favCount: 39,
40-
},
41-
{
42-
id: 6,
43-
name: 'DevOps Pipeline',
44-
description: 'Complete CI/CD infrastructure',
45-
techCount: 9,
46-
favCount: 45,
47-
},
48-
]
8+
export const dynamic = 'force-dynamic'
9+
10+
export default async function StacksPage({
11+
searchParams,
12+
}: {
13+
searchParams: Promise<{ q?: string }>
14+
}) {
15+
const params = await searchParams
16+
const searchQuery = params.q
4917

5018
return (
5119
<div className="container py-8">
@@ -57,29 +25,78 @@ export default function StacksPage() {
5725
</div>
5826

5927
<div className="mb-6">
60-
<input
61-
type="search"
62-
placeholder="Search stacks..."
63-
className="w-full rounded-md border border-input bg-background px-4 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
64-
/>
28+
<form action="/stacks" method="get">
29+
<input
30+
type="search"
31+
name="q"
32+
defaultValue={searchQuery}
33+
placeholder="Search stacks..."
34+
className="w-full rounded-md border border-input bg-background px-4 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
35+
/>
36+
</form>
6537
</div>
6638

39+
<Suspense fallback={<GridSkeleton />}>
40+
<StacksGrid searchQuery={searchQuery} />
41+
</Suspense>
42+
</div>
43+
)
44+
}
45+
46+
async function StacksGrid({ searchQuery }: { searchQuery?: string }) {
47+
const response = await getTechnologyStacks({
48+
nameContains: searchQuery,
49+
take: 50,
50+
})
51+
52+
const stacks = response.results || []
53+
54+
if (stacks.length === 0) {
55+
return (
56+
<EmptyState
57+
icon="🔍"
58+
title="No stacks found"
59+
message={
60+
searchQuery
61+
? `No results for "${searchQuery}". Try a different search term.`
62+
: 'No stacks available at the moment.'
63+
}
64+
/>
65+
)
66+
}
67+
68+
return (
69+
<>
70+
{searchQuery && (
71+
<div className="mb-4 text-sm text-muted-foreground">
72+
Found {stacks.length} result{stacks.length !== 1 ? 's' : ''} for &quot;{searchQuery}
73+
&quot;
74+
</div>
75+
)}
76+
6777
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
68-
{mockStacks.map((stack) => (
69-
<Card key={stack.id} className="hover:shadow-lg transition-shadow">
70-
<CardHeader>
71-
<CardTitle>{stack.name}</CardTitle>
72-
<CardDescription>{stack.description}</CardDescription>
73-
</CardHeader>
74-
<CardContent>
75-
<div className="flex items-center gap-4 text-sm text-muted-foreground">
76-
<span>🔧 {stack.techCount} technologies</span>
77-
<span>❤️ {stack.favCount} favorites</span>
78-
</div>
79-
</CardContent>
80-
</Card>
78+
{stacks.map((stack) => (
79+
<Link key={stack.id} href={`/stacks/${stack.slug}`}>
80+
<Card className="h-full hover:shadow-lg transition-shadow cursor-pointer">
81+
<CardHeader>
82+
<CardTitle>{stack.name}</CardTitle>
83+
<CardDescription>
84+
{stack.vendorName && `by ${stack.vendorName}`}
85+
</CardDescription>
86+
</CardHeader>
87+
<CardContent>
88+
<p className="text-sm text-muted-foreground line-clamp-2">
89+
{stack.description || 'No description available'}
90+
</p>
91+
<div className="flex items-center gap-4 mt-4 text-xs text-muted-foreground">
92+
<span>❤️ {stack.favCount || 0} favorites</span>
93+
<span>👁️ {stack.viewCount || 0} views</span>
94+
</div>
95+
</CardContent>
96+
</Card>
97+
</Link>
8198
))}
8299
</div>
83-
</div>
100+
</>
84101
)
85102
}

0 commit comments

Comments
 (0)