@@ -11,9 +11,8 @@ import {
1111 CardFooter ,
1212 CardHeader ,
1313} from "@/components/ui/card" ;
14- import { IconBrandGithub , IconRocket } from "@tabler/icons-react" ;
14+ import { IconBrandGithub , IconRocket , IconBook } from "@tabler/icons-react" ;
1515import { Button } from "@/components/ui/button" ;
16- import { Separator } from "@/components/ui/separator" ;
1716import {
1817 Snippet ,
1918 SnippetCopyButton ,
@@ -53,6 +52,7 @@ export function TemplateCard({ item, className }: TemplateCardProps) {
5352 const isTemplate = item . type === "template" ;
5453 const template = isTemplate ? ( item as TemplateMetadata ) : null ;
5554 const app = ! isTemplate ? ( item as AppMetadata ) : null ;
55+ const [ chipsExpanded , setChipsExpanded ] = React . useState ( false ) ;
5656
5757 const categoryLabels = {
5858 starter : "Starter" ,
@@ -74,6 +74,17 @@ export function TemplateCard({ item, className }: TemplateCardProps) {
7474 const description = isTemplate ? template ! . description : app ! . description ;
7575 const name = isTemplate ? template ! . name : app ! . name ;
7676
77+ // Combine frameworks and features into a single array with type info
78+ const allChips = [
79+ ...frameworks . map ( ( f ) => ( { value : f , type : "framework" as const } ) ) ,
80+ ...features . map ( ( f ) => ( { value : f , type : "feature" as const } ) ) ,
81+ ] ;
82+
83+ const MAX_VISIBLE_CHIPS = 3 ;
84+ const visibleChips =
85+ chipsExpanded ? allChips : allChips . slice ( 0 , MAX_VISIBLE_CHIPS ) ;
86+ const hiddenCount = allChips . length - MAX_VISIBLE_CHIPS ;
87+
7788 return (
7889 < Card
7990 className = { cn (
@@ -84,26 +95,63 @@ export function TemplateCard({ item, className }: TemplateCardProps) {
8495 < CardHeader >
8596 < div className = "flex items-start justify-between gap-2" >
8697 < div className = "flex-1 min-w-0" >
87- < div className = "flex items-center gap-2 mb-2" >
88- { language && (
89- < Badge variant = "secondary" className = "text-xs" >
90- { language === "typescript" ? "TypeScript" : "Python" }
91- </ Badge >
92- ) }
93- { isTemplate && template && (
94- < Badge variant = "outline" className = "text-xs" >
95- { categoryLabels [ template . category ] }
96- </ Badge >
97- ) }
98- { ! isTemplate && (
99- < Badge variant = "outline" className = "text-xs" >
100- Demo App
101- </ Badge >
102- ) }
98+ < div className = "flex items-center gap-1 mb-2" >
99+ { ( ( ) => {
100+ const labels : string [ ] = [ ] ;
101+ if ( language ) {
102+ labels . push (
103+ language === "typescript" ? "TypeScript" : "Python" ,
104+ ) ;
105+ }
106+ if ( isTemplate && template ) {
107+ labels . push ( categoryLabels [ template . category ] ) ;
108+ }
109+ if ( ! isTemplate ) {
110+ labels . push ( "Demo App" ) ;
111+ }
112+ return (
113+ < span className = "text-xs text-muted-foreground" >
114+ { labels . join ( " • " ) }
115+ </ span >
116+ ) ;
117+ } ) ( ) }
103118 </ div >
104119 < h3 className = "text-xl text-foreground mb-1" >
105120 { isTemplate ? formatTemplateName ( name ) : name }
106121 </ h3 >
122+ { allChips . length > 0 && (
123+ < div className = "flex flex-wrap gap-1.5 justify-start w-full mt-2" >
124+ { visibleChips . map ( ( chip ) => (
125+ < Badge
126+ key = { `${ chip . type } -${ chip . value } ` }
127+ variant = {
128+ chip . type === "framework" ? "secondary" : "outline"
129+ }
130+ className = "text-xs"
131+ >
132+ { chip . value }
133+ </ Badge >
134+ ) ) }
135+ { ! chipsExpanded && hiddenCount > 0 && (
136+ < Badge
137+ variant = "outline"
138+ className = "text-xs cursor-pointer hover:bg-accent"
139+ onClick = { ( ) => setChipsExpanded ( true ) }
140+ >
141+ { hiddenCount } more
142+ </ Badge >
143+ ) }
144+ { chipsExpanded && (
145+ < Badge
146+ variant = "outline"
147+ className = "text-xs cursor-pointer hover:bg-accent"
148+ onClick = { ( ) => setChipsExpanded ( false ) }
149+ >
150+ Show less
151+ </ Badge >
152+ ) }
153+ </ div >
154+ ) }
107155 </ div >
108156 </ div >
109157 </ CardHeader >
@@ -116,66 +164,6 @@ export function TemplateCard({ item, className }: TemplateCardProps) {
116164 ) }
117165 </ CardContent >
118166 < CardFooter className = "flex flex-col gap-2 pt-4 w-full" >
119- { isTemplate && template && (
120- < >
121- { ( frameworks . length > 0 || features . length > 0 ) && (
122- < >
123- < Separator className = "my-2" />
124- < div className = "flex flex-wrap gap-1.5 justify-start w-full" >
125- { frameworks . map ( ( framework ) => (
126- < Badge
127- key = { framework }
128- variant = "secondary"
129- className = "text-xs"
130- >
131- { framework }
132- </ Badge >
133- ) ) }
134- { features . map ( ( feature ) => (
135- < Badge key = { feature } variant = "outline" className = "text-xs" >
136- { feature }
137- </ Badge >
138- ) ) }
139- </ div >
140- </ >
141- ) }
142- </ >
143- ) }
144- { ! isTemplate && app && (
145- < >
146- { app . blogPost && (
147- < Link
148- href = { app . blogPost }
149- target = "_blank"
150- rel = "noopener noreferrer"
151- className = "text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 w-full"
152- >
153- Read Blog Post →
154- </ Link >
155- ) }
156- { app . blogPost && ( frameworks . length > 0 || features . length > 0 ) && (
157- < Separator className = "my-2" />
158- ) }
159- { ( frameworks . length > 0 || features . length > 0 ) && (
160- < div className = "flex flex-wrap gap-1.5 justify-start w-full" >
161- { frameworks . map ( ( framework ) => (
162- < Badge
163- key = { framework }
164- variant = "secondary"
165- className = "text-xs"
166- >
167- { framework }
168- </ Badge >
169- ) ) }
170- { features . map ( ( feature ) => (
171- < Badge key = { feature } variant = "outline" className = "text-xs" >
172- { feature }
173- </ Badge >
174- ) ) }
175- </ div >
176- ) }
177- </ >
178- ) }
179167 < div className = "flex w-full items-center justify-start gap-2 mt-auto pt-2" >
180168 < Button variant = "default" asChild >
181169 < Link
@@ -185,6 +173,18 @@ export function TemplateCard({ item, className }: TemplateCardProps) {
185173 Deploy
186174 </ Link >
187175 </ Button >
176+ { ! isTemplate && app && app . blogPost && (
177+ < Button variant = "outline" size = "icon" asChild >
178+ < Link
179+ href = { app . blogPost }
180+ target = "_blank"
181+ rel = "noopener noreferrer"
182+ aria-label = "Read Blog Post"
183+ >
184+ < IconBook className = "h-4 w-4" />
185+ </ Link >
186+ </ Button >
187+ ) }
188188 < Button variant = "outline" size = "icon" asChild >
189189 < Link
190190 href = { githubUrl }
0 commit comments