Skip to content

Commit 5b8ed82

Browse files
feat: press & newsroom (#190)
* fix: missing li tag * chore: remove unused code * feat: add news * wip * fix: sitemap * chore: self review * chore: self review * chore: self review --------- Co-authored-by: Tom Schönmann <tom@wild.as>
1 parent fa9048a commit 5b8ed82

24 files changed

+645
-186
lines changed

web/app/modules/article.tsx

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { PropsWithChildren } from "react";
2+
import { mergeMeta } from "./meta";
3+
import { InfoPill } from "./info-pill";
4+
import clsx from "clsx";
5+
import { InfoCard } from "./info-card";
6+
7+
export type NewsArticleHandle = {
8+
slug: string;
9+
title: string;
10+
subline?: string;
11+
createdAt: string;
12+
updatedAt?: string;
13+
authors: string[];
14+
categories: Array<"general" | "announcement">;
15+
sections: Array<{ name: string; fragment: string }>;
16+
};
17+
18+
export const composeArticleMeta = mergeMeta(({ matches }) => {
19+
const article = findArticleMatch(matches);
20+
return [
21+
{ title: `${article?.title} | CRAN/E` },
22+
{ name: "description", content: article?.subline },
23+
];
24+
});
25+
26+
export function findArticleMatch(
27+
matches: Array<{ handle?: unknown }>,
28+
): NewsArticleHandle | undefined {
29+
const match = matches.find(
30+
(match) =>
31+
match.handle &&
32+
(match.handle as { article?: NewsArticleHandle })?.article,
33+
);
34+
35+
return (match?.handle as { article?: NewsArticleHandle })?.article;
36+
}
37+
38+
export function ArticleSynopsis(
39+
props: PropsWithChildren<{
40+
createdAt: string;
41+
updatedAt?: string;
42+
authors: string[];
43+
}>,
44+
) {
45+
const { createdAt, updatedAt, authors, children } = props;
46+
47+
return (
48+
<section className="space-y-12">
49+
<p
50+
className={clsx(
51+
"inline-block bg-gradient-to-tl from-violet-9 to-purple-11 bg-clip-text font-semibold text-transparent dark:from-violet-10 dark:to-purple-7",
52+
"text-gray-dim mt-8 w-3/4 text-xl leading-relaxed md:w-2/3",
53+
)}
54+
>
55+
{children}
56+
</p>
57+
<footer className="flex gap-2">
58+
<InfoPill size="sm" label="Publication">
59+
<time dateTime={createdAt}>{createdAt}</time>
60+
{updatedAt && (
61+
<>
62+
{" "}
63+
(updated <time dateTime={updatedAt}>{updatedAt}</time>)
64+
</>
65+
)}
66+
</InfoPill>
67+
<InfoPill size="sm" label="Authors">
68+
{authors.join(", ")}
69+
</InfoPill>
70+
</footer>
71+
</section>
72+
);
73+
}
74+
75+
export function ProminentArticleImage(props: { src: string; caption: string }) {
76+
const { src, caption } = props;
77+
78+
return (
79+
<figure className="my-4 space-y-2 md:my-8">
80+
<img
81+
src={src}
82+
alt="Screenshot of the CRAN/E 2.0 start page"
83+
className="mx-auto md:max-w-prose"
84+
/>
85+
<figcaption className="text-gray-dim px-4 text-center text-xs">
86+
{caption}
87+
</figcaption>
88+
</figure>
89+
);
90+
}
91+
92+
export function ArticlePreviewInfoCard(
93+
props: PropsWithChildren<{
94+
headline: string;
95+
subline: string;
96+
createdAt: string;
97+
}>,
98+
) {
99+
const { headline, subline, createdAt, children } = props;
100+
101+
return (
102+
<InfoCard variant="none" className="relative isolate">
103+
<div className="grid gap-4 sm:min-h-60 sm:grid-cols-2">
104+
<div
105+
className={clsx(
106+
"absolute inset-0 bg-gradient-to-br from-violet-6 dark:from-violet-12",
107+
"opacity-0 transition-opacity duration-500 group-hover/card:opacity-100",
108+
)}
109+
/>
110+
<div className="z-10 space-y-1">
111+
<span className="text-gray-dim font-mono text-xs">{createdAt}</span>
112+
<h3 className="text-lg">{headline}</h3>
113+
<p className="text-gray-dim pt-2">{subline}</p>
114+
</div>
115+
<div
116+
className={clsx(
117+
"text-gray-dim relative -z-10 hidden overflow-hidden px-4 text-xl leading-relaxed opacity-50 sm:block",
118+
"after:absolute after:inset-x-0 after:bottom-0 after:h-full after:bg-gradient-to-t after:content-[''] after:dark:from-black",
119+
)}
120+
>
121+
{children}
122+
</div>
123+
</div>
124+
</InfoCard>
125+
);
126+
}

web/app/modules/binary-download-link.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const twGradient = cva({
2424
},
2525
});
2626

