Skip to content

Commit d62fc5b

Browse files
committed
Add homepage alerts and site notification banner
1 parent d79798f commit d62fc5b

8 files changed

Lines changed: 567 additions & 1 deletion

File tree

src/components/HomeAlerts/index.js

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import React, { useMemo, useState, useEffect } from 'react';
2+
import Link from '@docusaurus/Link';
3+
import styles from './styles.module.css';
4+
import alertsData from '@site/src/data/homeAlerts.json';
5+
6+
const AUTO_ROTATE_MS = 8000;
7+
8+
function parseStartDate(dateValue) {
9+
if (!dateValue) {
10+
return null;
11+
}
12+
13+
return new Date(`${dateValue}T00:00:00`);
14+
}
15+
16+
function parseEndDate(dateValue) {
17+
if (!dateValue) {
18+
return null;
19+
}
20+
21+
return new Date(`${dateValue}T23:59:59`);
22+
}
23+
24+
function isDateActive(alert, now) {
25+
const startDate = parseStartDate(alert.startDate);
26+
const endDate = parseEndDate(alert.endDate);
27+
28+
if (startDate && now < startDate) {
29+
return false;
30+
}
31+
32+
if (endDate && now > endDate) {
33+
return false;
34+
}
35+
36+
return true;
37+
}
38+
39+
function sortAlerts(a, b) {
40+
if (Boolean(a.pinned) !== Boolean(b.pinned)) {
41+
return a.pinned ? -1 : 1;
42+
}
43+
44+
const aDate = a.publishedAt ? new Date(`${a.publishedAt}T00:00:00`).getTime() : 0;
45+
const bDate = b.publishedAt ? new Date(`${b.publishedAt}T00:00:00`).getTime() : 0;
46+
47+
return bDate - aDate;
48+
}
49+
50+
function severityLabel(severity) {
51+
if (severity === 'high') {
52+
return 'High';
53+
}
54+
55+
if (severity === 'medium') {
56+
return 'Medium';
57+
}
58+
59+
return 'Info';
60+
}
61+
62+
export default function HomeAlerts() {
63+
const alerts = useMemo(() => {
64+
const now = new Date();
65+
66+
return alertsData
67+
.filter((alert) => alert.enabled !== false)
68+
.filter((alert) => isDateActive(alert, now))
69+
.sort(sortAlerts);
70+
}, []);
71+
72+
const [activeIndex, setActiveIndex] = useState(0);
73+
const [isPaused, setIsPaused] = useState(false);
74+
75+
useEffect(() => {
76+
if (alerts.length <= 1 || isPaused) {
77+
return undefined;
78+
}
79+
80+
const intervalId = setInterval(() => {
81+
setActiveIndex((prev) => (prev + 1) % alerts.length);
82+
}, AUTO_ROTATE_MS);
83+
84+
return () => clearInterval(intervalId);
85+
}, [alerts.length, isPaused]);
86+
87+
useEffect(() => {
88+
if (activeIndex >= alerts.length) {
89+
setActiveIndex(0);
90+
}
91+
}, [activeIndex, alerts.length]);
92+
93+
if (alerts.length === 0) {
94+
return null;
95+
}
96+
97+
const activeAlert = alerts[activeIndex];
98+
const badgeClassName = `${styles.badge} ${styles[`badge${severityLabel(activeAlert.severity)}`]}`;
99+
100+
const showControls = alerts.length > 1;
101+
102+
const nextAlert = () => {
103+
setActiveIndex((prev) => (prev + 1) % alerts.length);
104+
};
105+
106+
const prevAlert = () => {
107+
setActiveIndex((prev) => (prev - 1 + alerts.length) % alerts.length);
108+
};
109+
110+
const pauseRotation = () => {
111+
setIsPaused(true);
112+
};
113+
114+
const resumeRotation = () => {
115+
setIsPaused(false);
116+
};
117+
118+
const handleBlurCapture = (event) => {
119+
if (!event.currentTarget.contains(event.relatedTarget)) {
120+
resumeRotation();
121+
}
122+
};
123+
124+
return (
125+
<section
126+
className={styles.wrapper}
127+
aria-label="Home alerts"
128+
onMouseEnter={pauseRotation}
129+
onMouseLeave={resumeRotation}
130+
onFocusCapture={pauseRotation}
131+
onBlurCapture={handleBlurCapture}
132+
>
133+
<div className={`container ${styles.inner}`}>
134+
<div className={styles.content}>
135+
<span className={badgeClassName}>{severityLabel(activeAlert.severity)}</span>
136+
<div className={styles.copy}>
137+
{activeAlert.href ? (
138+
<Link className={styles.titleLink} to={activeAlert.href}>
139+
<strong>{activeAlert.title}</strong>
140+
</Link>
141+
) : (
142+
<strong>{activeAlert.title}</strong>
143+
)}
144+
<p>{activeAlert.message}</p>
145+
</div>
146+
</div>
147+
148+
{showControls && (
149+
<div className={styles.controls}>
150+
<button
151+
type="button"
152+
className={styles.controlButton}
153+
onClick={prevAlert}
154+
aria-label="Previous alert"
155+
>
156+
Prev
157+
</button>
158+
<span className={styles.counter}>{activeIndex + 1} / {alerts.length}</span>
159+
<button
160+
type="button"
161+
className={styles.controlButton}
162+
onClick={nextAlert}
163+
aria-label="Next alert"
164+
>
165+
Next
166+
</button>
167+
</div>
168+
)}
169+
</div>
170+
</section>
171+
);
172+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
.wrapper {
2+
background: linear-gradient(90deg, rgba(24, 24, 37, 0.94), rgba(36, 39, 58, 0.94));
3+
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
4+
}
5+
6+
.inner {
7+
display: flex;
8+
align-items: center;
9+
justify-content: space-between;
10+
gap: 1rem;
11+
min-height: 64px;
12+
padding-top: 0.5rem;
13+
padding-bottom: 0.5rem;
14+
}
15+
16+
.content {
17+
display: flex;
18+
align-items: center;
19+
gap: 0.85rem;
20+
min-width: 0;
21+
flex: 1;
22+
}
23+
24+
.badge {
25+
border-radius: 999px;
26+
font-size: 0.72rem;
27+
font-weight: 700;
28+
letter-spacing: 0.04em;
29+
line-height: 1;
30+
padding: 0.45rem 0.65rem;
31+
text-transform: uppercase;
32+
flex: 0 0 auto;
33+
}
34+
35+
.badgeHigh {
36+
color: #2f0a12;
37+
background: #f38ba8;
38+
}
39+
40+
.badgeMedium {
41+
color: #3a2d0e;
42+
background: #f9e2af;
43+
}
44+
45+
.badgeInfo {
46+
color: #081326;
47+
background: #89b4fa;
48+
}
49+
50+
.copy {
51+
min-width: 0;
52+
}
53+
54+
.copy strong {
55+
color: #f5f5f7;
56+
display: block;
57+
line-height: 1.3;
58+
}
59+
60+
.titleLink {
61+
color: inherit;
62+
text-decoration: none;
63+
}
64+
65+
.titleLink:hover strong,
66+
.titleLink:focus-visible strong {
67+
text-decoration: underline;
68+
text-underline-offset: 0.2rem;
69+
}
70+
71+
.copy p {
72+
margin: 0.15rem 0 0;
73+
color: rgba(245, 245, 247, 0.88);
74+
font-size: 0.92rem;
75+
line-height: 1.35;
76+
}
77+
78+
.controls {
79+
display: flex;
80+
align-items: center;
81+
gap: 0.45rem;
82+
flex: 0 0 auto;
83+
}
84+
85+
.controlButton {
86+
border: 1px solid rgba(255, 255, 255, 0.25);
87+
background: rgba(255, 255, 255, 0.08);
88+
color: #ffffff;
89+
border-radius: 999px;
90+
padding: 0.25rem 0.65rem;
91+
font-size: 0.8rem;
92+
cursor: pointer;
93+
}
94+
95+
.controlButton:hover,
96+
.controlButton:focus-visible {
97+
background: rgba(255, 255, 255, 0.2);
98+
}
99+
100+
.counter {
101+
color: rgba(255, 255, 255, 0.85);
102+
font-size: 0.8rem;
103+
min-width: 2.9rem;
104+
text-align: center;
105+
}
106+
107+
html:not([data-theme='dark']) .wrapper {
108+
background: linear-gradient(90deg, rgba(235, 239, 248, 0.94), rgba(220, 228, 244, 0.94));
109+
border-bottom: 1px solid rgba(63, 79, 117, 0.2);
110+
}
111+
112+
html:not([data-theme='dark']) .copy strong {
113+
color: #1d2a44;
114+
}
115+
116+
html:not([data-theme='dark']) .copy p {
117+
color: #2d3f66;
118+
}
119+
120+
html:not([data-theme='dark']) .controlButton {
121+
border: 1px solid rgba(29, 42, 68, 0.24);
122+
background: rgba(29, 42, 68, 0.08);
123+
color: #1d2a44;
124+
}
125+
126+
html:not([data-theme='dark']) .controlButton:hover,
127+
html:not([data-theme='dark']) .controlButton:focus-visible {
128+
background: rgba(29, 42, 68, 0.14);
129+
}
130+
131+
html:not([data-theme='dark']) .counter {
132+
color: #1d2a44;
133+
}
134+
135+
@media (max-width: 996px) {
136+
.inner {
137+
flex-direction: column;
138+
align-items: stretch;
139+
padding-top: 0.7rem;
140+
padding-bottom: 0.7rem;
141+
}
142+
143+
.content {
144+
align-items: flex-start;
145+
flex-wrap: wrap;
146+
}
147+
148+
.controls {
149+
justify-content: flex-end;
150+
}
151+
}

0 commit comments

Comments
 (0)