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 1/9] 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. + +
+ )} ) } From cde8e5f48e297c3e702aa7315516d8b42021115e Mon Sep 17 00:00:00 2001 From: Victor Chukwuebuka Umeh <41862157+vyktoremario@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:43:39 +0100 Subject: [PATCH 2/9] update dropdown to match design --- .../JourneyCardsDesktop.module.css | 21 +-- .../JourneyCards/JourneyCardsDesktop.tsx | 99 ++++++------ .../ProductFilterDropdown.module.css | 105 ++++++++++++ .../JourneyCards/ProductFilterDropdown.tsx | 150 ++++++++++++++++++ 4 files changed, 313 insertions(+), 62 deletions(-) create mode 100644 src/components/JourneyCards/ProductFilterDropdown.module.css create mode 100644 src/components/JourneyCards/ProductFilterDropdown.tsx diff --git a/src/components/JourneyCards/JourneyCardsDesktop.module.css b/src/components/JourneyCards/JourneyCardsDesktop.module.css index 0bb8488946f..6af3ffd93fb 100644 --- a/src/components/JourneyCards/JourneyCardsDesktop.module.css +++ b/src/components/JourneyCards/JourneyCardsDesktop.module.css @@ -20,24 +20,18 @@ display: flex; align-items: center; gap: var(--space-2x); - min-width: 200px; } -.filterSelect { - min-width: 200px; +.journeyRows { + display: flex; + flex-direction: column; } -.journeyCards { +.journeyRow { 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); @@ -45,6 +39,7 @@ text-decoration: none; color: inherit; transition: background-color 0.2s ease; + border-left: 1px solid var(--border); } .journeyCard:hover { @@ -115,11 +110,7 @@ width: 100%; } - .filterSelect { - width: 100%; - } - - .journeyCards { + .journeyRow { grid-template-columns: 1fr; } } diff --git a/src/components/JourneyCards/JourneyCardsDesktop.tsx b/src/components/JourneyCards/JourneyCardsDesktop.tsx index f4c3371f569..937bfb7b601 100644 --- a/src/components/JourneyCards/JourneyCardsDesktop.tsx +++ b/src/components/JourneyCards/JourneyCardsDesktop.tsx @@ -1,6 +1,7 @@ import { useState, useMemo } from "react" -import { SimpleSelect, Typography, Tag } from "@chainlink/blocks" +import { Typography, Tag } from "@chainlink/blocks" import styles from "./JourneyCardsDesktop.module.css" +import { ProductFilterDropdown } from "./ProductFilterDropdown.tsx" export interface JourneyItem { title: string @@ -52,11 +53,12 @@ function validateBadge(badge: string): boolean { } export const JourneyCardsDesktop = ({ columns }: JourneyCardsDesktopProps) => { - const [selectedFilter, setSelectedFilter] = useState("all") + const [selectedFilters, setSelectedFilters] = useState(["all"]) - // Filter columns based on selected product + // Filter columns based on selected products const filteredColumns = useMemo(() => { - if (selectedFilter === "all") { + // If "all" is selected or no filters selected, show all items + if (selectedFilters.includes("all") || selectedFilters.length === 0) { return columns } @@ -69,20 +71,22 @@ export const JourneyCardsDesktop = ({ columns }: JourneyCardsDesktopProps) => { console.warn(`Invalid badge value: ${item.badge}`) return false } - return item.badge.toLowerCase() === selectedFilter.toLowerCase() + // Show item if it matches ANY of the selected filters (OR logic) + return selectedFilters.some((filter) => item.badge.toLowerCase() === filter.toLowerCase()) }), })) .filter((column) => column.items.length > 0) // Hide columns with no matching cards - }, [columns, selectedFilter]) + }, [columns, selectedFilters]) - 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) - } + // Transform columns to rows (row-based layout) + const rows = useMemo(() => { + if (filteredColumns.length === 0) return [] + + const maxItems = Math.max(...filteredColumns.map((col) => col.items.length)) + return Array.from({ length: maxItems }, (_, rowIndex) => ({ + items: filteredColumns.map((col) => col.items[rowIndex]).filter(Boolean), + })) + }, [filteredColumns]) return (
@@ -91,47 +95,48 @@ export const JourneyCardsDesktop = ({ columns }: JourneyCardsDesktopProps) => { Start your Chainlink journey
-
-
- {filteredColumns.map((column) => ( -
-
- - {column.title} - -
- {column.items.map((item) => ( - -
- {item.title} - - {item.description} - -
- -
- - {item.badge} - - -
-
+ {filteredColumns.length > 0 ? ( +
+
+ {filteredColumns.map((column) => ( +
+ + {column.title} + +
))}
- ))} -
+ {rows.map((row, i) => ( + + ))} +
+ ) : (
No journey cards match the selected filter. diff --git a/src/components/JourneyCards/ProductFilterDropdown.module.css b/src/components/JourneyCards/ProductFilterDropdown.module.css new file mode 100644 index 00000000000..3a54d50c977 --- /dev/null +++ b/src/components/JourneyCards/ProductFilterDropdown.module.css @@ -0,0 +1,105 @@ +.dropdown { + position: relative; + width: 148px; +} + +.trigger { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 10px 12px; + max-height: 40px; + background-color: var(--input); + border: 1px solid var(--input-border-active); + border-radius: var(--rounded-sm, 4px); + font-size: 14px; + font-weight: 500; + color: var(--foreground); + cursor: pointer; + transition: all 0.2s ease; +} + +.trigger:hover { + border-color: var(--brand); + background-color: var(--muted); +} + +.trigger:focus { + outline: none; + border-color: var(--brand); + box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1); +} + +.chevronIcon { + flex-shrink: 0; + transition: transform 0.2s ease; + color: var(--foreground); +} + +.chevronOpen { + transform: rotate(180deg); +} + +.menu { + position: absolute; + top: calc(100% + 4px); + left: 0; + right: 0; + background-color: var(--background); + border: 1px solid var(--border); + border-radius: var(--rounded-sm, 4px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + z-index: 1000; + overflow-y: auto; + padding: 8px 0; + box-shadow: 0 16px 24px 0 rgba(0, 0, 0, 0.04); +} + +.checkboxLabel { + display: flex; + align-items: center; + padding: 12px 16px; + cursor: pointer; + transition: background-color 0.15s ease; + gap: 8px; + user-select: none; + width: 100%; +} + +.checkboxLabel:hover { + background-color: var(--muted); +} + +.checkboxInput { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.customCheckbox { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + border-radius: var(--rounded-sm, 4px); + border: 1px solid var(--input-border); + background: var(--input); + flex-shrink: 0; + transition: all 0.2s ease; +} + +.checkboxInput:checked + .customCheckbox { + background-color: var(--link); +} + +.checkboxLabel:hover .customCheckbox { + border-color: var(--link); +} + +@media (max-width: 768px) { + .dropdown { + width: 100%; + } +} diff --git a/src/components/JourneyCards/ProductFilterDropdown.tsx b/src/components/JourneyCards/ProductFilterDropdown.tsx new file mode 100644 index 00000000000..32965dac87c --- /dev/null +++ b/src/components/JourneyCards/ProductFilterDropdown.tsx @@ -0,0 +1,150 @@ +import { useState, useEffect, useRef } from "react" +import styles from "./ProductFilterDropdown.module.css" +import { Typography } from "@chainlink/blocks" + +export interface ProductFilterOption { + label: string + value: string +} + +interface ProductFilterDropdownProps { + selectedFilters: string[] + onFiltersChange: (filters: string[]) => void + options: ProductFilterOption[] +} + +export const ProductFilterDropdown = ({ selectedFilters, onFiltersChange, options }: ProductFilterDropdownProps) => { + const [isOpen, setIsOpen] = useState(false) + const dropdownRef = useRef(null) + + // Close dropdown when clicking outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false) + } + } + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside) + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside) + } + }, [isOpen]) + + const handleCheckboxChange = (value: string) => { + if (value === "all") { + // When clicking "All Products" + if (selectedFilters.includes("all")) { + // If already checked, uncheck it and default to showing all + onFiltersChange(["all"]) + } else { + // If not checked, check it and clear all individual selections + onFiltersChange(["all"]) + } + } else { + // Handle individual product selection + if (selectedFilters.includes("all")) { + // If "All Products" is currently selected, uncheck it and select only this product + onFiltersChange([value]) + } else { + // "All Products" is not selected, toggle the individual product + if (selectedFilters.includes(value)) { + // Uncheck the item + const updated = selectedFilters.filter((f) => f !== value) + // If no products are selected, default to showing all + onFiltersChange(updated.length === 0 ? ["all"] : updated) + } else { + // Check the item (keep other individual selections) + onFiltersChange([...selectedFilters, value]) + } + } + } + } + + const isChecked = (value: string) => { + if (value === "all") { + return selectedFilters.includes("all") + } + // Individual products are only checked if explicitly in the filters array + // (not when "all" is selected) + return selectedFilters.includes(value) && !selectedFilters.includes("all") + } + + // Get the display text for the trigger button + const getTriggerText = () => { + // If "All Products" is selected or no filters selected + if (selectedFilters.includes("all") || selectedFilters.length === 0) { + return "All Products" + } + + // If exactly 1 product is selected + if (selectedFilters.length === 1) { + const selectedProduct = options.find((filter) => filter.value === selectedFilters[0]) + return selectedProduct?.label || "All Products" + } + + // If 2 or more products are selected + return "Multiple Products" + } + + return ( +
+ + + {isOpen && ( +
+ {options.map((filter) => ( + + ))} +
+ )} +
+ ) +} From 4533b0da35ffe927a459b237f6e180c8c89e7071 Mon Sep 17 00:00:00 2001 From: Victor Chukwuebuka Umeh <41862157+vyktoremario@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:46:53 +0100 Subject: [PATCH 3/9] re-use hook --- .../JourneyCards/ProductFilterDropdown.tsx | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/components/JourneyCards/ProductFilterDropdown.tsx b/src/components/JourneyCards/ProductFilterDropdown.tsx index 32965dac87c..26ee7b34fe5 100644 --- a/src/components/JourneyCards/ProductFilterDropdown.tsx +++ b/src/components/JourneyCards/ProductFilterDropdown.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect, useRef } from "react" +import { useState, useRef } from "react" import styles from "./ProductFilterDropdown.module.css" import { Typography } from "@chainlink/blocks" +import { useClickOutside } from "~/hooks/useClickOutside.tsx" export interface ProductFilterOption { label: string @@ -17,22 +18,7 @@ export const ProductFilterDropdown = ({ selectedFilters, onFiltersChange, option const [isOpen, setIsOpen] = useState(false) const dropdownRef = useRef(null) - // Close dropdown when clicking outside - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setIsOpen(false) - } - } - - if (isOpen) { - document.addEventListener("mousedown", handleClickOutside) - } - - return () => { - document.removeEventListener("mousedown", handleClickOutside) - } - }, [isOpen]) + useClickOutside(dropdownRef, () => setIsOpen(false), { enabled: isOpen }) const handleCheckboxChange = (value: string) => { if (value === "all") { From d775f3b2e5d8ac7faa68e75759f7b739a50a4a9c Mon Sep 17 00:00:00 2001 From: Victor Chukwuebuka Umeh <41862157+vyktoremario@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:49:58 +0100 Subject: [PATCH 4/9] empty commit message From 480b4a948ed8a18ed090257049775de6da077a57 Mon Sep 17 00:00:00 2001 From: Victor Chukwuebuka Umeh <41862157+vyktoremario@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:13:33 +0100 Subject: [PATCH 5/9] AI comments --- .../JourneyCards/JourneyCardsDesktop.tsx | 9 ++-- .../JourneyCards/JourneyTabGrid.tsx | 45 +++++++++---------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/components/JourneyCards/JourneyCardsDesktop.tsx b/src/components/JourneyCards/JourneyCardsDesktop.tsx index 937bfb7b601..83cf57e1f2b 100644 --- a/src/components/JourneyCards/JourneyCardsDesktop.tsx +++ b/src/components/JourneyCards/JourneyCardsDesktop.tsx @@ -84,6 +84,7 @@ export const JourneyCardsDesktop = ({ columns }: JourneyCardsDesktopProps) => { const maxItems = Math.max(...filteredColumns.map((col) => col.items.length)) return Array.from({ length: maxItems }, (_, rowIndex) => ({ + id: `row-${rowIndex}`, items: filteredColumns.map((col) => col.items[rowIndex]).filter(Boolean), })) }, [filteredColumns]) @@ -114,10 +115,10 @@ export const JourneyCardsDesktop = ({ columns }: JourneyCardsDesktopProps) => { ))}
- {rows.map((row, i) => ( -
- {row.items.map((item, j) => ( - + {rows.map((row) => ( +
+ {row.items.map((item) => ( +
{item.title} diff --git a/src/components/JourneyCards/JourneyTabGrid.tsx b/src/components/JourneyCards/JourneyTabGrid.tsx index e6dd8d130ac..f64cfea1fb8 100644 --- a/src/components/JourneyCards/JourneyTabGrid.tsx +++ b/src/components/JourneyCards/JourneyTabGrid.tsx @@ -1,6 +1,7 @@ -import { useState, useMemo } from "react" +import { useState, useMemo, useEffect } from "react" import styles from "./JourneyTabGrid.module.css" -import { Tabs, TabsContent, TabsList, TabsTrigger, Typography, Tag, SimpleSelect } from "@chainlink/blocks" +import { Tabs, TabsContent, TabsList, TabsTrigger, Typography, Tag } from "@chainlink/blocks" +import { ProductFilterDropdown } from "./ProductFilterDropdown.tsx" export interface JourneyItem { title: string @@ -53,11 +54,13 @@ function validateBadge(badge: string): boolean { } export const JourneyTabGrid = ({ tabs, header }: JourneyTabGridProps) => { - const [selectedFilter, setSelectedFilter] = useState("all") + const [selectedFilters, setSelectedFilters] = useState(["all"]) + const [activeTab, setActiveTab] = useState() - // Filter tabs based on selected product + // Filter tabs based on selected products const filteredTabs = useMemo(() => { - if (selectedFilter === "all") { + // If "all" is selected or no filters selected, show all items + if (selectedFilters.includes("all") || selectedFilters.length === 0) { return tabs } @@ -71,24 +74,20 @@ export const JourneyTabGrid = ({ tabs, header }: JourneyTabGridProps) => { console.warn(`Invalid badge value: ${item.badge}`) return false } - return item.badge.toLowerCase() === selectedFilter.toLowerCase() + // Show item if it matches ANY of the selected filters (OR logic) + return selectedFilters.some((filter) => item.badge!.toLowerCase() === filter.toLowerCase()) }), })) .filter((tab) => tab.items.length > 0) // Hide tabs with no matching items - }, [tabs, selectedFilter]) + }, [tabs, selectedFilters]) - 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) - } + // Reset activeTab when filteredTabs changes + useEffect(() => { + setActiveTab(filteredTabs[0]?.name) + }, [filteredTabs]) return ( - +
{ {header}
-
{filteredTabs.length > 0 && ( @@ -123,8 +120,8 @@ export const JourneyTabGrid = ({ tabs, header }: JourneyTabGridProps) => {
- {tab.items.map((item, index) => ( - + {tab.items.map((item) => ( +
{item.title} From c99b35796752b61c6ef3c8ee0f10221d3022ce3e Mon Sep 17 00:00:00 2001 From: Victor Chukwuebuka Umeh <41862157+vyktoremario@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:08:57 +0100 Subject: [PATCH 6/9] pagination --- .../JourneyCardsDesktop.module.css | 37 ++++++++++++++ .../JourneyCards/JourneyCardsDesktop.tsx | 51 +++++++++++++++++-- .../JourneyCards/PaginationControls.tsx | 43 ++++++++++++++++ 3 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 src/components/JourneyCards/PaginationControls.tsx diff --git a/src/components/JourneyCards/JourneyCardsDesktop.module.css b/src/components/JourneyCards/JourneyCardsDesktop.module.css index 6af3ffd93fb..4ae33d5852e 100644 --- a/src/components/JourneyCards/JourneyCardsDesktop.module.css +++ b/src/components/JourneyCards/JourneyCardsDesktop.module.css @@ -94,6 +94,43 @@ text-align: center; } +.paginationControls { + display: flex; + align-items: center; + justify-content: flex-end; + margin-top: var(--space-2x); +} + +.paginationButton { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + min-width: 40px; + max-width: 40px; + min-height: 40px; + max-height: 40px; + padding: var(--space-4x, 14px); + border: 1px solid var(--border); + background-color: var(--background); + color: var(--foreground); + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: all 0.2s ease; +} + +.paginationButton:hover:not(:disabled) { + background-color: var(--muted); + border-color: var(--brand); +} + +.paginationButton:disabled { + opacity: 0.4; + cursor: not-allowed; +} + @media screen and (min-width: 62em) { .sectionTitle { font-size: 32px; diff --git a/src/components/JourneyCards/JourneyCardsDesktop.tsx b/src/components/JourneyCards/JourneyCardsDesktop.tsx index 83cf57e1f2b..9496e93fff0 100644 --- a/src/components/JourneyCards/JourneyCardsDesktop.tsx +++ b/src/components/JourneyCards/JourneyCardsDesktop.tsx @@ -1,7 +1,8 @@ -import { useState, useMemo } from "react" +import { useState, useMemo, useEffect } from "react" import { Typography, Tag } from "@chainlink/blocks" import styles from "./JourneyCardsDesktop.module.css" import { ProductFilterDropdown } from "./ProductFilterDropdown.tsx" +import { PaginationControls } from "./PaginationControls.tsx" export interface JourneyItem { title: string @@ -52,8 +53,12 @@ function validateBadge(badge: string): boolean { return VALID_BADGE_VALUES.has(badge) } +const ITEMS_PER_PAGE = 4 + export const JourneyCardsDesktop = ({ columns }: JourneyCardsDesktopProps) => { const [selectedFilters, setSelectedFilters] = useState(["all"]) + const [currentPage, setCurrentPage] = useState(0) + // Filter columns based on selected products const filteredColumns = useMemo(() => { @@ -78,16 +83,45 @@ export const JourneyCardsDesktop = ({ columns }: JourneyCardsDesktopProps) => { .filter((column) => column.items.length > 0) // Hide columns with no matching cards }, [columns, selectedFilters]) - // Transform columns to rows (row-based layout) + // Transform columns to rows (row-based layout) with pagination const rows = useMemo(() => { if (filteredColumns.length === 0) return [] - const maxItems = Math.max(...filteredColumns.map((col) => col.items.length)) + const startIndex = currentPage * ITEMS_PER_PAGE + const endIndex = startIndex + ITEMS_PER_PAGE + + // Slice items from each column based on current page + const paginatedColumns = filteredColumns.map((col) => ({ + ...col, + items: col.items.slice(startIndex, endIndex), + })) + + const maxItems = Math.max(...paginatedColumns.map((col) => col.items.length)) return Array.from({ length: maxItems }, (_, rowIndex) => ({ id: `row-${rowIndex}`, - items: filteredColumns.map((col) => col.items[rowIndex]).filter(Boolean), + items: paginatedColumns.map((col) => col.items[rowIndex]).filter(Boolean), })) + }, [filteredColumns, currentPage]) + + // Calculate total pages based on max items across all columns + const totalPages = useMemo(() => { + if (filteredColumns.length === 0) return 0 + const maxItems = Math.max(...filteredColumns.map((col) => col.items.length)) + return Math.ceil(maxItems / ITEMS_PER_PAGE) + }, [filteredColumns]) + + // Reset pagination when filters change + useEffect(() => { + setCurrentPage(0) }, [filteredColumns]) + + const handlePreviousPage = () => { + setCurrentPage((prev) => Math.max(0, prev - 1)) + } + + const handleNextPage = () => { + setCurrentPage((prev) => Math.min(totalPages - 1, prev + 1)) + } return (
@@ -136,6 +170,15 @@ export const JourneyCardsDesktop = ({ columns }: JourneyCardsDesktopProps) => { ))}
))} + +
) : (
diff --git a/src/components/JourneyCards/PaginationControls.tsx b/src/components/JourneyCards/PaginationControls.tsx new file mode 100644 index 00000000000..d97002c5e5a --- /dev/null +++ b/src/components/JourneyCards/PaginationControls.tsx @@ -0,0 +1,43 @@ +interface PaginationControlsProps { + currentPage: number + totalPages: number + onPrevious: () => void + onNext: () => void + containerClassName?: string + buttonClassName?: string +} + +export const PaginationControls = ({ + currentPage, + totalPages, + onPrevious, + onNext, + containerClassName, + buttonClassName, +}: PaginationControlsProps) => { + // Only render when there's more than one page + if (totalPages <= 1) { + return null + } + + return ( +
+ + +
+ ) +} From a13e22643151ad198c522eb58b490558c60be285 Mon Sep 17 00:00:00 2001 From: Victor Chukwuebuka Umeh <41862157+vyktoremario@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:13:56 +0100 Subject: [PATCH 7/9] buttons --- .../JourneyCardsDesktop.module.css | 6 ++++ .../JourneyCards/JourneyCardsDesktop.tsx | 15 +++++----- .../JourneyCards/PaginationControls.tsx | 30 ++++++++++++++----- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/components/JourneyCards/JourneyCardsDesktop.module.css b/src/components/JourneyCards/JourneyCardsDesktop.module.css index 4ae33d5852e..1270644aae2 100644 --- a/src/components/JourneyCards/JourneyCardsDesktop.module.css +++ b/src/components/JourneyCards/JourneyCardsDesktop.module.css @@ -121,9 +121,15 @@ transition: all 0.2s ease; } +.paginationButton svg { + display: block; + flex-shrink: 0; +} + .paginationButton:hover:not(:disabled) { background-color: var(--muted); border-color: var(--brand); + color: var(--brand); } .paginationButton:disabled { diff --git a/src/components/JourneyCards/JourneyCardsDesktop.tsx b/src/components/JourneyCards/JourneyCardsDesktop.tsx index 9496e93fff0..327684c29ac 100644 --- a/src/components/JourneyCards/JourneyCardsDesktop.tsx +++ b/src/components/JourneyCards/JourneyCardsDesktop.tsx @@ -58,7 +58,6 @@ const ITEMS_PER_PAGE = 4 export const JourneyCardsDesktop = ({ columns }: JourneyCardsDesktopProps) => { const [selectedFilters, setSelectedFilters] = useState(["all"]) const [currentPage, setCurrentPage] = useState(0) - // Filter columns based on selected products const filteredColumns = useMemo(() => { @@ -89,36 +88,36 @@ export const JourneyCardsDesktop = ({ columns }: JourneyCardsDesktopProps) => { const startIndex = currentPage * ITEMS_PER_PAGE const endIndex = startIndex + ITEMS_PER_PAGE - + // Slice items from each column based on current page const paginatedColumns = filteredColumns.map((col) => ({ ...col, items: col.items.slice(startIndex, endIndex), })) - + const maxItems = Math.max(...paginatedColumns.map((col) => col.items.length)) return Array.from({ length: maxItems }, (_, rowIndex) => ({ id: `row-${rowIndex}`, items: paginatedColumns.map((col) => col.items[rowIndex]).filter(Boolean), })) }, [filteredColumns, currentPage]) - + // Calculate total pages based on max items across all columns const totalPages = useMemo(() => { if (filteredColumns.length === 0) return 0 const maxItems = Math.max(...filteredColumns.map((col) => col.items.length)) return Math.ceil(maxItems / ITEMS_PER_PAGE) }, [filteredColumns]) - + // Reset pagination when filters change useEffect(() => { setCurrentPage(0) }, [filteredColumns]) - + const handlePreviousPage = () => { setCurrentPage((prev) => Math.max(0, prev - 1)) } - + const handleNextPage = () => { setCurrentPage((prev) => Math.min(totalPages - 1, prev + 1)) } @@ -170,7 +169,7 @@ export const JourneyCardsDesktop = ({ columns }: JourneyCardsDesktopProps) => { ))}
))} - + -
) From ee4b6840fd922f0f233f24783f647f0e0344da8c Mon Sep 17 00:00:00 2001 From: Victor Chukwuebuka Umeh <41862157+vyktoremario@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:20:20 +0100 Subject: [PATCH 8/9] remove unused type --- src/components/JourneyCards/JourneyTabGrid.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/JourneyCards/JourneyTabGrid.tsx b/src/components/JourneyCards/JourneyTabGrid.tsx index f64cfea1fb8..610380c3d8e 100644 --- a/src/components/JourneyCards/JourneyTabGrid.tsx +++ b/src/components/JourneyCards/JourneyTabGrid.tsx @@ -34,8 +34,6 @@ const PRODUCT_FILTERS = [ { 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", From 13ba759e4ffb351781835ab871c27f72a2642622 Mon Sep 17 00:00:00 2001 From: Victor Chukwuebuka Umeh <41862157+vyktoremario@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:24:10 +0100 Subject: [PATCH 9/9] Ai comments --- src/components/JourneyCards/JourneyCardsDesktop.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/JourneyCards/JourneyCardsDesktop.tsx b/src/components/JourneyCards/JourneyCardsDesktop.tsx index 327684c29ac..6178e4177af 100644 --- a/src/components/JourneyCards/JourneyCardsDesktop.tsx +++ b/src/components/JourneyCards/JourneyCardsDesktop.tsx @@ -86,7 +86,13 @@ export const JourneyCardsDesktop = ({ columns }: JourneyCardsDesktopProps) => { const rows = useMemo(() => { if (filteredColumns.length === 0) return [] - const startIndex = currentPage * ITEMS_PER_PAGE + const maxItems = Math.max(...filteredColumns.map((col) => col.items.length)) + const maxPage = Math.max(0, Math.ceil(maxItems / ITEMS_PER_PAGE) - 1) + + // Clamp currentPage to valid bounds to prevent flash of empty content + const validPage = Math.min(currentPage, maxPage) + + const startIndex = validPage * ITEMS_PER_PAGE const endIndex = startIndex + ITEMS_PER_PAGE // Slice items from each column based on current page @@ -95,8 +101,8 @@ export const JourneyCardsDesktop = ({ columns }: JourneyCardsDesktopProps) => { items: col.items.slice(startIndex, endIndex), })) - const maxItems = Math.max(...paginatedColumns.map((col) => col.items.length)) - return Array.from({ length: maxItems }, (_, rowIndex) => ({ + const maxPaginatedItems = Math.max(...paginatedColumns.map((col) => col.items.length)) + return Array.from({ length: maxPaginatedItems }, (_, rowIndex) => ({ id: `row-${rowIndex}`, items: paginatedColumns.map((col) => col.items[rowIndex]).filter(Boolean), }))