Skip to content

Commit bceb026

Browse files
Oreoxmtshhdgitgemini-code-assist[bot]
authored
feat: support RelatedResources and ResourceCard (#700)
* feat: support ResourceCard * Update src/components/MDXComponents/ResourceCard/ResourceCard.tsx Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * refactor(ResourceCard): always treat link as external --------- Co-authored-by: Suhaha <jklopsdfw@gmail.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent d8adcc4 commit bceb026

10 files changed

Lines changed: 358 additions & 0 deletions

File tree

locale/en/translation.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,5 +212,13 @@
212212
"content": "Your insights are invaluable in shaping the future of TiDB docs.",
213213
"action": "Take the survey"
214214
}
215+
},
216+
"resourceCard": {
217+
"learnMore": "Learn More ↗",
218+
"type": {
219+
"blog": "Blog",
220+
"lab": "Lab",
221+
"video": "Video"
222+
}
215223
}
216224
}

locale/ja/translation.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,5 +200,13 @@
200200
"content": "TiDBドキュメント向上のためご協力をお願いします。",
201201
"action": "アンケートへ"
202202
}
203+
},
204+
"resourceCard": {
205+
"learnMore": "詳しくはこちら ↗",
206+
"type": {
207+
"blog": "ブログ",
208+
"lab": "ラボ",
209+
"video": "動画"
210+
}
203211
}
204212
}

