Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
170 changes: 170 additions & 0 deletions components/Activity/HeroCarousel.module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
.heroCarousel {
--hero-carousel-offset: 4.125rem;
background: #050816;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
min-height: calc(100vh - var(--hero-carousel-offset) - 2.5rem);
}

.carousel,
.item,
.slideCard {
min-height: inherit;
}

.carousel {
:global(.carousel-inner) {
min-height: inherit;
}

:global(.carousel-item) {
min-height: inherit;
}

:global(.carousel-indicators) {
right: auto;
bottom: 2rem;
left: 1rem;
justify-content: flex-start;
margin: 0;
}

:global(.carousel-indicators button) {
opacity: 0.9;
margin: 0 0.45rem 0 0;
border: 0;
border-radius: 999px;
background-color: rgb(255 255 255 / 65%);
width: 2.25rem;
height: 0.32rem;
}

:global(.carousel-control-prev),
:global(.carousel-control-next) {
opacity: 0.95;
width: 6rem;
}

:global(.carousel-control-prev-icon),
:global(.carousel-control-next-icon) {
Comment thread
dethan3 marked this conversation as resolved.
filter: drop-shadow(0 12px 24px rgb(0 0 0 / 50%));
}
}

.slideCard {
background: linear-gradient(
90deg,
rgb(5 8 22 / 96%) 0%,
rgb(5 8 22 / 88%) 28%,
rgb(5 8 22 / 46%) 58%,
rgb(5 8 22 / 16%) 100%
);
}

.mediaPane {
min-height: 19rem;
}

.mediaPane::after {
position: absolute;
inset: 0;
background:
linear-gradient(180deg, rgb(5 8 22 / 18%) 0%, rgb(5 8 22 / 48%) 58%, rgb(5 8 22 / 82%) 100%),
linear-gradient(90deg, rgb(5 8 22 / 10%) 0%, rgb(5 8 22 / 24%) 32%, rgb(5 8 22 / 72%) 100%);
pointer-events: none;
content: '';
}

.description {
display: -webkit-box;
max-width: 40rem;
overflow: hidden;
font-size: clamp(1rem, 1.6vw, 1.28rem);
line-height: 1.55;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
}
Comment thread
dethan3 marked this conversation as resolved.

.actionButton {
transition:
transform 180ms ease,
box-shadow 180ms ease,
background 180ms ease,
border-color 180ms ease;
box-shadow: 0 12px 28px rgb(14 165 233 / 18%);
border: 1px solid rgb(255 255 255 / 20%);
border-radius: 0.9rem;
background: linear-gradient(135deg, rgb(255 255 255 / 92%) 0%, rgb(224 242 254 / 96%) 100%);
color: #0f172a;
letter-spacing: 0.02em;
}

