@@ -7,20 +7,49 @@ import routes from '@/lib/utils/routes';
77import * as gateway from '@/lib/api/gateway' ;
88import { useAuth , PrimaryButton } from '@servicestack/react' ;
99import { 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
1120export 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