Skip to content

Commit f527d0e

Browse files
committed
refactor(home): address hero carousel review feedback
1 parent 51d09ee commit f527d0e

5 files changed

Lines changed: 316 additions & 130 deletions

File tree

.stylelintrc.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"rules": {
3+
"selector-pseudo-class-no-unknown": [
4+
true,
5+
{
6+
"ignorePseudoClasses": ["global"]
7+
}
8+
]
9+
}
10+
}

components/Activity/HeroCarousel.module.less

Lines changed: 65 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
--hero-carousel-offset: 4.125rem;
33
background: #050816;
44
min-height: calc(100vh - var(--hero-carousel-offset) - 2.5rem);
5+
6+
@media (max-width: 991.98px) {
7+
min-height: calc(100vh - var(--hero-carousel-offset) - 2rem);
8+
}
9+
10+
@media (max-width: 767.98px) {
11+
min-height: calc(100svh - var(--hero-carousel-offset) - 1.25rem);
12+
}
513
}
614

715
.carousel,
@@ -47,6 +55,26 @@
4755
:global(.carousel-control-next-icon) {
4856
filter: drop-shadow(0 12px 24px rgb(0 0 0 / 50%));
4957
}
58+
59+
@media (max-width: 767.98px) {
60+
:global(.carousel-indicators) {
61+
right: 1rem;
62+
bottom: 1rem;
63+
left: 1rem;
64+
justify-content: center;
65+
}
66+
67+
:global(.carousel-indicators button) {
68+
margin: 0 0.3rem;
69+
width: 1.4rem;
70+
height: 0.26rem;
71+
}
72+
73+
:global(.carousel-control-prev),
74+
:global(.carousel-control-next) {
75+
display: none;
76+
}
77+
}
5078
}
5179

5280
.slideCard {
@@ -57,10 +85,23 @@
5785
rgb(5 8 22 / 46%) 58%,
5886
rgb(5 8 22 / 16%) 100%
5987
);
88+
89+
@media (max-width: 767.98px) {
90+
background: linear-gradient(
91+
180deg,
92+
rgb(5 8 22 / 18%) 0%,
93+
rgb(5 8 22 / 54%) 44%,
94+
rgb(5 8 22 / 92%) 100%
95+
);
96+
}
6097
}
6198

6299
.mediaPane {
63100
min-height: 19rem;
101+
102+
@media (max-width: 767.98px) {
103+
min-height: 14rem;
104+
}
64105
}
65106

66107
.mediaPane::after {
@@ -71,17 +112,30 @@
71112
linear-gradient(90deg, rgb(5 8 22 / 10%) 0%, rgb(5 8 22 / 24%) 32%, rgb(5 8 22 / 72%) 100%);
72113
pointer-events: none;
73114
content: '';
115+
116+
@media (max-width: 767.98px) {
117+
background:
118+
linear-gradient(180deg, rgb(5 8 22 / 16%) 0%, rgb(5 8 22 / 42%) 42%, rgb(5 8 22 / 88%) 100%),
119+
linear-gradient(90deg, rgb(5 8 22 / 18%) 0%, rgb(5 8 22 / 16%) 100%);
120+
}
74121
}
75122

76123
.description {
77-
display: -webkit-box;
78124
max-width: 40rem;
79-
overflow: hidden;
80-
font-size: clamp(1rem, 1.6vw, 1.28rem);
81-
line-height: 1.55;
82-
-webkit-box-orient: vertical;
83-
-webkit-line-clamp: 3;
84-
line-clamp: 3;
125+
126+
:global(p) {
127+
margin-bottom: 0;
128+
font-size: clamp(1rem, 1.6vw, 1.28rem);
129+
line-height: 1.55;
130+
}
131+
132+
:global(button) {
133+
display: none;
134+
}
135+
136+
@media (max-width: 767.98px) {
137+
max-width: none;
138+
}
85139
}
86140

87141
.actionButton {
@@ -96,6 +150,10 @@
96150
background: linear-gradient(135deg, rgb(255 255 255 / 92%) 0%, rgb(224 242 254 / 96%) 100%);
97151
color: #0f172a;
98152
letter-spacing: 0.02em;
153+
154+
@media (max-width: 767.98px) {
155+
border-radius: 0.8rem;
156+
}
99157
}
100158

