From 1c44f02b91a9683d5a22ef7e0aea32a5fca02664 Mon Sep 17 00:00:00 2001 From: Victor Chukwuebuka Umeh <41862157+vyktoremario@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:49:58 +0100 Subject: [PATCH] Implement Navigation in Start your journey --- .../JourneyCards/JourneyCards.astro | 36 +--- .../JourneyCardsDesktop.module.css | 125 ++++++++++++++ .../JourneyCards/JourneyCardsDesktop.tsx | 143 ++++++++++++++++ .../JourneyCards/JourneyTabGrid.tsx | 155 ++++++++++++++---- 4 files changed, 392 insertions(+), 67 deletions(-) create mode 100644 src/components/JourneyCards/JourneyCardsDesktop.module.css create mode 100644 src/components/JourneyCards/JourneyCardsDesktop.tsx diff --git a/src/components/JourneyCards/JourneyCards.astro b/src/components/JourneyCards/JourneyCards.astro index 39e85bdf1b5..3aacde1b1b7 100644 --- a/src/components/JourneyCards/JourneyCards.astro +++ b/src/components/JourneyCards/JourneyCards.astro @@ -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 = [ { @@ -106,37 +106,7 @@ const tabs = columns.map((column) => ({
- Start your Chainlink journey -
- { - columns.map((column) => ( -
-
- - {column.title} - -
- {column.items.map((item) => ( - -
- {item.title} - - {item.description} - -
- -
- - {item.badge} - - -
-
- ))} -
- )) - } -
+
diff --git a/src/components/JourneyCards/JourneyCardsDesktop.module.css b/src/components/JourneyCards/JourneyCardsDesktop.module.css new file mode 100644 index 00000000000..0bb8488946f --- /dev/null +++ b/src/components/JourneyCards/JourneyCardsDesktop.module.css @@ -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; + } +} diff --git a/src/components/JourneyCards/JourneyCardsDesktop.tsx b/src/components/JourneyCards/JourneyCardsDesktop.tsx new file mode 100644 index 00000000000..f4c3371f569 --- /dev/null +++ b/src/components/JourneyCards/JourneyCardsDesktop.tsx @@ -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("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 ( +
+
+ + Start your Chainlink journey + +
+ +
+
+ +
+ {filteredColumns.map((column) => ( +
+
+ + {column.title} + +
+ {column.items.map((item) => ( + +
+ {item.title} + + {item.description} + +
+ +
+ + {item.badge} + + +
+
+ ))} +
+ ))} +
+ + {filteredColumns.length === 0 && ( +
+ + No journey cards match the selected filter. + +
+ )} +
+ ) +} diff --git a/src/components/JourneyCards/JourneyTabGrid.tsx b/src/components/JourneyCards/JourneyTabGrid.tsx index ab44a3b0fba..e6dd8d130ac 100644 --- a/src/components/JourneyCards/JourneyTabGrid.tsx +++ b/src/components/JourneyCards/JourneyTabGrid.tsx @@ -1,5 +1,6 @@ +import { useState, useMemo } from "react" import styles from "./JourneyTabGrid.module.css" -import { Tabs, TabsContent, TabsList, TabsTrigger, Typography, Tag } from "@chainlink/blocks" +import { Tabs, TabsContent, TabsList, TabsTrigger, Typography, Tag, SimpleSelect } from "@chainlink/blocks" export interface JourneyItem { title: string @@ -18,9 +19,76 @@ interface JourneyTabGridProps { header: string } +// 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 JourneyTabGrid = ({ tabs, header }: JourneyTabGridProps) => { + const [selectedFilter, setSelectedFilter] = useState("all") + + // Filter tabs based on selected product + const filteredTabs = useMemo(() => { + if (selectedFilter === "all") { + return tabs + } + + return tabs + .map((tab) => ({ + ...tab, + items: tab.items.filter((item) => { + if (!item.badge) return false + // Validate badge value + if (!validateBadge(item.badge)) { + console.warn(`Invalid badge value: ${item.badge}`) + return false + } + return item.badge.toLowerCase() === selectedFilter.toLowerCase() + }), + })) + .filter((tab) => tab.items.length > 0) // Hide tabs with no matching items + }, [tabs, selectedFilter]) + + const handleFilterChange = (value: string) => { + // Validate filter value + const isValid = PRODUCT_FILTERS.some((f) => f.value === value) + if (!isValid) { + console.error(`Invalid filter value: ${value}`) + return + } + setSelectedFilter(value as ProductFilterValue) + } + return ( - +
{ > {header} - - {tabs.map((tab) => ( - -

{tab.name}

-
- ))} -
+
+ +
+ {filteredTabs.length > 0 && ( + + {filteredTabs.map((tab) => ( + +

{tab.name}

+
+ ))} +
+ )}
- {tabs.map((tab) => ( - -
- - - ))} + + )) + ) : ( +
+ + No journey cards match the selected filter. + +
+ )} ) }