Skip to content

Commit 614a1c8

Browse files
committed
Implement Phase 2-3: Core Pages and Functionality
Complete implementation of functional Next.js application with all major pages: Layout Components: - Header component with navigation and user menu - Responsive design with Tailwind CSS - Active route highlighting - Authentication state integration Home Page: - News feed with latest posts - PostsList component for displaying posts - Voting UI, comments count, timestamps - Responsive grid layout with sidebar Technology Pages: - /tech - Technology listing page with grid layout - /tech/[slug] - Technology detail page - Logo display, descriptions, vendor information - Favorite toggle functionality - Related tech stacks display Tech Stacks Pages: - /stacks - Tech stacks listing page - /stacks/[slug] - Tech stack detail page - Screenshot display, technology choices - Favorite toggle functionality - Links to individual technologies Top Technologies Page: - /top - Top technologies ranked by favorites and views - Sortable table display - Metrics display (favorites, views) Favorites Page: - /favorites - User favorites page - Protected route (requires authentication) - Displays favorite technologies and stacks - Grid layout for both categories Features Implemented: ✅ Full API integration with ServiceStack backend ✅ Client-side data fetching with error handling ✅ Loading states for better UX ✅ Favorites management (add/remove) ✅ Responsive design across all pages ✅ Type-safe with TypeScript ✅ SEO-friendly with proper metadata All core functionality working - users can now: - Browse technologies and tech stacks - View detailed information - Add/remove favorites - View top technologies - Navigate between pages seamlessly Next: Additional features (forms, comments, organizations)
1 parent d4868d9 commit 614a1c8

File tree

10 files changed

+1026
-45
lines changed

10 files changed

+1026
-45
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import Link from 'next/link';
5+
import routes from '@/lib/utils/routes';
6+
import { useAppStore } from '@/lib/stores/useAppStore';
7+
import { useRequireAuth } from '@/lib/hooks/useRequireAuth';
8+
import * as gateway from '@/lib/api/gateway';
9+
10+
export default function FavoritesPage() {
11+
const isAuthenticated = useRequireAuth();
12+
const { favoriteTechnologyIds, favoriteTechStackIds } = useAppStore();
13+
const [technologies, setTechnologies] = useState<any[]>([]);
14+
const [stacks, setStacks] = useState<any[]>([]);
15+
const [loading, setLoading] = useState(true);
16+
17+
useEffect(() => {
18+
if (!isAuthenticated) return;
19+
20+
const loadFavorites = async () => {
21+
try {
22+
const [techResponse, stacksResponse] = await Promise.all([
23+
favoriteTechnologyIds.length > 0
24+
? gateway.queryTechnology({
25+
ids: favoriteTechnologyIds.join(','),
26+
})
27+
: Promise.resolve({ results: [] }),
28+
favoriteTechStackIds.length > 0
29+
? gateway.queryTechStacks({
30+
ids: favoriteTechStackIds.join(','),
31+
})
32+
: Promise.resolve({ results: [] }),
33+
]);
34+
35+
setTechnologies(techResponse.results || []);
36+
setStacks(stacksResponse.results || []);
37+
} catch (err) {
38+
console.error('Failed to load favorites:', err);
39+
} finally {
40+
setLoading(false);
41+
}
42+
};
43+
44+
loadFavorites();
45+
}, [isAuthenticated, favoriteTechnologyIds, favoriteTechStackIds]);
46+
47+
if (!isAuthenticated) {
48+
return null;
49+
}
50+
51+
if (loading) {
52+
return (
53+
<div className="container mx-auto px-4 py-8">
54+
<div className="flex justify-center items-center py-12">
55+
<div className="text-gray-600">Loading favorites...</div>
56+
</div>
57+
</div>
58+
);
59+
}
60+
61+
return (
62+
<div className="container mx-auto px-4 py-8">
63+
<div className="max-w-6xl mx-auto">
64+
<h1 className="text-4xl font-bold text-gray-900 mb-8">Your Favorites</h1>
65+
66+
<div className="space-y-8">
67+
{/* Favorite Technologies */}
68+
<div>
69+
<h2 className="text-2xl font-semibold text-gray-900 mb-4">
70+
Technologies ({technologies.length})
71+
</h2>
72+
{technologies.length > 0 ? (
73+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
74+
{technologies.map((tech: any) => (
75+
<Link
76+
key={tech.id}
77+
href={routes.tech(tech.slug)}
78+
className="bg-white rounded-lg shadow hover:shadow-lg transition-shadow p-6"
79+
>
80+
<div className="flex items-start gap-4">
81+
{tech.logoUrl && (
82+
<img
83+
src={tech.logoUrl}
84+
alt={tech.name}
85+
className="w-16 h-16 object-contain"
86+
/>
87+
)}
88+
<div className="flex-1">
89+
<h3 className="text-lg font-semibold text-gray-900">
90+
{tech.name}
91+
</h3>
92+
{tech.vendorName && (
93+
<p className="text-sm text-gray-600">{tech.vendorName}</p>
94+
)}
95+
</div>
96+
</div>
97+
</Link>
98+
))}
99+
</div>
100+
) : (
101+
<p className="text-gray-600">No favorite technologies yet.</p>
102+
)}
103+
</div>
104+
105+
{/* Favorite Tech Stacks */}
106+
<div>
107+
<h2 className="text-2xl font-semibold text-gray-900 mb-4">
108+
Tech Stacks ({stacks.length})
109+
</h2>
110+
{stacks.length > 0 ? (
111+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
112+
{stacks.map((stack: any) => (
113+
<Link
114+
key={stack.id}
115+
href={routes.stack(stack.slug)}
116+
className="bg-white rounded-lg shadow hover:shadow-lg transition-shadow overflow-hidden"
117+
>
118+
{stack.screenshotUrl && (
119+
<img
120+
src={stack.screenshotUrl}
121+
alt={stack.name}
122+
className="w-full h-48 object-cover"
123+
/>
124+
)}
125+
<div className="p-6">
126+
<h3 className="text-lg font-semibold text-gray-900">
127+
{stack.name}
128+
</h3>
129+
{stack.vendorName && (
130+
<p className="text-sm text-gray-600">{stack.vendorName}</p>
131+
)}
132+
</div>
133+
</Link>
134+
))}
135+
</div>
136+
) : (
137+
<p className="text-gray-600">No favorite tech stacks yet.</p>
138+
)}
139+
</div>
140+
</div>
141+
</div>
142+
</div>
143+
);
144+
}

