Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/next/components/news/news.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2023 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Alert, Button, Card, Flex, Space, Tag, Tooltip } from "antd";6import { useRouter } from "next/router";7import { Fragment } from "react";89import { Icon, IconName } from "@cocalc/frontend/components/icon";10import Markdown from "@cocalc/frontend/editors/slate/static-markdown";11import {12capitalize,13getRandomColor,14plural,15unreachable,16} from "@cocalc/util/misc";17import { slugURL } from "@cocalc/util/news";18import { COLORS } from "@cocalc/util/theme";19import {20CHANNELS_DESCRIPTIONS,21CHANNELS_ICONS,22NewsItem,23} from "@cocalc/util/types/news";24import { CSS, Paragraph, Text, Title } from "components/misc";25import A from "components/misc/A";26import TimeAgo from "timeago-react";27import { useDateStr } from "./useDateStr";28import { useCustomize } from "lib/customize";29import { KUCALC_COCALC_COM } from "@cocalc/util/db-schema/site-defaults";30import { SocialMediaShareLinks } from "../landing/social-media-share-links";3132const STYLE: CSS = {33borderColor: COLORS.GRAY_M,34boxShadow: "0 0 0 1px rgba(0,0,0,.1), 0 3px 3px rgba(0,0,0,.3)",35} as const;3637interface Props {38// NewsWithFuture with optional future property39news: NewsItem & { future?: boolean };40dns?: string;41showEdit?: boolean;42small?: boolean; // limit height, essentially43standalone?: boolean; // default false44historyMode?: boolean; // default false45onTagClick?: (tag: string) => void;46}4748export function News(props: Props) {49const {50news,51showEdit = false,52small = false,53standalone = false,54historyMode = false,55onTagClick,56} = props;57const { id, url, tags, title, date, channel, text, future, hide } = news;58const dateStr = useDateStr(news, historyMode);59const permalink = slugURL(news);60const { kucalc, siteURL } = useCustomize();61const isCoCalcCom = kucalc === KUCALC_COCALC_COM;62const showShareLinks = typeof siteURL === "string" && isCoCalcCom;6364const bottomLinkStyle: CSS = {65color: COLORS.ANTD_LINK_BLUE,66...(standalone ? { fontSize: "125%", fontWeight: "bold" } : {}),67};6869function editLink() {70return (71<A72key="edit"73href={`/news/edit/${id}`}74style={{75...bottomLinkStyle,76color: COLORS.ANTD_RED_WARN,77}}78>79<Icon name="edit" /> Edit80</A>81);82}8384function readMoreLink(iconOnly = false, button = false) {85if (button) {86return (87<Button88type="primary"89style={{ color: "white", marginBottom: "30px" }}90href={url}91target="_blank"92key="url"93size={small ? undefined : "large"}94>95<Icon name="external-link" />96{iconOnly ? "" : " Read more"}97</Button>98);99} else {100return (101<A102key="url"103href={url}104style={{105...bottomLinkStyle,106...(small ? { color: COLORS.GRAY } : { fontWeight: "bold" }),107}}108>109<Icon name="external-link" />110{iconOnly ? "" : " Read more"}111</A>112);113}114}115116function renderOpenLink() {117return (118<A119key="permalink"120href={permalink}121style={{122...bottomLinkStyle,123...(small ? { fontWeight: "bold" } : {}),124}}125>126<Icon name="external-link" /> Open127</A>128);129}130131function shareLinks(text = false) {132return (133<SocialMediaShareLinks134title={title}135url={encodeURIComponent(`${siteURL}${permalink}`)}136showText={text}137standalone={standalone}138/>139);140}141142function actions() {143const actions = [renderOpenLink()];144if (url) actions.push(readMoreLink());145if (showEdit) actions.push(editLink());146if (showShareLinks) actions.push(shareLinks());147return actions;148}149150const style = small ? { height: "200px", overflow: "auto" } : undefined;151152function renderFuture() {153if (future) {154return (155<Alert156banner157message={158<>159Future event, not shown to users.160{typeof date === "number" && (161<>162{" "}163Will be live in <TimeAgo datetime={new Date(1000 * date)} />.164</>165)}166</>167}168/>169);170}171}172173function renderHidden() {174if (hide) {175return (176<Alert177banner178type="error"179message="Hidden, will not be shown to users."180/>181);182}183}184185function renderTags() {186return <TagList mode="news" tags={tags} onTagClick={onTagClick} />;187}188189function extra() {190return (191<>192{renderTags()}193<Text type="secondary" style={{ float: "right" }}>194{dateStr}195</Text>196</>197);198}199200function renderTitle() {201return (202<>203<Tooltip204title={205<>206{capitalize(channel)}: {CHANNELS_DESCRIPTIONS[channel]}207</>208}209>210<Icon name={CHANNELS_ICONS[channel] as IconName} />211</Tooltip>{" "}212<A href={permalink}>{title}</A>213</>214);215}216217function renderHistory() {218const { history } = news;219if (!history) return;220// Object.keys always returns strings, so we need to parse them221const timestamps = Object.keys(history)222.map(Number)223.filter((ts) => !Number.isNaN(ts))224.sort()225.reverse();226if (timestamps.length > 0) {227return (228<Paragraph style={{ textAlign: "center" }}>229{historyMode && (230<>231<A href={permalink}>Current version</A> ·{" "}232</>233)}234Previous {plural(timestamps.length, "version")}:{" "}235{timestamps236.map((ts) => [237<A key={ts} href={`/news/${id}/${ts}`}>238<TimeAgo datetime={new Date(1000 * ts)} />239</A>,240<Fragment key={`m-${ts}`}> · </Fragment>,241])242.flat()243.slice(0, -1)}244</Paragraph>245);246}247}248249if (standalone) {250const renderedTags = renderTags();251return (252<>253{historyMode && (254<Paragraph>255<Text type="danger" strong>256Archived version257</Text>258<Text type="secondary" style={{ float: "right" }}>259Published: {dateStr}260</Text>261</Paragraph>262)}263<Title level={2}>264<Icon name={CHANNELS_ICONS[channel] as IconName} /> {title}265{renderedTags && (266<span style={{ float: "right" }}>{renderedTags}</span>267)}268</Title>269{renderFuture()}270{renderHidden()}271<Markdown value={text} style={{ ...style, minHeight: "20vh" }} />272273<Flex align="baseline" justify="space-between" wrap="wrap">274{url && (275<Paragraph style={{ textAlign: "center" }}>276{readMoreLink(false, true)}277</Paragraph>278)}279<Paragraph280style={{281fontWeight: "bold",282textAlign: "center",283}}284>285<Space size="middle" direction="horizontal">286{showEdit ? editLink() : undefined}287{showShareLinks ? shareLinks(true) : undefined}288</Space>289</Paragraph>290{renderHistory()}291</Flex>292</>293);294} else {295return (296<>297<Card298title={renderTitle()}299style={STYLE}300extra={extra()}301actions={actions()}302>303{renderFuture()}304{renderHidden()}305<Markdown value={text} style={style} />306</Card>307</>308);309}310}311312interface TagListProps {313tags?: string[];314onTagClick?: (tag: string) => void;315style?: CSS;316styleTag?: CSS;317mode: "news" | "event";318}319320export function TagList({321tags,322onTagClick,323style,324styleTag,325mode,326}: TagListProps) {327if (tags == null || !Array.isArray(tags) || tags.length === 0) return null;328329const router = useRouter();330331function onTagClickStandalone(tag: string) {332router.push(`/news?tag=${tag}`);333}334335function onClick(tag) {336switch (mode) {337case "news":338(onTagClick ?? onTagClickStandalone)(tag);339case "event":340return;341default:342unreachable(mode);343}344}345346function getStyle(): CSS {347return {348...(mode === "news" ? { cursor: "pointer" } : {}),349...styleTag,350};351}352353return (354<Space size={[0, 4]} wrap={false} style={style}>355{tags.sort().map((tag) => (356<Tag357color={getRandomColor(tag)}358key={tag}359style={getStyle()}360onClick={() => onClick(tag)}361>362{tag}363</Tag>364))}365</Space>366);367}368369370