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

Catalog cards #63

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 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
41 changes: 41 additions & 0 deletions src/components/composite/Catalog.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.catalogContainer,
.catalogList {
display: flex;
flex-direction: column;
}

.catalogList {
gap: 24px;
}

.catalogContainer {
height: 100%;
overflow-y: auto;
gap: 16px;
}

.catalogHeader {
display: flex;
align-items: center;
gap: 8px;
}

.catalogHeader h2 {
font-size: 28;
font-weight: 700;
}

.catalogHeader span {
padding: 4px;
background: #000;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 10px;
Copy link
Collaborator

@JBurkinshaw JBurkinshaw Jan 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@max172-hqt Another thought on this. Can you use CSS variables from src/components/styles/theme.css to set as many of these as possible please? Thinking in terms of color, font size, border radius and whatever else you can apply. In the work I'm doing on the datepicker I have added a new variable --border-radius: 4px; but we'll need some input from the design team to align on the approach we take here Edit: Forget this. Let's stay with the existing border-radius-* CSS vars

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On this, I feel like we’re trying to figure out some default values / spacing in the theme.css file, which are very similar to defaults from TailwindCSS or similar libraries. A few things that made it work really well that I think we can adopt are:

  • consistent spacing. Everything is proportional to 1rem, which is the default browser / html font size (16px). So when user changes default font size, everything will scale correctly. Right now we override default font size to 14px, so it doesn’t make sense to use rem. Reference
  • have a normalize css rules. This is to solve browser inconsistencies and make working with semantic HTML tags easier (eg. remove margins from h1,h2…p tags). Looks like we’re mostly using div tags at the moment.
  • other than that, a lot of default colors, utility classes, easy to work with dark themes and responsive UIs

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For context, currently we're using the Vanilla CSS "starter kit" found here. It comes with css files for the base react-aria components, along with a theme.css file which we based our theme.css file in spk-components on.

  1. I am fine with using consistent spacing proportional to 1rem if we're all onboard.
  2. Normalize css is something I've thought about and I think it's a good idea. I think adding it should be tackled as a separate issue though.
  3. I think the default colours are deliberately minimal in our theme.css. It should handle dark/light themes too but we haven't focussed on that yet

white-space: nowrap;
aspect-ratio: 1/1;
flex-shrink: 0;
min-width: 16px;
}
34 changes: 34 additions & 0 deletions src/components/composite/Catalog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import styles from "./Catalog.module.css";
import { type BaseCatalog, CatalogCard } from "../core/CatalogCard";

export interface CatalogProps {
catalogs: BaseCatalog[] | null | undefined;
isLoading?: boolean;
maxDescriptionLength?: number;
}

const Loading = () => <div>Loading...</div>;

const Empty = () => <div>No catalogs available.</div>;

export const Catalog = ({ catalogs, isLoading, ...props }: CatalogProps) => (
<div className={styles.catalogContainer} {...props}>
<div className={styles.catalogHeader}>
<h2>Catalogs</h2>
<span>{Array.isArray(catalogs) ? catalogs.length : 0}</span>
</div>
{isLoading ? (
<Loading />
) : (
<div className={styles.catalogList}>
{Array.isArray(catalogs) ? (
catalogs.map((catalog) => (
<CatalogCard {...catalog} key={catalog.id} />
))
) : (
<Empty />
)}
</div>
)}
</div>
);
69 changes: 69 additions & 0 deletions src/components/core/CatalogCard.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
.cardContainer * {
padding: 0;
margin: 0;
}

.cardContainer {
display: flex;
flex-direction: column;
border-radius: 8px;
padding: 12px;
border: 1px solid #00000027;
gap: 32px;
}

.infoContainer {
display: flex;
flex-direction: column;
gap: 8px;
}

.title {
font-size: 20px;
}

.description {
font-size: var(--font-size-base);
font-weight: 400;
}

.date {
font-size: var(--font-size-small);
font-weight: 400;
color: #0000009b;
}

.footerContainer {
display: flex;
justify-content: space-between;
align-items: center;
}

.tag {
font-size: var(--font-size-base);
padding: 2px 6px;
border-radius: 3px;
color: white;
}

.tagAPI {
background-color: #FFC5003B;
color: #A16B00;
}

