1 | import React, { memo, forwardRef, type ReactNode, type CSSProperties } from "react";
|
2 | import { symToStr } from "tsafe/symToStr";
|
3 | import { assert } from "tsafe/assert";
|
4 | import type { Equals } from "tsafe";
|
5 |
|
6 | import type { FrIconClassName, RiIconClassName } from "./fr/generatedFromCss/classNames";
|
7 | import { fr } from "./fr";
|
8 | import type { RegisteredLinkProps } from "./link";
|
9 | import { getLink } from "./link";
|
10 | import { cx } from "./tools/cx";
|
11 | import { useAnalyticsId } from "./tools/useAnalyticsId";
|
12 |
|
13 |
|
14 | export 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 |
|
25 | footer?: ReactNode;
|
26 |
|
27 | size?: "small" | "medium" | "large";
|
28 |
|
29 | enlargeLink?: boolean;
|
30 |
|
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 |
|
62 | export 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 |
|
76 | horizontal: true;
|
77 | ratio?: "33/66" | "50/50";
|
78 | };
|
79 |
|
80 | export type Vertical = {
|
81 |
|
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 |
|
109 | export 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 | {}
|
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 |
|
256 | Card.displayName = symToStr({ Card });
|
257 |
|
258 | export default Card;
|
259 |
|
\ | No newline at end of file |