Skip to content

Commit 14493a3

Browse files
committed
embed
1 parent 0a5c46c commit 14493a3

File tree

8 files changed

+2986
-5437
lines changed

8 files changed

+2986
-5437
lines changed

package-lock.json

-5,334
This file was deleted.

package.json

+9-7
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,26 @@
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
6-
"dev": "next dev --turbopack",
6+
"dev": "next dev",
77
"build": "next build",
88
"start": "next start",
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12+
"jose": "^5.9.6",
13+
"next": "15.0.2",
1214
"react": "19.0.0-rc-02c0e824-20241028",
13-
"react-dom": "19.0.0-rc-02c0e824-20241028",
14-
"next": "15.0.2"
15+
"react-dom": "19.0.0-rc-02c0e824-20241028"
1516
},
1617
"devDependencies": {
17-
"typescript": "^5",
1818
"@types/node": "^20",
1919
"@types/react": "^18",
2020
"@types/react-dom": "^18",
21+
"eslint": "^8",
22+
"eslint-config-next": "15.0.2",
2123
"postcss": "^8",
2224
"tailwindcss": "^3.4.1",
23-
"eslint": "^8",
24-
"eslint-config-next": "15.0.2"
25-
}
25+
"typescript": "^5"
26+
},
27+
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
2628
}

src/app/[continentCode]/page.tsx

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { ObservableEmbed } from "@/components/observableEmbed/server";
2+
3+
export default async function Home({ params }: {params: Promise<{continentCode: string}>}) {
4+
const { continentCode } = await params;
5+
return (
6+
<div>
7+
<ObservableEmbed
8+
module={`https://observablehq.observablehq.cloud/olympian-embeds/continent/${continentCode.toUpperCase()}/chart.js`}
9+
importName="MedalsChart"
10+
/>
11+
</div>
12+
);
13+
}

src/app/layout.tsx

+20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Metadata } from "next";
22
import localFont from "next/font/local";
33
import "./globals.css";
4+
import Link from "next/link";
45

