diff --git a/src/components/ChangelogSnippet/ChangelogCard.astro b/src/components/ChangelogSnippet/ChangelogCard.astro index a22d05ad2bd..589bfb6e280 100644 --- a/src/components/ChangelogSnippet/ChangelogCard.astro +++ b/src/components/ChangelogSnippet/ChangelogCard.astro @@ -2,12 +2,26 @@ import { SvgTaillessArrowDownSmall, Typography } from "@chainlink/blocks" import styles from "./ChangelogCard.module.css" import type { ChangelogItem } from "./types" +import { clsx } from "~/lib/clsx/clsx" +import CopyButton from "./CopyButton.tsx" interface Props { item: ChangelogItem + showBorder?: boolean + autoExpand?: boolean + showCopyButton?: boolean + disableHover?: boolean + showNetworksAndTopic?: boolean } -const { item } = Astro.props +const { + item, + showBorder = true, + autoExpand = false, + showCopyButton = true, + disableHover = false, + showNetworksAndTopic = false, +} = Astro.props // Format the date const formatDate = (dateString: string) => { @@ -22,20 +36,34 @@ const formatDate = (dateString: string) => { const formattedDate = formatDate(item["date-of-release"]) --- -
+
- + {item.type} - + {formattedDate}
+ { + showNetworksAndTopic && (item.networks || item.topic) && ( +
+ {item.networks &&
} + {item.topic && {item.topic}} +
+ ) + } +
- {item["text-description"] &&
} + {item["text-description"] &&
}
+ + {showCopyButton && }
@@ -112,6 +142,18 @@ const formattedDate = formatDate(item["date-of-release"]) if (!card || !content || !button) return + // Check if auto-expand is enabled + const autoExpand = (wrapper as HTMLElement).dataset.autoExpand === "true" + + if (autoExpand) { + // Auto-expand: set to full height and hide footer + ;(wrapper as HTMLElement).style.maxHeight = "none" + wrapper.classList.add("expanded") + footer.style.opacity = "0" + footer.style.pointerEvents = "none" + return // Skip the rest of the logic + } + // Wait for images to load before checking height const images = card.querySelectorAll("img") let loadedImages = 0 @@ -283,13 +325,12 @@ const formattedDate = formatDate(item["date-of-release"]) .log-item__list-chains { display: flex; align-items: center; - gap: 8px; + gap: var(--space-1x); } .log-item__img-chain { width: 24px; height: 24px; - border-radius: 50%; object-fit: cover; } @@ -299,15 +340,44 @@ const formattedDate = formatDate(item["date-of-release"]) justify-content: center; width: 24px; height: 24px; - border-radius: 50%; - background-color: var(--muted); + background-color: var(--gray-950); font-size: 10px; font-weight: 600; - color: var(--color-text-secondary); + color: var(--white); + border-radius: 3px; } /* Hidden filter elements */ .log-item__list-chains .hidden { display: none; } + + /* Disable hover effects when data-disable-hover is true */ + [data-disable-hover="true"]:hover { + background-color: transparent !important; + } + + [data-disable-hover="true"]:hover .copyButton { + opacity: 0 !important; + } + + [data-disable-hover="true"].withBorder:hover { + background-color: transparent !important; + } + + [data-disable-hover="true"].withBorder:hover .contentFooter { + background: none !important; + } + + /* Product Chip */ + .prod-chip { + background-color: var(--muted-more); + color: var(--mirage); + border-radius: 4px; + margin-bottom: 0; + padding: var(--space-1x) var(--space-2x); + font-size: 12px; + line-height: 16px; + display: inline-block; + } diff --git a/src/components/ChangelogSnippet/ChangelogCard.module.css b/src/components/ChangelogSnippet/ChangelogCard.module.css index 7e3dda0edeb..631d86ed1b3 100644 --- a/src/components/ChangelogSnippet/ChangelogCard.module.css +++ b/src/components/ChangelogSnippet/ChangelogCard.module.css @@ -1,27 +1,58 @@ -/* Card Wrapper */ +/* Card Wrapper used in Changelog page */ .cardWrapper { max-height: 400px; overflow: hidden; - transition: max-height 0.5s ease; + transition: all 0.5s ease; position: relative; + + &:hover { + background-color: var(--gray-50); + } + + &:hover .copyButton { + opacity: 1; + } + + & .card { + padding: var(--space-4x); + } +} + +/* Used on individual pages like CCIP/DataFeeds */ +.cardWrapper.withBorder { border: 1px solid var(--border); + + &:hover { + background-color: var(--muted); + } +} + +/* Disable hover background when disableHover is true */ +.cardWrapper[data-disable-hover="true"]:hover { + background-color: transparent !important; +} + +.cardWrapper.withBorder[data-disable-hover="true"]:hover { + background-color: transparent !important; +} + +/* Remove padding when disableHover is true */ +.cardWrapper[data-disable-hover="true"] .card { + padding: 0 !important; } /* Card Container */ .card { display: flex; gap: 82px; - padding: var(--space-6x); } -.cardWrapper.expanded .card { +.cardWrapper.withBorder.expanded .card { -webkit-mask-image: none; mask-image: none; } -.cardWrapper:hover { - background-color: var(--muted); - +.cardWrapper.withBorder:hover { & .contentFooter { background: linear-gradient(to top, var(--muted) 50%, transparent); } @@ -54,11 +85,11 @@ .description p { margin: 0; - color: var(--foreground); + color: var(--muted-foreground); } .description a { - color: var(--color-blue-600); + color: var(--link); text-decoration: none; } @@ -68,7 +99,7 @@ .header { display: flex; - width: 150px; + width: 160px; } .content { @@ -82,6 +113,15 @@ flex: 1; } +/* Networks and Topic Section */ +.networksAndTopicSection { + display: flex; + align-items: center; + gap: var(--space-3x); + margin-bottom: var(--space-4x); + flex-wrap: wrap; +} + /* Content Footer */ .contentFooter { position: absolute; @@ -119,16 +159,58 @@ transition: transform 0.3s ease; } -@media (max-width: 768px) { +.copyButton { + display: flex; + height: fit-content; + gap: var(--space-2x); + border-radius: var(--space-1x); + border: 1px solid var(--border); + width: 118px; + justify-content: center; + align-items: center; + padding: var(--space-2x) 0; + transition: all 0.2s ease; + opacity: 0; + + & > svg { + color: var(--muted-foreground); + } + + &:hover { + border: 1px solid var(--foreground); + } +} + +.checkmark { + stroke: var(--success-foreground); +} +.copyIconMobile { + display: none; +} + +.copyIconDesktop { + display: block; +} + +@media screen and (max-width: 425px) { + .changelogType { + font-size: 14px; + } + + .metaSection { + gap: 0 !important; + } +} + +@media screen and (max-width: 768px) { .card { - padding: var(--space-4x); + padding: 0 !important; + gap: 0; flex-direction: column; - gap: var(--space-4x); } .header { gap: var(--space-3x); - width: 100%; } .metaSection { @@ -147,4 +229,28 @@ .contentFooter { padding-left: var(--space-4x); } + + .copyIconDesktop { + display: none; + } + + .copyIconMobile { + display: block; + } + .copyText { + display: none; + } + + .copyButton { + width: 32px; + opacity: 1; + } +} + +@media screen and (max-width: 990px) { + .copyButton { + position: absolute; + right: var(--space-4x); + top: var(--space-4x); + } } diff --git a/src/components/ChangelogSnippet/ChangelogSnippet.astro b/src/components/ChangelogSnippet/ChangelogSnippet.astro index aa2930cb346..7c251b503ac 100644 --- a/src/components/ChangelogSnippet/ChangelogSnippet.astro +++ b/src/components/ChangelogSnippet/ChangelogSnippet.astro @@ -55,7 +55,7 @@ if (appId && apiKey) { Changelog - + diff --git a/src/components/ChangelogSnippet/CopyButton.tsx b/src/components/ChangelogSnippet/CopyButton.tsx new file mode 100644 index 00000000000..d52f32aa96f --- /dev/null +++ b/src/components/ChangelogSnippet/CopyButton.tsx @@ -0,0 +1,68 @@ +import { SvgCopy, Typography } from "@chainlink/blocks" +import styles from "./ChangelogCard.module.css" +import { useState } from "react" + +export default function CopyButton({ url }: { url: string }) { + const [isCopied, setIsCopied] = useState(false) + + const copyToClipboard = () => { + const mode = import.meta.env.MODE === "development" ? "http://localhost:4321" : "https://dev.chain.link" + navigator.clipboard.writeText(`${mode}/changelog/${url}`) + setIsCopied(true) + setTimeout(() => setIsCopied(false), 2000) + } + + return ( + + ) +} diff --git a/src/components/ChangelogSnippet/README.md b/src/components/ChangelogSnippet/README.md index bce2e2505ae..eab5afed7c6 100644 --- a/src/components/ChangelogSnippet/README.md +++ b/src/components/ChangelogSnippet/README.md @@ -4,6 +4,10 @@ The ChangelogSnippet component displays the most recent changelog entry for a specific product or topic. It searches through changelog entries and shows the latest update in a card format with an expandable description. +## Notes + +On pages like "CCIP" (individual product pages), the changelog card has a border and has slightly different styling than the component shown on the Changelog page. This component has no border and some different hover effects. This styling is controlled by the "showBorder" prop. The showBorder prop when set to true (true is the default value) is what you see on individual product pages. This will be the default styling when using the changelog components. On the Changelog page you can see the styling when showBorder is set to false. + ## How to Use It Import the component into your MDX file and provide a search query: @@ -16,9 +20,10 @@ import ChangelogSnippet from "@components/ChangelogSnippet/ChangelogSnippet.astr ## Props -| Prop | Type | Required | Description | -| ------- | ------ | -------- | ------------------------------------------------------------------------------------------- | -| `query` | string | Yes | The search term used to find relevant changelog entries (e.g., "ccip", "vrf", "automation") | +| Prop | Type | Required | Description | +| ------------ | ------- | -------- | ------------------------------------------------------------------------------------------- | +| `query` | string | Yes | The search term used to find relevant changelog entries (e.g., "ccip", "vrf", "automation") | +| `showBorder` | boolean | No | Whether to show a border around the card. Defaults to true. | ## Complete Example @@ -35,3 +40,61 @@ import ChangelogSnippet from "@components/ChangelogSnippet/ChangelogSnippet.astr ``` This will display the latest CCIP-related changelog entry with a link to view the full changelog. + +--- + +# ChangelogCard Component + +## What This Component Does + +The ChangelogCard component displays a single changelog item in a card format. It shows the changelog entry's type, date, title, and description with optional expand/collapse functionality. + +## How to Use It + +Import the component and pass a changelog item: + +```astro +import ChangelogCard from "@components/ChangelogSnippet/ChangelogCard.astro" import type {ChangelogItem} from "@components/ChangelogSnippet/types" +const item: ChangelogItem = { + // ... changelog item data +} + + +``` + +## Props + +| Prop | Type | Required | Default | Description | +| ---------------------- | ------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------- | +| `item` | ChangelogItem | Yes | - | The changelog item to display | +| `showBorder` | boolean | No | `true` | Whether to show a border around the card | +| `autoExpand` | boolean | No | `false` | Whether to automatically expand the card to full height (skips height restrictions and hides expand/collapse button) | +| `showCopyButton` | boolean | No | `true` | Whether to show the "Copy URL" button | +| `disableHover` | boolean | No | `false` | Whether to disable hover effects (background color change and card padding) | +| `showNetworksAndTopic` | boolean | No | `false` | Whether to show the networks icons and topic chip above the title | + +## Usage Examples + +### Default Card (with border and interactions) + +```astro + +``` + +### Card without border (like on main changelog page) + +```astro + +``` + +### Fully expanded card without interactions (like on individual pages) + +```astro + +``` + +### Card with networks and topic displayed + +```astro + +``` diff --git a/src/components/Header/Nav/ProductNavigation/Desktop/ProductNavigation.tsx b/src/components/Header/Nav/ProductNavigation/Desktop/ProductNavigation.tsx index 9f9cfe6853c..5c9979f9a7c 100644 --- a/src/components/Header/Nav/ProductNavigation/Desktop/ProductNavigation.tsx +++ b/src/components/Header/Nav/ProductNavigation/Desktop/ProductNavigation.tsx @@ -68,7 +68,7 @@ export const ProductNavigation = ({ setNavMenuOpen, showMegaMenu, isMegamenuOpen - + Changelog diff --git a/src/components/Header/Nav/ProductNavigation/Mobile/ProductNavigation.tsx b/src/components/Header/Nav/ProductNavigation/Mobile/ProductNavigation.tsx index b00ccb80fb2..5520d7817df 100644 --- a/src/components/Header/Nav/ProductNavigation/Mobile/ProductNavigation.tsx +++ b/src/components/Header/Nav/ProductNavigation/Mobile/ProductNavigation.tsx @@ -83,7 +83,7 @@ export function ProductNavigation() { Tools - + Changelog diff --git a/src/pages/changelog/[...id].astro b/src/pages/changelog/[...id].astro new file mode 100644 index 00000000000..5ce2f8ddd24 --- /dev/null +++ b/src/pages/changelog/[...id].astro @@ -0,0 +1,153 @@ +--- +import BaseLayout from "~/layouts/BaseLayout.astro" +import * as CONFIG from "../../config" +import { getSecret } from "astro:env/server" +import { searchClient } from "@algolia/client-search" +import { ChangelogItem } from "~/components/ChangelogSnippet/types" +import ChangelogCard from "~/components/ChangelogSnippet/ChangelogCard.astro" +import { SvgArrowLeft2, Typography } from "@chainlink/blocks" + +export async function getStaticPaths() { + const appId = getSecret("ALGOLIA_APP_ID") + const apiKey = getSecret("PUBLIC_ALGOLIA_SEARCH_PUBLIC_API_KEY") + + // Return empty array if credentials are not available (CI/CD environments) + if (!appId || !apiKey) { + console.warn("Algolia credentials not available, skipping changelog static generation") + return [] + } + + const client = searchClient(appId, apiKey) + + try { + const records: ChangelogItem[] = [] + let currentPage = 0 + let nbPages = 1 + const hitsPerPage = 1000 // Maximum allowed by Algolia + + // Fetch all changelog items from Algolia with pagination + while (currentPage < nbPages) { + const req = await client.search({ + requests: [ + { + indexName: "Changelog", + hitsPerPage, + page: currentPage, + }, + ], + }) + + const firstResult = req.results[0] + + if ("hits" in firstResult) { + // Add hits from current page to records + const hits = firstResult.hits as ChangelogItem[] + records.push(...hits) + + // Update nbPages from the response + if ("nbPages" in firstResult) { + nbPages = firstResult.nbPages as number + } + } + + currentPage++ + } + + // Generate static paths for each changelog item + return records.map((log) => ({ + params: { id: log.slug }, + props: { log }, + })) + } catch (error) { + console.error("Error fetching changelog items:", error) + return [] + } +} + +interface Props { + log: ChangelogItem +} + +const { log } = Astro.props + +const pageTitle = `Changelog and Releases | ${CONFIG.SITE.title}` +--- + + +
+ + +
+ +
+
+ + + diff --git a/src/pages/changelog/index.astro b/src/pages/changelog/index.astro new file mode 100644 index 00000000000..bd5a13d790d --- /dev/null +++ b/src/pages/changelog/index.astro @@ -0,0 +1,103 @@ +--- +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[]) : [] + console.log(results[1]) + // logs are returned sorted by created_at DESC + logs = results +} +--- + + +
+
+ Changelog + Never miss an update +
+ +
+ {logs?.map((log) => )} +
+
+
+ +