101159
.actionButton:hover,
@@ -107,64 +165,3 @@
107165
background: linear-gradient(135deg, #ffffff 0%, #e0f2fe 100%);
108166
color: #0f172a;
109167
}
110-
111-
@media (max-width: 991.98px) {
112-
.heroCarousel {
113-
min-height: calc(100vh - var(--hero-carousel-offset) - 2rem);
114-
}
115-
}
116-
117-
@media (max-width: 767.98px) {
118-
.heroCarousel {
119-
min-height: calc(100svh - var(--hero-carousel-offset) - 1.25rem);
120-
}
121-
122-
.carousel {
123-
:global(.carousel-indicators) {
124-
right: 1rem;
125-
bottom: 1rem;
126-
left: 1rem;
127-
justify-content: center;
128-
}
129-
130-
:global(.carousel-indicators button) {
131-
margin: 0 0.3rem;
132-
width: 1.4rem;
133-
height: 0.26rem;
134-
}
135-
136-
:global(.carousel-control-prev),
137-
:global(.carousel-control-next) {
138-
display: none;
139-
}
140-
}
141-
142-
.slideCard {
143-
background: linear-gradient(
144-
180deg,
145-
rgb(5 8 22 / 18%) 0%,
146-
rgb(5 8 22 / 54%) 44%,
147-
rgb(5 8 22 / 92%) 100%
148-
);
149-
}
150-
151-
.mediaPane {
152-
min-height: 14rem;
153-
}
154-
155-
.mediaPane::after {
156-
background:
157-
linear-gradient(180deg, rgb(5 8 22 / 16%) 0%, rgb(5 8 22 / 42%) 42%, rgb(5 8 22 / 88%) 100%),
158-
linear-gradient(90deg, rgb(5 8 22 / 18%) 0%, rgb(5 8 22 / 16%) 100%);
159-
}
160-
161-
.description {
162-
max-width: none;
163-
-webkit-line-clamp: 4;
164-
line-clamp: 4;
165-
}
166-
167-
.actionButton {
168-
border-radius: 0.8rem;
169-
}
170-
}

components/Activity/HeroCarousel.tsx

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { CSSProperties, FC, useContext, useEffect, useState } from 'react';
2+
import { TextTruncate } from 'idea-react';
23
import { TableCellLocation } from 'mobx-lark';
34
import { Badge, Button, Card, Carousel, Col, Container, Row, Stack } from 'react-bootstrap';
45

@@ -7,7 +8,6 @@ import { I18nContext } from '../../models/Translation';
78
import { LarkImage } from '../LarkImage';
89
import styles from './HeroCarousel.module.less';
910

10-
const FALLBACK_LINK = '/hackathon/Labor-AI-hackathon-2026';
1111
const MAX_ITEMS = 5;
1212

