1
1
import type { Interpolation , Theme } from "@emotion/react" ;
2
- import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined " ;
2
+ import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline " ;
3
3
import Button from "@mui/material/Button" ;
4
+ import Divider from "@mui/material/Divider" ;
4
5
import Link from "@mui/material/Link" ;
5
6
import Tooltip from "@mui/material/Tooltip" ;
6
7
import { visuallyHidden } from "@mui/utils" ;
7
8
import type { FC , HTMLAttributes } from "react" ;
8
9
import { Link as RouterLink , useNavigate } from "react-router-dom" ;
9
10
import type { Template } from "api/typesGenerated" ;
10
- import { ExternalAvatar , Avatar } from "components/Avatar/Avatar" ;
11
+ import { Avatar } from "components/Avatar/Avatar" ;
11
12
import { AvatarData } from "components/AvatarData/AvatarData" ;
12
13
import { DeprecatedBadge } from "components/Badges/Badges" ;
13
- import { ExternalImage } from "components/ExternalImage/ExternalImage" ;
14
14
import { Pill } from "components/Pill/Pill" ;
15
+ import { Stack } from "components/Stack/Stack" ;
16
+ import { createDayString } from "utils/createDayString" ;
15
17
import { formatTemplateBuildTime } from "utils/templates" ;
16
18
17
19
type TemplateCardProps = HTMLAttributes < HTMLDivElement > & {
@@ -28,54 +30,51 @@ export const TemplateCard: FC<TemplateCardProps> = ({
28
30
} ) => {
29
31
const navigate = useNavigate ( ) ;
30
32
const templatePageLink = `/templates/${ template . name } ` ;
31
- const hasIcon = template . icon && template . icon !== "" ;
32
33
33
34
const handleKeyDown = ( e : React . KeyboardEvent ) => {
34
35
if ( e . key === "Enter" && e . currentTarget === e . target ) {
35
36
navigate ( templatePageLink ) ;
36
37
}
37
38
} ;
39
+
40
+ const truncatedDescription =
41
+ template . description . length >= 60
42
+ ? template . description . substring ( 0 , 60 ) + "..."
43
+ : template . description ;
44
+
38
45
return (
39
46
< div
40
47
css = { styles . card }
41
- { ...divProps }
42
48
role = "button"
43
- tabIndex = { 0 }
44
49
onClick = { ( ) => navigate ( templatePageLink ) }
45
50
onKeyDown = { handleKeyDown }
51
+ tabIndex = { 0 }
52
+ { ...divProps }
46
53
>
47
- < div css = { styles . header } >
48
- < div css = { { display : "flex" , alignItems : "center" } } >
49
- < AvatarData
50
- displayTitle = { false }
51
- subtitle = ""
52
- title = {
53
- template . display_name . length > 0
54
- ? template . display_name
55
- : template . name
56
- }
57
- avatar = { hasIcon && < Avatar src = { template . icon } size = "xl" /> }
58
- />
59
- < p
60
- css = { ( theme ) => ( {
61
- fontSize : 13 ,
62
- margin : "0 0 0 auto" ,
63
- color : theme . palette . text . secondary ,
64
- } ) }
65
- >
66
- < span css = { { ...visuallyHidden } } > Build time: </ span >
67
- < Tooltip title = "Build time" placement = "bottom-start" >
68
- < span >
69
- { formatTemplateBuildTime ( template . build_time_stats . start . P50 ) }
70
- </ span >
71
- </ Tooltip >
72
- </ p >
73
- </ div >
54
+ < Stack
55
+ alignItems = "center"
56
+ justifyContent = "space-between"
57
+ direction = "row"
58
+ css = { { marginBottom : 24 } }
59
+ >
60
+ < AvatarData
61
+ displayTitle = { false }
62
+ subtitle = ""
63
+ title = {
64
+ template . display_name . length > 0
65
+ ? template . display_name
66
+ : template . name
67
+ }
68
+ avatar = {
69
+ template . icon &&
70
+ template . icon !== "" && < Avatar src = { template . icon } size = "md" />
71
+ }
72
+ />
74
73
75
74
{ hasMultipleOrgs && (
76
75
< div css = { styles . orgs } >
77
76
< RouterLink
78
- to = { `/organizations/ ${ template . organization_name } ` }
77
+ to = { `/templates?org= ${ template . organization_id } ` }
79
78
onClick = { ( e ) => e . stopPropagation ( ) }
80
79
>
81
80
< Pill
@@ -89,39 +88,75 @@ export const TemplateCard: FC<TemplateCardProps> = ({
89
88
</ RouterLink >
90
89
</ div >
91
90
) }
92
- </ div >
93
-
94
- < div >
95
- < h4 css = { { fontSize : 14 , fontWeight : 600 , margin : 0 , marginBottom : 4 } } >
96
- { template . display_name }
97
- </ h4 >
98
- < span css = { styles . description } >
99
- { template . description } { " " }
100
- < Link
101
- component = { RouterLink }
102
- onClick = { ( e ) => e . stopPropagation ( ) }
103
- to = { `/templates/${ template . name } /docs` }
104
- css = { { display : "inline-block" , fontSize : 13 , marginTop : 4 } }
105
- >
106
- Read more
107
- </ Link >
108
- </ span >
109
- </ div >
110
-
111
- < div css = { styles . useButtonContainer } >
112
- { template . deprecated ? (
113
- < DeprecatedBadge />
114
- ) : (
115
- < Button
116
- component = { RouterLink }
117
- onClick = { ( e ) => e . stopPropagation ( ) }
118
- fullWidth
119
- to = { `/templates/${ template . name } /workspace` }
91
+ </ Stack >
92
+
93
+ < Stack justifyContent = "space-between" css = { { height : "100%" } } >
94
+ < Stack direction = "column" spacing = { 0 } >
95
+ < h4
96
+ css = { { fontSize : 14 , fontWeight : 600 , margin : 0 , marginBottom : 4 } }
120
97
>
121
- Use template
122
- </ Button >
123
- ) }
124
- </ div >
98
+ { template . display_name }
99
+ </ h4 >
100
+
101
+ { template . description && (
102
+ < div css = { styles . description } >
103
+ { truncatedDescription } { " " }
104
+ < Link
105
+ component = { RouterLink }
106
+ onClick = { ( e ) => e . stopPropagation ( ) }
107
+ to = { `${ templatePageLink } /docs` }
108
+ css = { { display : "inline-block" , fontSize : 13 , marginTop : 4 } }
109
+ >
110
+ Read more
111
+ </ Link >
112
+ </ div >
113
+ ) }
114
+ </ Stack >
115
+
116
+ < Stack direction = "column" alignItems = "flex-start" spacing = { 1 } >
117
+ < Stack direction = "row" >
118
+ < span css = { { ...visuallyHidden } } > Used by</ span >
119
+ < Tooltip title = "Used by" placement = "bottom-start" >
120
+ < span css = { styles . templateStat } >
121
+ { `${ template . active_user_count } ${
122
+ template . active_user_count === 1 ? "developer" : "developers"
123
+ } `}
124
+ </ span >
125
+ </ Tooltip >
126
+ < Divider orientation = "vertical" variant = "middle" flexItem />
127
+ < span css = { { ...visuallyHidden } } > Build time</ span >
128
+ < Tooltip title = "Build time" placement = "bottom-start" >
129
+ < span css = { styles . templateStat } >
130
+ { `${ formatTemplateBuildTime (
131
+ template . build_time_stats . start . P50 ,
132
+ ) } `}
133
+ </ span >
134
+ </ Tooltip >
135
+ < Divider orientation = "vertical" variant = "middle" flexItem />
136
+ < span css = { { ...visuallyHidden } } > Last updated</ span >
137
+ < Tooltip title = "Last updated" placement = "bottom-start" >
138
+ < span css = { styles . templateStat } >
139
+ { `${ createDayString ( template . updated_at ) } ` }
140
+ </ span >
141
+ </ Tooltip >
142
+ </ Stack >
143
+ { template . deprecated ? (
144
+ < DeprecatedBadge />
145
+ ) : (
146
+ < Button
147
+ component = { RouterLink }
148
+ onClick = { ( e ) => e . stopPropagation ( ) }
149
+ fullWidth
150
+ size = "small"
151
+ startIcon = { < AddCircleOutlineIcon /> }
152
+ title = { `Create a workspace using the ${ template . display_name } template` }
153
+ to = { `${ templatePageLink } /workspace` }
154
+ >
155
+ Create workspace
156
+ </ Button >
157
+ ) }
158
+ </ Stack >
159
+ </ Stack >
125
160
</ div >
126
161
) ;
127
162
} ;
@@ -143,13 +178,6 @@ const styles = {
143
178
} ,
144
179
} ) ,
145
180
146
- header : {
147
- display : "flex" ,
148
- alignItems : "center" ,
149
- justifyContent : "space-between" ,
150
- marginBottom : 24 ,
151
- } ,
152
-
153
181
icon : {
154
182
flexShrink : 0 ,
155
183
paddingTop : 4 ,
@@ -164,14 +192,10 @@ const styles = {
164
192
display : "block" ,
165
193
} ) ,
166
194
167
- useButtonContainer : {
168
- display : "flex" ,
169
- gap : 12 ,
170
- flexDirection : "column" ,
171
- paddingTop : 24 ,
172
- marginTop : "auto" ,
173
- alignItems : "center" ,
174
- } ,
195
+ templateStat : ( theme ) => ( {
196
+ fontSize : 13 ,
197
+ color : theme . palette . text . secondary ,
198
+ } ) ,
175
199
176
200
actionButton : ( theme ) => ( {
177
201
transition : "none" ,
0 commit comments