locale/zh/translation.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,5 +215,13 @@
215215
"content": "我们一起让 TiDB 文档变得更好!",
216216
"action": "立即参与"
217217
}
218+
},
219+
"resourceCard": {
220+
"learnMore": "了解更多 ↗",
221+
"type": {
222+
"blog": "博客",
223+
"lab": "实验",
224+
"video": "视频"
225+
}
218226
}
219227
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Box, Stack } from "@mui/material";
2+
3+
export function RelatedResources({ children }: React.PropsWithChildren) {
4+
return (
5+
<Box component="section" sx={{ width: "100%" }}>
6+
<Stack spacing={2}>{children}</Stack>
7+
</Box>
8+
);
9+
}
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import {
2+
Box,
3+
Card,
4+
CardActionArea,
5+
Stack,
6+
Typography,
7+
} from "@mui/material";
8+
import { Trans, useI18next } from "gatsby-plugin-react-i18next";
9+
import CalendarIcon from "media/icons/calendar.svg";
10+
import ClockIcon from "media/icons/clock.svg";
11+
import UserIcon from "media/icons/user.svg";
12+
13+
const BADGE_COLORS = {
14+
blog: { bg: "#E0F0F8", text: "#1480B8" },
15+
lab: { bg: "#E8E7F9", text: "#4A40BF" },
16+
video: { bg: "#FBE7E7", text: "#BF404B" },
17+
} as const;
18+
19+
const parseDateOnly = (date: string) => {
20+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
21+
22+
if (!match) {
23+
return undefined;
24+
}
25+
26+
const [, year, month, day] = match;
27+
28+
return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
29+
};
30+
31+
interface MetaRowProps {
32+
author?: string;
33+
formattedDate?: string;
34+
duration?: string;
35+
}
36+
37+
function MetaRow({ author, formattedDate, duration }: MetaRowProps) {
38+
const items = [
39+
author && (
40+
<Stack key="author" direction="row" spacing={0.5} alignItems="center">
41+
<UserIcon width="14px" height="14px" aria-hidden="true" />
42+
<Typography component="span" variant="body2" color="text.secondary" sx={{ fontSize: "14px" }}>
43+
{author}
44+
</Typography>
45+
</Stack>
46+
),
47+
formattedDate && (
48+
<Stack key="date" direction="row" spacing={0.5} alignItems="center">
49+
<CalendarIcon width="14px" height="14px" aria-hidden="true" />
50+
<Typography component="span" variant="body2" color="text.secondary" sx={{ fontSize: "14px" }}>
51+
{formattedDate}
52+
</Typography>
53+
</Stack>
54+
),
55+
duration && (
56+
<Stack key="duration" direction="row" spacing={0.5} alignItems="center">
57+
<ClockIcon width="14px" height="14px" aria-hidden="true" />
58+
<Typography component="span" variant="body2" color="text.secondary" sx={{ fontSize: "14px" }}>
59+
{duration}
60+
</Typography>
61+
</Stack>
62+
),
63+
].filter(Boolean);
64+
65+
if (items.length === 0) {
66+
return null;
67+
}
68+
69+
return (
70+
<Stack
71+
direction={{ xs: "column", sm: "row" }}
72+
spacing={1.5}
73+
alignItems={{ xs: "flex-start", sm: "center" }}
74+
>
75+
{items.flatMap((item, i) =>
76+
i === 0
77+
? [item]
78+
: [
79+
<Box
80+
key={`sep-${i}`}
81+
sx={{
82+
width: "1px",
83+
height: "14px",
84+
bgcolor: "divider",
85+
flexShrink: 0,
86+
display: { xs: "none", sm: "block" },
87+
}}
88+
/>,
89+
item,
90+
]
91+
)}
92+
</Stack>
93+
);
94+
}
95+
96+
interface ResourceCardProps {
97+
/** Card title */
98+
title: string;
99+
/** Resource type — controls badge label and color */
100+
type: "blog" | "video" | "lab";
101+
/** Destination URL; always opens in a new tab (resource cards point to external blogs, videos, and labs) */
102+
link: string;
103+
/**
104+
* Thumbnail image CDN URL, e.g.
105+
* "https://docs-download.pingcap.com/media/blog-cover.png"
106+
*/
107+
imgSrc: string;
108+
/** Author display name */
109+
author?: string;
110+
/**
111+
* Publication date in ISO 8601 format (YYYY-MM-DD), e.g. "2025-12-12".
112+
* Formatted automatically per locale: en → "December 12, 2025", zh → "2025年12月12日".
113+
*/
114+
date?: string;
115+
/** Estimated reading / watch time, e.g. "120 mins" */
116+
duration?: string;
117+
}
118+
119+
export function ResourceCard({
120+
title,
121+
type,
122+
link,
123+
imgSrc,
124+
author,
125+
date,
126+
duration,
127+
}: ResourceCardProps) {
128+
const { language } = useI18next();
129+
const safeType = BADGE_COLORS[type] ? type : "blog";
130+
const colors = BADGE_COLORS[safeType];
131+
const parsedDate = date ? parseDateOnly(date) : undefined;
132+
133+
const formattedDate = parsedDate
134+
? new Intl.DateTimeFormat(language, {
135+
year: "numeric",
136+
month: "long",
137+
day: "numeric",
138+
timeZone: "UTC",
139+
}).format(parsedDate)
140+
: undefined;
141+
142+
return (
143+
<Card
144+
variant="outlined"
145+
sx={(theme) => ({
146+
borderRadius: 0,
147+
borderColor: theme.palette.carbon[400],
148+
overflow: "hidden",
149+
display: "flex",
150+
flexDirection: "column",
151+
transition: "all 0.3s ease-in-out",
152+
minHeight: {
153+
xs: "auto",
154+
sm: "126px",
155+
},
156+
"&:hover": {
157+
background:
158+
"linear-gradient(120.16deg, #FFFFFF 63.62%, #FBFDFD 74.22%, #F5F8FA 98.17%)",
159+
borderColor: theme.palette.secondary.main,
160+
boxShadow: "0px 1px 5px 0px rgba(76, 81, 83, 0.1)",
161+
},
162+
"> a.MuiCardActionArea-root:hover": { textDecoration: "none" },
163+
})}
164+
>
165+
<CardActionArea
166+
component="a"
167+
href={link}
168+
target="_blank"
169+
rel="noopener noreferrer"
170+
disableRipple
171+
disableTouchRipple
172+
draggable="false"
173+
style={{ display: "block", width: "100%" }}
174+
sx={(theme) => ({
175+
flex: 1,
176+
outline: "none",
177+
"&.Mui-focusVisible": {
178+
outline: `2px solid ${theme.palette.carbon[700]}`,
179+
outlineOffset: "-2px",
180+
},
181+
"> .MuiCardActionArea-focusHighlight": { display: "none" },
182+
})}
183+
>
184+
<Box
185+
sx={{
186+
display: "flex",
187+
flexDirection: {
188+
xs: "column",
189+
sm: "row",
190+
},
191+
padding: "8px",
192+
gap: {
193+
xs: 2,
194+
sm: 3,
195+
},
196+
alignItems: {
197+
xs: "stretch",
198+
sm: "center",
199+
},
200+
}}
201+
>
202+
{/* Thumbnail */}
203+
<Box
204+
component="img"
205+
src={imgSrc}
206+
alt=""
207+
loading="lazy"
208+
sx={{
209+
margin: 0,
210+
flexShrink: 0,
211+
display: "block",
212+
width: {
213+
xs: "100%",
214+
sm: "224px",
215+
},
216+
height: {
217+
xs: "180px",
218+
sm: "110px",
219+
},
220+
objectFit: "cover",
221+
objectPosition: "left center",
222+
}}
223+
/>
224+
225+
{/* Content column */}
226+
<Box
227+
sx={{ flex: 1, display: "flex", flexDirection: "column", gap: "8px", minWidth: 0 }}
228+
>
229+
{/* Row 1: title + type badge */}
230+
<Box
231+
sx={{
232+
display: "grid",
233+
gridTemplateColumns: {
234+
xs: "1fr",
235+
sm: "minmax(0, 1fr) auto",
236+
},
237+
alignItems: {
238+
xs: "start",
239+
sm: "end",
240+
},
241+
columnGap: "16px",
242+
rowGap: {
243+
xs: 1,
244+
sm: 0,
245+
},
246+
width: {
247+
xs: "100%",
248+
sm: "fit-content",
249+
},
250+
maxWidth: "100%",
251+
minWidth: 0,
252+
}}
253+
>
254+
<Typography
255+
component="div"
256+
variant="h5"
257+
sx={{
258+
minWidth: 0,
259+
overflow: "hidden",
260+
textOverflow: "ellipsis",
261+
whiteSpace: {
262+
xs: "normal",
263+
sm: "nowrap",
264+
},
265+
}}
266+
>
267+
{title}
268+
</Typography>
269+
<Box
270+
component="span"
271+
sx={{
272+
display: "inline-flex",
273+
alignItems: "center",
274+
justifySelf: "start",
275+
height: "28px",
276+
padding: "0 12px",
277+
borderRadius: "999px",
278+
backgroundColor: colors.bg,
279+
color: colors.text,
280+
fontWeight: 500,
281+
fontSize: "14px",
282+
lineHeight: "16px",
283+
}}
284+
>
285+
<Trans i18nKey={`resourceCard.type.${safeType}`} />
286+
</Box>
287+
</Box>
288+
289+
{/* Row 2: author / date / duration — only rendered items, separators between adjacent pairs */}
290+
<MetaRow author={author} formattedDate={formattedDate} duration={duration} />
291+
292+
{/* Row 3: "Learn More ↗" — visual affordance; card itself is the link */}
293+
<Typography
294+
component="div"
295+
sx={(theme) => ({
296+
color: theme.palette.secondary.light,
297+
fontWeight: 500,
298+
fontSize: "16px",
299+
lineHeight: "20px",
300+
padding: "4px 0",
301+
})}
302+
>
303+
<Trans i18nKey="resourceCard.learnMore" />
304+
</Typography>
305+
</Box>
306+
</Box>
307+
</CardActionArea>
308+
</Card>
309+
);
310+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { RelatedResources } from "./RelatedResources";
2+
export { ResourceCard } from "./ResourceCard";

src/components/MDXComponents/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export {
3434
DevToolGroup,
3535
DevToolCard,
3636
} from "components/MDXComponents/developer";
37+
export {
38+
RelatedResources,
39+
ResourceCard,
40+
} from "components/MDXComponents/ResourceCard";
3741

3842
export { TargetLink as Link } from "./Link";
3943
export { ExpandableTable as table } from "./ExpandableTable";

src/media/icons/calendar.svg

Lines changed: 3 additions & 0 deletions
Loading

src/media/icons/clock.svg

Lines changed: 3 additions & 0 deletions
Loading

src/media/icons/user.svg

Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)