Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(card): deprecate linkProperties on the Card Component #1365

Merged
merged 5 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 124 additions & 91 deletions app/scripts/components/common/card/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { lazy, MouseEventHandler } from 'react';
import React, { MouseEventHandler } from 'react';
import styled, { css } from 'styled-components';
import format from 'date-fns/format';
import { CollecticonExpandTopRight } from '@devseed-ui/collecticons';
Expand All @@ -9,7 +9,6 @@ import {
themeVal,
listReset
} from '@devseed-ui/theme-provider';
const SmartLink = lazy(() => import('../smart-link'));

import {
CardBody,
Expand All @@ -22,9 +21,11 @@ import {
import HorizontalInfoCard, {
HorizontalCardStyles
} from './horizontal-info-card';
import * as utils from '$utils/utils';
import { variableBaseType, variableGlsp } from '$styles/variable-utils';
import { ElementInteractive } from '$components/common/element-interactive';
import { Figure } from '$components/common/figure';
import { useVedaUI } from '$context/veda-ui-provider';
import { LinkProperties } from '$types/veda';

type CardType = 'classic' | 'cover' | 'featured' | 'horizontal-info';
Expand All @@ -36,6 +37,43 @@ interface CardItemProps {
cardType?: CardType;
}

interface BaseCardComponentProps {
title: JSX.Element | string;
linkLabel?: string;
className?: string;
cardType?: CardType;
description?: JSX.Element | string;
date?: Date;
overline?: JSX.Element;
imgSrc?: string;
imgAlt?: string;
parentTo?: string;
tagLabels?: string[];
footerContent?: JSX.Element;
hideExternalLinkBadge?: boolean;
onCardClickCapture?: MouseEventHandler;
}

interface LinkCardComponentProps extends BaseCardComponentProps {
to: string;
onClick?: never;
}

interface ClickCardComponentProps extends BaseCardComponentProps {
to?: never;
onClick: MouseEventHandler;
}

export type CardComponentProps =
| LinkCardComponentProps
| ClickCardComponentProps
| BaseCardComponentProps;

export interface DeprecatedCardComponentProps {
linkProperties?: LinkProperties & { linkTo?: string };
linkTo?: string;
}

/**
@NOTE: CardList & CardFooter have been moved over to /common/card/styles and has modified styles
These styles are used in GHG instance, so we leave these for now. We should move these styles to GHG instances
Expand Down Expand Up @@ -233,54 +271,40 @@ export function ExternalLinkFlag() {
);
}

export interface LinkWithPathProperties extends LinkProperties {
linkTo: string;
}

export interface CardComponentBaseProps {
title: JSX.Element | string;
linkLabel?: string;
className?: string;
cardType?: CardType;
description?: JSX.Element | string;
date?: Date;
overline?: JSX.Element;
imgSrc?: string;
imgAlt?: string;
parentTo?: string;
tagLabels?: string[];
footerContent?: JSX.Element;
hideExternalLinkBadge?: boolean;
onCardClickCapture?: MouseEventHandler;
onClick?: MouseEventHandler;
}

// @TODO: Created because GHG uses the card component directly and passes in "linkTo" prop. Consolidate these props when the instance adapts the new syntax
// Specifically: https://github.com/US-GHG-Center/veda-config-ghg/blob/develop/custom-pages/news-and-events/component.tsx#L108
export interface CardComponentPropsDeprecated extends CardComponentBaseProps {
linkTo: string;
}

export interface CardComponentProps extends CardComponentBaseProps {
linkProperties?: LinkWithPathProperties;
}

type CardComponentPropsType = CardComponentProps | CardComponentPropsDeprecated;

// Type guard to check if props has linkProperties
function hasLinkProperties(
props: CardComponentPropsType
): props is CardComponentProps {
return !!(props as CardComponentProps).linkProperties;
}

function CardComponent(props: CardComponentPropsType) {
/**
* CardComponent
*
* This component renders a card with various styles and content based on the provided props.
* It can behave as a link if the `to` prop is provided, using the `Link` component from the Veda UI provider.
* The `onClick` and `to` props are mutually exclusive.
*
* @param {string | JSX.Element} title - The title of the card.
* @param {string} [linkLabel] - The label for the link.
* @param {string} [className] - Additional class names for the card.
* @param {CardType} [cardType] - The type of the card, which determines its style.
* @param {string | JSX.Element} [description] - The description of the card.
* @param {Date} [date] - The date associated with the card.
* @param {JSX.Element} [overline] - The overline content for the card.
* @param {string} [imgSrc] - The source URL for the card image.
* @param {string} [imgAlt] - The alt text for the card image.
* @param {string} [parentTo] - The URL for the parent link.
* @param {string[]} [tagLabels] - The labels for the tags.
* @param {JSX.Element} [footerContent] - The content for the card footer.
* @param {boolean} [hideExternalLinkBadge] - Whether to hide the external link badge.
* @param {MouseEventHandler} [onCardClickCapture] - The click capture handler for the card.
* @param {MouseEventHandler} [onClick] - The click handler for the card. Mutually exclusive with `to`.
* @param {string} [to] - The URL to link to. If provided, the card behaves as a link. Mutually exclusive with `onClick`.
* @returns {JSX.Element} The rendered CardComponent.
*/
function CardComponent(
props: CardComponentProps & DeprecatedCardComponentProps
) {
const {
className,
title,
cardType,
description,
linkLabel,
linkLabel = 'View more',
date,
overline,
imgSrc,
Expand All @@ -289,50 +313,33 @@ function CardComponent(props: CardComponentPropsType) {
parentTo,
footerContent,
hideExternalLinkBadge,
onCardClickCapture,
onClick
onCardClickCapture
} = props;
// @TODO: This process is not necessary once all the instances adapt the linkProperties syntax
// Consolidate them to use LinkProperties only
let linkProperties: LinkWithPathProperties | undefined;

if (hasLinkProperties(props)) {
// Handle new props with linkProperties
const { linkProperties: linkPropertiesProps } = props;
linkProperties = linkPropertiesProps;
} else {
// @NOTE: This currently just exists for GHG which uses the Card component
const { linkTo } = props;
linkProperties = linkTo
? {
linkTo,
pathAttributeKeyName: 'to',
LinkElement: SmartLink
}
: undefined;

const { Link } = useVedaUI();

// For backwards compatibility with deprecated props
const to =
('to' in props && props.to) || props.linkTo || props.linkProperties?.linkTo;

if (props.linkProperties || props.linkTo) {
// eslint-disable-next-line no-console
console.warn(
'linkProperties and linkTo are deprecated in Card component. Please use the "to" prop instead.'
);

if ('onClick' in props && props.onClick) {
// eslint-disable-next-line no-console
console.warn(
'onClick and linkProperties/linkTo are mutually exclusive. Please use only one of them.'
);
}
}

const isExternalLink = linkProperties
? /^https?:\/\//.test(linkProperties.linkTo)
: false;
const isExternalLink = to ? utils.isExternalLink(to) : false;

return (
<ElementInteractive
{...(linkProperties
? {
linkProps: {
as: linkProperties.LinkElement,
[linkProperties.pathAttributeKeyName]: linkProperties.linkTo
}
}
: {})}
as={CardItem}
cardType={cardType}
className={className}
linkLabel={linkLabel ?? 'View more'}
onClickCapture={onCardClickCapture}
onClick={onClick}
>
const CardContent = (
<>
{cardType !== 'horizontal-info' && (
<>
<CardHeader>
Expand All @@ -346,11 +353,7 @@ function CardComponent(props: CardComponentPropsType) {
tagLabels &&
parentTo &&
tagLabels.map((label) => (
<CardLabel
as={linkProperties?.LinkElement}
to={parentTo}
key={label}
>
<CardLabel as={Link} to={parentTo} key={label}>
{label}
</CardLabel>
))}
Expand Down Expand Up @@ -390,8 +393,38 @@ function CardComponent(props: CardComponentPropsType) {
tagLabels={tagLabels}
/>
)}
</ElementInteractive>
</>
);

const baseProps = {
as: CardItem,
cardType,
className,
linkLabel,
onCardClickCapture,
children: CardContent
};

// Link variant
if (to) {
return (
<ElementInteractive
{...baseProps}
linkProps={{
as: Link,
to
}}
/>
);
}

// Clickable variant
if ('onClick' in props && props.onClick) {
return <ElementInteractive {...baseProps} onClick={props.onClick} />;
}

// Non-interactive variant
return <ElementInteractive {...baseProps} />;
}

export const Card = styled(CardComponent)`
Expand Down
10 changes: 10 additions & 0 deletions app/scripts/context/veda-ui-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { createContext, ReactNode, useContext } from 'react';
import { DATASETS_PATH, STORIES_PATH } from '$utils/routes';
import { isExternalLink } from '$utils/url';

interface EnvironmentConfig {
envMapboxToken: string;
Expand Down Expand Up @@ -91,6 +92,15 @@ export function VedaUIProvider({ config, children }: VedaUIProviderProps) {

const Link: React.FC<LinkProps> = ({ to, children, ...props }) => {
const { LinkComponent, linkProps } = navigation;

if (isExternalLink(to)) {
return (
<a href={to} target='_blank' rel='noopener noreferrer' {...props}>
{children}
</a>
);
}

return (
<LinkComponent
{...{ [linkProps.pathAttributeKeyName]: to, ...linkProps }}
Expand Down
12 changes: 12 additions & 0 deletions app/scripts/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,15 @@ export function composeVisuallyDisabled(
export function checkEnvFlag(value?: string): boolean {
return (value ?? '').toLowerCase() === 'true';
}

/**
* Checks if a given URL is an external link.
*
* An external link is defined as a URL that starts with "http://" or "https://".
*
* @param {string} url - The URL to check.
* @returns {boolean} True if the URL is an external link, false otherwise.
*/
export const isExternalLink = (url: string): boolean => {
return /^https?:\/\//.test(url);
};
Loading