Skip to content

Commit ed2cf15

Browse files
committed
refactor: rewrite PromoBar with React Bootstrap + dynamic Activity fetch
- Replace raw HTML with Alert, Alert.Link, CloseButton - Move complex styles from styled-jsx to PromoBar.module.less - Add Bootstrap utility classes for layout - Fetch activity name/link from Lark Bitable via ActivityModel - Keep localStorage persistence commented out
1 parent 3b66c69 commit ed2cf15

6 files changed

Lines changed: 245 additions & 254 deletions

File tree

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
.promoBar {
2+
position: sticky;
3+
top: var(--promo-bar-offset, 4.125rem);
4+
z-index: 1020;
5+
margin-top: var(--promo-bar-gap, 0.625rem);
6+
box-shadow: 0 10px 32px rgb(0 0 0 / 35%);
7+
background: linear-gradient(
8+
135deg,
9+
#0c0e2b 0%,
10+
#1a0b4e 20%,
11+
#312e81 40%,
12+
#1e40af 60%,
13+
#0891b2 80%,
14+
#0d9488 100%
15+
);
16+
}
17+
18+
.promoBarInner {
19+
width: min(100%, 1180px);
20+
}
21+
22+
.promoBarContent {
23+
flex: 1 1 auto;
24+
gap: 1.25rem;
25+
padding: 0.75rem 0;
26+
min-width: 0;
27+
color: inherit;
28+
29+
&:hover,
30+
&:focus,
31+
&:active {
32+
color: inherit;
33+
text-decoration: none;
34+
35+
.promoBarAction {
36+
box-shadow: 0 0 12px rgb(139 92 246 / 40%);
37+
background: linear-gradient(135deg, #818cf8 0%, #a78bfa 50%, #22d3ee 100%);
38+
color: #ffffff;
39+
}
40+
}
41+
}
42+
43+
.promoBarText {
44+
flex: 0 0 auto;
45+
gap: 0.7rem;
46+
min-width: 0;
47+
line-height: 1.35;
48+
49+
strong {
50+
flex: 0 0 auto;
51+
color: #ffffff;
52+
font-size: 1rem;
53+
text-shadow: 0 0 8px rgb(139 92 246 / 30%);
54+
}
55+
56+
span {
57+
min-width: 0;
58+
color: #a5f3fc;
59+
font-size: 0.92rem;
60+
}
61+
}
62+
63+
.promoBarEventName {
64+
flex: 0 0 auto;
65+
background: linear-gradient(90deg, #c4b5fd, #67e8f9);
66+
background-clip: text;
67+
-webkit-background-clip: text;
68+
-webkit-text-fill-color: transparent;
69+
font-weight: 700;
70+
font-size: 0.92rem;
71+
white-space: nowrap;
72+
}
73+
74+
.promoBarAction {
75+
flex: 0 0 auto;
76+
transition: all 200ms ease;
77+
box-shadow: 0 0 10px rgb(139 92 246 / 25%);
78+
border: 1px solid rgb(255 255 255 / 20%);
79+
border-radius: 999px;
80+
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #06b6d4 100%);
81+
padding: 0.42rem 0.85rem;
82+
color: #ffffff;
83+
font-weight: 700;
84+
font-size: 0.88rem;
85+
line-height: 1.1;
86+
white-space: nowrap;
87+
}
88+
89+
.promoBarClose {
90+
flex: 0 0 2.5rem;
91+
opacity: 0.6;
92+
margin: 0 0 0 0.5rem;
93+
width: 2.5rem;
94+
95+
&:hover,
96+
&:focus {
97+
opacity: 1;
98+
}
99+
}
100+
101+
@media (max-width: 767.98px) {
102+
.promoBar {
103+
top: var(--promo-bar-offset, 4.125rem);
104+
}
105+
106+
.promoBarInner {
107+
padding: 0 0.75rem;
108+
}
109+
110+
.promoBarContent {
111+
flex-wrap: nowrap;
112+
gap: 0.55rem 0.75rem;
113+
padding: 0.58rem 0;
114+
}
115+
116+
.promoBarText {
117+
flex: 1 1 auto;
118+
flex-direction: column;
119+
align-items: flex-start;
120+
gap: 0.15rem;
121+
122+
strong {
123+
flex: 1 1 auto;
124+
max-width: 100%;
125+
overflow: hidden;
126+
font-size: 0.9rem;
127+
text-overflow: ellipsis;
128+
white-space: nowrap;
129+
}
130+
131+
span {
132+
display: -webkit-box;
133+
overflow: hidden;
134+
font-size: 0.82rem;
135+
line-height: 1.25;
136+
-webkit-box-orient: vertical;
137+
-webkit-line-clamp: 1;
138+
line-clamp: 1;
139+
}
140+
}
141+
142+
.promoBarAction {
143+
margin-left: 0;
144+
padding: 0.38rem 0.55rem;
145+
font-size: 0.8rem;
146+
}
147+
148+
.promoBarClose {
149+
flex-basis: 2rem;
150+
margin-left: 0.1rem;
151+
width: 2rem;
152+
}
153+
}

components/Activity/PromoBar.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { CSSProperties, FC, useContext, useEffect, useState } from 'react';
2+
import { Alert, CloseButton } from 'react-bootstrap';
3+
4+
import { normalizeText, TableCellText } from 'mobx-lark';
5+
6+
import { Activity, ActivityModel } from '../../models/Activity';
7+
import { I18nContext } from '../../models/Translation';
8+
import styles from './PromoBar.module.less';
9+
10+
export const PromoBar: FC = () => {
11+
const { t } = useContext(I18nContext);
12+
const [isVisible, setIsVisible] = useState(true);
13+
const [barStyle, setBarStyle] = useState<CSSProperties>();
14+
const [activity, setActivity] = useState<Activity>();
15+
16+
useEffect(() => {
17+
const navbar = document.querySelector('nav');
18+
const syncTopBarOffset = () => {
19+
const navbarHeight = navbar?.getBoundingClientRect().height || 56;
20+
21+
setBarStyle({
22+
'--promo-bar-gap': `${Math.max(navbarHeight - 56, 0)}px`,
23+
'--promo-bar-offset': `${navbarHeight}px`,
24+
} as CSSProperties);
25+
};
26+
const observer =
27+
typeof ResizeObserver === 'undefined' || !navbar
28+
? undefined
29+
: new ResizeObserver(syncTopBarOffset);
30+
31+
syncTopBarOffset();
32+
if (navbar) observer?.observe(navbar);
33+
window.addEventListener('resize', syncTopBarOffset);
34+
35+
return () => {
36+
observer?.disconnect();
37+
window.removeEventListener('resize', syncTopBarOffset);
38+
};
39+
}, []);
40+
41+
useEffect(() => {
42+
(async () => {
43+
try {
44+
const model = new ActivityModel();
45+
const data = await model.getOne('Labor-AI-hackathon-2026');
46+
setActivity(data);
47+
} catch (err) {
48+
console.error('Failed to load activity:', err);
49+
}
50+
})();
51+
}, []);
52+
53+
const closeBar = () => setIsVisible(false);
54+
55+
if (!isVisible) return null;
56+
57+
return (
58+
<Alert
59+
role="banner"
60+
className={`${styles.promoBar} d-flex flex-column w-100 text-white mb-0 p-0 border-0 rounded-0`}
61+
aria-label={t('home_hackathon_top_bar_aria_label')}
62+
style={barStyle}
63+
>
64+
<div className={`${styles.promoBarInner} d-flex align-items-center mx-auto px-3`}>
65+
<Alert.Link
66+
className={`${styles.promoBarContent} d-flex justify-content-center align-items-center text-decoration-none`}
67+
href={activity ? ActivityModel.getLink(activity) : '/hackathon/Labor-AI-hackathon-2026'}
68+
>
69+
<span className={`${styles.promoBarText} d-flex align-items-baseline`}>
70+
<strong>{t('home_hackathon_top_bar_title')}</strong>
71+
<span>{t('home_hackathon_top_bar_description')}</span>
72+
</span>
73+
<span className={styles.promoBarEventName}>
74+
{activity ? normalizeText(activity.name as TableCellText) : 'Labor AI Hackathon 2026'}
75+
</span>
76+
<span className={styles.promoBarAction}>{t('home_hackathon_top_bar_action')}</span>
77+
</Alert.Link>
78+
<CloseButton
79+
className={`${styles.promoBarClose} p-0 rounded`}
80+
variant="white"
81+
aria-label={t('home_hackathon_top_bar_close')}
82+
onClick={closeBar}
83+
/>
84+
</div>
85+
</Alert>
86+
);
87+
};

components/Navigator/MainNavigator.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export const MainNavigator: FC<MainNavigatorProps> = observer(({ menu }) => {
9797
return (
9898
<Navbar bg="dark" variant="dark" fixed="top" expand="lg">
9999
<Container>
100-
<Navbar.Brand href="/" className="fw-bolder d-flex align-items-center gap-2">
100+
<Navbar.Brand href="/" className="fw-bolder d-flex align-items-center gap-2 text-nowrap">
101101
<Image width={40} src={DefaultImage} alt={t('open_source_bazaar')} />
102102
{t('open_source_bazaar')}
103103
</Navbar.Brand>
@@ -117,7 +117,7 @@ export const MainNavigator: FC<MainNavigatorProps> = observer(({ menu }) => {
117117
<Nav.Link
118118
key={`${href}-${title}`}
119119
href={href}
120-
className={pathname === `${href}` ? 'fw-bolder text-light' : ''}
120+
className={`text-nowrap ${pathname === `${href}` ? 'fw-bolder text-light' : ''}`}
121121
>
122122
{title}
123123
</Nav.Link>

pages/index.tsx

Lines changed: 3 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,22 @@
11
import { observer } from 'mobx-react';
2-
import { CSSProperties, FC, useContext, useEffect, useState } from 'react';
2+
import { FC, useContext } from 'react';
33
import { Card, Col, Row } from 'react-bootstrap';
44
import { renderToStaticMarkup } from 'react-dom/server';
55
import ReactTyped from 'react-typed-component';
66

7+
import { PromoBar } from '../components/Activity/PromoBar';
78
import { PageHead } from '../components/Layout/PageHead';
89
import { I18nContext } from '../models/Translation';
910
import styles from '../styles/Home.module.less';
1011

11-
// Temporarily disable localStorage persistence so the bar returns after refresh.
12-
// const HackathonTopBarStorageKey = 'labor-ai-hackathon-2026-top-bar-dismissed';
13-
const HackathonTopBarLink = '/hackathon/Labor-AI-hackathon-2026';
14-
1512
const HomePage: FC = observer(() => {
1613
const { t } = useContext(I18nContext);
17-
const [isHackathonTopBarVisible, setIsHackathonTopBarVisible] = useState(true);
18-
const [hackathonTopBarStyle, setHackathonTopBarStyle] = useState<CSSProperties>();
19-
20-
// useEffect(() => {
21-
// setIsHackathonTopBarVisible(localStorage.getItem(HackathonTopBarStorageKey) !== 'true');
22-
// }, []);
23-
useEffect(() => {
24-
const navbar = document.querySelector('nav');
25-
const syncTopBarOffset = () => {
26-
const navbarHeight = navbar?.getBoundingClientRect().height || 56;
27-
28-
setHackathonTopBarStyle({
29-
'--hackathon-top-bar-gap': `${Math.max(navbarHeight - 56, 0)}px`,
30-
'--hackathon-top-bar-offset': `${navbarHeight}px`,
31-
} as CSSProperties);
32-
};
33-
const observer =
34-
typeof ResizeObserver === 'undefined' || !navbar
35-
? undefined
36-
: new ResizeObserver(syncTopBarOffset);
37-
38-
syncTopBarOffset();
39-
if (navbar) observer?.observe(navbar);
40-
window.addEventListener('resize', syncTopBarOffset);
41-
42-
return () => {
43-
observer?.disconnect();
44-
window.removeEventListener('resize', syncTopBarOffset);
45-
};
46-
}, []);
47-
48-
const closeHackathonTopBar = () => {
49-
setIsHackathonTopBarVisible(false);
50-
// localStorage.setItem(HackathonTopBarStorageKey, 'true');
51-
};
5214

5315
return (
5416
<>
5517
<PageHead />
5618

57-
{isHackathonTopBarVisible && (
58-
<aside
59-
className={styles.hackathonTopBar}
60-
aria-label={t('home_hackathon_top_bar_aria_label')}
61-
style={hackathonTopBarStyle}
62-
>
63-
<div className={styles.hackathonTopBarInner}>
64-
<a className={styles.hackathonTopBarContent} href={HackathonTopBarLink}>
65-
<span className={styles.hackathonTopBarText}>
66-
<strong>{t('home_hackathon_top_bar_title')}</strong>
67-
<span>{t('home_hackathon_top_bar_description')}</span>
68-
</span>
69-
<span className={styles.hackathonTopBarEventName}>Labor AI Hackathon 2026</span>
70-
<span className={styles.hackathonTopBarAction}>
71-
{t('home_hackathon_top_bar_action')}
72-
</span>
73-
</a>
74-
<button
75-
className={styles.hackathonTopBarClose}
76-
type="button"
77-
aria-label={t('home_hackathon_top_bar_close')}
78-
title={t('home_hackathon_top_bar_close')}
79-
onClick={closeHackathonTopBar}
80-
>
81-
×
82-
</button>
83-
</div>
84-
</aside>
85-
)}
19+
<PromoBar />
8620

8721
<section
8822
className={`flex-fill d-flex flex-column justify-content-center align-items-center bg-secondary bg-gradient text-dark bg-opacity-10 ${styles.main}`}

0 commit comments

Comments
 (0)