.actionButton:hover,
.actionButton:focus,
.actionButton:active {
transform: translateY(-2px);
box-shadow: 0 18px 40px rgb(14 165 233 / 22%);
border-color: rgb(255 255 255 / 34%);
background: linear-gradient(135deg, #ffffff 0%, #e0f2fe 100%);
color: #0f172a;
}

@media (max-width: 991.98px) {
.heroCarousel {
min-height: calc(100vh - var(--hero-carousel-offset) - 2rem);
}
}

@media (max-width: 767.98px) {
.heroCarousel {
min-height: calc(100svh - var(--hero-carousel-offset) - 1.25rem);
}
Comment thread
dethan3 marked this conversation as resolved.
Outdated

.carousel {
:global(.carousel-indicators) {
right: 1rem;
bottom: 1rem;
left: 1rem;
justify-content: center;
}

:global(.carousel-indicators button) {
margin: 0 0.3rem;
width: 1.4rem;
height: 0.26rem;
}

:global(.carousel-control-prev),
:global(.carousel-control-next) {
display: none;
}
}

.slideCard {
background: linear-gradient(
180deg,
rgb(5 8 22 / 18%) 0%,
rgb(5 8 22 / 54%) 44%,
rgb(5 8 22 / 92%) 100%
);
}

.mediaPane {
min-height: 14rem;
}

.mediaPane::after {
background:
linear-gradient(180deg, rgb(5 8 22 / 16%) 0%, rgb(5 8 22 / 42%) 42%, rgb(5 8 22 / 88%) 100%),
linear-gradient(90deg, rgb(5 8 22 / 18%) 0%, rgb(5 8 22 / 16%) 100%);
}

.description {
max-width: none;
-webkit-line-clamp: 4;
line-clamp: 4;
}

.actionButton {
border-radius: 0.8rem;
}
}
211 changes: 211 additions & 0 deletions components/Activity/HeroCarousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { CSSProperties, FC, useContext, useEffect, useState } from 'react';
import { TableCellLocation } from 'mobx-lark';
import { Badge, Button, Card, Carousel, Col, Container, Row, Stack } from 'react-bootstrap';

import { Activity, ActivityModel } from '../../models/Activity';
import { I18nContext } from '../../models/Translation';
import { LarkImage } from '../LarkImage';
import styles from './HeroCarousel.module.less';
Comment on lines +1 to +9
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

修复 ESLint 报错:导入排序不符合 simple-import-sort/imports 规则。

该插件设计为配合 autofix 使用(eslint --fix 或编辑器 ESLint 扩展),可一键修复。外部包按字母序 idea-react → mobx-lark → react → react-bootstrap 排列即可通过 lint 检查。

🔧 建议修改
-import { CSSProperties, FC, useContext, useEffect, useState } from 'react';
-import { TextTruncate } from 'idea-react';
-import { TableCellLocation } from 'mobx-lark';
-import { Badge, Button, Card, Carousel, Col, Container, Row, Stack } from 'react-bootstrap';
+import { TextTruncate } from 'idea-react';
+import { TableCellLocation } from 'mobx-lark';
+import { CSSProperties, FC, useContext, useEffect, useState } from 'react';
+import { Badge, Button, Card, Carousel, Col, Container, Row, Stack } from 'react-bootstrap';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { CSSProperties, FC, useContext, useEffect, useState } from 'react';
import { TextTruncate } from 'idea-react';
import { TableCellLocation } from 'mobx-lark';
import { Badge, Button, Card, Carousel, Col, Container, Row, Stack } from 'react-bootstrap';
import { Activity, ActivityModel } from '../../models/Activity';
import { I18nContext } from '../../models/Translation';
import { LarkImage } from '../LarkImage';
import styles from './HeroCarousel.module.less';
import { TextTruncate } from 'idea-react';
import { TableCellLocation } from 'mobx-lark';
import { CSSProperties, FC, useContext, useEffect, useState } from 'react';
import { Badge, Button, Card, Carousel, Col, Container, Row, Stack } from 'react-bootstrap';
import { Activity, ActivityModel } from '../../models/Activity';
import { I18nContext } from '../../models/Translation';
import { LarkImage } from '../LarkImage';
import styles from './HeroCarousel.module.less';
🧰 Tools
🪛 ESLint

[error] 1-9: Run autofix to sort these imports!

(simple-import-sort/imports)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Activity/HeroCarousel.tsx` around lines 1 - 9, Reorder the top
imports in HeroCarousel.tsx to satisfy simple-import-sort/imports: group and
alphabetize external package imports so they read idea-react (TextTruncate) →
mobx-lark (TableCellLocation) → react (CSSProperties, FC, useContext, useEffect,
useState) → react-bootstrap (Badge, Button, Card, Carousel, Col, Container, Row,
Stack); keep local imports (Activity, ActivityModel, I18nContext, LarkImage,
styles) after external packages and preserve existing named imports and spacing.


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

const timestampOf = (value: unknown) => {
if (typeof value === 'number') return value;
if (typeof value === 'string') {
const time = new Date(value).getTime();

return Number.isFinite(time) ? time : 0;
}

return 0;
};

const formatDateLabel = (value: unknown) => {
const timestamp = timestampOf(value);

if (!timestamp) return '';

return new Intl.DateTimeFormat('zh-CN', {
month: 'short',
day: 'numeric',
}).format(timestamp);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
};

const locationTextOf = ({ city, location }: Activity) =>
[(city as string) || '', (location as TableCellLocation | undefined)?.full_address || '']
.filter(Boolean)
.join(' · ');

const descriptionOf = (activity: Activity) =>
(activity.summary as string) || locationTextOf(activity) || (activity.type as string) || '';

export const HeroCarousel: FC = () => {
const { t } = useContext(I18nContext);
const [heroStyle, setHeroStyle] = useState<CSSProperties>();
const [activities, setActivities] = useState<Activity[]>([]);
const infoBodyStyle = { minHeight: 'clamp(0rem, 38vh, 24rem)' } as CSSProperties;

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

setHeroStyle({
'--hero-carousel-offset': `${navbarHeight}px`,
} as CSSProperties);
};
const observer =
typeof ResizeObserver === 'undefined' || !navbar
? undefined
: new ResizeObserver(syncHeroOffset);

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

return () => {
observer?.disconnect();
window.removeEventListener('resize', syncHeroOffset);
};
Comment thread
dethan3 marked this conversation as resolved.
Outdated
}, []);

useEffect(() => {
(async () => {
try {
const model = new ActivityModel();
const data = await model.getAll();
const latestActivities = data
.filter(({ name }) => Boolean(name))
.sort(
({ startTime: left }, { startTime: right }) => timestampOf(right) - timestampOf(left),
)
.slice(0, MAX_ITEMS);

setActivities(latestActivities);
} catch (err) {
console.error('Failed to load activities:', err);
}
})();
}, []);
Comment thread
dethan3 marked this conversation as resolved.
Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

数据从服务端加载最近几条,直接照抄我以前的开源代码:
https://github.com/kaiyuanshe/kaiyuanshe.github.io/blob/a9afddbee79d1e3fe501d491d2aa7f0699ac56c9/pages/index.tsx#L29-L50

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

GPT 认为他写得更符合现在的项目

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

GPT 认为他写得更符合现在的项目

是它更了解本项目,还是我更了解本项目?

我们为什么自己建站、不用飞书多维表格应用来开发?就是因为它是服务端渲染,可以很容易被搜索引擎和 AI 抓取。

而且你的代码为了显示“近期活动”,查询了所有活动,没有加分页参数,后面活动越来越多,这个加载性能有多差?


const slides = activities.length
? activities
: [
{
id: 'fallback',
name: 'Labor AI Hackathon 2026',
summary: t('home_hackathon_top_bar_description'),
} as Activity,
];
Comment thread
dethan3 marked this conversation as resolved.
Outdated

return (
<section
className={`${styles.heroCarousel} position-relative`}
aria-label={t('home_hackathon_top_bar_aria_label')}
style={heroStyle}
>
Comment thread
dethan3 marked this conversation as resolved.
Outdated
<Carousel
fade
touch
pause="hover"
interval={6500}
indicators={slides.length > 1}
controls={slides.length > 1}
className={`${styles.carousel} h-100`}
>
{slides.map(activity => {
const href =
(activity.id as string) === 'fallback'
? FALLBACK_LINK
: ActivityModel.getLink(activity);
const hosts = ((activity.host as string[]) || []).slice(0, 2);
const locationText = locationTextOf(activity);
const dateText = formatDateLabel(activity.startTime);
const title = (activity.name as string) || 'Activity';
const description = descriptionOf(activity);
const image = activity.cardImage || activity.image;

return (
<Carousel.Item key={activity.id as string} className={`${styles.item} h-100`}>
<Card
className={`${styles.slideCard} h-100 rounded-0 border-0 bg-transparent text-white`}
>
<Row className="g-0 h-100 flex-column-reverse flex-md-row">
<Col
xs={12}
md={6}
lg={5}
className="d-flex align-items-center position-relative z-1"
>
<Container fluid="md" className="px-3 px-md-4 px-xl-5 py-5">
<Card.Body
className="p-0 d-flex flex-column justify-content-center"
style={infoBodyStyle}
>
<Stack direction="horizontal" gap={2} className="flex-wrap mb-3 mb-md-4">
{(hosts.length
? hosts
: [(activity.type as string) || t('hackathon')]
).map(item => (
<Badge
key={item}
pill
bg="info"
text="dark"
className="px-3 py-2 fw-semibold"
>
{item}
</Badge>
))}
{(dateText || t('event_duration')) && (
<Badge pill bg="light" text="dark" className="px-3 py-2 fw-semibold">
{dateText || t('event_duration')}
</Badge>
)}
</Stack>

<Card.Title
as="h1"
className="display-3 fw-bold lh-1 mb-3 mb-md-4"
style={{ textWrap: 'balance' }}
>
{title}
</Card.Title>

<Card.Text className={`${styles.description} text-white-50 mb-4`}>
{description}
</Card.Text>

<Stack
direction="horizontal"
gap={3}
className="flex-wrap align-items-start align-items-md-center"
>
<Card.Text className="mb-0 fs-6 text-info-emphasis fw-semibold">
{locationText || 'Open Source Bazaar'}
</Card.Text>
<Button
href={href}
variant="light"
className={`${styles.actionButton} px-4 py-2 fw-semibold text-uppercase`}
>
{t('home_hackathon_top_bar_action')}
</Button>
</Stack>
</Card.Body>
</Container>
</Col>

<Col xs={12} md={6} lg={7} className={`${styles.mediaPane} position-relative`}>
<LarkImage src={image} alt={title} className="w-100 h-100 object-fit-cover" />
</Col>
</Row>
</Card>
</Carousel.Item>
);
})}
</Carousel>
</section>
);
};
Loading
Loading