-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: main
Are you sure you want to change the base?
Catalog cards #63
Changes from 11 commits
82710ca
66774c3
4a683af
7f2343c
1e6cd1c
379d5ec
1ce92c5
fb6fb3f
a0014b9
841eef4
53df28e
071ca25
3f43f0f
1936678
b8e0e2d
63aa0fa
2df5b84
a99a3f5
5921457
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
white-space: nowrap; | ||
aspect-ratio: 1/1; | ||
flex-shrink: 0; | ||
min-width: 16px; | ||
} |
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> | ||
); |
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; | ||
} |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you think of a way to use id in here? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also add an |
||
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> | ||
); | ||
}; |
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, | ||
}, | ||
}; |
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> | ||
), | ||
}, | ||
}; |
This file was deleted.
This file was deleted.
There was a problem hiding this comment.
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 variableEdit: Forget this. Let's stay with the existing--border-radius: 4px;
but we'll need some input from the design team to align on the approach we take hereborder-radius-*
CSS varsThere was a problem hiding this comment.
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:
There was a problem hiding this comment.
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 ourtheme.css
file in spk-components on.theme.css
. It should handle dark/light themes too but we haven't focussed on that yet