CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/components/landing/news-banner.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2023 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { useEffect, useRef, useState } from "react";
7
import { useAsyncEffect } from "use-async-effect";
8
9
import { Icon, IconName } from "@cocalc/frontend/components/icon";
10
import { slugURL } from "@cocalc/util/news";
11
import { COLORS } from "@cocalc/util/theme";
12
import { CHANNELS_ICONS, RecentHeadline } from "@cocalc/util/types/news";
13
import { Paragraph } from "components/misc";
14
import A from "components/misc/A";
15
import { TagList } from "components/news/news";
16
import { useDateStr } from "components/news/useDateStr";
17
import { MAX_WIDTH } from "lib/config";
18
19
const PADDING = "15px";
20
const FONT_SIZE = "16px";
21
const ROTATE_DELAY_S = 15; // every that number of second a new news item is shown
22
const ANIMATE_DELAY_MS = 10; // less means faster
23
24
// This is similar to the "BannerWithLinks" component, but showing recent news
25
export function NewsBanner({
26
recentHeadlines,
27
headlineIndex,
28
}: {
29
recentHeadlines: RecentHeadline[] | null;
30
headlineIndex: number;
31
}) {
32
// we have to initialize it with a value from the server to avoid these hydration errors
33
const [index, setIndex] = useState<number>(headlineIndex);
34
35
useEffect(() => {
36
// every $ROTATE_DELAY_S, rotate to the next headline
37
const interval = setInterval(() => {
38
setIndex((i) => ((i ?? 0) + 1) % (recentHeadlines?.length ?? 0));
39
}, ROTATE_DELAY_S * 1000);
40
41
return () => clearInterval(interval);
42
}, []);
43
44
if (recentHeadlines == null || recentHeadlines.length === 0) return null;
45
46
return (
47
<div style={{ backgroundColor: COLORS.YELL_LL, overflow: "hidden" }}>
48
<NewsHeader item={recentHeadlines[index]} />
49
</div>
50
);
51
}
52
53
function NewsHeader({ item }: { item: RecentHeadline }) {
54
const [first, setFirst] = useState(true);
55
const textRef = useRef<HTMLDivElement>(null);
56
const [cur, setCur] = useState<RecentHeadline>(item);
57
const [top, setTop] = useState(0);
58
const [opacity, setOpacity] = useState(1);
59
60
useAsyncEffect(
61
async (isMounted) => {
62
if (first) {
63
setFirst(false);
64
return;
65
}
66
67
// height of the textRef element
68
const offset = textRef.current?.offsetHeight ?? 0;
69
for (let i = 0; i < offset; i++) {
70
await new Promise((resolve) => setTimeout(resolve, ANIMATE_DELAY_MS));
71
if (!isMounted()) return;
72
setTop(i);
73
setOpacity(Math.max(0, 1 - (2 * i) / offset));
74
}
75
setTop(-offset);
76
setCur(item);
77
for (let i = 0; i < offset; i++) {
78
await new Promise((resolve) => setTimeout(resolve, ANIMATE_DELAY_MS));
79
if (!isMounted()) return;
80
setTop(-offset + i);
81
setOpacity(Math.min(1, (2 * i) / offset));
82
}
83
},
84
[item],
85
);
86
87
const permalink = slugURL(cur);
88
const dateStr = useDateStr(cur);
89
90
function renderHeadline() {
91
if (cur == null) return null;
92
const { channel, title, tags } = cur;
93
return (
94
<>
95
<div style={{ paddingRight: ".5em" }}>
96
<Icon name={CHANNELS_ICONS[channel] as IconName} />
97
</div>
98
{dateStr}{" "}
99
<A
100
href={permalink}
101
style={{
102
paddingLeft: PADDING,
103
fontWeight: "bold",
104
// https://github.com/sagemathinc/cocalc/issues/6684
105
maxWidth: "800px",
106
textOverflow: "ellipsis",
107
overflow: "hidden",
108
whiteSpace: "nowrap",
109
}}
110
>
111
{title}
112
</A>{" "}
113
<TagList
114
tags={tags}
115
mode="news"
116
style={{ paddingLeft: PADDING }}
117
styleTag={{ fontSize: FONT_SIZE }}
118
/>
119
</>
120
);
121
}
122
123
return (
124
<div
125
ref={textRef}
126
style={{
127
padding: "10px",
128
textAlign: "center",
129
whiteSpace: "nowrap",
130
}}
131
>
132
<Paragraph
133
style={{
134
display: "flex",
135
flexDirection: "row",
136
justifyContent: "center",
137
margin: "0 auto",
138
position: "relative",
139
top,
140
opacity,
141
fontSize: FONT_SIZE,
142
maxWidth: MAX_WIDTH,
143
}}
144
>
145
{renderHeadline()}
146
<span style={{ paddingLeft: PADDING }}>
147
<A href={"/news"}>All news...</A>
148
</span>
149
</Paragraph>
150
</div>
151
);
152
}
153
154