From 3e1d612abf1a5ee3f65882bb6398daf4404fb99a Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Tue, 18 Nov 2025 11:56:18 -0500 Subject: [PATCH 01/21] add page --- src/pages/changelog.astro | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/pages/changelog.astro diff --git a/src/pages/changelog.astro b/src/pages/changelog.astro new file mode 100644 index 00000000000..f9f33f31a19 --- /dev/null +++ b/src/pages/changelog.astro @@ -0,0 +1,10 @@ +--- +import BaseLayout from "~/layouts/BaseLayout.astro" +import * as CONFIG from "../config" + +const formattedContentTitle = `${CONFIG.PAGE.titleFallback} | ${CONFIG.SITE.title}` +--- + + +
+
From d522c3fcff795c6fe6bd034e227dffe51041d5bc Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Tue, 18 Nov 2025 13:49:38 -0500 Subject: [PATCH 02/21] add changelog page --- .../ChangelogSnippet/ChangelogCard.module.css | 5 +- src/pages/changelog.astro | 86 ++++++++++++++++++- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/components/ChangelogSnippet/ChangelogCard.module.css b/src/components/ChangelogSnippet/ChangelogCard.module.css index 631d86ed1b3..90b017694cb 100644 --- a/src/components/ChangelogSnippet/ChangelogCard.module.css +++ b/src/components/ChangelogSnippet/ChangelogCard.module.css @@ -204,9 +204,8 @@ @media screen and (max-width: 768px) { .card { - padding: 0 !important; - gap: 0; - flex-direction: column; + padding: var(--space-4x); + gap: var(--space-4x); } .header { diff --git a/src/pages/changelog.astro b/src/pages/changelog.astro index f9f33f31a19..a5301884c34 100644 --- a/src/pages/changelog.astro +++ b/src/pages/changelog.astro @@ -1,10 +1,94 @@ --- import BaseLayout from "~/layouts/BaseLayout.astro" import * as CONFIG from "../config" +import { Typography } from "@chainlink/blocks" +import { getSecret } from "astro:env/server" +import { searchClient, SearchClient } from "@algolia/client-search" +import { ChangelogItem } from "~/components/ChangelogSnippet/types" +import ChangelogCard from "~/components/ChangelogSnippet/ChangelogCard.astro" const formattedContentTitle = `${CONFIG.PAGE.titleFallback} | ${CONFIG.SITE.title}` + +const appId = getSecret("ALGOLIA_APP_ID") +const apiKey = getSecret("PUBLIC_ALGOLIA_SEARCH_PUBLIC_API_KEY") + +let client: SearchClient +let logs: ChangelogItem[] | undefined = undefined + +// Initialize client if appId and apiKey are available to avoid needing to update +// the github actions with the new keys (satisfies linkcheck-internal) +if (appId && apiKey) { + client = searchClient(appId, apiKey) + + const req = await client.search({ + requests: [ + { + indexName: "Changelog", + restrictSearchableAttributes: ["topic"], + }, + ], + }) + + const firstResult = req.results[0] + const results = "hits" in firstResult ? (firstResult.hits as ChangelogItem[]) : [] + + // logs are returned sorted by created_at DESC + logs = results +} --- -
+
+
+ Changelog + Never miss an update +
+ +
+ {logs?.map((log) => )} +
+
+ + From 5bbf1ee469d6a7d6de3f1dc0d6fafeb110682ebc Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Thu, 20 Nov 2025 15:07:17 -0500 Subject: [PATCH 03/21] add pagination --- src/pages/changelog.astro | 117 +++++++++++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 9 deletions(-) diff --git a/src/pages/changelog.astro b/src/pages/changelog.astro index a5301884c34..e9d39c02d80 100644 --- a/src/pages/changelog.astro +++ b/src/pages/changelog.astro @@ -15,25 +15,40 @@ const apiKey = getSecret("PUBLIC_ALGOLIA_SEARCH_PUBLIC_API_KEY") let client: SearchClient let logs: ChangelogItem[] | undefined = undefined -// Initialize client if appId and apiKey are available to avoid needing to update -// the github actions with the new keys (satisfies linkcheck-internal) if (appId && apiKey) { client = searchClient(appId, apiKey) - const req = await client.search({ + const firstReq = await client.search({ requests: [ { indexName: "Changelog", - restrictSearchableAttributes: ["topic"], + page: 0, + hitsPerPage: 1000, }, ], }) - const firstResult = req.results[0] - const results = "hits" in firstResult ? (firstResult.hits as ChangelogItem[]) : [] + const firstResult = firstReq.results[0] + let allHits = "hits" in firstResult ? (firstResult.hits as ChangelogItem[]) : [] + const nbPages = "nbPages" in firstResult ? firstResult.nbPages : 1 - // logs are returned sorted by created_at DESC - logs = results + if (nbPages && nbPages > 1) { + const remainingRequests = Array.from({ length: nbPages - 1 }, (_, i) => ({ + indexName: "Changelog", + page: i + 1, + hitsPerPage: 1000, + })) + + const remainingResults = await client.search({ requests: remainingRequests }) + + remainingResults.results.forEach((result) => { + if ("hits" in result) { + allHits = [...allHits, ...(result.hits as ChangelogItem[])] + } + }) + } + + logs = allHits } --- @@ -45,11 +60,64 @@ if (appId && apiKey) {
- {logs?.map((log) => )} + { + logs?.map((log, index) => ( +
= 25 ? "display: none;" : ""}> + +
+ )) + }
+ + { + logs && logs.length > 25 && ( +
+ +

+ Showing 25 of {logs.length} updates +

+
+ ) + } + + diff --git a/src/utils/changelogFilters.ts b/src/utils/changelogFilters.ts new file mode 100644 index 00000000000..1c74c8851f6 --- /dev/null +++ b/src/utils/changelogFilters.ts @@ -0,0 +1,106 @@ +import { ChangelogItem } from "~/components/ChangelogSnippet/types" + +/** + * Extracts network names from the HTML networks field + * Networks are hidden in divs with fs-cmsfilter-field="network" class + */ +export function extractNetworkFromHtml(html: string): string[] { + const networks: string[] = [] + const regex = /]*fs-cmsfilter-field="network"[^>]*class="hidden"[^>]*>(.*?)<\/div>/g + let match + + while ((match = regex.exec(html)) !== null) { + const networkName = match[1].trim() + if (networkName) { + networks.push(networkName) + } + } + + return networks +} + +/** + * Extracts all unique networks from changelog items + */ +export function getUniqueNetworks(items: ChangelogItem[]): string[] { + const networksSet = new Set() + + items.forEach((item) => { + if (item.networks) { + const networks = extractNetworkFromHtml(item.networks) + networks.forEach((network) => networksSet.add(network)) + } + }) + + return Array.from(networksSet).sort() +} + +/** + * Extracts all unique topics from changelog items + */ +export function getUniqueTopics(items: ChangelogItem[]): string[] { + const topicsSet = new Set() + + items.forEach((item) => { + if (item.topic) { + topicsSet.add(item.topic) + } + }) + + return Array.from(topicsSet).sort() +} + +/** + * Extracts all unique types from changelog items + */ +export function getUniqueTypes(items: ChangelogItem[]): string[] { + const typesSet = new Set() + + items.forEach((item) => { + if (item.type) { + typesSet.add(item.type) + } + }) + + return Array.from(typesSet).sort() +} + +/** + * Checks if a changelog item matches the selected filters + */ +export function matchesFilters( + item: ChangelogItem, + selectedTopics: string[], + selectedNetworks: string[], + selectedTypes: string[] +): boolean { + // If no filters selected, show all items + const hasTopicFilter = selectedTopics.length > 0 + const hasNetworkFilter = selectedNetworks.length > 0 + const hasTypeFilter = selectedTypes.length > 0 + + if (!hasTopicFilter && !hasNetworkFilter && !hasTypeFilter) { + return true + } + + // Check topic filter + if (hasTopicFilter && !selectedTopics.includes(item.topic)) { + return false + } + + // Check type filter + if (hasTypeFilter && !selectedTypes.includes(item.type)) { + return false + } + + // Check network filter + if (hasNetworkFilter) { + const itemNetworks = extractNetworkFromHtml(item.networks) + const hasMatchingNetwork = selectedNetworks.some((network) => itemNetworks.includes(network)) + if (!hasMatchingNetwork) { + return false + } + } + + return true +} From 4c220ca0b652b2933c7bf47fc2e1a8d6648c1c2a Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Wed, 26 Nov 2025 15:31:39 -0500 Subject: [PATCH 06/21] lots of styling --- .../ChangelogFilters/ChangelogFilters.tsx | 56 ++++++++++++++- .../ChangelogFilters/styles.module.css | 69 ++++++++++++++++++- 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/src/components/ChangelogFilters/ChangelogFilters.tsx b/src/components/ChangelogFilters/ChangelogFilters.tsx index 75dc72ddb47..91965aa8580 100644 --- a/src/components/ChangelogFilters/ChangelogFilters.tsx +++ b/src/components/ChangelogFilters/ChangelogFilters.tsx @@ -1,5 +1,59 @@ +import { SvgSearch, SvgTaillessArrowDownSmall, SvgX } from "@chainlink/blocks" import styles from "./styles.module.css" +import { useState } from "react" +import { clsx } from "~/lib/clsx/clsx.ts" + +const SearchInput = ({ isExpanded, onClick }: { isExpanded: boolean; onClick: (value: boolean) => void }) => { + return ( +
onClick(true)}> + + + {isExpanded && ( + { + e.stopPropagation() + onClick(false) + }} + style={{ + marginRight: "var(--space-4x)", + }} + /> + )} +
+ ) +} + +const Trigger = ({ label }: { label: string }) => { + return ( + + ) +} export const ChangelogFilters = () => { - return
hi
+ const [searchExpanded, setSearchExpanded] = useState(false) + + const searchClickHandler = (value: boolean) => { + setSearchExpanded(value) + } + return ( +
+
+
+ {!searchExpanded && ( + <> + + + + + )} + +
+
+ ) } diff --git a/src/components/ChangelogFilters/styles.module.css b/src/components/ChangelogFilters/styles.module.css index 6abbeff003b..0fa20b00c95 100644 --- a/src/components/ChangelogFilters/styles.module.css +++ b/src/components/ChangelogFilters/styles.module.css @@ -1,9 +1,74 @@ .wrapper { - background: red; + background: #252e42e6; + border-radius: var(--space-8x); max-width: 492px; width: 100%; margin: 0 auto; position: fixed; - bottom: 0; + bottom: var(--space-4x); height: 56px; + z-index: 11; + left: 0; + right: 0; + + padding: var(--space-2x); + backdrop-filter: blur(2px); + -webkit-backdrop-filter: blur(2px); +} + +.content { + display: grid; + grid-template-columns: repeat(4, 1fr); + align-items: center; + justify-content: space-evenly; +} + +.btn { + border-radius: var(--space-8x); + padding: 0 var(--space-4x); + color: var(--gray-300); + display: flex; + transition: all 0.1s ease; + align-items: center; + justify-content: center; + height: 100%; + &:hover { + background-color: #252e42; + } + + & span { + margin-right: var(--space-2x); + font-size: 16px; + display: block; + } +} + +.searchInputWrapper.expanded { + width: 100%; + grid-column-end: span 4; +} + +.searchIcon { + width: 22px; + height: 22px; +} + +.searchInputWrapper { + background-color: #252e42; + display: flex; + padding: var(--space-2x) var(--space-3x); + border-radius: var(--space-8x); + align-items: center; + gap: var(--space-2x); + transition: all 0.1s ease; +} + +.searchInput { + background: transparent; + color: var(--gray-300); + font-size: 14px; + &::placeholder { + color: var(--gray-400); + font-style: normal; + } } From 405a4f13d3494348f04620508d23e45475135148 Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Wed, 26 Nov 2025 15:34:19 -0500 Subject: [PATCH 07/21] more fixes --- src/components/ChangelogFilters/ChangelogFilters.tsx | 10 +++++++++- src/components/ChangelogFilters/styles.module.css | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/ChangelogFilters/ChangelogFilters.tsx b/src/components/ChangelogFilters/ChangelogFilters.tsx index 91965aa8580..9b80e203bbf 100644 --- a/src/components/ChangelogFilters/ChangelogFilters.tsx +++ b/src/components/ChangelogFilters/ChangelogFilters.tsx @@ -35,7 +35,15 @@ const Trigger = ({ label }: { label: string }) => { ) } -export const ChangelogFilters = () => { +export const ChangelogFilters = ({ + products, + networks, + types, +}: { + products: string[] + networks: string[] + types: string[] +}) => { const [searchExpanded, setSearchExpanded] = useState(false) const searchClickHandler = (value: boolean) => { diff --git a/src/components/ChangelogFilters/styles.module.css b/src/components/ChangelogFilters/styles.module.css index 0fa20b00c95..bf1dc9d9081 100644 --- a/src/components/ChangelogFilters/styles.module.css +++ b/src/components/ChangelogFilters/styles.module.css @@ -1,8 +1,8 @@ .wrapper { background: #252e42e6; border-radius: var(--space-8x); - max-width: 492px; - width: 100%; + min-width: 492px; + margin: 0 auto; position: fixed; bottom: var(--space-4x); From 90137b5c928c20b5f97c08eff87634ef7815de38 Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Thu, 27 Nov 2025 13:23:52 -0500 Subject: [PATCH 08/21] styling --- .../ChangelogFilters/ChangelogFilters.tsx | 295 +++++++++++++++++- .../ChangelogFilters/styles.module.css | 89 +++++- src/pages/changelog.astro | 8 +- 3 files changed, 369 insertions(+), 23 deletions(-) diff --git a/src/components/ChangelogFilters/ChangelogFilters.tsx b/src/components/ChangelogFilters/ChangelogFilters.tsx index 9b80e203bbf..bad0d85dd2b 100644 --- a/src/components/ChangelogFilters/ChangelogFilters.tsx +++ b/src/components/ChangelogFilters/ChangelogFilters.tsx @@ -1,15 +1,28 @@ import { SvgSearch, SvgTaillessArrowDownSmall, SvgX } from "@chainlink/blocks" import styles from "./styles.module.css" -import { useState } from "react" +import { useState, useEffect, useCallback } from "react" import { clsx } from "~/lib/clsx/clsx.ts" +import type { ChangelogItem } from "~/components/ChangelogSnippet/types" +import { matchesFilters } from "~/utils/changelogFilters" -const SearchInput = ({ isExpanded, onClick }: { isExpanded: boolean; onClick: (value: boolean) => void }) => { +type FilterType = "product" | "network" | "type" | null + +interface SearchInputProps { + isExpanded: boolean + onClick: (value: boolean) => void + value: string + onChange: (value: string) => void +} + +const SearchInput = ({ isExpanded, onClick, value, onChange }: SearchInputProps) => { return (
onClick(true)}> onChange(e.target.value)} /> {isExpanded && ( { e.stopPropagation() onClick(false) + onChange("") }} style={{ marginRight: "var(--space-4x)", @@ -27,40 +41,289 @@ const SearchInput = ({ isExpanded, onClick }: { isExpanded: boolean; onClick: (v ) } -const Trigger = ({ label }: { label: string }) => { +interface TriggerProps { + label: string + count: number + isActive: boolean + onClick: () => void +} + +const Trigger = ({ label, count, isActive, onClick }: TriggerProps) => { + return ( + + ) +} + +interface FilterPillProps { + label: string + isSelected: boolean + onClick: () => void +} + +const FilterPill = ({ label, isSelected, onClick }: FilterPillProps) => { return ( - ) } -export const ChangelogFilters = ({ - products, - networks, - types, -}: { +export interface ChangelogFiltersProps { products: string[] networks: string[] types: string[] -}) => { + items: ChangelogItem[] +} + +export const ChangelogFilters = ({ products, networks, types, items }: ChangelogFiltersProps) => { const [searchExpanded, setSearchExpanded] = useState(false) + const [searchTerm, setSearchTerm] = useState("") + const [activeFilter, setActiveFilter] = useState(null) + const [selectedProducts, setSelectedProducts] = useState([]) + const [selectedNetworks, setSelectedNetworks] = useState([]) + const [selectedTypes, setSelectedTypes] = useState([]) + + // Read URL parameters on mount + useEffect(() => { + if (typeof window === "undefined") return + + const params = new URLSearchParams(window.location.search) + const productParam = params.get("product") + const networkParam = params.get("network") + const typeParam = params.get("type") + const searchParam = params.get("*") + + if (productParam) { + setSelectedProducts(productParam.split(",")) + } + if (networkParam) { + setSelectedNetworks(networkParam.split(",")) + } + if (typeParam) { + setSelectedTypes(typeParam.split(",")) + } + if (searchParam) { + setSearchTerm(searchParam) + setSearchExpanded(true) + } + }, []) + + // Update URL whenever filters change + const updateURL = useCallback((products: string[], networks: string[], types: string[], search: string) => { + if (typeof window === "undefined") return + + const params = new URLSearchParams() + + if (search) { + params.set("*", search) + } else { + if (products.length > 0) { + params.set("product", products.join(",")) + } + if (networks.length > 0) { + params.set("network", networks.join(",")) + } + if (types.length > 0) { + params.set("type", types.join(",")) + } + } + + const newURL = params.toString() ? `?${params.toString()}` : window.location.pathname + window.history.replaceState({}, "", newURL) + }, []) + + // Update URL when filters change + useEffect(() => { + updateURL(selectedProducts, selectedNetworks, selectedTypes, searchTerm) + }, [selectedProducts, selectedNetworks, selectedTypes, searchTerm, updateURL]) + + // Filter items and update the display + useEffect(() => { + if (typeof window === "undefined") return + + const changelogItems = document.querySelectorAll(".changelog-item") + const loadMoreSection = document.querySelector(".load-more-section") as HTMLElement + const visibleCountSpan = document.getElementById("visible-count") + + if (searchTerm) { + // Search takes priority - filter by search term + const searchLower = searchTerm.toLowerCase() + let visibleCount = 0 + + changelogItems.forEach((item) => { + const index = parseInt(item.getAttribute("data-index") || "0") + const changelogItem = items[index] + + const matchesSearch = + changelogItem?.name.toLowerCase().includes(searchLower) || + changelogItem?.["text-description"]?.toLowerCase().includes(searchLower) + + if (matchesSearch) { + ;(item as HTMLElement).style.display = "" + visibleCount++ + } else { + ;(item as HTMLElement).style.display = "none" + } + }) + + // Hide load more section when searching + if (loadMoreSection) { + loadMoreSection.style.display = "none" + } + } else { + // Apply filter logic + let visibleCount = 0 + const hasFilters = selectedProducts.length > 0 || selectedNetworks.length > 0 || selectedTypes.length > 0 + + changelogItems.forEach((item) => { + const index = parseInt(item.getAttribute("data-index") || "0") + const changelogItem = items[index] + + if (hasFilters && changelogItem) { + const matches = matchesFilters(changelogItem, selectedProducts, selectedNetworks, selectedTypes) + if (matches) { + ;(item as HTMLElement).style.display = "" + visibleCount++ + } else { + ;(item as HTMLElement).style.display = "none" + } + } else { + // No filters - show first 25 items by default + if (visibleCount < 25) { + ;(item as HTMLElement).style.display = "" + visibleCount++ + } else { + ;(item as HTMLElement).style.display = "none" + } + } + }) + + // Show/hide load more section based on filters + if (loadMoreSection) { + if (hasFilters) { + loadMoreSection.style.display = "none" + } else { + loadMoreSection.style.display = visibleCount >= items.length ? "none" : "flex" + } + } + + // Update visible count + if (visibleCountSpan) { + visibleCountSpan.textContent = visibleCount.toString() + } + } + }, [searchTerm, selectedProducts, selectedNetworks, selectedTypes, items]) const searchClickHandler = (value: boolean) => { setSearchExpanded(value) } + + const handleSearchChange = (value: string) => { + setSearchTerm(value) + } + + const toggleFilter = (filterType: FilterType) => { + setActiveFilter(activeFilter === filterType ? null : filterType) + } + + const toggleSelection = (type: "product" | "network" | "type", value: string) => { + switch (type) { + case "product": + setSelectedProducts((prev) => (prev.includes(value) ? prev.filter((p) => p !== value) : [...prev, value])) + break + case "network": + setSelectedNetworks((prev) => (prev.includes(value) ? prev.filter((n) => n !== value) : [...prev, value])) + break + case "type": + setSelectedTypes((prev) => (prev.includes(value) ? prev.filter((t) => t !== value) : [...prev, value])) + break + } + } + + const getFilterOptions = () => { + switch (activeFilter) { + case "product": + return products + case "network": + return networks + case "type": + return types + default: + return [] + } + } + + const getSelectedValues = () => { + switch (activeFilter) { + case "product": + return selectedProducts + case "network": + return selectedNetworks + case "type": + return selectedTypes + default: + return [] + } + } + return (
-
+ {activeFilter && ( +
+ {getFilterOptions().map((option) => ( + toggleSelection(activeFilter, option)} + /> + ))} +
+ )}
{!searchExpanded && ( <> - - - + toggleFilter("product")} + /> + toggleFilter("network")} + /> + toggleFilter("type")} + /> )} - +
) diff --git a/src/components/ChangelogFilters/styles.module.css b/src/components/ChangelogFilters/styles.module.css index bf1dc9d9081..b3c9cd0d40f 100644 --- a/src/components/ChangelogFilters/styles.module.css +++ b/src/components/ChangelogFilters/styles.module.css @@ -1,12 +1,13 @@ .wrapper { background: #252e42e6; border-radius: var(--space-8x); - min-width: 492px; - + max-width: 492px; + width: 100%; margin: 0 auto; position: fixed; bottom: var(--space-4x); - height: 56px; + height: fit-content; + min-height: 56px; z-index: 11; left: 0; right: 0; @@ -36,10 +37,21 @@ background-color: #252e42; } - & span { + & div { margin-right: var(--space-2x); font-size: 16px; - display: block; + display: flex; + align-items: center; + gap: var(--space-2x); + & span { + display: flex; + color: var(--white); + align-items: center; + gap: var(--space-2x); + background-color: var(--gray-500); + border-radius: var(--space-8x); + padding: var(--space-1x) var(--space-3x); + } } } @@ -72,3 +84,70 @@ font-style: normal; } } + +.expandedContent { + max-height: 312px; + overflow-y: auto; + overflow-x: hidden; + padding: var(--space-4x); + display: flex; + flex-direction: column; + gap: var(--space-1x); + margin-bottom: var(--space-2x); +} + +.expandedContent::-webkit-scrollbar { + width: 6px; +} + +.expandedContent::-webkit-scrollbar-track { + background: transparent; +} + +.expandedContent::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; +} + +.expandedContent::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} + +.pill { + background-color: transparent; + color: var(--white); + border-radius: var(--space-8x); + padding: var(--space-1x) var(--space-3x); + font-weight: 400; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-2x); + transition: all 0.2s ease; + white-space: nowrap; + cursor: pointer; + border: none; + width: fit-content; + text-align: left; +} + +.pill:hover { + background-color: rgba(255, 255, 255, 0.05); +} + +.pillSelected { + color: var(--white); + + & span { + background-color: var(--gray-700); + border-radius: var(--space-2x); + } +} + +.pillSelected:hover { + background-color: rgba(255, 255, 255, 0.05); +} + +.btnActive { + background-color: #1e2635; +} diff --git a/src/pages/changelog.astro b/src/pages/changelog.astro index 4c39d70a23f..03043581b1c 100644 --- a/src/pages/changelog.astro +++ b/src/pages/changelog.astro @@ -7,6 +7,7 @@ import { getSecret } from "astro:env/server" import { searchClient, SearchClient } from "@algolia/client-search" import { ChangelogItem } from "~/components/ChangelogSnippet/types" import ChangelogCard from "~/components/ChangelogSnippet/ChangelogCard.astro" +import { getUniqueNetworks, getUniqueTopics, getUniqueTypes } from "~/utils/changelogFilters" const formattedContentTitle = `${CONFIG.PAGE.titleFallback} | ${CONFIG.SITE.title}` const appId = getSecret("ALGOLIA_APP_ID") @@ -51,7 +52,10 @@ if (appId && apiKey) { logs = allHits } -console.log(logs[0]) +// Extract unique filter values +const products = logs ? getUniqueTopics(logs) : [] +const networks = logs ? getUniqueNetworks(logs) : [] +const types = logs ? getUniqueTypes(logs) : [] --- @@ -84,7 +88,7 @@ console.log(logs[0]) ) } - + From fa8e3b0d028a0b1e19ee788c77c9a67da7ea62ee Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Thu, 27 Nov 2025 14:12:38 -0500 Subject: [PATCH 09/21] add not found --- public/images/not-found.svg | 1 + .../ChangelogFilters/ChangelogFilters.tsx | 86 +++++++++++++++++-- .../ChangelogFilters/styles.module.css | 19 ++-- src/pages/changelog.astro | 33 +++++++ 4 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 public/images/not-found.svg diff --git a/public/images/not-found.svg b/public/images/not-found.svg new file mode 100644 index 00000000000..cb212359571 --- /dev/null +++ b/public/images/not-found.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ChangelogFilters/ChangelogFilters.tsx b/src/components/ChangelogFilters/ChangelogFilters.tsx index bad0d85dd2b..748ed3c2646 100644 --- a/src/components/ChangelogFilters/ChangelogFilters.tsx +++ b/src/components/ChangelogFilters/ChangelogFilters.tsx @@ -46,20 +46,43 @@ interface TriggerProps { count: number isActive: boolean onClick: () => void + onClose: () => void + onClearAll: () => void } -const Trigger = ({ label, count, isActive, onClick }: TriggerProps) => { +const Trigger = ({ label, count, isActive, onClick, onClose, onClearAll }: TriggerProps) => { return ( ) } @@ -72,7 +95,13 @@ interface FilterPillProps { const FilterPill = ({ label, isSelected, onClick }: FilterPillProps) => { return ( - + ) +} + +interface FilterSectionProps { + title: string + count: number + isExpanded: boolean + options: string[] + selectedValues: string[] + onToggle: () => void + onSelect: (value: string) => void + onClearAll: () => void +} + +const FilterSection = ({ + title, + count, + isExpanded, + options, + selectedValues, + onToggle, + onSelect, + onClearAll, +}: FilterSectionProps) => { + return ( +
+ + {isExpanded && ( +
+ {options.map((option) => ( + onSelect(option)} + /> + ))} +
+ )} +
+ ) +} + +interface MobileFiltersModalProps { + isOpen: boolean + onClose: () => void + products: string[] + networks: string[] + types: string[] + selectedProducts: string[] + selectedNetworks: string[] + selectedTypes: string[] + onSelectProduct: (value: string) => void + onSelectNetwork: (value: string) => void + onSelectType: (value: string) => void + onClearAll: () => void + expandedSection: FilterType + onToggleSection: (section: FilterType) => void + onClearProducts: () => void + onClearNetworks: () => void + onClearTypes: () => void +} + +const MobileFiltersModal = ({ + isOpen, + onClose, + products, + networks, + types, + selectedProducts, + selectedNetworks, + selectedTypes, + onSelectProduct, + onSelectNetwork, + onSelectType, + onClearAll, + expandedSection, + onToggleSection, + onClearProducts, + onClearNetworks, + onClearTypes, +}: MobileFiltersModalProps) => { + if (!isOpen) return null + + return ( + <> +
+
+
+

Filters

+ +
+
+ onToggleSection(expandedSection === "product" ? null : "product")} + onSelect={onSelectProduct} + onClearAll={onClearProducts} + /> + onToggleSection(expandedSection === "network" ? null : "network")} + onSelect={onSelectNetwork} + onClearAll={onClearNetworks} + /> + onToggleSection(expandedSection === "type" ? null : "type")} + onSelect={onSelectType} + onClearAll={onClearTypes} + /> +
+
+ + +
+
+ + ) +} + export interface ChangelogFiltersProps { products: string[] networks: string[] @@ -126,6 +306,8 @@ export const ChangelogFilters = ({ products, networks, types, items }: Changelog const [selectedProducts, setSelectedProducts] = useState([]) const [selectedNetworks, setSelectedNetworks] = useState([]) const [selectedTypes, setSelectedTypes] = useState([]) + const [isMobile, setIsMobile] = useState(false) + const [isMobileFiltersOpen, setIsMobileFiltersOpen] = useState(false) // Read URL parameters on mount useEffect(() => { @@ -152,6 +334,35 @@ export const ChangelogFilters = ({ products, networks, types, items }: Changelog } }, []) + // Detect mobile viewport + useEffect(() => { + if (typeof window === "undefined") return + + const checkMobile = () => { + setIsMobile(window.innerWidth <= 576) + } + + checkMobile() + window.addEventListener("resize", checkMobile) + + return () => window.removeEventListener("resize", checkMobile) + }, []) + + // Disable body scroll when mobile modal is open + useEffect(() => { + if (typeof window === "undefined") return + + if (isMobileFiltersOpen) { + document.body.style.overflow = "hidden" + } else { + document.body.style.overflow = "" + } + + return () => { + document.body.style.overflow = "" + } + }, [isMobileFiltersOpen]) + // Update URL whenever filters change const updateURL = useCallback((products: string[], networks: string[], types: string[], search: string) => { if (typeof window === "undefined") return @@ -311,6 +522,12 @@ export const ChangelogFilters = ({ products, networks, types, items }: Changelog setSelectedTypes([]) } + const clearAllFilters = () => { + setSelectedProducts([]) + setSelectedNetworks([]) + setSelectedTypes([]) + } + const toggleSelection = (type: "product" | "network" | "type", value: string) => { switch (type) { case "product": @@ -351,56 +568,85 @@ export const ChangelogFilters = ({ products, networks, types, items }: Changelog } } + const totalFilterCount = selectedProducts.length + selectedNetworks.length + selectedTypes.length + return ( -
- {activeFilter && ( -
- {getFilterOptions().map((option) => ( - toggleSelection(activeFilter, option)} - /> - ))} -
- )} -
- {!searchExpanded && ( - <> - toggleFilter("product")} - onClose={closeFilter} - onClearAll={clearProductFilters} - /> - toggleFilter("network")} - onClose={closeFilter} - onClearAll={clearNetworkFilters} - /> - toggleFilter("type")} - onClose={closeFilter} - onClearAll={clearTypeFilters} - /> - + <> +
+ {!isMobile && activeFilter && ( +
+ {getFilterOptions().map((option) => ( + toggleSelection(activeFilter, option)} + /> + ))} +
)} - +
+ {!isMobile && !searchExpanded && ( + <> + toggleFilter("product")} + onClose={closeFilter} + onClearAll={clearProductFilters} + /> + toggleFilter("network")} + onClose={closeFilter} + onClearAll={clearNetworkFilters} + /> + toggleFilter("type")} + onClose={closeFilter} + onClearAll={clearTypeFilters} + /> + + )} + {isMobile && ( + setIsMobileFiltersOpen(true)} /> + )} + +
-
+ + {isMobile && ( + setIsMobileFiltersOpen(false)} + products={products} + networks={networks} + types={types} + selectedProducts={selectedProducts} + selectedNetworks={selectedNetworks} + selectedTypes={selectedTypes} + onSelectProduct={(value) => toggleSelection("product", value)} + onSelectNetwork={(value) => toggleSelection("network", value)} + onSelectType={(value) => toggleSelection("type", value)} + onClearAll={clearAllFilters} + expandedSection={activeFilter} + onToggleSection={setActiveFilter} + onClearProducts={clearProductFilters} + onClearNetworks={clearNetworkFilters} + onClearTypes={clearTypeFilters} + /> + )} + ) } diff --git a/src/components/ChangelogFilters/styles.module.css b/src/components/ChangelogFilters/styles.module.css index ef43e8d5c5f..41dfd111d44 100644 --- a/src/components/ChangelogFilters/styles.module.css +++ b/src/components/ChangelogFilters/styles.module.css @@ -150,3 +150,233 @@ .btnActive { background-color: #1e2635; } + +/* Mobile Styles */ +.mobileFiltersBtn { + background-color: #252e42; + border-radius: var(--space-8x); + padding: var(--space-2x) var(--space-3x); + display: flex; + align-items: center; + justify-content: center; + position: relative; + color: var(--gray-300); + transition: all 0.2s ease; + cursor: pointer; +} + +.mobileFiltersBtn:hover { + background-color: #1e2635; +} + +.mobileBadge { + position: absolute; + top: -4px; + right: -4px; + background-color: var(--blue-500); + color: var(--white); + border-radius: 50%; + width: 18px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: 600; +} + +/* Mobile Modal */ +.mobileModalBackdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 100; + animation: fadeIn 0.2s ease; +} + +.mobileModal { + position: fixed; + bottom: 0; + left: 0; + right: 0; + max-height: 80vh; + background: #252e42; + border-radius: var(--space-6x) var(--space-6x) 0 0; + z-index: 101; + display: flex; + flex-direction: column; + animation: slideUp 0.3s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +.mobileModalHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-6x) var(--space-6x) var(--space-4x); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.mobileModalTitle { + font-size: 20px; + font-weight: 600; + color: var(--white); + margin: 0; +} + +.mobileModalClose { + background: transparent; + padding: var(--space-2x); + color: var(--gray-300); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.mobileModalBody { + flex: 1; + overflow-y: auto; + padding: var(--space-4x) var(--space-6x); +} + +.mobileModalFooter { + display: flex; + gap: var(--space-3x); + padding: var(--space-4x) var(--space-6x); + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.mobileModalClearAll { + flex: 1; + padding: var(--space-3x) var(--space-4x); + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: var(--space-2x); + color: var(--gray-300); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.mobileModalClearAll:hover { + background-color: rgba(255, 255, 255, 0.05); +} + +.mobileModalApply { + flex: 1; + padding: var(--space-3x) var(--space-4x); + background: var(--blue-500); + border-radius: var(--space-2x); + color: var(--white); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.mobileModalApply:hover { + background: var(--blue-600); +} + +/* Filter Section */ +.filterSection { + margin-bottom: var(--space-4x); +} + +.filterSectionHeader { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-4x); + background: rgba(255, 255, 255, 0.05); + border-radius: var(--space-2x); + cursor: pointer; + transition: all 0.2s ease; +} + +.filterSectionHeader:hover { + background: rgba(255, 255, 255, 0.08); +} + +.filterSectionTitle { + display: flex; + align-items: center; + gap: var(--space-2x); + font-size: 16px; + font-weight: 500; + color: var(--white); +} + +.filterSectionCount { + display: flex; + align-items: center; + gap: var(--space-2x); + background-color: var(--gray-500); + border-radius: var(--space-8x); + padding: var(--space-1x) var(--space-3x); + font-size: 14px; + color: var(--white); +} + +.filterSectionChevron { + transition: transform 0.2s ease; + color: var(--gray-300); +} + +.filterSectionChevronOpen { + transform: rotate(180deg); +} + +.filterSectionContent { + padding: var(--space-4x); + display: flex; + flex-direction: column; + gap: var(--space-1x); + max-height: 300px; + overflow-y: auto; +} + +/* Mobile Responsive */ +@media screen and (max-width: 576px) { + .wrapper { + min-width: auto; + max-width: calc(100% - var(--space-4x)); + padding: var(--space-2x); + } + + .content { + grid-template-columns: auto 1fr; + gap: var(--space-2x); + } + + .searchInputWrapper { + padding: var(--space-2x); + } + + .searchInput { + font-size: 14px; + } +} From 9fea20640c2edafcb7afb809378ef92abe4196c7 Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Thu, 27 Nov 2025 14:44:17 -0500 Subject: [PATCH 11/21] refactor components --- .../ChangelogFilters/ChangelogFilters.tsx | 481 ++---------------- .../ChangelogFilters/DesktopFilters.tsx | 232 +++++++++ .../ChangelogFilters/MobileFilters.tsx | 335 ++++++++++++ .../ChangelogFilters/styles.module.css | 17 + 4 files changed, 628 insertions(+), 437 deletions(-) create mode 100644 src/components/ChangelogFilters/DesktopFilters.tsx create mode 100644 src/components/ChangelogFilters/MobileFilters.tsx diff --git a/src/components/ChangelogFilters/ChangelogFilters.tsx b/src/components/ChangelogFilters/ChangelogFilters.tsx index 5ad7d1eb458..e3d4b81eb6b 100644 --- a/src/components/ChangelogFilters/ChangelogFilters.tsx +++ b/src/components/ChangelogFilters/ChangelogFilters.tsx @@ -1,296 +1,9 @@ -import { SvgSearch, SvgTaillessArrowDownSmall, SvgX } from "@chainlink/blocks" import styles from "./styles.module.css" import { useState, useEffect, useCallback } from "react" -import { clsx } from "~/lib/clsx/clsx.ts" import type { ChangelogItem } from "~/components/ChangelogSnippet/types" import { matchesFilters } from "~/utils/changelogFilters" - -type FilterType = "product" | "network" | "type" | null - -interface SearchInputProps { - isExpanded: boolean - onClick: (value: boolean) => void - value: string - onChange: (value: string) => void -} - -const SearchInput = ({ isExpanded, onClick, value, onChange }: SearchInputProps) => { - return ( -
onClick(true)}> - - onChange(e.target.value)} - /> - {isExpanded && ( - { - e.stopPropagation() - onClick(false) - onChange("") - }} - style={{ - marginRight: "var(--space-4x)", - }} - /> - )} -
- ) -} - -interface TriggerProps { - label: string - count: number - isActive: boolean - onClick: () => void - onClose: () => void - onClearAll: () => void -} - -const Trigger = ({ label, count, isActive, onClick, onClose, onClearAll }: TriggerProps) => { - return ( - - ) -} - -interface FilterPillProps { - label: string - isSelected: boolean - onClick: () => void -} - -const FilterPill = ({ label, isSelected, onClick }: FilterPillProps) => { - return ( - - ) -} - -interface MobileFiltersButtonProps { - totalCount: number - onClick: () => void -} - -const MobileFiltersButton = ({ totalCount, onClick }: MobileFiltersButtonProps) => { - return ( - - ) -} - -interface FilterSectionProps { - title: string - count: number - isExpanded: boolean - options: string[] - selectedValues: string[] - onToggle: () => void - onSelect: (value: string) => void - onClearAll: () => void -} - -const FilterSection = ({ - title, - count, - isExpanded, - options, - selectedValues, - onToggle, - onSelect, - onClearAll, -}: FilterSectionProps) => { - return ( -
- - {isExpanded && ( -
- {options.map((option) => ( - onSelect(option)} - /> - ))} -
- )} -
- ) -} - -interface MobileFiltersModalProps { - isOpen: boolean - onClose: () => void - products: string[] - networks: string[] - types: string[] - selectedProducts: string[] - selectedNetworks: string[] - selectedTypes: string[] - onSelectProduct: (value: string) => void - onSelectNetwork: (value: string) => void - onSelectType: (value: string) => void - onClearAll: () => void - expandedSection: FilterType - onToggleSection: (section: FilterType) => void - onClearProducts: () => void - onClearNetworks: () => void - onClearTypes: () => void -} - -const MobileFiltersModal = ({ - isOpen, - onClose, - products, - networks, - types, - selectedProducts, - selectedNetworks, - selectedTypes, - onSelectProduct, - onSelectNetwork, - onSelectType, - onClearAll, - expandedSection, - onToggleSection, - onClearProducts, - onClearNetworks, - onClearTypes, -}: MobileFiltersModalProps) => { - if (!isOpen) return null - - return ( - <> -
-
-
-

Filters

- -
-
- onToggleSection(expandedSection === "product" ? null : "product")} - onSelect={onSelectProduct} - onClearAll={onClearProducts} - /> - onToggleSection(expandedSection === "network" ? null : "network")} - onSelect={onSelectNetwork} - onClearAll={onClearNetworks} - /> - onToggleSection(expandedSection === "type" ? null : "type")} - onSelect={onSelectType} - onClearAll={onClearTypes} - /> -
-
- - -
-
- - ) -} +import { DesktopFilters } from "./DesktopFilters" +import { MobileFilters } from "./MobileFilters" export interface ChangelogFiltersProps { products: string[] @@ -302,12 +15,9 @@ export interface ChangelogFiltersProps { export const ChangelogFilters = ({ products, networks, types, items }: ChangelogFiltersProps) => { const [searchExpanded, setSearchExpanded] = useState(false) const [searchTerm, setSearchTerm] = useState("") - const [activeFilter, setActiveFilter] = useState(null) const [selectedProducts, setSelectedProducts] = useState([]) const [selectedNetworks, setSelectedNetworks] = useState([]) const [selectedTypes, setSelectedTypes] = useState([]) - const [isMobile, setIsMobile] = useState(false) - const [isMobileFiltersOpen, setIsMobileFiltersOpen] = useState(false) // Read URL parameters on mount useEffect(() => { @@ -334,34 +44,6 @@ export const ChangelogFilters = ({ products, networks, types, items }: Changelog } }, []) - // Detect mobile viewport - useEffect(() => { - if (typeof window === "undefined") return - - const checkMobile = () => { - setIsMobile(window.innerWidth <= 576) - } - - checkMobile() - window.addEventListener("resize", checkMobile) - - return () => window.removeEventListener("resize", checkMobile) - }, []) - - // Disable body scroll when mobile modal is open - useEffect(() => { - if (typeof window === "undefined") return - - if (isMobileFiltersOpen) { - document.body.style.overflow = "hidden" - } else { - document.body.style.overflow = "" - } - - return () => { - document.body.style.overflow = "" - } - }, [isMobileFiltersOpen]) // Update URL whenever filters change const updateURL = useCallback((products: string[], networks: string[], types: string[], search: string) => { @@ -493,21 +175,26 @@ export const ChangelogFilters = ({ products, networks, types, items }: Changelog } }, [searchTerm, selectedProducts, selectedNetworks, selectedTypes, items]) - const searchClickHandler = (value: boolean) => { - setSearchExpanded(value) - } - const handleSearchChange = (value: string) => { setSearchTerm(value) } - const toggleFilter = (filterType: FilterType) => { - // Only open, don't close if already active - setActiveFilter(filterType) + const handleSearchToggle = (expanded: boolean) => { + setSearchExpanded(expanded) } - const closeFilter = () => { - setActiveFilter(null) + const toggleSelection = (type: "product" | "network" | "type", value: string) => { + switch (type) { + case "product": + setSelectedProducts((prev) => (prev.includes(value) ? prev.filter((p) => p !== value) : [...prev, value])) + break + case "network": + setSelectedNetworks((prev) => (prev.includes(value) ? prev.filter((n) => n !== value) : [...prev, value])) + break + case "type": + setSelectedTypes((prev) => (prev.includes(value) ? prev.filter((t) => t !== value) : [...prev, value])) + break + } } const clearProductFilters = () => { @@ -528,125 +215,45 @@ export const ChangelogFilters = ({ products, networks, types, items }: Changelog setSelectedTypes([]) } - const toggleSelection = (type: "product" | "network" | "type", value: string) => { - switch (type) { - case "product": - setSelectedProducts((prev) => (prev.includes(value) ? prev.filter((p) => p !== value) : [...prev, value])) - break - case "network": - setSelectedNetworks((prev) => (prev.includes(value) ? prev.filter((n) => n !== value) : [...prev, value])) - break - case "type": - setSelectedTypes((prev) => (prev.includes(value) ? prev.filter((t) => t !== value) : [...prev, value])) - break - } - } - - const getFilterOptions = () => { - switch (activeFilter) { - case "product": - return products - case "network": - return networks - case "type": - return types - default: - return [] - } - } - - const getSelectedValues = () => { - switch (activeFilter) { - case "product": - return selectedProducts - case "network": - return selectedNetworks - case "type": - return selectedTypes - default: - return [] - } - } - - const totalFilterCount = selectedProducts.length + selectedNetworks.length + selectedTypes.length - return ( - <> -
- {!isMobile && activeFilter && ( -
- {getFilterOptions().map((option) => ( - toggleSelection(activeFilter, option)} - /> - ))} -
- )} -
- {!isMobile && !searchExpanded && ( - <> - toggleFilter("product")} - onClose={closeFilter} - onClearAll={clearProductFilters} - /> - toggleFilter("network")} - onClose={closeFilter} - onClearAll={clearNetworkFilters} - /> - toggleFilter("type")} - onClose={closeFilter} - onClearAll={clearTypeFilters} - /> - - )} - {isMobile && ( - setIsMobileFiltersOpen(true)} /> - )} - -
+
+
+
- - {isMobile && ( - setIsMobileFiltersOpen(false)} +
+ toggleSelection("product", value)} - onSelectNetwork={(value) => toggleSelection("network", value)} - onSelectType={(value) => toggleSelection("type", value)} - onClearAll={clearAllFilters} - expandedSection={activeFilter} - onToggleSection={setActiveFilter} + onToggleSelection={toggleSelection} onClearProducts={clearProductFilters} onClearNetworks={clearNetworkFilters} onClearTypes={clearTypeFilters} + onClearAll={clearAllFilters} + searchTerm={searchTerm} + searchExpanded={searchExpanded} + onSearchChange={handleSearchChange} + onSearchToggle={handleSearchToggle} /> - )} - +
+
) } diff --git a/src/components/ChangelogFilters/DesktopFilters.tsx b/src/components/ChangelogFilters/DesktopFilters.tsx new file mode 100644 index 00000000000..4ae6d043a42 --- /dev/null +++ b/src/components/ChangelogFilters/DesktopFilters.tsx @@ -0,0 +1,232 @@ +import { SvgSearch, SvgTaillessArrowDownSmall, SvgX } from "@chainlink/blocks" +import styles from "./styles.module.css" +import { useState } from "react" +import { clsx } from "~/lib/clsx/clsx.ts" + +type FilterType = "product" | "network" | "type" | null + +interface SearchInputProps { + isExpanded: boolean + onClick: (value: boolean) => void + value: string + onChange: (value: string) => void +} + +const SearchInput = ({ isExpanded, onClick, value, onChange }: SearchInputProps) => { + return ( +
onClick(true)}> + + onChange(e.target.value)} + /> + {isExpanded && ( + { + e.stopPropagation() + onClick(false) + onChange("") + }} + style={{ + marginRight: "var(--space-4x)", + }} + /> + )} +
+ ) +} + +interface TriggerProps { + label: string + count: number + isActive: boolean + onClick: () => void + onClose: () => void + onClearAll: () => void +} + +const Trigger = ({ label, count, isActive, onClick, onClose, onClearAll }: TriggerProps) => { + return ( + + ) +} + +interface FilterPillProps { + label: string + isSelected: boolean + onClick: () => void +} + +const FilterPill = ({ label, isSelected, onClick }: FilterPillProps) => { + return ( + + ) +} + +interface DesktopFiltersProps { + products: string[] + networks: string[] + types: string[] + selectedProducts: string[] + selectedNetworks: string[] + selectedTypes: string[] + onToggleSelection: (type: "product" | "network" | "type", value: string) => void + onClearProducts: () => void + onClearNetworks: () => void + onClearTypes: () => void + searchTerm: string + searchExpanded: boolean + onSearchChange: (value: string) => void + onSearchToggle: (expanded: boolean) => void +} + +export const DesktopFilters = ({ + products, + networks, + types, + selectedProducts, + selectedNetworks, + selectedTypes, + onToggleSelection, + onClearProducts, + onClearNetworks, + onClearTypes, + searchTerm, + searchExpanded, + onSearchChange, + onSearchToggle, +}: DesktopFiltersProps) => { + const [activeFilter, setActiveFilter] = useState(null) + + const toggleFilter = (filterType: FilterType) => { + setActiveFilter(filterType) + } + + const closeFilter = () => { + setActiveFilter(null) + } + + const getFilterOptions = () => { + switch (activeFilter) { + case "product": + return products + case "network": + return networks + case "type": + return types + default: + return [] + } + } + + const getSelectedValues = () => { + switch (activeFilter) { + case "product": + return selectedProducts + case "network": + return selectedNetworks + case "type": + return selectedTypes + default: + return [] + } + } + + return ( + <> + {activeFilter && ( +
+ {getFilterOptions().map((option) => ( + { + const type = activeFilter as "product" | "network" | "type" + onToggleSelection(type, option) + }} + /> + ))} +
+ )} +
+ {!searchExpanded && ( + <> + toggleFilter("product")} + onClose={closeFilter} + onClearAll={onClearProducts} + /> + toggleFilter("network")} + onClose={closeFilter} + onClearAll={onClearNetworks} + /> + toggleFilter("type")} + onClose={closeFilter} + onClearAll={onClearTypes} + /> + + )} + +
+ + ) +} diff --git a/src/components/ChangelogFilters/MobileFilters.tsx b/src/components/ChangelogFilters/MobileFilters.tsx new file mode 100644 index 00000000000..6480cb8ca1e --- /dev/null +++ b/src/components/ChangelogFilters/MobileFilters.tsx @@ -0,0 +1,335 @@ +import { SvgSearch, SvgTaillessArrowDownSmall, SvgX } from "@chainlink/blocks" +import styles from "./styles.module.css" +import { useState, useEffect } from "react" +import { createPortal } from "react-dom" +import { clsx } from "~/lib/clsx/clsx.ts" + +type FilterType = "product" | "network" | "type" | null + +interface SearchInputProps { + isExpanded: boolean + onClick: (value: boolean) => void + value: string + onChange: (value: string) => void +} + +const SearchInput = ({ isExpanded, onClick, value, onChange }: SearchInputProps) => { + return ( +
onClick(true)}> + + onChange(e.target.value)} + /> + {isExpanded && ( + { + e.stopPropagation() + onClick(false) + onChange("") + }} + style={{ + marginRight: "var(--space-4x)", + }} + /> + )} +
+ ) +} + +interface FilterPillProps { + label: string + isSelected: boolean + onClick: () => void +} + +const FilterPill = ({ label, isSelected, onClick }: FilterPillProps) => { + return ( + + ) +} + +interface MobileFiltersButtonProps { + totalCount: number + onClick: () => void +} + +const MobileFiltersButton = ({ totalCount, onClick }: MobileFiltersButtonProps) => { + return ( + + ) +} + +interface FilterSectionProps { + title: string + count: number + isExpanded: boolean + options: string[] + selectedValues: string[] + onToggle: () => void + onSelect: (value: string) => void + onClearAll: () => void +} + +const FilterSection = ({ + title, + count, + isExpanded, + options, + selectedValues, + onToggle, + onSelect, + onClearAll, +}: FilterSectionProps) => { + return ( +
+ + {isExpanded && ( +
+ {options.map((option) => ( + onSelect(option)} + /> + ))} +
+ )} +
+ ) +} + +interface MobileFiltersModalProps { + isOpen: boolean + onClose: () => void + products: string[] + networks: string[] + types: string[] + selectedProducts: string[] + selectedNetworks: string[] + selectedTypes: string[] + onSelectProduct: (value: string) => void + onSelectNetwork: (value: string) => void + onSelectType: (value: string) => void + onClearAll: () => void + expandedSection: FilterType + onToggleSection: (section: FilterType) => void + onClearProducts: () => void + onClearNetworks: () => void + onClearTypes: () => void +} + +const MobileFiltersModal = ({ + isOpen, + onClose, + products, + networks, + types, + selectedProducts, + selectedNetworks, + selectedTypes, + onSelectProduct, + onSelectNetwork, + onSelectType, + onClearAll, + expandedSection, + onToggleSection, + onClearProducts, + onClearNetworks, + onClearTypes, +}: MobileFiltersModalProps) => { + if (!isOpen) return null + + return ( + <> +
+
+
+

Filters

+ +
+
+ onToggleSection(expandedSection === "product" ? null : "product")} + onSelect={onSelectProduct} + onClearAll={onClearProducts} + /> + onToggleSection(expandedSection === "network" ? null : "network")} + onSelect={onSelectNetwork} + onClearAll={onClearNetworks} + /> + onToggleSection(expandedSection === "type" ? null : "type")} + onSelect={onSelectType} + onClearAll={onClearTypes} + /> +
+
+ + +
+
+ + ) +} + +interface MobileFiltersProps { + products: string[] + networks: string[] + types: string[] + selectedProducts: string[] + selectedNetworks: string[] + selectedTypes: string[] + onToggleSelection: (type: "product" | "network" | "type", value: string) => void + onClearProducts: () => void + onClearNetworks: () => void + onClearTypes: () => void + onClearAll: () => void + searchTerm: string + searchExpanded: boolean + onSearchChange: (value: string) => void + onSearchToggle: (expanded: boolean) => void +} + +export const MobileFilters = ({ + products, + networks, + types, + selectedProducts, + selectedNetworks, + selectedTypes, + onToggleSelection, + onClearProducts, + onClearNetworks, + onClearTypes, + onClearAll, + searchTerm, + searchExpanded, + onSearchChange, + onSearchToggle, +}: MobileFiltersProps) => { + const [isMobileFiltersOpen, setIsMobileFiltersOpen] = useState(false) + const [expandedSection, setExpandedSection] = useState(null) + + const totalFilterCount = selectedProducts.length + selectedNetworks.length + selectedTypes.length + + // Disable body scroll when mobile modal is open + useEffect(() => { + if (typeof window === "undefined") return + + if (isMobileFiltersOpen) { + document.body.style.overflow = "hidden" + } else { + document.body.style.overflow = "" + } + + return () => { + document.body.style.overflow = "" + } + }, [isMobileFiltersOpen]) + + const modalContent = ( + setIsMobileFiltersOpen(false)} + products={products} + networks={networks} + types={types} + selectedProducts={selectedProducts} + selectedNetworks={selectedNetworks} + selectedTypes={selectedTypes} + onSelectProduct={(value) => onToggleSelection("product", value)} + onSelectNetwork={(value) => onToggleSelection("network", value)} + onSelectType={(value) => onToggleSelection("type", value)} + onClearAll={onClearAll} + expandedSection={expandedSection} + onToggleSection={setExpandedSection} + onClearProducts={onClearProducts} + onClearNetworks={onClearNetworks} + onClearTypes={onClearTypes} + /> + ) + + return ( + <> +
+ setIsMobileFiltersOpen(true)} /> + +
+ + {typeof document !== "undefined" && createPortal(modalContent, document.body)} + + ) +} diff --git a/src/components/ChangelogFilters/styles.module.css b/src/components/ChangelogFilters/styles.module.css index 41dfd111d44..bea9da490d1 100644 --- a/src/components/ChangelogFilters/styles.module.css +++ b/src/components/ChangelogFilters/styles.module.css @@ -359,8 +359,25 @@ overflow-y: auto; } +/* Desktop/Mobile Filter Visibility */ +.desktopFilters { + display: block; +} + +.mobileFilters { + display: none; +} + /* Mobile Responsive */ @media screen and (max-width: 576px) { + .desktopFilters { + display: none; + } + + .mobileFilters { + display: block; + } + .wrapper { min-width: auto; max-width: calc(100% - var(--space-4x)); From 03478db21de3c7fd5e46dfd8179bfe111a145fbe Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Thu, 27 Nov 2025 14:49:31 -0500 Subject: [PATCH 12/21] utils file --- .../ChangelogFilters/ChangelogFilters.tsx | 63 +++----------- src/utils/changelogFilterUtils.ts | 85 +++++++++++++++++++ 2 files changed, 98 insertions(+), 50 deletions(-) create mode 100644 src/utils/changelogFilterUtils.ts diff --git a/src/components/ChangelogFilters/ChangelogFilters.tsx b/src/components/ChangelogFilters/ChangelogFilters.tsx index e3d4b81eb6b..46bf96dc69e 100644 --- a/src/components/ChangelogFilters/ChangelogFilters.tsx +++ b/src/components/ChangelogFilters/ChangelogFilters.tsx @@ -1,7 +1,8 @@ import styles from "./styles.module.css" -import { useState, useEffect, useCallback } from "react" +import { useState, useEffect } from "react" import type { ChangelogItem } from "~/components/ChangelogSnippet/types" import { matchesFilters } from "~/utils/changelogFilters" +import { parseURLParams, updateFilterURL, toggleItemInArray } from "~/utils/changelogFilterUtils" import { DesktopFilters } from "./DesktopFilters" import { MobileFilters } from "./MobileFilters" @@ -21,58 +22,20 @@ export const ChangelogFilters = ({ products, networks, types, items }: Changelog // Read URL parameters on mount useEffect(() => { - if (typeof window === "undefined") return - - const params = new URLSearchParams(window.location.search) - const productParam = params.get("product") - const networkParam = params.get("network") - const typeParam = params.get("type") - const searchParam = params.get("*") + const urlParams = parseURLParams() - if (productParam) { - setSelectedProducts(productParam.split(",")) - } - if (networkParam) { - setSelectedNetworks(networkParam.split(",")) - } - if (typeParam) { - setSelectedTypes(typeParam.split(",")) - } - if (searchParam) { - setSearchTerm(searchParam) - setSearchExpanded(true) - } + setSelectedProducts(urlParams.products) + setSelectedNetworks(urlParams.networks) + setSelectedTypes(urlParams.types) + setSearchTerm(urlParams.searchTerm) + setSearchExpanded(urlParams.searchExpanded) }, []) - // Update URL whenever filters change - const updateURL = useCallback((products: string[], networks: string[], types: string[], search: string) => { - if (typeof window === "undefined") return - - const params = new URLSearchParams() - - if (search) { - params.set("*", search) - } else { - if (products.length > 0) { - params.set("product", products.join(",")) - } - if (networks.length > 0) { - params.set("network", networks.join(",")) - } - if (types.length > 0) { - params.set("type", types.join(",")) - } - } - - const newURL = params.toString() ? `?${params.toString()}` : window.location.pathname - window.history.replaceState({}, "", newURL) - }, []) - // Update URL when filters change useEffect(() => { - updateURL(selectedProducts, selectedNetworks, selectedTypes, searchTerm) - }, [selectedProducts, selectedNetworks, selectedTypes, searchTerm, updateURL]) + updateFilterURL(selectedProducts, selectedNetworks, selectedTypes, searchTerm) + }, [selectedProducts, selectedNetworks, selectedTypes, searchTerm]) // Filter items and update the display useEffect(() => { @@ -186,13 +149,13 @@ export const ChangelogFilters = ({ products, networks, types, items }: Changelog const toggleSelection = (type: "product" | "network" | "type", value: string) => { switch (type) { case "product": - setSelectedProducts((prev) => (prev.includes(value) ? prev.filter((p) => p !== value) : [...prev, value])) + setSelectedProducts((prev) => toggleItemInArray(prev, value)) break case "network": - setSelectedNetworks((prev) => (prev.includes(value) ? prev.filter((n) => n !== value) : [...prev, value])) + setSelectedNetworks((prev) => toggleItemInArray(prev, value)) break case "type": - setSelectedTypes((prev) => (prev.includes(value) ? prev.filter((t) => t !== value) : [...prev, value])) + setSelectedTypes((prev) => toggleItemInArray(prev, value)) break } } diff --git a/src/utils/changelogFilterUtils.ts b/src/utils/changelogFilterUtils.ts new file mode 100644 index 00000000000..718e96c11ff --- /dev/null +++ b/src/utils/changelogFilterUtils.ts @@ -0,0 +1,85 @@ +/** + * Utility functions for changelog filter management + */ + +/** + * Parse URL parameters to extract filter state + */ +export function parseURLParams(): { + products: string[] + networks: string[] + types: string[] + searchTerm: string + searchExpanded: boolean +} { + if (typeof window === "undefined") { + return { + products: [], + networks: [], + types: [], + searchTerm: "", + searchExpanded: false, + } + } + + const params = new URLSearchParams(window.location.search) + const productParam = params.get("product") + const networkParam = params.get("network") + const typeParam = params.get("type") + const searchParam = params.get("*") + + return { + products: productParam ? productParam.split(",") : [], + networks: networkParam ? networkParam.split(",") : [], + types: typeParam ? typeParam.split(",") : [], + searchTerm: searchParam || "", + searchExpanded: !!searchParam, + } +} + +/** + * Build URL search parameters from filter state + */ +export function buildFilterURL( + products: string[], + networks: string[], + types: string[], + searchTerm: string +): URLSearchParams { + const params = new URLSearchParams() + + if (searchTerm) { + params.set("*", searchTerm) + } else { + if (products.length > 0) { + params.set("product", products.join(",")) + } + if (networks.length > 0) { + params.set("network", networks.join(",")) + } + if (types.length > 0) { + params.set("type", types.join(",")) + } + } + + return params +} + +/** + * Update browser URL with filter parameters + */ +export function updateFilterURL(products: string[], networks: string[], types: string[], searchTerm: string): void { + if (typeof window === "undefined") return + + const params = buildFilterURL(products, networks, types, searchTerm) + const newURL = params.toString() ? `?${params.toString()}` : window.location.pathname + + window.history.replaceState({}, "", newURL) +} + +/** + * Toggle an item in an array (add if not present, remove if present) + */ +export function toggleItemInArray(array: T[], item: T): T[] { + return array.includes(item) ? array.filter((i) => i !== item) : [...array, item] +} From 1187728a64740cbdf361182c04e43504cc33d29e Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Thu, 27 Nov 2025 14:50:03 -0500 Subject: [PATCH 13/21] Update ChangelogFilters.tsx --- src/components/ChangelogFilters/ChangelogFilters.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/ChangelogFilters/ChangelogFilters.tsx b/src/components/ChangelogFilters/ChangelogFilters.tsx index 46bf96dc69e..1ff7540c356 100644 --- a/src/components/ChangelogFilters/ChangelogFilters.tsx +++ b/src/components/ChangelogFilters/ChangelogFilters.tsx @@ -1,10 +1,10 @@ import styles from "./styles.module.css" import { useState, useEffect } from "react" -import type { ChangelogItem } from "~/components/ChangelogSnippet/types" -import { matchesFilters } from "~/utils/changelogFilters" -import { parseURLParams, updateFilterURL, toggleItemInArray } from "~/utils/changelogFilterUtils" -import { DesktopFilters } from "./DesktopFilters" -import { MobileFilters } from "./MobileFilters" +import type { ChangelogItem } from "~/components/ChangelogSnippet/types.ts" +import { matchesFilters } from "~/utils/changelogFilters.ts" +import { parseURLParams, updateFilterURL, toggleItemInArray } from "~/utils/changelogFilterUtils.ts" +import { DesktopFilters } from "./DesktopFilters.tsx" +import { MobileFilters } from "./MobileFilters.tsx" export interface ChangelogFiltersProps { products: string[] @@ -31,7 +31,6 @@ export const ChangelogFilters = ({ products, networks, types, items }: Changelog setSearchExpanded(urlParams.searchExpanded) }, []) - // Update URL when filters change useEffect(() => { updateFilterURL(selectedProducts, selectedNetworks, selectedTypes, searchTerm) From aedb89b7dd86cf98cbfb8813dc268cae2680bc5b Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Thu, 27 Nov 2025 14:50:46 -0500 Subject: [PATCH 14/21] Update MobileFilters.tsx --- src/components/ChangelogFilters/MobileFilters.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/ChangelogFilters/MobileFilters.tsx b/src/components/ChangelogFilters/MobileFilters.tsx index 6480cb8ca1e..b815e8744d8 100644 --- a/src/components/ChangelogFilters/MobileFilters.tsx +++ b/src/components/ChangelogFilters/MobileFilters.tsx @@ -326,7 +326,12 @@ export const MobileFilters = ({ <>
setIsMobileFiltersOpen(true)} /> - +
{typeof document !== "undefined" && createPortal(modalContent, document.body)} From b7f259e1e723d28ce18ee72cd9842fd256bc6076 Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Thu, 27 Nov 2025 14:58:10 -0500 Subject: [PATCH 15/21] use hooks --- .../ChangelogFilters/ChangelogFilters.tsx | 201 +----------------- .../ChangelogFilters/DesktopFilters.tsx | 55 ++--- .../ChangelogFilters/MobileFilters.tsx | 66 +++--- .../ChangelogFilters/useChangelogFilters.ts | 191 +++++++++++++++++ 4 files changed, 244 insertions(+), 269 deletions(-) create mode 100644 src/components/ChangelogFilters/useChangelogFilters.ts diff --git a/src/components/ChangelogFilters/ChangelogFilters.tsx b/src/components/ChangelogFilters/ChangelogFilters.tsx index 1ff7540c356..d90bb56385f 100644 --- a/src/components/ChangelogFilters/ChangelogFilters.tsx +++ b/src/components/ChangelogFilters/ChangelogFilters.tsx @@ -1,8 +1,5 @@ import styles from "./styles.module.css" -import { useState, useEffect } from "react" import type { ChangelogItem } from "~/components/ChangelogSnippet/types.ts" -import { matchesFilters } from "~/utils/changelogFilters.ts" -import { parseURLParams, updateFilterURL, toggleItemInArray } from "~/utils/changelogFilterUtils.ts" import { DesktopFilters } from "./DesktopFilters.tsx" import { MobileFilters } from "./MobileFilters.tsx" @@ -14,207 +11,13 @@ export interface ChangelogFiltersProps { } export const ChangelogFilters = ({ products, networks, types, items }: ChangelogFiltersProps) => { - const [searchExpanded, setSearchExpanded] = useState(false) - const [searchTerm, setSearchTerm] = useState("") - const [selectedProducts, setSelectedProducts] = useState([]) - const [selectedNetworks, setSelectedNetworks] = useState([]) - const [selectedTypes, setSelectedTypes] = useState([]) - - // Read URL parameters on mount - useEffect(() => { - const urlParams = parseURLParams() - - setSelectedProducts(urlParams.products) - setSelectedNetworks(urlParams.networks) - setSelectedTypes(urlParams.types) - setSearchTerm(urlParams.searchTerm) - setSearchExpanded(urlParams.searchExpanded) - }, []) - - // Update URL when filters change - useEffect(() => { - updateFilterURL(selectedProducts, selectedNetworks, selectedTypes, searchTerm) - }, [selectedProducts, selectedNetworks, selectedTypes, searchTerm]) - - // Filter items and update the display - useEffect(() => { - if (typeof window === "undefined") return - - const changelogItems = document.querySelectorAll(".changelog-item") - const loadMoreSection = document.querySelector(".load-more-section") as HTMLElement - const visibleCountSpan = document.getElementById("visible-count") - const emptyState = document.querySelector(".empty-state") as HTMLElement - const changelogList = document.querySelector(".changelog-list") as HTMLElement - - if (searchTerm) { - // Search takes priority - filter by search term - const searchLower = searchTerm.toLowerCase() - let visibleCount = 0 - - changelogItems.forEach((item) => { - const index = parseInt(item.getAttribute("data-index") || "0") - const changelogItem = items[index] - - const matchesSearch = - changelogItem?.name.toLowerCase().includes(searchLower) || - changelogItem?.["text-description"]?.toLowerCase().includes(searchLower) - - if (matchesSearch) { - ;(item as HTMLElement).style.display = "" - visibleCount++ - } else { - ;(item as HTMLElement).style.display = "none" - } - }) - - // Hide load more section when searching - if (loadMoreSection) { - loadMoreSection.style.display = "none" - } - - // Show/hide empty state - if (emptyState && changelogList) { - if (visibleCount === 0) { - emptyState.style.display = "flex" - changelogList.style.display = "none" - } else { - emptyState.style.display = "none" - changelogList.style.display = "flex" - } - } - } else { - // Apply filter logic - let visibleCount = 0 - const hasFilters = selectedProducts.length > 0 || selectedNetworks.length > 0 || selectedTypes.length > 0 - - changelogItems.forEach((item) => { - const index = parseInt(item.getAttribute("data-index") || "0") - const changelogItem = items[index] - - if (hasFilters && changelogItem) { - const matches = matchesFilters(changelogItem, selectedProducts, selectedNetworks, selectedTypes) - if (matches) { - ;(item as HTMLElement).style.display = "" - visibleCount++ - } else { - ;(item as HTMLElement).style.display = "none" - } - } else { - // No filters - show first 25 items by default - if (visibleCount < 25) { - ;(item as HTMLElement).style.display = "" - visibleCount++ - } else { - ;(item as HTMLElement).style.display = "none" - } - } - }) - - // Show/hide load more section based on filters - if (loadMoreSection) { - if (hasFilters) { - loadMoreSection.style.display = "none" - } else { - loadMoreSection.style.display = visibleCount >= items.length ? "none" : "flex" - } - } - - // Update visible count - if (visibleCountSpan) { - visibleCountSpan.textContent = visibleCount.toString() - } - - // Show/hide empty state - if (emptyState && changelogList) { - if (hasFilters && visibleCount === 0) { - emptyState.style.display = "flex" - changelogList.style.display = "none" - } else { - emptyState.style.display = "none" - changelogList.style.display = "flex" - } - } - } - }, [searchTerm, selectedProducts, selectedNetworks, selectedTypes, items]) - - const handleSearchChange = (value: string) => { - setSearchTerm(value) - } - - const handleSearchToggle = (expanded: boolean) => { - setSearchExpanded(expanded) - } - - const toggleSelection = (type: "product" | "network" | "type", value: string) => { - switch (type) { - case "product": - setSelectedProducts((prev) => toggleItemInArray(prev, value)) - break - case "network": - setSelectedNetworks((prev) => toggleItemInArray(prev, value)) - break - case "type": - setSelectedTypes((prev) => toggleItemInArray(prev, value)) - break - } - } - - const clearProductFilters = () => { - setSelectedProducts([]) - } - - const clearNetworkFilters = () => { - setSelectedNetworks([]) - } - - const clearTypeFilters = () => { - setSelectedTypes([]) - } - - const clearAllFilters = () => { - setSelectedProducts([]) - setSelectedNetworks([]) - setSelectedTypes([]) - } - return (
- +
- +
) diff --git a/src/components/ChangelogFilters/DesktopFilters.tsx b/src/components/ChangelogFilters/DesktopFilters.tsx index 4ae6d043a42..e9a35346004 100644 --- a/src/components/ChangelogFilters/DesktopFilters.tsx +++ b/src/components/ChangelogFilters/DesktopFilters.tsx @@ -2,6 +2,8 @@ import { SvgSearch, SvgTaillessArrowDownSmall, SvgX } from "@chainlink/blocks" import styles from "./styles.module.css" import { useState } from "react" import { clsx } from "~/lib/clsx/clsx.ts" +import type { ChangelogItem } from "~/components/ChangelogSnippet/types.ts" +import { useChangelogFilters } from "./useChangelogFilters.ts" type FilterType = "product" | "network" | "type" | null @@ -114,37 +116,26 @@ interface DesktopFiltersProps { products: string[] networks: string[] types: string[] - selectedProducts: string[] - selectedNetworks: string[] - selectedTypes: string[] - onToggleSelection: (type: "product" | "network" | "type", value: string) => void - onClearProducts: () => void - onClearNetworks: () => void - onClearTypes: () => void - searchTerm: string - searchExpanded: boolean - onSearchChange: (value: string) => void - onSearchToggle: (expanded: boolean) => void + items: ChangelogItem[] } -export const DesktopFilters = ({ - products, - networks, - types, - selectedProducts, - selectedNetworks, - selectedTypes, - onToggleSelection, - onClearProducts, - onClearNetworks, - onClearTypes, - searchTerm, - searchExpanded, - onSearchChange, - onSearchToggle, -}: DesktopFiltersProps) => { +export const DesktopFilters = ({ products, networks, types, items }: DesktopFiltersProps) => { const [activeFilter, setActiveFilter] = useState(null) + const { + searchExpanded, + searchTerm, + selectedProducts, + selectedNetworks, + selectedTypes, + handleSearchChange, + handleSearchToggle, + toggleSelection, + clearProductFilters, + clearNetworkFilters, + clearTypeFilters, + } = useChangelogFilters({ products, networks, types, items }) + const toggleFilter = (filterType: FilterType) => { setActiveFilter(filterType) } @@ -190,7 +181,7 @@ export const DesktopFilters = ({ isSelected={getSelectedValues().includes(option)} onClick={() => { const type = activeFilter as "product" | "network" | "type" - onToggleSelection(type, option) + toggleSelection(type, option) }} /> ))} @@ -205,7 +196,7 @@ export const DesktopFilters = ({ isActive={activeFilter === "product"} onClick={() => toggleFilter("product")} onClose={closeFilter} - onClearAll={onClearProducts} + onClearAll={clearProductFilters} /> toggleFilter("network")} onClose={closeFilter} - onClearAll={onClearNetworks} + onClearAll={clearNetworkFilters} /> toggleFilter("type")} onClose={closeFilter} - onClearAll={onClearTypes} + onClearAll={clearTypeFilters} /> )} - +
) diff --git a/src/components/ChangelogFilters/MobileFilters.tsx b/src/components/ChangelogFilters/MobileFilters.tsx index b815e8744d8..2e62568f758 100644 --- a/src/components/ChangelogFilters/MobileFilters.tsx +++ b/src/components/ChangelogFilters/MobileFilters.tsx @@ -3,6 +3,8 @@ import styles from "./styles.module.css" import { useState, useEffect } from "react" import { createPortal } from "react-dom" import { clsx } from "~/lib/clsx/clsx.ts" +import type { ChangelogItem } from "~/components/ChangelogSnippet/types.ts" +import { useChangelogFilters } from "./useChangelogFilters.ts" type FilterType = "product" | "network" | "type" | null @@ -249,40 +251,28 @@ interface MobileFiltersProps { products: string[] networks: string[] types: string[] - selectedProducts: string[] - selectedNetworks: string[] - selectedTypes: string[] - onToggleSelection: (type: "product" | "network" | "type", value: string) => void - onClearProducts: () => void - onClearNetworks: () => void - onClearTypes: () => void - onClearAll: () => void - searchTerm: string - searchExpanded: boolean - onSearchChange: (value: string) => void - onSearchToggle: (expanded: boolean) => void + items: ChangelogItem[] } -export const MobileFilters = ({ - products, - networks, - types, - selectedProducts, - selectedNetworks, - selectedTypes, - onToggleSelection, - onClearProducts, - onClearNetworks, - onClearTypes, - onClearAll, - searchTerm, - searchExpanded, - onSearchChange, - onSearchToggle, -}: MobileFiltersProps) => { +export const MobileFilters = ({ products, networks, types, items }: MobileFiltersProps) => { const [isMobileFiltersOpen, setIsMobileFiltersOpen] = useState(false) const [expandedSection, setExpandedSection] = useState(null) + const { + searchExpanded, + searchTerm, + selectedProducts, + selectedNetworks, + selectedTypes, + handleSearchChange, + handleSearchToggle, + toggleSelection, + clearProductFilters, + clearNetworkFilters, + clearTypeFilters, + clearAllFilters, + } = useChangelogFilters({ products, networks, types, items }) + const totalFilterCount = selectedProducts.length + selectedNetworks.length + selectedTypes.length // Disable body scroll when mobile modal is open @@ -310,15 +300,15 @@ export const MobileFilters = ({ selectedProducts={selectedProducts} selectedNetworks={selectedNetworks} selectedTypes={selectedTypes} - onSelectProduct={(value) => onToggleSelection("product", value)} - onSelectNetwork={(value) => onToggleSelection("network", value)} - onSelectType={(value) => onToggleSelection("type", value)} - onClearAll={onClearAll} + onSelectProduct={(value) => toggleSelection("product", value)} + onSelectNetwork={(value) => toggleSelection("network", value)} + onSelectType={(value) => toggleSelection("type", value)} + onClearAll={clearAllFilters} expandedSection={expandedSection} onToggleSection={setExpandedSection} - onClearProducts={onClearProducts} - onClearNetworks={onClearNetworks} - onClearTypes={onClearTypes} + onClearProducts={clearProductFilters} + onClearNetworks={clearNetworkFilters} + onClearTypes={clearTypeFilters} /> ) @@ -328,9 +318,9 @@ export const MobileFilters = ({ setIsMobileFiltersOpen(true)} />
diff --git a/src/components/ChangelogFilters/useChangelogFilters.ts b/src/components/ChangelogFilters/useChangelogFilters.ts new file mode 100644 index 00000000000..7a8b393a1c5 --- /dev/null +++ b/src/components/ChangelogFilters/useChangelogFilters.ts @@ -0,0 +1,191 @@ +import { useState, useEffect } from "react" +import type { ChangelogItem } from "~/components/ChangelogSnippet/types.ts" +import { matchesFilters } from "~/utils/changelogFilters.ts" +import { parseURLParams, updateFilterURL, toggleItemInArray } from "~/utils/changelogFilterUtils.ts" + +export interface UseChangelogFiltersProps { + products: string[] + networks: string[] + types: string[] + items: ChangelogItem[] +} + +export const useChangelogFilters = ({ products, networks, types, items }: UseChangelogFiltersProps) => { + const [searchExpanded, setSearchExpanded] = useState(false) + const [searchTerm, setSearchTerm] = useState("") + const [selectedProducts, setSelectedProducts] = useState([]) + const [selectedNetworks, setSelectedNetworks] = useState([]) + const [selectedTypes, setSelectedTypes] = useState([]) + + // Read URL parameters on mount + useEffect(() => { + const urlParams = parseURLParams() + + setSelectedProducts(urlParams.products) + setSelectedNetworks(urlParams.networks) + setSelectedTypes(urlParams.types) + setSearchTerm(urlParams.searchTerm) + setSearchExpanded(urlParams.searchExpanded) + }, []) + + // Update URL when filters change + useEffect(() => { + updateFilterURL(selectedProducts, selectedNetworks, selectedTypes, searchTerm) + }, [selectedProducts, selectedNetworks, selectedTypes, searchTerm]) + + // Filter items and update the display + useEffect(() => { + if (typeof window === "undefined") return + + const changelogItems = document.querySelectorAll(".changelog-item") + const loadMoreSection = document.querySelector(".load-more-section") as HTMLElement + const visibleCountSpan = document.getElementById("visible-count") + const emptyState = document.querySelector(".empty-state") as HTMLElement + const changelogList = document.querySelector(".changelog-list") as HTMLElement + + if (searchTerm) { + // Search takes priority - filter by search term + const searchLower = searchTerm.toLowerCase() + let visibleCount = 0 + + changelogItems.forEach((item) => { + const index = parseInt(item.getAttribute("data-index") || "0") + const changelogItem = items[index] + + const matchesSearch = + changelogItem?.name.toLowerCase().includes(searchLower) || + changelogItem?.["text-description"]?.toLowerCase().includes(searchLower) + + if (matchesSearch) { + ;(item as HTMLElement).style.display = "" + visibleCount++ + } else { + ;(item as HTMLElement).style.display = "none" + } + }) + + // Hide load more section when searching + if (loadMoreSection) { + loadMoreSection.style.display = "none" + } + + // Show/hide empty state + if (emptyState && changelogList) { + if (visibleCount === 0) { + emptyState.style.display = "flex" + changelogList.style.display = "none" + } else { + emptyState.style.display = "none" + changelogList.style.display = "flex" + } + } + } else { + // Apply filter logic + let visibleCount = 0 + const hasFilters = selectedProducts.length > 0 || selectedNetworks.length > 0 || selectedTypes.length > 0 + + changelogItems.forEach((item) => { + const index = parseInt(item.getAttribute("data-index") || "0") + const changelogItem = items[index] + + if (hasFilters && changelogItem) { + const matches = matchesFilters(changelogItem, selectedProducts, selectedNetworks, selectedTypes) + if (matches) { + ;(item as HTMLElement).style.display = "" + visibleCount++ + } else { + ;(item as HTMLElement).style.display = "none" + } + } else { + // No filters - show first 25 items by default + if (visibleCount < 25) { + ;(item as HTMLElement).style.display = "" + visibleCount++ + } else { + ;(item as HTMLElement).style.display = "none" + } + } + }) + + // Show/hide load more section based on filters + if (loadMoreSection) { + if (hasFilters) { + loadMoreSection.style.display = "none" + } else { + loadMoreSection.style.display = visibleCount >= items.length ? "none" : "flex" + } + } + + // Update visible count + if (visibleCountSpan) { + visibleCountSpan.textContent = visibleCount.toString() + } + + // Show/hide empty state + if (emptyState && changelogList) { + if (hasFilters && visibleCount === 0) { + emptyState.style.display = "flex" + changelogList.style.display = "none" + } else { + emptyState.style.display = "none" + changelogList.style.display = "flex" + } + } + } + }, [searchTerm, selectedProducts, selectedNetworks, selectedTypes, items]) + + const handleSearchChange = (value: string) => { + setSearchTerm(value) + } + + const handleSearchToggle = (expanded: boolean) => { + setSearchExpanded(expanded) + } + + const toggleSelection = (type: "product" | "network" | "type", value: string) => { + switch (type) { + case "product": + setSelectedProducts((prev) => toggleItemInArray(prev, value)) + break + case "network": + setSelectedNetworks((prev) => toggleItemInArray(prev, value)) + break + case "type": + setSelectedTypes((prev) => toggleItemInArray(prev, value)) + break + } + } + + const clearProductFilters = () => { + setSelectedProducts([]) + } + + const clearNetworkFilters = () => { + setSelectedNetworks([]) + } + + const clearTypeFilters = () => { + setSelectedTypes([]) + } + + const clearAllFilters = () => { + setSelectedProducts([]) + setSelectedNetworks([]) + setSelectedTypes([]) + } + + return { + searchExpanded, + searchTerm, + selectedProducts, + selectedNetworks, + selectedTypes, + handleSearchChange, + handleSearchToggle, + toggleSelection, + clearProductFilters, + clearNetworkFilters, + clearTypeFilters, + clearAllFilters, + } +} From 53c525b397ef2c172f4e2b4026166e289e73380d Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Thu, 27 Nov 2025 14:59:43 -0500 Subject: [PATCH 16/21] Update DesktopFilters.tsx --- src/components/ChangelogFilters/DesktopFilters.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/ChangelogFilters/DesktopFilters.tsx b/src/components/ChangelogFilters/DesktopFilters.tsx index e9a35346004..b2ba7bafb82 100644 --- a/src/components/ChangelogFilters/DesktopFilters.tsx +++ b/src/components/ChangelogFilters/DesktopFilters.tsx @@ -216,7 +216,12 @@ export const DesktopFilters = ({ products, networks, types, items }: DesktopFilt /> )} - +
) From 1e4815dfba613727842ec257f34a623e44bb2701 Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Thu, 27 Nov 2025 15:05:08 -0500 Subject: [PATCH 17/21] Update changelogFilters.ts --- src/utils/changelogFilters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/changelogFilters.ts b/src/utils/changelogFilters.ts index 1c74c8851f6..3005c5992b9 100644 --- a/src/utils/changelogFilters.ts +++ b/src/utils/changelogFilters.ts @@ -1,4 +1,4 @@ -import { ChangelogItem } from "~/components/ChangelogSnippet/types" +import { ChangelogItem } from "~/components/ChangelogSnippet/types.ts" /** * Extracts network names from the HTML networks field From 400767094f87a46d1829244cf3328204dc1aae68 Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Fri, 28 Nov 2025 10:19:33 -0500 Subject: [PATCH 18/21] fix race condition with updating url with search params on intitial load --- .../ChangelogFilters/DesktopFilters.tsx | 2 +- .../ChangelogFilters/MobileFilters.tsx | 2 +- .../ChangelogFilters/useChangelogFilters.ts | 124 +++++++++++------- src/hooks/useQueryStringReact.ts | 77 ----------- src/utils/changelogFilters.ts | 10 +- 5 files changed, 84 insertions(+), 131 deletions(-) delete mode 100644 src/hooks/useQueryStringReact.ts diff --git a/src/components/ChangelogFilters/DesktopFilters.tsx b/src/components/ChangelogFilters/DesktopFilters.tsx index b2ba7bafb82..6f7d8b174f9 100644 --- a/src/components/ChangelogFilters/DesktopFilters.tsx +++ b/src/components/ChangelogFilters/DesktopFilters.tsx @@ -134,7 +134,7 @@ export const DesktopFilters = ({ products, networks, types, items }: DesktopFilt clearProductFilters, clearNetworkFilters, clearTypeFilters, - } = useChangelogFilters({ products, networks, types, items }) + } = useChangelogFilters({ items }) const toggleFilter = (filterType: FilterType) => { setActiveFilter(filterType) diff --git a/src/components/ChangelogFilters/MobileFilters.tsx b/src/components/ChangelogFilters/MobileFilters.tsx index 2e62568f758..208e2add488 100644 --- a/src/components/ChangelogFilters/MobileFilters.tsx +++ b/src/components/ChangelogFilters/MobileFilters.tsx @@ -271,7 +271,7 @@ export const MobileFilters = ({ products, networks, types, items }: MobileFilter clearNetworkFilters, clearTypeFilters, clearAllFilters, - } = useChangelogFilters({ products, networks, types, items }) + } = useChangelogFilters({ items }) const totalFilterCount = selectedProducts.length + selectedNetworks.length + selectedTypes.length diff --git a/src/components/ChangelogFilters/useChangelogFilters.ts b/src/components/ChangelogFilters/useChangelogFilters.ts index 7a8b393a1c5..ef18c108cb3 100644 --- a/src/components/ChangelogFilters/useChangelogFilters.ts +++ b/src/components/ChangelogFilters/useChangelogFilters.ts @@ -1,37 +1,62 @@ -import { useState, useEffect } from "react" +import { useState, useEffect, useRef } from "react" import type { ChangelogItem } from "~/components/ChangelogSnippet/types.ts" import { matchesFilters } from "~/utils/changelogFilters.ts" import { parseURLParams, updateFilterURL, toggleItemInArray } from "~/utils/changelogFilterUtils.ts" export interface UseChangelogFiltersProps { - products: string[] - networks: string[] - types: string[] items: ChangelogItem[] } -export const useChangelogFilters = ({ products, networks, types, items }: UseChangelogFiltersProps) => { - const [searchExpanded, setSearchExpanded] = useState(false) - const [searchTerm, setSearchTerm] = useState("") - const [selectedProducts, setSelectedProducts] = useState([]) - const [selectedNetworks, setSelectedNetworks] = useState([]) - const [selectedTypes, setSelectedTypes] = useState([]) +interface FilterState { + selectedProducts: string[] + selectedNetworks: string[] + selectedTypes: string[] + searchTerm: string + searchExpanded: boolean +} + +export const useChangelogFilters = ({ items }: UseChangelogFiltersProps) => { + const [filters, setFilters] = useState({ + selectedProducts: [], + selectedNetworks: [], + selectedTypes: [], + searchTerm: "", + searchExpanded: false, + }) + const isInitialMount = useRef(true) + const hasLoadedFromURL = useRef(false) // Read URL parameters on mount useEffect(() => { const urlParams = parseURLParams() - setSelectedProducts(urlParams.products) - setSelectedNetworks(urlParams.networks) - setSelectedTypes(urlParams.types) - setSearchTerm(urlParams.searchTerm) - setSearchExpanded(urlParams.searchExpanded) + setFilters({ + selectedProducts: urlParams.products, + selectedNetworks: urlParams.networks, + selectedTypes: urlParams.types, + searchTerm: urlParams.searchTerm, + searchExpanded: urlParams.searchExpanded, + }) + + hasLoadedFromURL.current = true }, []) - // Update URL when filters change + // Update URL when filters change (but not on initial mount) useEffect(() => { - updateFilterURL(selectedProducts, selectedNetworks, selectedTypes, searchTerm) - }, [selectedProducts, selectedNetworks, selectedTypes, searchTerm]) + // Skip the first render and the initial load from URL + if (isInitialMount.current) { + isInitialMount.current = false + return + } + + // Skip if we just loaded from URL + if (hasLoadedFromURL.current) { + hasLoadedFromURL.current = false + return + } + + updateFilterURL(filters.selectedProducts, filters.selectedNetworks, filters.selectedTypes, filters.searchTerm) + }, [filters]) // Filter items and update the display useEffect(() => { @@ -43,9 +68,9 @@ export const useChangelogFilters = ({ products, networks, types, items }: UseCha const emptyState = document.querySelector(".empty-state") as HTMLElement const changelogList = document.querySelector(".changelog-list") as HTMLElement - if (searchTerm) { + if (filters.searchTerm) { // Search takes priority - filter by search term - const searchLower = searchTerm.toLowerCase() + const searchLower = filters.searchTerm.toLowerCase() let visibleCount = 0 changelogItems.forEach((item) => { @@ -82,14 +107,20 @@ export const useChangelogFilters = ({ products, networks, types, items }: UseCha } else { // Apply filter logic let visibleCount = 0 - const hasFilters = selectedProducts.length > 0 || selectedNetworks.length > 0 || selectedTypes.length > 0 + const hasFilters = + filters.selectedProducts.length > 0 || filters.selectedNetworks.length > 0 || filters.selectedTypes.length > 0 changelogItems.forEach((item) => { const index = parseInt(item.getAttribute("data-index") || "0") const changelogItem = items[index] if (hasFilters && changelogItem) { - const matches = matchesFilters(changelogItem, selectedProducts, selectedNetworks, selectedTypes) + const matches = matchesFilters( + changelogItem, + filters.selectedProducts, + filters.selectedNetworks, + filters.selectedTypes + ) if (matches) { ;(item as HTMLElement).style.display = "" visibleCount++ @@ -132,54 +163,53 @@ export const useChangelogFilters = ({ products, networks, types, items }: UseCha } } } - }, [searchTerm, selectedProducts, selectedNetworks, selectedTypes, items]) + }, [filters, items]) const handleSearchChange = (value: string) => { - setSearchTerm(value) + setFilters((prev) => ({ ...prev, searchTerm: value })) } const handleSearchToggle = (expanded: boolean) => { - setSearchExpanded(expanded) + setFilters((prev) => ({ ...prev, searchExpanded: expanded })) } const toggleSelection = (type: "product" | "network" | "type", value: string) => { - switch (type) { - case "product": - setSelectedProducts((prev) => toggleItemInArray(prev, value)) - break - case "network": - setSelectedNetworks((prev) => toggleItemInArray(prev, value)) - break - case "type": - setSelectedTypes((prev) => toggleItemInArray(prev, value)) - break - } + setFilters((prev) => { + switch (type) { + case "product": + return { ...prev, selectedProducts: toggleItemInArray(prev.selectedProducts, value) } + case "network": + return { ...prev, selectedNetworks: toggleItemInArray(prev.selectedNetworks, value) } + case "type": + return { ...prev, selectedTypes: toggleItemInArray(prev.selectedTypes, value) } + default: + return prev + } + }) } const clearProductFilters = () => { - setSelectedProducts([]) + setFilters((prev) => ({ ...prev, selectedProducts: [] })) } const clearNetworkFilters = () => { - setSelectedNetworks([]) + setFilters((prev) => ({ ...prev, selectedNetworks: [] })) } const clearTypeFilters = () => { - setSelectedTypes([]) + setFilters((prev) => ({ ...prev, selectedTypes: [] })) } const clearAllFilters = () => { - setSelectedProducts([]) - setSelectedNetworks([]) - setSelectedTypes([]) + setFilters((prev) => ({ ...prev, selectedProducts: [], selectedNetworks: [], selectedTypes: [] })) } return { - searchExpanded, - searchTerm, - selectedProducts, - selectedNetworks, - selectedTypes, + searchExpanded: filters.searchExpanded, + searchTerm: filters.searchTerm, + selectedProducts: filters.selectedProducts, + selectedNetworks: filters.selectedNetworks, + selectedTypes: filters.selectedTypes, handleSearchChange, handleSearchToggle, toggleSelection, diff --git a/src/hooks/useQueryStringReact.ts b/src/hooks/useQueryStringReact.ts deleted file mode 100644 index ac3160858c5..00000000000 --- a/src/hooks/useQueryStringReact.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useState, useCallback, useEffect } from "react" - -type SearchParamValue = string | string[] - -export const setQueryStringValue = (searchParamKey: string, value: SearchParamValue): SearchParamValue | undefined => { - if (typeof window === "undefined") return - - const currentSearchParams = new URLSearchParams(window.location.search) - if (typeof value !== "string") { - currentSearchParams.delete(searchParamKey) - value.forEach((val) => { - currentSearchParams.append(searchParamKey, val) - }) - } else { - currentSearchParams.set(searchParamKey, value) - } - // Preserve the hash fragment if it exists - const hashFragment = window.location.hash - const newurl = - window.location.protocol + - "//" + - window.location.host + - window.location.pathname + - `?${currentSearchParams.toString()}` + - hashFragment - - window.history.replaceState({ path: newurl }, "", newurl) - - return getQueryStringValue(searchParamKey) -} - -export const getQueryStringValue = (searchParamKey: string): SearchParamValue | undefined => { - if (typeof window === "undefined") return undefined - const values = new URLSearchParams(window.location.search).getAll(searchParamKey) - return values.length > 1 ? values : values[0] -} - -function useQueryStringReact( - searchParamKey: string, - initialValue?: SearchParamValue -): [SearchParamValue | undefined, (newValue: SearchParamValue) => void] { - const [value, setValue] = useState(getQueryStringValue(searchParamKey) || initialValue) - - // Keep URL in sync when memory is updated using initial value. - useEffect(() => { - if (initialValue && !getQueryStringValue(searchParamKey)) { - setQueryStringValue(searchParamKey, initialValue) - } - }, []) - - const onSetValue = useCallback( - (newValue: SearchParamValue) => { - setValue(newValue) - setQueryStringValue(searchParamKey, newValue) - }, - [searchParamKey] - ) - - // Keep memory in sync when search params are updated. - useEffect(() => { - const body = document.querySelector("body") - if (!body) return - const observer = new MutationObserver(() => { - const newQueryStringValue = getQueryStringValue(searchParamKey) - if (newQueryStringValue !== value && newQueryStringValue) { - setValue(newQueryStringValue) - } - }) - observer.observe(body, { childList: true, subtree: true }) - - return () => observer.disconnect() - }, [searchParamKey, value]) - - return [value, onSetValue] -} - -export default useQueryStringReact diff --git a/src/utils/changelogFilters.ts b/src/utils/changelogFilters.ts index 3005c5992b9..f9d2fc3290b 100644 --- a/src/utils/changelogFilters.ts +++ b/src/utils/changelogFilters.ts @@ -70,21 +70,21 @@ export function getUniqueTypes(items: ChangelogItem[]): string[] { */ export function matchesFilters( item: ChangelogItem, - selectedTopics: string[], + selectedProducts: string[], selectedNetworks: string[], selectedTypes: string[] ): boolean { // If no filters selected, show all items - const hasTopicFilter = selectedTopics.length > 0 + const hasProductFilter = selectedProducts.length > 0 const hasNetworkFilter = selectedNetworks.length > 0 const hasTypeFilter = selectedTypes.length > 0 - if (!hasTopicFilter && !hasNetworkFilter && !hasTypeFilter) { + if (!hasProductFilter && !hasNetworkFilter && !hasTypeFilter) { return true } - // Check topic filter - if (hasTopicFilter && !selectedTopics.includes(item.topic)) { + // Check product filter (matches against item.topic field) + if (hasProductFilter && !selectedProducts.includes(item.topic)) { return false } From 54b65ffcd8f168d79c5144b366418c3019b95830 Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Mon, 1 Dec 2025 14:46:12 -0500 Subject: [PATCH 19/21] bug and merge conflicts --- .../ChangelogSnippet/ChangelogCard.astro | 32 ++++++++----------- .../ChangelogSnippet/ChangelogCard.module.css | 11 +++++++ src/pages/changelog.astro | 2 +- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/components/ChangelogSnippet/ChangelogCard.astro b/src/components/ChangelogSnippet/ChangelogCard.astro index 589bfb6e280..3ac9a16021b 100644 --- a/src/components/ChangelogSnippet/ChangelogCard.astro +++ b/src/components/ChangelogSnippet/ChangelogCard.astro @@ -160,19 +160,18 @@ const formattedDate = formatDate(item["date-of-release"]) const totalImages = images.length const checkCardHeight = () => { - // Determine max height based on viewport - const isMobile = window.innerWidth <= 768 - const wrapperMaxHeight = isMobile ? 300 : 400 + // Check if wrapper's content exceeds its visible height + const wrapperEl = wrapper as HTMLElement + const hasOverflow = card.scrollHeight > wrapperEl.clientHeight - // Check if card exceeds wrapper height - if (card.scrollHeight <= wrapperMaxHeight) { - // Card fits, hide the button and fade - footer.style.opacity = "0" - footer.style.pointerEvents = "none" - } else { - // Card exceeds wrapper, show button and fade + if (hasOverflow) { + // Content exceeds wrapper, show button and fade footer.style.opacity = "1" footer.style.pointerEvents = "auto" + } else { + // Content fits within wrapper, hide the button and fade + footer.style.opacity = "0" + footer.style.pointerEvents = "none" } } @@ -199,11 +198,11 @@ const formattedDate = formatDate(item["date-of-release"]) }) } }) - } else { - // No images, check immediately - checkCardHeight() } + // this is needed to make the fade/show more work. + setTimeout(checkCardHeight, 2000) + let isExpanded = false button.addEventListener("click", () => { @@ -230,12 +229,7 @@ const formattedDate = formatDate(item["date-of-release"]) }) } - // Initialize on page load - swapDataAttributeImages() - initExpandableCards() - - // Re-initialize after navigation (for SPA-like behavior) - document.addEventListener("astro:page-load", () => { + document.addEventListener("DOMContentLoaded", () => { swapDataAttributeImages() initExpandableCards() }) diff --git a/src/components/ChangelogSnippet/ChangelogCard.module.css b/src/components/ChangelogSnippet/ChangelogCard.module.css index 2bf937ddaea..5e9d3674fe8 100644 --- a/src/components/ChangelogSnippet/ChangelogCard.module.css +++ b/src/components/ChangelogSnippet/ChangelogCard.module.css @@ -16,6 +16,13 @@ & .card { padding: var(--space-4x); } + + & ul { + padding-left: var(--space-6x); + } + & li { + list-style-type: disc; + } } /* Used on individual pages like CCIP/DataFeeds */ @@ -203,9 +210,13 @@ } @media screen and (max-width: 768px) { + .cardWrapper { + max-height: 440px; + } .card { padding: var(--space-4x); gap: var(--space-4x); + flex-direction: column; } .header { diff --git a/src/pages/changelog.astro b/src/pages/changelog.astro index 0a7a040d581..dd94e3cc890 100644 --- a/src/pages/changelog.astro +++ b/src/pages/changelog.astro @@ -69,7 +69,7 @@ const types = logs ? getUniqueTypes(logs) : [] { logs?.map((log, index) => (
= 25 ? "display: none;" : ""}> - +
)) } From b7d6a1cafd2b209e8af0e9e2d3ae28336db1cb31 Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Mon, 1 Dec 2025 14:50:43 -0500 Subject: [PATCH 20/21] click out side handler --- .../ChangelogFilters/DesktopFilters.tsx | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/ChangelogFilters/DesktopFilters.tsx b/src/components/ChangelogFilters/DesktopFilters.tsx index 6f7d8b174f9..c54ce85b814 100644 --- a/src/components/ChangelogFilters/DesktopFilters.tsx +++ b/src/components/ChangelogFilters/DesktopFilters.tsx @@ -1,6 +1,6 @@ import { SvgSearch, SvgTaillessArrowDownSmall, SvgX } from "@chainlink/blocks" import styles from "./styles.module.css" -import { useState } from "react" +import { useState, useEffect, useRef } from "react" import { clsx } from "~/lib/clsx/clsx.ts" import type { ChangelogItem } from "~/components/ChangelogSnippet/types.ts" import { useChangelogFilters } from "./useChangelogFilters.ts" @@ -121,6 +121,7 @@ interface DesktopFiltersProps { export const DesktopFilters = ({ products, networks, types, items }: DesktopFiltersProps) => { const [activeFilter, setActiveFilter] = useState(null) + const wrapperRef = useRef(null) const { searchExpanded, @@ -144,6 +145,22 @@ export const DesktopFilters = ({ products, networks, types, items }: DesktopFilt setActiveFilter(null) } + // Close filter when clicking outside + useEffect(() => { + if (!activeFilter) return + + const handleClickOutside = (event: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { + closeFilter() + } + } + + document.addEventListener("mousedown", handleClickOutside) + return () => { + document.removeEventListener("mousedown", handleClickOutside) + } + }, [activeFilter]) + const getFilterOptions = () => { switch (activeFilter) { case "product": @@ -171,7 +188,7 @@ export const DesktopFilters = ({ products, networks, types, items }: DesktopFilt } return ( - <> +
{activeFilter && (
{getFilterOptions().map((option) => ( @@ -223,6 +240,6 @@ export const DesktopFilters = ({ products, networks, types, items }: DesktopFilt onChange={handleSearchChange} />
- +
) } From 2b1218d8ee848e69f7240ae63a5d32e4c1f7407b Mon Sep 17 00:00:00 2001 From: Tyrel Chambers Date: Mon, 1 Dec 2025 15:18:33 -0500 Subject: [PATCH 21/21] fix style issue on mobile --- .../ChangelogFilters/ChangelogFilters.tsx | 15 +++++++++++-- .../ChangelogFilters/MobileFilters.tsx | 17 ++++++++++----- .../ChangelogFilters/styles.module.css | 21 ++++++++++++++++++- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/components/ChangelogFilters/ChangelogFilters.tsx b/src/components/ChangelogFilters/ChangelogFilters.tsx index d90bb56385f..2e0647230ba 100644 --- a/src/components/ChangelogFilters/ChangelogFilters.tsx +++ b/src/components/ChangelogFilters/ChangelogFilters.tsx @@ -2,6 +2,8 @@ import styles from "./styles.module.css" import type { ChangelogItem } from "~/components/ChangelogSnippet/types.ts" import { DesktopFilters } from "./DesktopFilters.tsx" import { MobileFilters } from "./MobileFilters.tsx" +import { useState } from "react" +import { clsx } from "~/lib/clsx/clsx.ts" export interface ChangelogFiltersProps { products: string[] @@ -11,13 +13,22 @@ export interface ChangelogFiltersProps { } export const ChangelogFilters = ({ products, networks, types, items }: ChangelogFiltersProps) => { + const [searchExpanded, setSearchExpanded] = useState(false) + return ( -
+
- +
) diff --git a/src/components/ChangelogFilters/MobileFilters.tsx b/src/components/ChangelogFilters/MobileFilters.tsx index 208e2add488..9240d6b7fe8 100644 --- a/src/components/ChangelogFilters/MobileFilters.tsx +++ b/src/components/ChangelogFilters/MobileFilters.tsx @@ -252,20 +252,27 @@ interface MobileFiltersProps { networks: string[] types: string[] items: ChangelogItem[] + searchExpanded: boolean + onSearchExpandedChange: (expanded: boolean) => void } -export const MobileFilters = ({ products, networks, types, items }: MobileFiltersProps) => { +export const MobileFilters = ({ + products, + networks, + types, + items, + searchExpanded, + onSearchExpandedChange, +}: MobileFiltersProps) => { const [isMobileFiltersOpen, setIsMobileFiltersOpen] = useState(false) const [expandedSection, setExpandedSection] = useState(null) const { - searchExpanded, searchTerm, selectedProducts, selectedNetworks, selectedTypes, handleSearchChange, - handleSearchToggle, toggleSelection, clearProductFilters, clearNetworkFilters, @@ -314,11 +321,11 @@ export const MobileFilters = ({ products, networks, types, items }: MobileFilter return ( <> -
+
setIsMobileFiltersOpen(true)} /> diff --git a/src/components/ChangelogFilters/styles.module.css b/src/components/ChangelogFilters/styles.module.css index bea9da490d1..83f34ade5f6 100644 --- a/src/components/ChangelogFilters/styles.module.css +++ b/src/components/ChangelogFilters/styles.module.css @@ -376,12 +376,18 @@ .mobileFilters { display: block; + width: 100%; } .wrapper { - min-width: auto; max-width: calc(100% - var(--space-4x)); padding: var(--space-2x); + min-width: unset; + } + + .wrapper.searchExpanded { + width: 100%; + max-width: 100%; } .content { @@ -389,6 +395,19 @@ gap: var(--space-2x); } + /* When search is expanded on mobile, keep both elements inline */ + .content.searchExpanded { + grid-template-columns: auto 1fr; + gap: var(--space-3x); + width: 100%; + } + + /* Override desktop expanded behavior for mobile */ + .searchInputWrapper.expanded { + width: auto; + grid-column-end: auto; + } + .searchInputWrapper { padding: var(--space-2x); }