@@ -1015,7 +1015,7 @@ test.describe('LandingCarousel component', () => {
|
|||||||
|
|
||||||
// Check that article cards are present
|
// Check that article cards are present
|
||||||
const items = page.locator('[data-testid="carousel-items"]')
|
const items = page.locator('[data-testid="carousel-items"]')
|
||||||
const cards = items.locator('div')
|
const cards = items.locator('a')
|
||||||
await expect(cards.first()).toBeVisible()
|
await expect(cards.first()).toBeVisible()
|
||||||
|
|
||||||
// Verify cards have real titles (not "Unknown Article" when article not found)
|
// Verify cards have real titles (not "Unknown Article" when article not found)
|
||||||
@@ -1190,7 +1190,7 @@ test.describe('LandingArticleGridWithFilter component', () => {
|
|||||||
await expect(articleCards.first()).toBeVisible()
|
await expect(articleCards.first()).toBeVisible()
|
||||||
|
|
||||||
const firstCard = articleCards.first()
|
const firstCard = articleCards.first()
|
||||||
const titleLink = firstCard.locator('h3 a')
|
const titleLink = firstCard.locator('h3 span')
|
||||||
await expect(titleLink).toBeVisible()
|
await expect(titleLink).toBeVisible()
|
||||||
|
|
||||||
const intro = firstCard.locator('div').last() // cardIntro is the last div
|
const intro = firstCard.locator('div').last() // cardIntro is the last div
|
||||||
|
|||||||
@@ -24,6 +24,19 @@
|
|||||||
box-shadow:
|
box-shadow:
|
||||||
0 0.0625rem 0.1875rem 0 rgba(31, 35, 40, 0.08),
|
0 0.0625rem 0.1875rem 0 rgba(31, 35, 40, 0.08),
|
||||||
0 0.0625rem 0 0 rgba(31, 35, 40, 0.06);
|
0 0.0625rem 0 0 rgba(31, 35, 40, 0.06);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none !important;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow:
|
||||||
|
0 0.25rem 0.5rem 0 rgba(31, 35, 40, 0.12),
|
||||||
|
0 0.125rem 0.25rem 0 rgba(31, 35, 40, 0.08);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
background-color: var(--bgColor-muted, var(--color-canvas-subtle));
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardHeader {
|
.cardHeader {
|
||||||
@@ -40,10 +53,6 @@
|
|||||||
.cardTitleLink {
|
.cardTitleLink {
|
||||||
color: var(--fgColor-accent);
|
color: var(--fgColor-accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardIntro {
|
.cardIntro {
|
||||||
@@ -51,6 +60,7 @@
|
|||||||
color: var(--fgColor-muted);
|
color: var(--fgColor-muted);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tagsContainer {
|
.tagsContainer {
|
||||||
@@ -145,6 +155,10 @@
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
// Medium screens: flexible width with spacing
|
// Medium screens: flexible width with spacing
|
||||||
@include breakpoint(md) {
|
@include breakpoint(md) {
|
||||||
flex: 0 1 25%;
|
flex: 0 1 25%;
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => {
|
|||||||
const articlesPerPage = useResponsiveArticlesPerPage()
|
const articlesPerPage = useResponsiveArticlesPerPage()
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const headingRef = useRef<HTMLHeadingElement>(null)
|
||||||
|
|
||||||
// Reset to first page when articlesPerPage changes (screen size changes)
|
// Reset to first page when articlesPerPage changes (screen size changes)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -112,6 +113,14 @@ export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (pageNumber >= 1 && pageNumber <= totalPages) {
|
if (pageNumber >= 1 && pageNumber <= totalPages) {
|
||||||
setCurrentPage(pageNumber)
|
setCurrentPage(pageNumber)
|
||||||
|
if (headingRef.current) {
|
||||||
|
const elementPosition = headingRef.current.getBoundingClientRect().top + window.scrollY
|
||||||
|
const offsetPosition = elementPosition - 140 // 140px offset from top
|
||||||
|
window.scrollTo({
|
||||||
|
top: offsetPosition,
|
||||||
|
behavior: 'smooth',
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +131,7 @@ export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => {
|
|||||||
{/* Title and Dropdown Row */}
|
{/* Title and Dropdown Row */}
|
||||||
<div className={styles.titleAndDropdownRow}>
|
<div className={styles.titleAndDropdownRow}>
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h2 className={cx(styles.headerTitle, styles.headerTitleText)}>
|
<h2 ref={headingRef} className={cx(styles.headerTitle, styles.headerTitleText)}>
|
||||||
{t('article_grid.heading')}
|
{t('article_grid.heading')}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -156,7 +165,6 @@ export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => {
|
|||||||
<form onSubmit={(e) => e.preventDefault()}>
|
<form onSubmit={(e) => e.preventDefault()}>
|
||||||
<TextInput
|
<TextInput
|
||||||
leadingVisual={SearchIcon}
|
leadingVisual={SearchIcon}
|
||||||
sx={{ width: '100%' }}
|
|
||||||
placeholder={t('article_grid.search_articles')}
|
placeholder={t('article_grid.search_articles')}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
autoComplete="false"
|
autoComplete="false"
|
||||||
@@ -210,7 +218,8 @@ type ArticleCardProps = {
|
|||||||
|
|
||||||
const ArticleCard = ({ article }: ArticleCardProps) => {
|
const ArticleCard = ({ article }: ArticleCardProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<Link
|
||||||
|
href={article.fullPath}
|
||||||
className={cx(
|
className={cx(
|
||||||
styles.articleCard,
|
styles.articleCard,
|
||||||
styles.articleCardBox,
|
styles.articleCardBox,
|
||||||
@@ -226,12 +235,10 @@ const ArticleCard = ({ article }: ArticleCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className={styles.cardTitle}>
|
<h3 className={styles.cardTitle}>
|
||||||
<Link href={article.fullPath} className={styles.cardTitleLink}>
|
<span className={styles.cardTitleLink}>{article.title}</span>
|
||||||
{article.title}
|
|
||||||
</Link>
|
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{article.intro && <div className={styles.cardIntro}>{article.intro}</div>}
|
{article.intro && <div className={styles.cardIntro}>{article.intro}</div>}
|
||||||
</div>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.carousel {
|
.carousel {
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
--carousel-transition-duration: 0.3s;
|
--carousel-transition-duration: 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
transition: opacity var(--carousel-transition-duration) ease-out;
|
transition: opacity var(--carousel-transition-duration) ease-in-out;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.animating {
|
&.animating {
|
||||||
opacity: 0;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +68,19 @@
|
|||||||
box-shadow:
|
box-shadow:
|
||||||
0px 1px 3px 0px rgba(31, 35, 40, 0.08),
|
0px 1px 3px 0px rgba(31, 35, 40, 0.08),
|
||||||
0px 1px 0px 0px rgba(31, 35, 40, 0.06);
|
0px 1px 0px 0px rgba(31, 35, 40, 0.06);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none !important;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow:
|
||||||
|
0 0.25rem 0.5rem 0 rgba(31, 35, 40, 0.12),
|
||||||
|
0 0.125rem 0.25rem 0 rgba(31, 35, 40, 0.08);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
background-color: var(--bgColor-muted, var(--color-canvas-subtle));
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.articleTitle {
|
.articleTitle {
|
||||||
@@ -78,10 +91,6 @@
|
|||||||
.articleLink {
|
.articleLink {
|
||||||
color: var(--fgColor-accent);
|
color: var(--fgColor-accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.articleDescription {
|
.articleDescription {
|
||||||
@@ -89,6 +98,7 @@
|
|||||||
color: var(--fgColor-muted);
|
color: var(--fgColor-muted);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from '@primer/octicons-react'
|
import { ChevronLeftIcon, ChevronRightIcon } from '@primer/octicons-react'
|
||||||
import { Token } from '@primer/react'
|
|
||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
import type { ResolvedArticle } from '@/types'
|
import type { ResolvedArticle } from '@/types'
|
||||||
import { useTranslation } from '@/languages/components/useTranslation'
|
import { useTranslation } from '@/languages/components/useTranslation'
|
||||||
@@ -78,11 +77,11 @@ export const LandingCarousel = ({ heading = '', recommended }: LandingCarouselPr
|
|||||||
setCurrentPage((prev) => Math.max(0, prev - 1))
|
setCurrentPage((prev) => Math.max(0, prev - 1))
|
||||||
|
|
||||||
// Set animation state to false after transition completes
|
// Set animation state to false after transition completes
|
||||||
// Duration matches CSS custom property --carousel-transition-duration (300ms)
|
// Duration matches CSS custom property --carousel-transition-duration (100ms)
|
||||||
animationTimeoutRef.current = setTimeout(() => {
|
animationTimeoutRef.current = setTimeout(() => {
|
||||||
setIsAnimating(false)
|
setIsAnimating(false)
|
||||||
animationTimeoutRef.current = null
|
animationTimeoutRef.current = null
|
||||||
}, 300)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToNext = () => {
|
const goToNext = () => {
|
||||||
@@ -97,11 +96,11 @@ export const LandingCarousel = ({ heading = '', recommended }: LandingCarouselPr
|
|||||||
setCurrentPage((prev) => Math.min(totalPages - 1, prev + 1))
|
setCurrentPage((prev) => Math.min(totalPages - 1, prev + 1))
|
||||||
|
|
||||||
// Set animation state to false after transition completes
|
// Set animation state to false after transition completes
|
||||||
// Duration matches CSS custom property --carousel-transition-duration (300ms)
|
// Duration matches CSS custom property --carousel-transition-duration (100ms)
|
||||||
animationTimeoutRef.current = setTimeout(() => {
|
animationTimeoutRef.current = setTimeout(() => {
|
||||||
setIsAnimating(false)
|
setIsAnimating(false)
|
||||||
animationTimeoutRef.current = null
|
animationTimeoutRef.current = null
|
||||||
}, 300)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the start index based on current page
|
// Calculate the start index based on current page
|
||||||
@@ -144,19 +143,13 @@ export const LandingCarousel = ({ heading = '', recommended }: LandingCarouselPr
|
|||||||
data-testid="carousel-items"
|
data-testid="carousel-items"
|
||||||
>
|
>
|
||||||
{visibleItems.map((article: ResolvedArticle, index) => (
|
{visibleItems.map((article: ResolvedArticle, index) => (
|
||||||
<div
|
<a
|
||||||
key={startIndex + index}
|
key={startIndex + index}
|
||||||
|
href={article.href}
|
||||||
className={cx(styles.articleCard, 'border', 'border-default', 'rounded-2')}
|
className={cx(styles.articleCard, 'border', 'border-default', 'rounded-2')}
|
||||||
>
|
>
|
||||||
<div className="mb-2">
|
|
||||||
{article.category.map((cat: string) => (
|
|
||||||
<Token key={cat} text={cat} className="mr-1 mb-2" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<h3 className={styles.articleTitle}>
|
<h3 className={styles.articleTitle}>
|
||||||
<a href={article.href} className={styles.articleLink}>
|
<span className={styles.articleLink}>{article.title}</span>
|
||||||
{article.title}
|
|
||||||
</a>
|
|
||||||
</h3>
|
</h3>
|
||||||
<div
|
<div
|
||||||
className={styles.articleDescription}
|
className={styles.articleDescription}
|
||||||
@@ -164,7 +157,7 @@ export const LandingCarousel = ({ heading = '', recommended }: LandingCarouselPr
|
|||||||
__html: article.intro as TrustedHTML,
|
__html: article.intro as TrustedHTML,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -98,6 +98,14 @@
|
|||||||
.heroSecondaryAction {
|
.heroSecondaryAction {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: var(--fgColor-default, var(--color-fg-default, #1f2328));
|
color: var(--fgColor-default, var(--color-fg-default, #1f2328));
|
||||||
|
|
||||||
|
@media (max-width: 865px) {
|
||||||
|
background-color: var(
|
||||||
|
--bgColor-muted,
|
||||||
|
var(--color-canvas-subtle, #f6f8fa)
|
||||||
|
) !important;
|
||||||
|
}
|
||||||
|
|
||||||
border-color: var(
|
border-color: var(
|
||||||
--borderColor-default,
|
--borderColor-default,
|
||||||
var(--color-border-default, #d1d9e0)
|
var(--color-border-default, #d1d9e0)
|
||||||
@@ -121,18 +129,37 @@
|
|||||||
|
|
||||||
@media (max-width: 865px) {
|
@media (max-width: 865px) {
|
||||||
.landingHero {
|
.landingHero {
|
||||||
height: 24rem;
|
height: 28rem;
|
||||||
background-image: none !important;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: var(--bgColor-muted, var(--color-canvas-subtle, #f6f8fa));
|
background-color: var(--bgColor-muted, var(--color-canvas-subtle, #f6f8fa));
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
background-position: unset;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-image: inherit;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: bottom;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
opacity: 0.5;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.heroContent {
|
.heroContent {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
order: 2;
|
order: 2;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heroHeading {
|
.heroHeading {
|
||||||
@@ -144,7 +171,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.heroActions {
|
.heroActions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-self: baseline;
|
||||||
|
margin-left: 5rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heroAction {
|
.heroAction {
|
||||||
@@ -162,19 +193,36 @@
|
|||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.landingHero {
|
.landingHero {
|
||||||
height: 32rem;
|
height: 28rem;
|
||||||
background-image: none !important;
|
|
||||||
background-color: var(--bgColor-muted, var(--color-canvas-subtle, #f6f8fa));
|
background-color: var(--bgColor-muted, var(--color-canvas-subtle, #f6f8fa));
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem 0 1rem;
|
padding: 1rem 0 1rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-image: inherit;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: bottom;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
opacity: 0.8;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.heroContent {
|
.heroContent {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
order: 2;
|
order: 2;
|
||||||
padding: 1rem;
|
padding: 0 1rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heroText {
|
.heroText {
|
||||||
@@ -184,8 +232,27 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.heroHeading {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.heroActions {
|
.heroActions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-self: baseline;
|
||||||
|
margin-left: 2rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.heroAction {
|
||||||
|
min-width: 9rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroDescription {
|
||||||
|
font-size: 1rem;
|
||||||
|
max-height: 14rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user