.tagCatalog {
background-color: #00000010;
color: #0000009B;
}

.browseButton {
color: #202020;
font-size: var(--font-size-base);
}

.button {
border: none;
text-decoration: underline;
cursor: pointer;
background: none;
}
112 changes: 112 additions & 0 deletions src/components/core/CatalogCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useMemo, useState, JSX } from "react";
import { Button } from "react-aria-components";

import type { PressEvent } from "react-aria";
import styles from "./CatalogCard.module.css";
import { convertDateToUTCString, truncateText } from "../../utils";

export type TemporalExtent = [Date, Date?];

export type IndicatorTag = "API" | "Catalog";

export interface BaseCatalog {
// eslint-disable-next-line react/no-unused-prop-types
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you think of a way to use id in here?
If not, would it make sense to make it optional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used in the Catalog component when rendering each Catalog card (set key), so we still expect the data to have id property. It's showing a warning because the file we're in doesn't use id. For the interfaces that are used in multiple places, should we move it to somewhere else?

I would love to do something like this since CatalogList (rename later) and CatalogCard are so closely related, but not sitting well with our structure.

  • components
    • Catalog (directory)
      • CatalogList.tsx
      • CatalogCard.tsx
      • ... others tsx
      • 1 file for typescript types if needed
      • css module
      • test
      • index.ts to export reusable components

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that structure makes sense here and allows the interface to be in a different file too. The current flat directory structure is no very scaleable so moving to something more like this makes more sense to me.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have done this change in my local. Should I push it now or should we discuss more about it, as it's gonna change the way we structure drastically? If doing this, we won't differentiate core / composite components anymore in the codebase, but still have component types on storybook.

id: string;
title: string;
description: string;
temporalExtent: TemporalExtent;
indicatorTag?: IndicatorTag;
}

export interface CatalogCardProps extends BaseCatalog {
onBrowsePress?: (e: PressEvent) => void;
maxDescriptionLength?: number;
renderDescription?: (description: string) => JSX.Element;
}

const MAX_LENGTH = 250;

const Tag = ({ indicatorTag }: { indicatorTag: IndicatorTag }) => (
<div
className={`${styles.tag} ${
indicatorTag === "API" ? styles.tagAPI : styles.tagCatalog
}`}
>
{indicatorTag}
</div>
);

export const CatalogCard = ({
title,
description: initialDescription,
temporalExtent,
indicatorTag,
maxDescriptionLength = MAX_LENGTH,
onBrowsePress,
renderDescription,
}: CatalogCardProps) => {
const [shouldTruncateDescription, setShouldTruncateDescription] =
useState(true);

const description = useMemo(
() =>
shouldTruncateDescription
? truncateText(initialDescription, maxDescriptionLength)
: initialDescription,
[initialDescription, maxDescriptionLength, shouldTruncateDescription],
);

const dateRange = useMemo(() => {
const startDate = convertDateToUTCString(temporalExtent[0]);

let endDate: string = "";

// Check if end date exists and compare with start date
if (
temporalExtent[1] &&
temporalExtent[1].getTime() > temporalExtent[0].getTime()
) {
endDate = convertDateToUTCString(temporalExtent[1]);
}

return `${startDate}${endDate ? ` - ${endDate}` : ""}`;
}, [temporalExtent]);

const renderDefaultDescription = () => (
<p className={styles.description}>
{description}{" "}
{initialDescription.length > maxDescriptionLength && (
<Button
className={styles.button}
onPress={() =>
setShouldTruncateDescription(!shouldTruncateDescription)
}
aria-expanded={!shouldTruncateDescription}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also add an aria-label property here for accessibility

aria-label="Read more"
>
{shouldTruncateDescription ? "Read More" : "Read Less"}
</Button>
)}
</p>
);

return (
<div className={styles.cardContainer}>
<div className={styles.infoContainer}>
<h3 className={styles.title}>{title}</h3>
{renderDescription
? renderDescription(initialDescription)
: renderDefaultDescription()}
<p className={styles.date}>{dateRange}</p>
</div>
<div className={styles.footerContainer}>
<div>{indicatorTag && <Tag indicatorTag={indicatorTag} />}</div>
<div>
<Button onPress={onBrowsePress} className={styles.button}>
Browse
</Button>
</div>
</div>
</div>
);
};
60 changes: 60 additions & 0 deletions src/stories/Catalog.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { Meta, StoryObj } from "@storybook/react";

