Skip to content

Commit 4f5caae

Browse files
authored
Merge pull request #32 from codegouvfr/card
feat: add Card component
2 parents fab3b16 + b328f4b commit 4f5caae

File tree

5 files changed

+446
-4
lines changed

5 files changed

+446
-4
lines changed

COMPONENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
- [ ] Radio button
1212
- [ ] Radio rich
1313
- [ ] Checkbox
14-
- [ ] Cards
14+
- [x] Cards
1515
- [x] Quote
1616
- [ ] Media
1717
- [x] Header

package.json

100755100644
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
"./Header": "./dist/Header/index.js",
133133
"./Footer": "./dist/Footer.js",
134134
"./Display": "./dist/Display.js",
135+
"./Card": "./dist/Card.js",
135136
"./ButtonsGroup": "./dist/ButtonsGroup.js",
136137
"./Button": "./dist/Button.js",
137138
"./Breadcrumb": "./dist/Breadcrumb.js",

src/Card.tsx

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import React, { memo, forwardRef } from "react";
2+
import type { ReactNode } from "react";
3+
import { symToStr } from "tsafe/symToStr";
4+
import { assert } from "tsafe/assert";
5+
import type { Equals } from "tsafe";
6+
7+
import { FrIconClassName, RiIconClassName } from "./lib/generatedFromCss/classNames";
8+
import { fr, RegisteredLinkProps } from "./lib";
9+
import { getLink } from "./lib/routing";
10+
import { cx } from "./lib/tools/cx";
11+
12+
import "./dsfr/component/card/card.css";
13+
14+
//https://main--ds-gouv.netlify.app/example/component/card/
15+
export type CardProps = {
16+
className?: string;
17+
title: ReactNode;
18+
linkProps: RegisteredLinkProps;
19+
desc?: ReactNode;
20+
imageUrl?: string;
21+
imageAlt?: string;
22+
start?: ReactNode;
23+
detail?: ReactNode;
24+
end?: ReactNode;
25+
endDetail?: ReactNode;
26+
badges?: ReactNode[]; // todo: restrict to badge component ? these badges are display on the image
27+
/** where actions can be placed */
28+
footer?: ReactNode;
29+
/** only affect the text */
30+
size?: "small" | "medium" | "large";
31+
/** make the whole card clickable */
32+
enlargeLink?: boolean;
33+
/** only needed when enlargeLink=true */
34+
iconId?: FrIconClassName | RiIconClassName;
35+
shadow?: boolean;
36+
background?: boolean;
37+
border?: boolean;
38+
grey?: boolean;
39+
classes?: Partial<
40+
Record<
41+
| "root"
42+
| "title"
43+
| "card"
44+
| "link"
45+
| "body"
46+
| "content"
47+
| "desc"
48+
| "header"
49+
| "img"
50+
| "imgTag"
51+
| "start"
52+
| "detail"
53+
| "end"
54+
| "endDetail"
55+
| "badges"
56+
| "footer",
57+
string
58+
>
59+
>;
60+
} & (CardProps.Default | CardProps.Horizontal);
61+
62+
export namespace CardProps {
63+
export type Default = {
64+
horizontal?: never;
65+
};
66+
67+
export type Horizontal = {
68+
horizontal: true;
69+
};
70+
}
71+
72+
/** @see <https://react-dsfr-components.etalab.studio/?path=/docs/components-alert> */
73+
export const Card = memo(
74+
forwardRef<HTMLDivElement, CardProps>((props, ref) => {
75+
const {
76+
className,
77+
title,
78+
linkProps,
79+
desc,
80+
imageUrl,
81+
imageAlt,
82+
start,
83+
detail,
84+
end,
85+
endDetail,
86+
badges,
87+
footer,
88+
horizontal = false,
89+
size = "medium",
90+
classes = {},
91+
enlargeLink = true,
92+
background = true,
93+
border = true,
94+
shadow = false,
95+
grey = false,
96+
iconId,
97+
...rest
98+
} = props;
99+
100+
assert<Equals<keyof typeof rest, never>>();
101+
102+
const { Link } = getLink();
103+
104+
return (
105+
<div
106+
className={cx(
107+
fr.cx("fr-card"),
108+
enlargeLink && fr.cx("fr-enlarge-link"),
109+
horizontal && fr.cx("fr-card--horizontal"),
110+
size === "small" && fr.cx("fr-card--sm"),
111+
size === "large" && fr.cx("fr-card--lg"),
112+
background === false && fr.cx("fr-card--no-background"),
113+
border === false && fr.cx("fr-card--no-border"),
114+
shadow && fr.cx("fr-card--shadow"),
115+
grey && fr.cx("fr-card--grey"),
116+
iconId !== undefined && fr.cx(iconId),
117+
classes.root,
118+
className
119+
)}
120+
ref={ref}
121+
>
122+
<div className={cx(fr.cx("fr-card__body"), classes.body)}>
123+
<div className={cx(fr.cx("fr-card__content"), classes.content)}>
124+
<h3 className={cx(fr.cx("fr-card__title"), classes.title)}>
125+
<Link {...linkProps} className={cx(linkProps.className, classes.link)}>
126+
{title}
127+
</Link>
128+
</h3>
129+
{desc !== undefined && (
130+
<p className={cx(fr.cx("fr-card__desc"), classes.desc)}>{desc}</p>
131+
)}
132+
<div className={cx(fr.cx("fr-card__start"), classes.start)}>
133+
{start}
134+
{detail !== undefined && (
135+
<p className={cx(fr.cx("fr-card__detail"), classes.detail)}>
136+
{detail}
137+
</p>
138+
)}
139+
</div>
140+
<div className={cx(fr.cx("fr-card__end"), classes.end)}>
141+
{end}
142+
{endDetail !== undefined && (
143+
<p className={cx(fr.cx("fr-card__detail"), classes.endDetail)}>
144+
{endDetail}
145+
</p>
146+
)}
147+
</div>
148+
</div>
149+
{footer !== undefined && (
150+
<p className={cx(fr.cx("fr-card__footer"), classes.footer)}>{footer}</p>
151+
)}
152+
</div>
153+
{/* ensure we dont have an empty imageUrl string */}
154+
{imageUrl !== undefined && imageUrl.length && (
155+
<div className={cx(fr.cx("fr-card__header"), classes.header)}>
156+
<div className={cx(fr.cx("fr-card__img"), classes.img)}>
157+
<img
158+
className={cx(fr.cx("fr-responsive-img"), classes.imgTag)}
159+
src={imageUrl}
160+
alt={imageAlt}
161+
/>
162+
</div>
163+
{badges !== undefined && badges.length && (
164+
<ul className={cx(fr.cx("fr-badges-group"), classes.badges)}>
165+
{badges.map((badge, i) => (
166+
<li key={i}>{badge}</li>
167+
))}
168+
</ul>
169+
)}
170+
</div>
171+
)}
172+
</div>
173+
);
174+
})
175+
);
176+
177+
Card.displayName = symToStr({ Card });
178+
179+
export default Card;

0 commit comments

Comments
 (0)