UNPKG

8.89 kBTypeScriptView Raw
1import React, { memo, forwardRef, type ReactNode, type CSSProperties } from "react";
2import { symToStr } from "tsafe/symToStr";
3import { assert } from "tsafe/assert";
4import type { Equals } from "tsafe";
5
6import type { FrIconClassName, RiIconClassName } from "./fr/generatedFromCss/classNames";
7import { fr } from "./fr";
8import type { RegisteredLinkProps } from "./link";
9import { getLink } from "./link";
10import { cx } from "./tools/cx";
11import { useAnalyticsId } from "./tools/useAnalyticsId";
12
13//https://main--ds-gouv.netlify.app/example/component/card/
14export type CardProps = {
15 id?: string;
16 className?: string;
17 title: ReactNode;
18 titleAs?: `h${2 | 3 | 4 | 5 | 6}`;
19 desc?: ReactNode;
20 start?: ReactNode;
21 detail?: ReactNode;
22 end?: ReactNode;
23 endDetail?: ReactNode;
24 /** where actions can be placed */
25 footer?: ReactNode;
26 /** Default: "medium", only affect the text */
27 size?: "small" | "medium" | "large";
28 /** make the whole card clickable */
29 enlargeLink?: boolean;
30 /** only needed when enlargeLink=true */
31 iconId?: FrIconClassName | RiIconClassName;
32 shadow?: boolean;
33 background?: boolean;
34 border?: boolean;
35 grey?: boolean;
36 classes?: Partial<
37 Record<
38 | "root"
39 | "title"
40 | "card"
41 | "link"
42 | "body"
43 | "content"
44 | "desc"
45 | "header"
46 | "img"
47 | "imgTag"
48 | "start"
49 | "detail"
50 | "end"
51 | "endDetail"
52 | "badge"
53 | "footer",
54 string
55 >
56 >;
57 style?: CSSProperties;
58} & (CardProps.EnlargedLink | CardProps.NotEnlargedLink) &
59 (CardProps.Horizontal | CardProps.Vertical) &
60 (CardProps.WithImageLink | CardProps.WithImageComponent | CardProps.WithoutImage);
61
62export namespace CardProps {
63 export type EnlargedLink = {
64 enlargeLink: true;
65 linkProps: RegisteredLinkProps;
66 iconId?: FrIconClassName | RiIconClassName;
67 };
68 export type NotEnlargedLink = {
69 enlargeLink?: false;
70 linkProps?: RegisteredLinkProps;
71 iconId?: never;
72 };
73
74 export type Horizontal = {
75 /** Default false */
76 horizontal: true;
77 ratio?: "33/66" | "50/50";
78 };
79
80 export type Vertical = {
81 /** Default false */
82 horizontal?: false;
83 ratio?: never;
84 };
85
86 export type WithImageLink = {
87 badge?: ReactNode;
88 imageUrl: string;
89 imageAlt: string;
90 imageComponent?: never;
91 };
92
93 export type WithImageComponent = {
94 badge?: ReactNode;
95 imageUrl?: never;
96 imageAlt?: never;
97 imageComponent: ReactNode;
98 };
99
100 export type WithoutImage = {
101 badge?: never;
102 imageUrl?: never;
103 imageAlt?: never;
104 imageComponent?: never;
105 };
106}
107
108/** @see <https://components.react-dsfr.codegouv.studio/?path=/docs/components-card> */
109export const Card = memo(
110 forwardRef<HTMLDivElement, CardProps>((props, ref) => {
111 const {
112 id: props_id,
113 className,
114 title,
115 titleAs: HtmlTitleTag = "h3",
116 linkProps,
117 desc,
118 imageUrl,
119 imageAlt,
120 imageComponent,
121 start,
122 detail,
123 end,
124 endDetail,
125 badge,
126 footer,
127 horizontal = false,
128 ratio,
129 size = "medium",
130 classes = {},
131 enlargeLink = false,
132 background = true,
133 border = true,
134 shadow = false,
135 grey = false,
136 iconId,
137 style,
138 ...rest
139 } = props;
140
141 assert<Equals<keyof typeof rest, never>>();
142
143 const id = useAnalyticsId({
144 "defaultIdPrefix": "fr-card",
145 "explicitlyProvidedId": props_id
146 });
147
148 const { Link } = getLink();
149
150 return (
151 <div
152 id={id}
153 className={cx(
154 fr.cx(
155 "fr-card",
156 enlargeLink && "fr-enlarge-link",
157 horizontal && "fr-card--horizontal",
158 horizontal &&
159 ratio !== undefined &&
160 `fr-card--horizontal-${ratio === "33/66" ? "tier" : "half"}`,
161 (() => {
162 switch (size) {
163 case "large":
164 return "fr-card--lg";
165 case "small":
166 return "fr-card--sm";
167 case "medium":
168 return undefined;
169 }
170 })(),
171 !background && "fr-card--no-background",
172 !border && "fr-card--no-border",
173 shadow && "fr-card--shadow",
174 grey && "fr-card--grey",
175 iconId !== undefined && iconId
176 ),
177 classes.root,
178 className
179 )}
180 style={style}
181 ref={ref}
182 {...rest}
183 >
184 <div className={cx(fr.cx("fr-card__body"), classes.body)}>
185 <div className={cx(fr.cx("fr-card__content"), classes.content)}>
186 <HtmlTitleTag className={cx(fr.cx("fr-card__title"), classes.title)}>
187 {linkProps !== undefined ? (
188 <Link
189 {...linkProps}
190 className={cx(linkProps.className, classes.link)}
191 >
192 {title}
193 </Link>
194 ) : (
195 title
196 )}
197 </HtmlTitleTag>
198 {desc !== undefined && (
199 <p className={cx(fr.cx("fr-card__desc"), classes.desc)}>{desc}</p>
200 )}
201 <div className={cx(fr.cx("fr-card__start"), classes.start)}>
202 {start}
203 {detail !== undefined && (
204 <p className={cx(fr.cx("fr-card__detail"), classes.detail)}>
205 {detail}
206 </p>
207 )}
208 </div>
209 <div className={cx(fr.cx("fr-card__end"), classes.end)}>
210 {end}
211 {endDetail !== undefined && (
212 <p className={cx(fr.cx("fr-card__detail"), classes.endDetail)}>
213 {endDetail}
214 </p>
215 )}
216 </div>
217 </div>
218 {footer !== undefined && (
219 <div className={cx(fr.cx("fr-card__footer"), classes.footer)}>{footer}</div>
220 )}
221 </div>
222 {/* ensure we don't have an empty imageUrl string */}
223 {imageUrl !== undefined && imageUrl.length && (
224 <div className={cx(fr.cx("fr-card__header"), classes.header)}>
225 <div className={cx(fr.cx("fr-card__img"), classes.img)}>
226 <img
227 className={cx(fr.cx("fr-responsive-img"), classes.imgTag)}
228 src={imageUrl}
229 alt={imageAlt}
230 />
231 </div>
232 {badge !== undefined && (
233 <ul className={cx(fr.cx("fr-badges-group"), classes.badge)}>
234 <li>{badge}</li>
235 </ul>
236 )}
237 </div>
238 )}
239 {imageComponent !== undefined && (
240 <div className={cx(fr.cx("fr-card__header"), classes.header)}>
241 <div className={cx(fr.cx("fr-card__img"), classes.img)}>
242 {imageComponent}
243 </div>
244 {badge !== undefined && (
245 <ul className={cx(fr.cx("fr-badges-group"), classes.badge)}>
246 <li>{badge}</li>
247 </ul>
248 )}
249 </div>
250 )}
251 </div>
252 );
253 })
254);
255
256Card.displayName = symToStr({ Card });
257
258export default Card;
259
\No newline at end of file