27-
export function BinaryDownloadListItem(props: Props) {
27+
export function BinaryDownloadLink(props: Props) {
2828
const { href, headline, os, arch, className, variant } = props;
2929

3030
return (

web/app/modules/contact-pill.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function ContactPill(props: Props) {
5151

5252
return (
5353
<div className={clsx("flex flex-col gap-4 sm:flex-row", className)}>
54-
<h4 className="shrink-0 text-lg">{name}</h4>
54+
<h3 className="shrink-0 text-lg">{name}</h3>
5555
<div className="flex flex-wrap gap-2">
5656
{isMaintainer ? (
5757
<InfoPill

web/app/modules/footer.tsx

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import { ExternalLink } from "./external-link";
33
import { cva, VariantProps } from "cva";
44
import { ReactNode } from "react";
55
import clsx from "clsx";
6+
import { RiGithubLine } from "@remixicon/react";
7+
8+
const BASE_ITEMS: Array<{ label: string; href: string }> = [
9+
{ label: "About", href: "/about" },
10+
{ label: "Privacy", href: "/privacy" },
11+
{ label: "Statistics", href: "/statistic" },
12+
{ label: "Newsroom", href: "/press/news" },
13+
];
614

715
const twBase = cva({
816
base: "text-sm flex flex-wrap items-center gap-6 text-gray-dim py-6 font-light",
@@ -33,32 +41,27 @@ export function Footer(props: Props) {
3341
<ul className={twBase({ variant, className })}>
3442
{start}
3543

36-
<li>
37-
<Link to="/about" className="underline-offset-4 hover:underline">
38-
About
39-
</Link>
40-
</li>
41-
<li>
42-
<Link to="/privacy" className="underline-offset-4 hover:underline">
43-
Privacy
44-
</Link>
45-
</li>
46-
<li>
47-
<Link to="/statistic" className="underline-offset-4 hover:underline">
48-
Statistics
49-
</Link>
50-
</li>
44+
{BASE_ITEMS.map(({ label, href }) => (
45+
<li key={label}>
46+
<Link to={href} className="underline-offset-4 hover:underline">
47+
{label}
48+
</Link>
49+
</li>
50+
))}
5151
<li>
5252
<ExternalLink
5353
href="https://github.com/flaming-codes/crane-app"
54-
className="underline-offset-4 hover:underline"
54+
className="underline-offset-4 hover:brightness-75"
5555
>
56-
Github
56+
<RiGithubLine size={16} />
57+
<span className="sr-only">Github</span>
5758
</ExternalLink>
5859
</li>
5960
{version && (
60-
<li className="text-gray-dim font-mono text-xs opacity-70">
61-
v{version}
61+
<li className="text-gray-dim font-mono text-xs opacity-70 hover:brightness-75">
62+
<ExternalLink href="https://github.com/flaming-codes/crane-app">
63+
v{version}
64+
</ExternalLink>
6265
</li>
6366
)}
6467
{end}

web/app/modules/header.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const twBase = cva({
1818
jade: "bg-gradient-to-bl from-jade-8 dark:from-jade-11",
1919
bronze: "bg-gradient-to-tl from-bronze-8 dark:from-bronze-11",
2020
sand: "bg-gradient-to-br from-sand-8 via-gold-6 dark:from-sand-11 dark:via-gold-12/60",
21+
amethyst:
22+
"bg-gradient-to-tl from-plum-7 via-violet-6 dark:from-plum-11 dark:via-violet-12",
2123
},
2224
},
2325
});

web/app/modules/info-card.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@ const twGradient = cva({
1717
base: "inset-0 absolute bg-gradient-to-tr -z-10 opacity-0 group-hover/card:opacity-100 transition-opacity duration-700",
1818
variants: {
1919
variant: {
20+
none: "",
2021
iris: "from-iris-4 dark:from-iris-11",
2122
ruby: "from-ruby-4 dark:from-ruby-11",
2223
jade: "from-jade-5 dark:from-jade-11",
2324
bronze: "from-bronze-6 dark:from-bronze-11",
2425
sand: "from-sand-8 via-gold-6 dark:from-sand-11 dark:via-gold-12",
26+
amethyst:
27+
"bg-gradient-to-tl from-plum-7 via-violet-6 dark:from-plum-11 dark:via-violet-12",
2528
},
2629
},
2730
});

web/app/modules/page-content-section.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
1-
import clsx from "clsx";
1+
import { cva, VariantProps } from "cva";
22
import { PropsWithChildren, ReactNode } from "react";
33

4-
type Props = PropsWithChildren<{
5-
headline?: ReactNode;
6-
subline?: ReactNode;
7-
fragment?: string;
8-
className?: string;
9-
}>;
4+
type Props = PropsWithChildren<
5+
VariantProps<typeof twBase> & {
6+
headline?: ReactNode;
7+
subline?: ReactNode;
8+
fragment?: string;
9+
className?: string;
10+
}
11+
>;
12+
13+
const twBase = cva({
14+
base: "flex flex-col gap-16",
15+
variants: {
16+
variant: {
17+
prose: "text-lg leading-loose text-gray-12 dark:text-gray-4",
18+
},
19+
},
20+
});
1021

1122
export function PageContentSection(props: Props) {
12-
const { children, headline, subline, fragment, className } = props;
23+
const { children, headline, subline, fragment, variant, className } = props;
1324

1425
return (
15-
<section className={clsx("flex flex-col gap-16", className)}>
26+
<section className={twBase({ variant, className })}>
1627
<div className="flex flex-col gap-3" id={fragment}>
1728
{headline ? (
1829
<h2 className="text-gray-normal text-xl font-semibold">{headline}</h2>

web/app/modules/sitemap.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ export async function mapDomainToSitemapData(
9393
return AuthorService.getAllSitemapAuthors();
9494

9595
default:
96-
throw new Error(`Invalid composition source: ${source}`);
96+
throw new Error(
97+
`[mapDomainToSitemapData] Invalid composition source: ${source}`,
98+
);
9799
}
98100
}
99101

@@ -105,6 +107,8 @@ export function mapComposerToDomain(source: string) {
105107
return composeAuthorUrl;
106108

107109
default:
108-
throw new Error(`Invalid composition source: ${source}`);
110+
throw new Error(
111+
`[mapComposerToDomain] Invalid composition source: ${source}`,
112+
);
109113
}
110114
}

web/app/root.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ export default function App() {
312312
start={
313313
<li>
314314
<Link to="/" className="underline-offset-4 hover:underline">
315-
Start
315+
Home
316316
</Link>
317317
</li>
318318
}

web/app/routes/$slug[.xml]._index.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ export async function loader(props: LoaderFunctionArgs) {
2424

2525
const today = getTodayLastmod();
2626

27-
if (slug === "sitemap-common.xml") {
27+
if (slug === "sitemap-common") {
2828
return new Response(
2929
`<?xml version="1.0" encoding="UTF-8" ?>
3030
<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">
3131
${composeUrlElement({ path: "", lastmod: today })}
32-
${composeUrlElement({ path: "/about", lastmod: "2022-08-20", changefreq: "monthly" })}
33-
${composeUrlElement({ path: "/how-to", lastmod: "2022-08-20", changefreq: "monthly" })}
32+
${composeUrlElement({ path: "/about", lastmod: "2024-10-29", changefreq: "yearly" })}
33+
${composeUrlElement({ path: "/privacy", lastmod: "2024-10-29", changefreq: "yearly" })}
34+
${composeUrlElement({ path: "/press/news", lastmod: "2024-10-29", changefreq: "monthly" })}
35+
${composeUrlElement({ path: "/press/news/crane-v2", lastmod: "2024-10-29", changefreq: "yearly" })}
3436
</urlset>`.trim(),
3537
{
3638
headers: {
@@ -41,7 +43,7 @@ export async function loader(props: LoaderFunctionArgs) {
4143
);
4244
}
4345

44-
if (slug === "sitemap-statistic.xml") {
46+
if (slug === "sitemap-statistic") {
4547
return new Response(
4648
`<?xml version="1.0" encoding="UTF-8" ?>
4749
<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">

web/app/routes/_page.about._index.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { json, Link, MetaFunction } from "@remix-run/react";
1+
import { Link } from "@remix-run/react";
22
import { PageContent } from "../modules/page-content";
33
import { PageContentSection } from "../modules/page-content-section";
44
import { AnchorLink, Anchors } from "../modules/anchors";
@@ -14,23 +14,16 @@ import {
1414
} from "@remixicon/react";
1515
import { Header } from "../modules/header";
1616
import { PlausibleChoicePillButton } from "../modules/plausible";
17+
import { mergeMeta } from "../modules/meta";
1718

1819
const anchors = ["Mission", "Team", "Analytics", "Source Code", "Licenses"];
1920

20-
export const handle = {
21-
hasFooter: true,
22-
};
23-
24-
export const meta: MetaFunction = () => {
21+
export const meta = mergeMeta(() => {
2522
return [
2623
{ title: "About | CRAN/E" },
2724
{ name: "description", content: "About the creators of CRAN/E" },
2825
];
29-
};
30-
31-
export function loader() {
32-
return json({});
33-
}
26+
});
3427

3528
export default function PrivacyPage() {
3629
return (

0 commit comments

Comments
 (0)