11import { CSSProperties , FC , useContext , useEffect , useState } from 'react' ;
2+ import { TextTruncate } from 'idea-react' ;
23import { TableCellLocation } from 'mobx-lark' ;
34import { Badge , Button , Card , Carousel , Col , Container , Row , Stack } from 'react-bootstrap' ;
45
@@ -7,7 +8,6 @@ import { I18nContext } from '../../models/Translation';
78import { LarkImage } from '../LarkImage' ;
89import styles from './HeroCarousel.module.less' ;
910
10- const FALLBACK_LINK = '/hackathon/Labor-AI-hackathon-2026' ;
1111const MAX_ITEMS = 5 ;
1212
1313const 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} ;
0 commit comments