Skip to content
Draft
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
36 changes: 3 additions & 33 deletions src/components/JourneyCards/JourneyCards.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
import { Tag, Typography } from "@chainlink/blocks"
import { JourneyTabGrid } from "./JourneyTabGrid"
import { JourneyCardsDesktop } from "./JourneyCardsDesktop.tsx"
import { JourneyTabGrid } from "./JourneyTabGrid.tsx"

const columns = [
{
Expand Down Expand Up @@ -106,37 +106,7 @@ const tabs = columns.map((column) => ({

<section>
<section class="desktop">
<Typography variant="h4" className="section-title">Start your Chainlink journey</Typography>
<div class="journey-cards">
{
columns.map((column) => (
<div class="journey-column">
<header class="column-header">
<Typography variant="h5" className="column-title">
{column.title}
</Typography>
</header>
{column.items.map((item) => (
<a href={item.href} class="journey-card">
<div class="card-content">
<Typography variant="body-semi">{item.title}</Typography>
<Typography variant="body-s" color="muted">
{item.description}
</Typography>
</div>

<footer class="journey-footer">
<Tag size="sm" className="footer-tag">
<Typography variant="code-s">{item.badge}</Typography>
</Tag>
<img src="/assets/icons/upper-right-arrow.svg" class="footer-icon" />
</footer>
</a>
))}
</div>
))
}
</div>
<JourneyCardsDesktop columns={columns} client:load />
</section>
<section class="mobile">
<JourneyTabGrid header="Start your Chainlink journey" tabs={tabs} client:only="react" />
Expand Down
125 changes: 125 additions & 0 deletions src/components/JourneyCards/JourneyCardsDesktop.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
.container {
width: 100%;
}

.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-10x);
gap: var(--space-4x);
flex-wrap: wrap;
}

.sectionTitle {
font-size: 28px;
margin: 0;
}

.filterWrapper {
display: flex;
align-items: center;
gap: var(--space-2x);
min-width: 200px;
}

.filterSelect {
min-width: 200px;
}

.journeyCards {
display: grid;
grid-template-columns: repeat(3, 1fr);
}

.journeyColumn {
display: flex;
flex-direction: column;
border-left: 1px solid var(--border);
}

.journeyCard {
gap: var(--space-6x);
padding: var(--space-6x);
display: block;
text-decoration: none;
color: inherit;
transition: background-color 0.2s ease;
}

.journeyCard:hover {
background-color: var(--muted);
}

.journeyCard:hover .footerTag {
background-color: var(--background) !important;
}

.journeyCard:hover .footerIcon {
opacity: 1;
}

.cardContent {
display: flex;
flex-direction: column;
gap: var(--space-2x);
margin-bottom: var(--space-8x);
}

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

.footerTag {
text-transform: uppercase;
}

.footerIcon {
height: 12px;
width: 12px;
opacity: 0;
transition: opacity 0.2s ease;
}

.columnHeader {
padding: var(--space-2x) var(--space-6x);
border-left: 3px solid var(--brand);
}

.columnTitle {
font-size: 22px;
line-height: 26px;
margin: 0;
}

.noResults {
padding: var(--space-10x);
text-align: center;
}

@media screen and (min-width: 62em) {
.sectionTitle {
font-size: 32px;
}
}

@media (max-width: 768px) {
.header {
flex-direction: column;
align-items: flex-start;
}

.filterWrapper {
width: 100%;
}

.filterSelect {
width: 100%;
}

.journeyCards {
grid-template-columns: 1fr;
}
}
143 changes: 143 additions & 0 deletions src/components/JourneyCards/JourneyCardsDesktop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { useState, useMemo } from "react"
import { SimpleSelect, Typography, Tag } from "@chainlink/blocks"
import styles from "./JourneyCardsDesktop.module.css"

export interface JourneyItem {
title: string
description: string
badge: string
href: string
}

export interface JourneyColumn {
title: string
items: JourneyItem[]
}

interface JourneyCardsDesktopProps {
columns: JourneyColumn[]
}

// Product filter options
const PRODUCT_FILTERS = [
{ label: "All Products", value: "all" },
{ label: "Automation", value: "automation" },
{ label: "CCIP", value: "ccip" },
{ label: "CRE", value: "cre" },
{ label: "DataLink", value: "datalink" },
{ label: "Data Feeds", value: "data feeds" },
{ label: "Data Streams", value: "data streams" },
{ label: "DTA", value: "dta" },
{ label: "Functions", value: "functions" },
{ label: "VRF", value: "vrf" },
]

type ProductFilterValue = (typeof PRODUCT_FILTERS)[number]["value"]

// Validate badge values against expected product types
const VALID_BADGE_VALUES = new Set([
"automation",
"ccip",
"cre",
"datalink",
"data feeds",
"data streams",
"dta",
"functions",
"vrf",
])

function validateBadge(badge: string): boolean {
return VALID_BADGE_VALUES.has(badge)
}

export const JourneyCardsDesktop = ({ columns }: JourneyCardsDesktopProps) => {
const [selectedFilter, setSelectedFilter] = useState<ProductFilterValue>("all")

// Filter columns based on selected product
const filteredColumns = useMemo(() => {
if (selectedFilter === "all") {
return columns
}

return columns
.map((column) => ({
...column,
items: column.items.filter((item) => {
// Validate badge value
if (!validateBadge(item.badge)) {
console.warn(`Invalid badge value: ${item.badge}`)
return false
}
return item.badge.toLowerCase() === selectedFilter.toLowerCase()
}),
}))
.filter((column) => column.items.length > 0) // Hide columns with no matching cards
}, [columns, selectedFilter])

const handleFilterChange = (value: string) => {
// Validate filter value
if (!PRODUCT_FILTERS.some((f) => f.value === value)) {
console.error(`Invalid filter value: ${value}`)
return
}
setSelectedFilter(value as ProductFilterValue)
}

return (
<div className={styles.container}>
<div className={styles.header}>
<Typography variant="h4" className={styles.sectionTitle}>
Start your Chainlink journey
</Typography>
<div className={styles.filterWrapper}>
<SimpleSelect
options={PRODUCT_FILTERS}
value={selectedFilter}
onValueChange={handleFilterChange}
placeholder="Filter by product"
className={styles.filterSelect}
size="default"
/>
</div>
</div>

<div className={styles.journeyCards}>
{filteredColumns.map((column) => (
<div key={column.title} className={styles.journeyColumn}>
<header className={styles.columnHeader}>
<Typography variant="h5" className={styles.columnTitle}>
{column.title}
</Typography>
</header>
{column.items.map((item) => (
<a key={item.title} href={item.href} className={styles.journeyCard}>
<div className={styles.cardContent}>
<Typography variant="body-semi">{item.title}</Typography>
<Typography variant="body-s" color="muted">
{item.description}
</Typography>
</div>

<footer className={styles.journeyFooter}>
<Tag size="sm" className={styles.footerTag}>
<Typography variant="code-s">{item.badge}</Typography>
</Tag>
<img src="/assets/icons/upper-right-arrow.svg" className={styles.footerIcon} alt="" />
</footer>
</a>
))}
</div>
))}
</div>

{filteredColumns.length === 0 && (
<div className={styles.noResults}>
<Typography variant="body" color="muted">
No journey cards match the selected filter.
</Typography>
</div>
)}
</div>
)
}
Loading
Loading