56
const geistSans = localFont({
67
src: "./fonts/GeistVF.woff",
@@ -23,11 +24,30 @@ export default function RootLayout({
2324
}: Readonly<{
2425
children: React.ReactNode;
2526
}>) {
27+
const navLinks = [
28+
{ name: "Overview", url: "/" },
29+
{ name: "Africa", url: "/afr" },
30+
{ name: "Americas", url: "/ame" },
31+
{ name: "Asia", url: "/asi" },
32+
{ name: "Europe", url: "/eur" },
33+
{ name: "Oceania", url: "/oce" },
34+
];
35+
2636
return (
2737
<html lang="en">
2838
<body
2939
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
3040
>
41+
<nav className="p-4">
42+
<ul className="flex gap-4">
43+
{navLinks.map((nav) => (
44+
<li key={nav.url} className="list-none">
45+
<Link href={nav.url} className="underline">{nav.name}</Link>
46+
</li>
47+
))}
48+
</ul>
49+
</nav>
50+
3151
{children}
3252
</body>
3353
</html>

src/app/page.tsx

+7-96
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,12 @@
1-
import Image from "next/image";
1+
import { ObservableEmbed } from "@/components/observableEmbed/server";
22

3-
export default function Home() {
3+
export default async function Home() {
44
return (
5-
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
6-
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
7-
<Image
8-
className="dark:invert"
9-
src="/next.svg"
10-
alt="Next.js logo"
11-
width={180}
12-
height={38}
13-
priority
14-
/>
15-
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
16-
<li className="mb-2">
17-
Get started by editing{" "}
18-
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
19-
src/app/page.tsx
20-
</code>
21-
.
22-
</li>
23-
<li>Save and see your changes instantly.</li>
24-
</ol>
25-
26-
<div className="flex gap-4 items-center flex-col sm:flex-row">
27-
<a
28-
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
29-
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
30-
target="_blank"
31-
rel="noopener noreferrer"
32-
>
33-
<Image
34-
className="dark:invert"
35-
src="/vercel.svg"
36-
alt="Vercel logomark"
37-
width={20}
38-
height={20}
39-
/>
40-
Deploy now
41-
</a>
42-
<a
43-
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
44-
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
45-
target="_blank"
46-
rel="noopener noreferrer"
47-
>
48-
Read our docs
49-
</a>
50-
</div>
51-
</main>
52-
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
53-
<a
54-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
55-
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
56-
target="_blank"
57-
rel="noopener noreferrer"
58-
>
59-
<Image
60-
aria-hidden
61-
src="/file.svg"
62-
alt="File icon"
63-
width={16}
64-
height={16}
65-
/>
66-
Learn
67-
</a>
68-
<a
69-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
70-
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
71-
target="_blank"
72-
rel="noopener noreferrer"
73-
>
74-
<Image
75-
aria-hidden
76-
src="/window.svg"
77-
alt="Window icon"
78-
width={16}
79-
height={16}
80-
/>
81-
Examples
82-
</a>
83-
<a
84-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
85-
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
86-
target="_blank"
87-
rel="noopener noreferrer"
88-
>
89-
<Image
90-
aria-hidden
91-
src="/globe.svg"
92-
alt="Globe icon"
93-
width={16}
94-
height={16}
95-
/>
96-
Go to nextjs.org →
97-
</a>
98-
</footer>
5+
<div>
6+
<ObservableEmbed
7+
module="https://observablehq.observablehq.cloud/olympian-embeds/medals-chart.js"
8+
importName="MedalsChart"
9+
/>
9910
</div>
10011
);
10112
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"use client";
2+
import { useEffect, useRef } from "react";
3+
4+
interface ObservableEmbedProps {
5+
module: string;
6+
importName: string;
7+
}
8+
9+
export const ObservableEmbedClient: React.FC<ObservableEmbedProps> = ({
10+
module,
11+
importName,
12+
}) => {
13+
const ref = useRef<HTMLDivElement | null>(null);
14+
15+
useEffect(() => {
16+
(async () => {
17+
const target = ref.current;
18+
if (!target) return;
19+
const mod = await import(/* webpackIgnore: true */ module);
20+
const component = mod[importName];
21+
const element = component instanceof Function ? await component() : component;
22+
target.innerHTML = "";
23+
target.append(element);
24+
})();
25+
}, [importName, module]);
26+
27+
return <div ref={ref} />;
28+
};
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import "server-only";
2+
import { SignJWT, importPKCS8 } from "jose";
3+
import { ObservableEmbedClient } from "./client";
4+
5+
const SIGNATURE_ALIGN_MS = 5 * 60 * 1000;
6+
const SIGNATURE_VALIDITY_MS = SIGNATURE_ALIGN_MS * 2;
7+
8+
const privateKey = process.env.EMBED_PRIVATE_KEY
9+
? await importPKCS8(process.env.EMBED_PRIVATE_KEY, "EdDSA")
10+
: null;
11+
12+
interface ObservableEmbedProps {
13+
module: string;
14+
importName: string
15+
}
16+
17+
export const ObservableEmbed: React.FC<ObservableEmbedProps> = async ({ module, importName }) => {
18+
const signedUrl = await signUrl(module);
19+
return <ObservableEmbedClient module={signedUrl.href} importName={importName} />;
20+
}
21+
22+
async function signUrl(url: string | URL): Promise<URL> {
23+
if (typeof url === "string") {
24+
url = new URL(url);
25+
}
26+
if (!privateKey) {
27+
console.warn("No private key available for signing");
28+
return url;
29+
}
30+
const now = Date.now();
31+
const notBefore = now - (now % SIGNATURE_ALIGN_MS);
32+
const notAfter = notBefore + SIGNATURE_VALIDITY_MS;
33+
const token = await new SignJWT({"urn:observablehq:path": url.pathname})
34+
.setProtectedHeader({alg: "EdDSA"})
35+
.setSubject("nextjs-example")
36+
.setNotBefore(notBefore / 1000)
37+
.setExpirationTime(notAfter / 1000)
38+
.sign(privateKey);
39+
url.searchParams.set("token", token);
40+
return url;
41+
}

0 commit comments

Comments
 (0)