1313
const timestampOf = (value: unknown) => {
@@ -44,33 +44,29 @@ export const HeroCarousel: FC = () => {
4444
const { t } = useContext(I18nContext);
4545
const [heroStyle, setHeroStyle] = useState<CSSProperties>();
4646
const [activities, setActivities] = useState<Activity[]>([]);
47+
const [descriptionRows, setDescriptionRows] = useState(3);
4748
const infoBodyStyle = { minHeight: 'clamp(0rem, 38vh, 24rem)' } as CSSProperties;
4849

4950
useEffect(() => {
50-
const navbar = document.querySelector('nav');
51+
const navbar = document.querySelector<HTMLElement>('nav');
5152
const syncHeroOffset = () => {
5253
const navbarHeight = navbar?.getBoundingClientRect().height || 56;
5354

5455
setHeroStyle({
5556
'--hero-carousel-offset': `${navbarHeight}px`,
5657
} as CSSProperties);
5758
};
58-
const observer =
59-
typeof ResizeObserver === 'undefined' || !navbar
60-
? undefined
61-
: new ResizeObserver(syncHeroOffset);
59+
const observer = navbar && new ResizeObserver(syncHeroOffset);
6260

6361
syncHeroOffset();
6462
if (navbar) observer?.observe(navbar);
65-
window.addEventListener('resize', syncHeroOffset);
6663

67-
return () => {
68-
observer?.disconnect();
69-
window.removeEventListener('resize', syncHeroOffset);
70-
};
64+
return () => observer?.disconnect();
7165
}, []);
7266

7367
useEffect(() => {
68+
let mounted = true;
69+
7470
(async () => {
7571
try {
7672
const model = new ActivityModel();
@@ -82,25 +78,36 @@ export const HeroCarousel: FC = () => {
8278
)
8379
.slice(0, MAX_ITEMS);
8480

85-
setActivities(latestActivities);
81+
if (mounted) setActivities(latestActivities);
8682
} catch (err) {
8783
console.error('Failed to load activities:', err);
8884
}
8985
})();
86+
87+
return () => {
88+
mounted = false;
89+
};
9090
}, []);
9191

92-
const slides = activities.length
93-
? activities
94-
: [
95-
{
96-
id: 'fallback',
97-
name: 'Labor AI Hackathon 2026',
98-
summary: t('home_hackathon_top_bar_description'),
99-
} as Activity,
100-
];
92+
useEffect(() => {
93+
const syncDescriptionRows = () => {
94+
setDescriptionRows(window.innerWidth <= 767.98 ? 4 : 3);
95+
};
96+
97+
syncDescriptionRows();
98+
window.addEventListener('resize', syncDescriptionRows);
99+
100+
return () => {
101+
window.removeEventListener('resize', syncDescriptionRows);
102+
};
103+
}, []);
104+
105+
if (!activities.length) return null;
101106

102107
return (
103-
<section
108+
<Container
109+
as="section"
110+
fluid
104111
className={`${styles.heroCarousel} position-relative`}
105112
aria-label={t('home_hackathon_top_bar_aria_label')}
106113
style={heroStyle}
@@ -110,19 +117,16 @@ export const HeroCarousel: FC = () => {
110117
touch
111118
pause="hover"
112119
interval={6500}
113-
indicators={slides.length > 1}
114-
controls={slides.length > 1}
120+
indicators={activities.length > 1}
121+
controls={activities.length > 1}
115122
className={`${styles.carousel} h-100`}
116123
>
117-
{slides.map(activity => {
118-
const href =
119-
(activity.id as string) === 'fallback'
120-
? FALLBACK_LINK
121-
: ActivityModel.getLink(activity);
124+
{activities.map(activity => {
125+
const href = (activity.link as string) || ActivityModel.getLink(activity);
122126
const hosts = ((activity.host as string[]) || []).slice(0, 2);
123127
const locationText = locationTextOf(activity);
124128
const dateText = formatDateLabel(activity.startTime);
125-
const title = (activity.name as string) || 'Activity';
129+
const title = (activity.name as string) || t('activity');
126130
const description = descriptionOf(activity);
127131
const image = activity.cardImage || activity.image;
128132

@@ -173,17 +177,20 @@ export const HeroCarousel: FC = () => {
173177
{title}
174178
</Card.Title>
175179

176-
<Card.Text className={`${styles.description} text-white-50 mb-4`}>
180+
<TextTruncate
181+
rows={descriptionRows}
182+
className={`${styles.description} text-white-50 mb-4`}
183+
>
177184
{description}
178-
</Card.Text>
185+
</TextTruncate>
179186

180187
<Stack
181188
direction="horizontal"
182189
gap={3}
183190
className="flex-wrap align-items-start align-items-md-center"
184191
>
185192
<Card.Text className="mb-0 fs-6 text-info-emphasis fw-semibold">
186-
{locationText || 'Open Source Bazaar'}
193+
{locationText || t('open_source_bazaar')}
187194
</Card.Text>
188195
<Button
189196
href={href}
@@ -206,6 +213,6 @@ export const HeroCarousel: FC = () => {
206213
);
207214
})}
208215
</Carousel>
209-
</section>
216+
</Container>
210217
);
211218
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"core-js": "^3.49.0",
2323
"echarts-jsx": "^0.6.0",
2424
"file-type": "^22.0.0",
25-
"idea-react": "^2.0.0-rc.13",
25+
"idea-react": "^2.2.2",
2626
"jsonwebtoken": "^9.0.3",
2727
"koa": "^3.2.0",
2828
"koa-jwt": "^4.0.4",

0 commit comments

Comments
 (0)