Skip to content

Commit 0d80652

Browse files
dethan3TechQuery
andauthored
[add] Hero Carousel component with Aurora Gradient style in Home page (#72)
Co-authored-by: TechQuery <shiy2008@gmail.com>
1 parent ee0cf17 commit 0d80652

7 files changed

Lines changed: 413 additions & 35 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+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
.heroCarousel {
2+
--hero-carousel-offset: 4.125rem;
3+
4+
background: #050816;
5+
min-height: calc(100vh - var(--hero-carousel-offset) - 2.5rem);
6+
7+
@media (max-width: 991.98px) {
8+
min-height: calc(100vh - var(--hero-carousel-offset) - 2rem);
9+
}
10+
11+
@media (max-width: 767.98px) {
12+
min-height: calc(100svh - var(--hero-carousel-offset) - 1.25rem);
13+
}
14+
}
15+
16+
.carousel,
17+
.item,
18+
.slideCard {
19+
min-height: inherit;
20+
}
21+
22+
.carousel {
23+
:global(.carousel-inner) {
24+
min-height: inherit;
25+
}
26+
27+
:global(.carousel-item) {
28+
min-height: inherit;
29+
}
30+
31+
:global(.carousel-indicators) {
32+
right: auto;
33+
bottom: 2rem;
34+
left: 1rem;
35+
justify-content: flex-start;
36+
margin: 0;
37+
}
38+
39+
:global(.carousel-indicators button) {
40+
opacity: 0.9;
41+
margin: 0 0.45rem 0 0;
42+
border: 0;
43+
border-radius: 999px;
44+
background-color: rgb(255 255 255 / 65%);
45+
width: 2.25rem;
46+
height: 0.32rem;
47+
}
48+
49+
:global(.carousel-control-prev),
50+
:global(.carousel-control-next) {
51+
opacity: 0.95;
52+
width: 6rem;
53+
}
54+
55+
:global(.carousel-control-prev-icon),
56+
:global(.carousel-control-next-icon) {
57+
filter: drop-shadow(0 12px 24px rgb(0 0 0 / 50%));
58+
}
59+
60+
@media (max-width: 767.98px) {
61+
:global(.carousel-indicators) {
62+
right: 1rem;
63+
bottom: 1rem;
64+
left: 1rem;
65+
justify-content: center;
66+
}
67+
68+
:global(.carousel-indicators button) {
69+
margin: 0 0.3rem;
70+
width: 1.4rem;
71+
height: 0.26rem;
72+
}
73+
74+
:global(.carousel-control-prev),
75+
:global(.carousel-control-next) {
76+
display: none;
77+
}
78+
}
79+
}
80+
81+
.slideCard {
82+
background: linear-gradient(
83+
90deg,
84+
rgb(5 8 22 / 96%) 0%,
85+
rgb(5 8 22 / 88%) 28%,
86+
rgb(5 8 22 / 46%) 58%,
87+
rgb(5 8 22 / 16%) 100%
88+
);
89+
90+
@media (max-width: 767.98px) {
91+
background: linear-gradient(
92+
180deg,
93+
rgb(5 8 22 / 18%) 0%,
94+
rgb(5 8 22 / 54%) 44%,
95+
rgb(5 8 22 / 92%) 100%
96+
);
97+
}
98+
}
99+
100+
.mediaPane {
101+
min-height: 19rem;
102+
103+
@media (max-width: 767.98px) {
104+
min-height: 14rem;
105+
}
106+
}
107+
108+
.mediaPane::after {
109+
position: absolute;
110+
inset: 0;
111+
background:
112+
linear-gradient(180deg, rgb(5 8 22 / 18%) 0%, rgb(5 8 22 / 48%) 58%, rgb(5 8 22 / 82%) 100%),
113+
linear-gradient(90deg, rgb(5 8 22 / 10%) 0%, rgb(5 8 22 / 24%) 32%, rgb(5 8 22 / 72%) 100%);
114+
pointer-events: none;
115+
content: '';
116+
117+
@media (max-width: 767.98px) {
118+
background:
119+
linear-gradient(180deg, rgb(5 8 22 / 16%) 0%, rgb(5 8 22 / 42%) 42%, rgb(5 8 22 / 88%) 100%),
120+
linear-gradient(90deg, rgb(5 8 22 / 18%) 0%, rgb(5 8 22 / 16%) 100%);
121+
}
122+
}
123+
124+
.description {
125+
max-width: 40rem;
126+
127+
:global(p) {
128+
margin-bottom: 0;
129+
font-size: clamp(1rem, 1.6vw, 1.28rem);
130+
line-height: 1.55;
131+
}
132+
133+
:global(button) {
134+
display: none;
135+
}
136+
137+
@media (max-width: 767.98px) {
138+
max-width: none;
139+
}
140+
}
141+
142+
.actionButton {
143+
transition:
144+
transform 180ms ease,
145+
box-shadow 180ms ease,
146+
background 180ms ease,
147+
border-color 180ms ease;
148+
box-shadow: 0 12px 28px rgb(14 165 233 / 18%);
149+
border: 1px solid rgb(255 255 255 / 20%);
150+
border-radius: 0.9rem;
151+
background: linear-gradient(135deg, rgb(255 255 255 / 92%) 0%, rgb(224 242 254 / 96%) 100%);
152+
color: #0f172a;
153+
letter-spacing: 0.02em;
154+
155+
@media (max-width: 767.98px) {
156+
border-radius: 0.8rem;
157+
}
158+
}
159+
160+
.actionButton:hover,
161+
.actionButton:focus,
162+
.actionButton:active {
163+
transform: translateY(-2px);
164+
box-shadow: 0 18px 40px rgb(14 165 233 / 22%);
165+
border-color: rgb(255 255 255 / 34%);
166+
background: linear-gradient(135deg, #ffffff 0%, #e0f2fe 100%);
167+
color: #0f172a;
168+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { CSSProperties, FC, useContext, useEffect, useState } from 'react';
2+
import { TextTruncate } from 'idea-react';
3+
import { TableCellLocation } from 'mobx-lark';
4+
import { Badge, Button, Card, Carousel, Col, Container, Row, Stack } from 'react-bootstrap';
5+
6+
import { Activity, ActivityModel } from '../../models/Activity';
7+
import { I18nContext } from '../../models/Translation';
8+
import { LarkImage } from '../LarkImage';
9+
import styles from './HeroCarousel.module.less';
10+
11+
const timestampOf = (value: unknown) => {
12+
if (typeof value === 'number') return value;
13+
if (typeof value === 'string') {
14+
const time = new Date(value).getTime();
15+
16+
return Number.isFinite(time) ? time : 0;
17+
}
18+
19+
return 0;
20+
};
21+
22+
const formatDateLabel = (value: unknown, locale: string) => {
23+
const timestamp = timestampOf(value);
24+
25+
return !timestamp
26+
? ''
27+
: new Intl.DateTimeFormat(locale, {
28+
month: 'short',
29+
day: 'numeric',
30+
}).format(timestamp);
31+
};
32+
33+
const locationTextOf = ({ city, location }: Activity) =>
34+
[(city as string) || '', (location as TableCellLocation | undefined)?.full_address || '']
35+
.filter(Boolean)
36+
.join(' · ');
37+
38+
const descriptionOf = (activity: Activity) =>
39+
(activity.summary as string) || locationTextOf(activity) || (activity.type as string) || '';
40+
41+
export const HeroCarousel: FC<{ activities: Activity[] }> = ({ activities }) => {
42+
const { currentLanguage, t } = useContext(I18nContext);
43+
const [heroStyle, setHeroStyle] = useState<CSSProperties>();
44+
const [descriptionRows, setDescriptionRows] = useState(3);
45+
const infoBodyStyle = { minHeight: 'clamp(0rem, 38vh, 24rem)' } as CSSProperties;
46+
47+
useEffect(() => {
48+
const navbar = document.querySelector<HTMLElement>('nav');
49+
const syncHeroOffset = () => {
50+
const navbarHeight = navbar?.getBoundingClientRect().height || 56;
51+
52+
setHeroStyle({
53+
'--hero-carousel-offset': `${navbarHeight}px`,
54+
} as CSSProperties);
55+
};
56+
const observer = navbar && new ResizeObserver(syncHeroOffset);
57+
58+
syncHeroOffset();
59+
if (navbar) observer?.observe(navbar);
60+
61+
return () => observer?.disconnect();
62+
}, []);
63+
64+
useEffect(() => {
65+
const syncDescriptionRows = () => setDescriptionRows(window.innerWidth <= 767.98 ? 4 : 3);
66+
syncDescriptionRows();
67+
68+
const observer = new ResizeObserver(syncDescriptionRows);
69+
observer.observe(document.body);
70+
71+
return () => observer.disconnect();
72+
}, []);
73+
74+
return (
75+
<Container
76+
as="section"
77+
fluid
78+
className={`${styles.heroCarousel} position-relative`}
79+
style={heroStyle}
80+
>
81+
<Carousel
82+
fade
83+
touch
84+
pause="hover"
85+
interval={6500}
86+
indicators={activities.length > 1}
87+
controls={activities.length > 1}
88+
className={`${styles.carousel} h-100`}
89+
>
90+
{activities.map(activity => {
91+
const { id, type, name, host, startTime, cardImage } = activity;
92+
93+
const href = ActivityModel.getLink(activity);
94+
const hosts = ((host as string[]) || []).slice(0, 2);
95+
const locationText = locationTextOf(activity);
96+
const dateText = formatDateLabel(startTime, currentLanguage);
97+
const title = (name as string) || t('activity');
98+
const description = descriptionOf(activity);
99+
const image = cardImage || activity.image;
100+
101+
return (
102+
<Carousel.Item key={id as string} className={`${styles.item} h-100`}>
103+
<Card
104+
className={`${styles.slideCard} h-100 rounded-0 border-0 bg-transparent text-white`}
105+
>
106+
<Row className="g-0 h-100 flex-column-reverse flex-md-row">
107+
<Col
108+
xs={12}
109+
md={6}
110+
lg={5}
111+
className="d-flex align-items-center position-relative z-1"
112+
>
113+
<Container fluid="md" className="px-3 px-md-4 px-xl-5 py-5">
114+
<Card.Body
115+
className="p-0 d-flex flex-column justify-content-center"
116+
style={infoBodyStyle}
117+
>
118+
<Stack direction="horizontal" gap={2} className="flex-wrap mb-3 mb-md-4">
119+
{(hosts.length ? hosts : [(type as string) || t('hackathon')]).map(
120+
item => (
121+
<Badge
122+
key={item}
123+
pill
124+
bg="info"
125+
text="dark"
126+
className="px-3 py-2 fw-semibold"
127+
>
128+
{item}
129+
</Badge>
130+
),
131+
)}
132+
{(dateText || t('event_duration')) && (
133+
<Badge pill bg="light" text="dark" className="px-3 py-2 fw-semibold">
134+
{dateText || t('event_duration')}
135+
</Badge>
136+
)}
137+
</Stack>
138+
139+
<Card.Title
140+
as="h1"
141+
className="display-3 fw-bold lh-1 mb-3 mb-md-4"
142+
style={{ textWrap: 'balance' }}
143+
>
144+
{title}
145+
</Card.Title>
146+
147+
<TextTruncate
148+
rows={descriptionRows}
149+
className={`${styles.description} text-white-50 mb-4`}
150+
>
151+
{description}
152+
</TextTruncate>
153+
154+
<Stack
155+
direction="horizontal"
156+
gap={3}
157+
className="flex-wrap align-items-start align-items-md-center"
158+
>
159+
<Card.Text className="mb-0 fs-6 text-info-emphasis fw-semibold">
160+
{locationText || t('open_source_bazaar')}
161+
</Card.Text>
162+
<Button
163+
href={href}
164+
variant="light"
165+
className={`${styles.actionButton} px-4 py-2 fw-semibold text-uppercase`}
166+
>
167+
{t('hackathon_register_now')}
168+
</Button>
169+
</Stack>
170+
</Card.Body>
171+
</Container>
172+
</Col>
173+
174+
<Col xs={12} md={6} lg={7} className={`${styles.mediaPane} position-relative`}>
175+
<LarkImage src={image} alt={title} className="w-100 h-100 object-fit-cover" />
176+
</Col>
177+
</Row>
178+
</Card>
179+
</Carousel.Item>
180+
);
181+
})}
182+
</Carousel>
183+
</Container>
184+
);
185+
};

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>

0 commit comments

Comments
 (0)