import { Catalog } from "../components/composite/Catalog";
import { BaseCatalog } from "../components/core/CatalogCard";

const meta = {
title: "Composite/Catalog",
component: Catalog,
} satisfies Meta<typeof Catalog>;

export default meta;

type Story = StoryObj<typeof meta>;

const longDescription =
"The Kentucky From Above ([KyFromAbove](https://kyfromabove.ky.gov)) program has acquired aerial imagery and elevation (LiDAR) data for the Commonwealth of Kentucky since 2011. The catalog will be subdivided into separate catalogs for orthorectified imagery, oblique imagery, LiDAR-derived digital elevation models (DEM), and LiDAR point cloud data. The data acquired through KyFromAbove program is free to download and consume. All imagery within the catalog uses the Cloud-Optimized Geotiff (COG) format. With the exception of the Phase1 point cloud LAZ files, all LAZ files use the Cloud-Optimized Point Cloud (COPC) format.\n\nSee also:\n\n- [STAC Browser version](https://radiantearth.github.io/stac-browser/#/external/kyfromabove-stac.s3.us-west-1.amazonaws.com/catalog.json)";

const catalogs: BaseCatalog[] = [
{
id: "1",
title: "Catalog 1",
description: longDescription,
temporalExtent: [
new Date(2024, 1, 1, 12, 22, 11),
],
indicatorTag: "API",
},
{
id: "2",
title: "Catalog 2",
description: "Catalog 2 Description",
temporalExtent: [
new Date(2024, 1, 1),
new Date(2024, 1, 1),
],
indicatorTag: "API",
},
{
id: "3",
title: "Catalog 3",
description: "Catalog 3 Description",
temporalExtent: [
new Date(2024, 1, 1),
new Date(),
],
indicatorTag: "API",
},
];

export const Default: Story = {
args: {
catalogs,
},
};

export const EmptyList: Story = {
args: {
catalogs: null,
},
};
52 changes: 52 additions & 0 deletions src/stories/CatalogCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Meta, StoryObj } from "@storybook/react";

import { CatalogCard } from "../components/core/CatalogCard";

const meta = {
title: "Core/CatalogCard",
component: CatalogCard,
} satisfies Meta<typeof CatalogCard>;

export default meta;

type Story = StoryObj<typeof meta>;

const shortDescription = "Lorem ipsum dolor sit amet";
const longDescription =
"The Kentucky From Above ([KyFromAbove](https://kyfromabove.ky.gov)) program has acquired aerial imagery and elevation (LiDAR) data for the Commonwealth of Kentucky since 2011. The catalog will be subdivided into separate catalogs for orthorectified imagery, oblique imagery, LiDAR-derived digital elevation models (DEM), and LiDAR point cloud data. The data acquired through KyFromAbove program is free to download and consume. All imagery within the catalog uses the Cloud-Optimized Geotiff (COG) format. With the exception of the Phase1 point cloud LAZ files, all LAZ files use the Cloud-Optimized Point Cloud (COPC) format.\n\nSee also:\n\n- [STAC Browser version](https://radiantearth.github.io/stac-browser/#/external/kyfromabove-stac.s3.us-west-1.amazonaws.com/catalog.json)";

export const Default: Story = {
args: {
id: "1",
title: "This is a title",
description: longDescription,
temporalExtent: [
new Date(2024, 1, 1),
new Date(2025, 1, 1),
],
indicatorTag: "API",
},
};

export const MissingDate: Story = {
args: {
id: "1",
title: "This is a title",
description: shortDescription,
temporalExtent: [new Date(2024, 1, 1)],
indicatorTag: "Catalog",
},
};

export const CustomDescriptionRender: Story = {
args: {
id: "1",
title: "This is a title",
description: longDescription,
temporalExtent: [new Date(2024, 1, 1)],
indicatorTag: "Catalog",
renderDescription: (description) => (
<p style={{ color: "red" }}>{description}</p>
),
},
};
19 changes: 0 additions & 19 deletions src/utils/exampleFunction.js

This file was deleted.

1 change: 0 additions & 1 deletion src/utils/index.js

This file was deleted.

Loading
Loading