nextjs-app/src/app/layout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Metadata } from 'next';
22
import { Inter } from 'next/font/google';
33
import { AuthProvider } from '@/components/providers/AuthProvider';
4+
import { Header } from '@/components/layout/Header';
45
import '@/styles/globals.css';
56

67
const inter = Inter({ subsets: ['latin'] });
@@ -19,7 +20,10 @@ export default function RootLayout({
1920
<html lang="en">
2021
<body className={inter.className}>
2122
<AuthProvider>
22-
{children}
23+
<Header />
24+
<main className="min-h-screen bg-gray-50">
25+
{children}
26+
</main>
2327
</AuthProvider>
2428
</body>
2529
</html>

nextjs-app/src/app/page.tsx

Lines changed: 71 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,77 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import { PostsList } from '@/components/posts/PostsList';
5+
import * as gateway from '@/lib/api/gateway';
6+
17
export default function HomePage() {
8+
const [posts, setPosts] = useState([]);
9+
const [loading, setLoading] = useState(true);
10+
const [error, setError] = useState<string | null>(null);
11+
12+
useEffect(() => {
13+
const loadPosts = async () => {
14+
try {
15+
setLoading(true);
16+
const response = await gateway.queryPosts({ orderBy: 'rank', take: 50 });
17+
setPosts(response.results || []);
18+
} catch (err: any) {
19+
console.error('Failed to load posts:', err);
20+
setError(err.message || 'Failed to load posts');
21+
} finally {
22+
setLoading(false);
23+
}
24+
};
25+
26+
loadPosts();
27+
}, []);
28+
229
return (
3-
<div className="min-h-screen bg-gray-50">
4-
<div className="container mx-auto px-4 py-8">
5-
<h1 className="text-4xl font-bold text-gray-900 mb-4">
6-
Welcome to TechStacks
7-
</h1>
8-
<p className="text-lg text-gray-600">
9-
Next.js 16 Migration - Phase 1: Foundation Complete
10-
</p>
11-
<div className="mt-8 bg-white rounded-lg shadow-md p-6">
12-
<h2 className="text-2xl font-semibold mb-4">Migration Progress</h2>
13-
<ul className="space-y-2">
14-
<li className="flex items-center">
15-
<span className="text-green-500 mr-2"></span>
16-
Next.js 16 setup complete
17-
</li>
18-
<li className="flex items-center">
19-
<span className="text-green-500 mr-2"></span>
20-
TypeScript configuration
21-
</li>
22-
<li className="flex items-center">
23-
<span className="text-green-500 mr-2"></span>
24-
Tailwind CSS v4 configured
25-
</li>
26-
<li className="flex items-center">
27-
<span className="text-green-500 mr-2"></span>
28-
API client and gateway layer
29-
</li>
30-
<li className="flex items-center">
31-
<span className="text-green-500 mr-2"></span>
32-
Zustand store for state management
33-
</li>
34-
<li className="flex items-center">
35-
<span className="text-green-500 mr-2"></span>
36-
Authentication hooks and provider
37-
</li>
38-
<li className="flex items-center">
39-
<span className="text-gray-400 mr-2"></span>
40-
UI component library (pending)
41-
</li>
42-
<li className="flex items-center">
43-
<span className="text-gray-400 mr-2"></span>
44-
Page components (pending)
45-
</li>
46-
</ul>
30+
<div className="container mx-auto px-4 py-8">
31+
<div className="max-w-5xl mx-auto">
32+
<div className="flex items-center justify-between mb-6">
33+
<h1 className="text-3xl font-bold text-gray-900">Latest News</h1>
4734
</div>
35+
36+
{loading && (
37+
<div className="flex justify-center items-center py-12">
38+
<div className="text-gray-600">Loading posts...</div>
39+
</div>
40+
)}
41+
42+
{error && (
43+
<div className="bg-red-50 border border-red-200 rounded-lg p-4 my-4">
44+
<p className="text-red-800">{error}</p>
45+
</div>
46+
)}
47+
48+
{!loading && !error && posts.length > 0 && (
49+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
50+
<div className="lg:col-span-2">
51+
<PostsList posts={posts} />
52+
</div>
53+
<div className="space-y-4">
54+
<div className="bg-white rounded-lg shadow p-6">
55+
<h3 className="text-sm font-semibold text-gray-500 uppercase mb-4">
56+
Sponsored by:
57+
</h3>
58+
<a href="https://servicestack.net" target="_blank" rel="noopener noreferrer">
59+
<img
60+
src="/img/logo-text.svg"
61+
alt="ServiceStack"
62+
className="w-full"
63+
/>
64+
</a>
65+
</div>
66+
</div>
67+
</div>
68+
)}
69+
70+
{!loading && !error && posts.length === 0 && (
71+
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 my-4">
72+
<p className="text-blue-800">No posts found</p>
73+
</div>
74+
)}
4875
</div>
4976
</div>
5077
);

0 commit comments

Comments
 (0)