Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions components/Activity/PromoBar.module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
.promoBar {
position: sticky;
top: var(--promo-bar-offset, 4.125rem);
z-index: 1020;
margin-top: var(--promo-bar-gap, 0.625rem);
box-shadow: 0 10px 32px rgb(0 0 0 / 35%);
background: linear-gradient(
135deg,
#0c0e2b 0%,
#1a0b4e 20%,
#312e81 40%,
#1e40af 60%,
#0891b2 80%,
#0d9488 100%
);
}

.promoBarInner {
width: min(100%, 1180px);
}

.promoBarContent {
flex: 1 1 auto;
gap: 1.25rem;
padding: 0.75rem 0;
min-width: 0;
color: inherit;

&:hover,
&:focus,
&:active {
color: inherit;
text-decoration: none;

.promoBarAction {
box-shadow: 0 0 12px rgb(139 92 246 / 40%);
background: linear-gradient(135deg, #818cf8 0%, #a78bfa 50%, #22d3ee 100%);
color: #ffffff;
}
}
}

.promoBarText {
flex: 0 0 auto;
gap: 0.7rem;
min-width: 0;
line-height: 1.35;

strong {
flex: 0 0 auto;
color: #ffffff;
font-size: 1rem;
text-shadow: 0 0 8px rgb(139 92 246 / 30%);
}

span {
min-width: 0;
color: #a5f3fc;
font-size: 0.92rem;
}
}

.promoBarEventName {
flex: 0 0 auto;
background: linear-gradient(90deg, #c4b5fd, #67e8f9);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 700;
font-size: 0.92rem;
white-space: nowrap;
}

.promoBarAction {
flex: 0 0 auto;
transition: all 200ms ease;
box-shadow: 0 0 10px rgb(139 92 246 / 25%);
border: 1px solid rgb(255 255 255 / 20%);
border-radius: 999px;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #06b6d4 100%);
padding: 0.42rem 0.85rem;
color: #ffffff;
font-weight: 700;
font-size: 0.88rem;
line-height: 1.1;
white-space: nowrap;
}

.promoBarClose {
flex: 0 0 2.5rem;
opacity: 0.6;
margin: 0 0 0 0.5rem;
width: 2.5rem;

&:hover,
&:focus {
opacity: 1;
}
}

@media (max-width: 767.98px) {
.promoBar {
top: var(--promo-bar-offset, 4.125rem);
}

.promoBarInner {
padding: 0 0.75rem;
}

.promoBarContent {
flex-wrap: nowrap;
gap: 0.55rem 0.75rem;
padding: 0.58rem 0;
}

.promoBarText {
flex: 1 1 auto;
flex-direction: column;
align-items: flex-start;
gap: 0.15rem;

strong {
flex: 1 1 auto;
max-width: 100%;
overflow: hidden;
font-size: 0.9rem;
text-overflow: ellipsis;
white-space: nowrap;
}

span {
display: -webkit-box;
overflow: hidden;
font-size: 0.82rem;
line-height: 1.25;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
line-clamp: 1;
}
}

.promoBarAction {
margin-left: 0;
padding: 0.38rem 0.55rem;
font-size: 0.8rem;
}

.promoBarClose {
flex-basis: 2rem;
margin-left: 0.1rem;
width: 2rem;
}
}
87 changes: 87 additions & 0 deletions components/Activity/PromoBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { CSSProperties, FC, useContext, useEffect, useState } from 'react';
import { Alert, CloseButton } from 'react-bootstrap';

import { normalizeText, TableCellText } from 'mobx-lark';

import { Activity, ActivityModel } from '../../models/Activity';
import { I18nContext } from '../../models/Translation';
import styles from './PromoBar.module.less';

export const PromoBar: FC = () => {
const { t } = useContext(I18nContext);
const [isVisible, setIsVisible] = useState(true);
const [barStyle, setBarStyle] = useState<CSSProperties>();
const [activity, setActivity] = useState<Activity>();

useEffect(() => {
const navbar = document.querySelector('nav');
const syncTopBarOffset = () => {
const navbarHeight = navbar?.getBoundingClientRect().height || 56;

setBarStyle({
'--promo-bar-gap': `${Math.max(navbarHeight - 56, 0)}px`,
'--promo-bar-offset': `${navbarHeight}px`,
} as CSSProperties);
};
const observer =
typeof ResizeObserver === 'undefined' || !navbar
? undefined
: new ResizeObserver(syncTopBarOffset);

syncTopBarOffset();
if (navbar) observer?.observe(navbar);
window.addEventListener('resize', syncTopBarOffset);

return () => {
observer?.disconnect();
window.removeEventListener('resize', syncTopBarOffset);
};
}, []);

useEffect(() => {
(async () => {
try {
const model = new ActivityModel();
const data = await model.getOne('Labor-AI-hackathon-2026');
setActivity(data);
} catch (err) {
console.error('Failed to load activity:', err);
}
})();
Comment on lines +41 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

补上异步加载的卸载保护。

getOne() 可能在组件卸载后才返回,当前代码会继续 setActivity,留下陈旧更新并浪费请求资源。

♻️ 建议修复
 useEffect(() => {
+  let cancelled = false;
+
   (async () => {
     try {
       const model = new ActivityModel();
       const data = await model.getOne('Labor-AI-hackathon-2026');
-      setActivity(data);
+      if (!cancelled) setActivity(data);
     } catch (err) {
+      if (cancelled) return;
       console.error('Failed to load activity:', err);
     }
   })();
+
+  return () => {
+    cancelled = true;
+  };
 }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Activity/PromoBar.tsx` around lines 41 - 50, 在 useEffect
中对异步加载添加卸载保护:在 useEffect 里创建一个标志(例如 let mounted = true 或使用 AbortController 如果
ActivityModel.getOne 支持取消),在 getOne 调用前保留该标志/控制器,并在 Promise resolve 前检查
mounted,只有 mounted 为 true 时才调用 setActivity;在 useEffect 返回的 cleanup 函数将 mounted
设为 false(或调用 controller.abort())以阻止组件卸载后继续更新或消耗请求资源。确保引用的符号为
useEffect、ActivityModel.getOne 和 setActivity。

}, []);

const closeBar = () => setIsVisible(false);

if (!isVisible) return null;

return (
<Alert
role="banner"
className={`${styles.promoBar} d-flex flex-column w-100 text-white mb-0 p-0 border-0 rounded-0`}
aria-label={t('home_hackathon_top_bar_aria_label')}
style={barStyle}
>
<div className={`${styles.promoBarInner} d-flex align-items-center mx-auto px-3`}>
<Alert.Link
className={`${styles.promoBarContent} d-flex justify-content-center align-items-center text-decoration-none`}
href={activity ? ActivityModel.getLink(activity) : '/hackathon/Labor-AI-hackathon-2026'}
>
<span className={`${styles.promoBarText} d-flex align-items-baseline`}>
<strong>{t('home_hackathon_top_bar_title')}</strong>
<span>{t('home_hackathon_top_bar_description')}</span>
</span>
<span className={styles.promoBarEventName}>
{activity ? normalizeText(activity.name as TableCellText) : 'Labor AI Hackathon 2026'}
</span>
<span className={styles.promoBarAction}>{t('home_hackathon_top_bar_action')}</span>
</Alert.Link>
<CloseButton
className={`${styles.promoBarClose} p-0 rounded`}
variant="white"
aria-label={t('home_hackathon_top_bar_close')}
onClick={closeBar}
/>
</div>
</Alert>
);
};
4 changes: 2 additions & 2 deletions components/Navigator/MainNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const MainNavigator: FC<MainNavigatorProps> = observer(({ menu }) => {
return (
<Navbar bg="dark" variant="dark" fixed="top" expand="lg">
<Container>
<Navbar.Brand href="/" className="fw-bolder d-flex align-items-center gap-2">
<Navbar.Brand href="/" className="fw-bolder d-flex align-items-center gap-2 text-nowrap">
<Image width={40} src={DefaultImage} alt={t('open_source_bazaar')} />
{t('open_source_bazaar')}
</Navbar.Brand>
Expand All @@ -117,7 +117,7 @@ export const MainNavigator: FC<MainNavigatorProps> = observer(({ menu }) => {
<Nav.Link
key={`${href}-${title}`}
href={href}
className={pathname === `${href}` ? 'fw-bolder text-light' : ''}
className={`text-nowrap ${pathname === `${href}` ? 'fw-bolder text-light' : ''}`}
>
{title}
</Nav.Link>
Expand Down
3 changes: 3 additions & 0 deletions pages/index.tsx
Comment thread
dethan3 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Card, Col, Row } from 'react-bootstrap';
import { renderToStaticMarkup } from 'react-dom/server';
import ReactTyped from 'react-typed-component';

import { PromoBar } from '../components/Activity/PromoBar';
import { PageHead } from '../components/Layout/PageHead';
import { I18nContext } from '../models/Translation';
import styles from '../styles/Home.module.less';
Expand All @@ -15,6 +16,8 @@ const HomePage: FC = observer(() => {
<>
<PageHead />

<PromoBar />

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