Skip to content

Commit 11d705c

Browse files
committed
Update TechnologyDetailClient.tsx
1 parent c2211c6 commit 11d705c

1 file changed

Lines changed: 103 additions & 23 deletions

File tree

TechStacks.Client/src/app/tech/[slug]/TechnologyDetailClient.tsx

Lines changed: 103 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,49 @@ import routes from '@/lib/utils/routes';
77
import * as gateway from '@/lib/api/gateway';
88
import { useAuth, PrimaryButton } from '@servicestack/react';
99
import { FavoriteButton } from '@/components/ui/FavoriteButton';
10+
import { PostsList } from '@/components/posts/PostsList';
11+
import { Post, PostType, QueryPosts } from '@/shared/dtos';
12+
13+
const POST_TYPE_OPTIONS = [
14+
{ value: '', label: 'All' },
15+
{ value: PostType.Announcement, label: 'Announcement' },
16+
{ value: PostType.Post, label: 'Post' },
17+
{ value: PostType.Showcase, label: 'Showcase' },
18+
];
1019

1120
export default function TechnologyDetailClient() {
1221
const pathname = usePathname();
1322
const segments = pathname.split('/').filter(Boolean);
1423
const slug = segments[1] ?? '';
1524
const [tech, setTech] = useState<any>(null);
25+
const [posts, setPosts] = useState<Post[]>([]);
1626
const [loading, setLoading] = useState(true);
27+
const [selectedPostType, setSelectedPostType] = useState<string>('');
28+
const [showAllStacks, setShowAllStacks] = useState(false);
1729
const { isAuthenticated } = useAuth();
1830

31+
const loadPosts = async (techId: number, postType: string) => {
32+
const query = new QueryPosts({
33+
anyTechnologyIds: [techId],
34+
orderBy: '-id',
35+
take: 10,
36+
});
37+
if (postType) {
38+
query.types = [postType];
39+
}
40+
const response = await gateway.queryPosts(query);
41+
setPosts(response.results);
42+
};
43+
1944
useEffect(() => {
2045
const loadTech = async () => {
2146
try {
2247
const data = await gateway.getTechnology(slug);
2348
setTech(data);
49+
50+
if (data?.id) {
51+
await loadPosts(data.id, '');
52+
}
2453
} catch (err) {
2554
console.error('Failed to load technology:', err);
2655
} finally {
@@ -31,6 +60,13 @@ export default function TechnologyDetailClient() {
3160
loadTech();
3261
}, [slug]);
3362

63+
const handlePostTypeChange = (postType: string) => {
64+
setSelectedPostType(postType);
65+
if (tech?.id) {
66+
loadPosts(tech.id, postType);
67+
}
68+
};
69+
3470
if (loading) {
3571
return (
3672
<div className="container mx-auto px-4 py-8">
@@ -100,31 +136,75 @@ export default function TechnologyDetailClient() {
100136
{tech.technologyStacks && tech.technologyStacks.length > 0 && (
101137
<div className="mt-8">
102138
<h2 className="text-2xl font-semibold mb-4">Used in Tech Stacks</h2>
103-
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
104-
{tech.technologyStacks.map((stack: any) => (
105-
<Link
106-
key={stack.id}
107-
href={routes.stack(stack.slug)}
108-
className="bg-gray-50 rounded-lg p-4 hover:bg-gray-100 transition"
109-
>
110-
<div className="flex items-center gap-3">
111-
{stack.screenshotUrl && (
112-
<img
113-
src={stack.screenshotUrl}
114-
alt={stack.name}
115-
className="w-12 h-12 object-cover rounded"
116-
/>
117-
)}
118-
<div>
119-
<h3 className="font-semibold text-gray-900">{stack.name}</h3>
120-
{stack.vendorName && (
121-
<p className="text-sm text-gray-600">{stack.vendorName}</p>
122-
)}
123-
</div>
139+
{(() => {
140+
const sorted = [...tech.technologyStacks].sort((a: any, b: any) => (b.viewCount ?? 0) - (a.viewCount ?? 0));
141+
const visible = showAllStacks ? sorted : sorted.slice(0, 6);
142+
return (
143+
<>
144+
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
145+
{visible.map((stack: any) => (
146+
<Link
147+
key={stack.id}
148+
href={routes.stack(stack.slug)}
149+
className="bg-gray-50 rounded-lg p-4 hover:bg-gray-100 transition"
150+
>
151+
<div className="flex items-center gap-3">
152+
{stack.screenshotUrl && (
153+
<img
154+
src={stack.screenshotUrl}
155+
alt={stack.name}
156+
className="w-12 h-12 object-cover rounded"
157+
/>
158+
)}
159+
<div>
160+
<h3 className="font-semibold text-gray-900">{stack.name}</h3>
161+
{stack.vendorName && (
162+
<p className="text-sm text-gray-600">{stack.vendorName}</p>
163+
)}
164+
</div>
165+
</div>
166+
</Link>
167+
))}
124168
</div>
125-
</Link>
126-
))}
169+
{sorted.length > 6 && !showAllStacks && (
170+
<div className="mt-4 text-center">
171+
<button
172+
type="button"
173+
onClick={() => setShowAllStacks(true)}
174+
className="text-sm text-primary-600 hover:text-primary-700 font-medium"
175+
>
176+
Show all {sorted.length} tech stacks
177+
</button>
178+
</div>
179+
)}
180+
</>
181+
);
182+
})()}
183+
</div>
184+
)}
185+
186+
{posts.length > 0 && (
187+
<div className="mt-8">
188+
<div className="flex items-center justify-between mb-4">
189+
<h2 className="text-2xl font-semibold">Recent Posts</h2>
190+
<div className="flex items-center gap-1 bg-gray-100 rounded-lg p-1">
191+
{POST_TYPE_OPTIONS.map((option) => (
192+
<button
193+
type="button"
194+
key={option.value}
195+
onClick={() => handlePostTypeChange(option.value)}
196+
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
197+
selectedPostType === option.value
198+
? 'bg-white text-gray-900 shadow-sm'
199+
: 'text-gray-600 hover:text-gray-900'
200+
}`}
201+
>
202+
{option.label}
203+
</button>
204+
))}
205+
</div>
127206
</div>
207+
<PostsList posts={posts} />
128208
</div>
129209
)}
130210
</div>

0 commit comments

Comments
 (0)