on master help sign in
Merge pull request #1896 from urbit/mp/btc
b8bd1bfbioy-casares
components/EventPreview.js+3−4
@@ -41,11 +41,10 @@ export default function EventPreview({ event, className, big }) {
41 )}
41 )}
42 >
42 >
43 <div
43 <div
44− className={`grow-1 flex ${
44+ className={`grow-1 flex ${big ? "justify-center" : ""
45− big ? "justify-center" : ""
46− } flex-col h-full`}
45+ } flex-col h-full`}
47 >
46 >
48− <h3 className={`${blackText} mb-2`}>{event.title}</h3>
47+ <h3 className={`${blackText} mb-2`} id={event.title}>{event.title}</h3>
49 <p className={blackText + " truncate text-sm"}>
48 <p className={blackText + " truncate text-sm"}>
50 {event.description}
49 {event.description}
51 </p>
50 </p>
content/bitcoin.mdnew file+125
@@ -0,0 +1,125 @@
1++++
2+title = "Bitcoin Development on Urbit"
3++++
4+
5+### Overview
6+
7+Urbit OS currently ships with our Bitcoin wallet, %btc-wallet, released in 2021,
8+and our Lightning implementation, %volt, is almost ready for live use on the network. Several integrations with the app ecosystem are planned immediately following its release, including subscription publishing, social payments, and more.
9+
10+**The Urbit Advantage**
11+
12+- Composability: Modular userspace applications expose a consistent API format to each other locally and over the network, making seamless integration of Lightning payments, Taro assets, and other protocols easy
13+- Networking and identity: Exactly-once message delivery, NAT traversal, and E2E encryption come built into Urbit's P2P protocol, and Urbit identities are sybil-resistant, human-readable network addresses bound to BIP32 extended public keys
14+- Efficient, secure development: Urbit OS is strictly typed and purely functional, and apps have reproducible builds on all client machines, removing much of the complexity, risk, and pain points from development
15+
16+**Getting Involved**
17+
18+Interested in contributing to or supporting Bitcoin development on Urbit? GitHub repos for individual projects and information on the Urbit Foundation grant program are linked below, and dedicated Urbit groups and Bitcoin node operators on Urbit are listed in the Community section. You can also reach out to current contributors ~tondes-sitrym and ~pindet-timmut on Urbit or at urbitcoin@protonmail.com.
19+
20+### Wallet
21+
22+Urbit's Bitcoin wallet consists of two agents: %btc-provider, which communicates with an external full node; and %btc-wallet, which handles transaction generation and signing, and uses a %btc-provider instance (on the same ship or elsewhere on the Urbit network) to broadcast transactions and receive data from the Bitcoin network.
23+
24+Wallet users can receive payments using the xpub associated with their Urbit ID, allowing Bitcoin payments to be sent between ships with a persistent, human-readable network address without compromising on-chain privacy.
25+
26+**Resources**
27+
28+GitHub
29+
30+- Agents: [https://github.com/urbit/urbit/tree/master/pkg/bitcoin](https://github.com/urbit/urbit/tree/master/pkg/bitcoin)
31+- UI: [https://github.com/urbit/urbit/tree/master/pkg/btc-wallet](https://github.com/urbit/urbit/tree/master/pkg/btc-wallet)
32+- Provider utilities: [https://github.com/urbit/urbit-bitcoin-rpc](https://github.com/urbit/urbit-bitcoin-rpc)
33+
34+Writeups
35+
36+- Grant proposal: [https://urbit.org/grants/bitcoin-full-node-provider-and-wallet](https://urbit.org/grants/bitcoin-full-node-provider-and-wallet)
37+- Architecture document: [https://gist.github.com/jalehman/e0c91071427ca4c349c0673f376945cb](https://gist.github.com/jalehman/e0c91071427ca4c349c0673f376945cb)
38+- Provider setup guide: [https://subject.network/posts/pi-fullnode-urbit/#bitcoind](https://subject.network/posts/pi-fullnode-urbit/#bitcoind)
39+
40+
41+**Continuing Work**
42+
43+The current %btc-wallet implementation is not a modern BIP-157/158 light client. While users who depend on another ship's %btc-provider/full node for communication with the Bitcoin network do not risk loss of on-chain funds, their lack of visibility on the network will be more problematic for those also using it with Lightning. Funding is available for developers interested in contributing: reach out to the Urbit Foundation, ~tondes-sitrym on Urbit, or urbitcoin@protonmail.com.
44+
45+### Lightning
46+
47+Volt, Urbit's Lightning implementation, like our Bitcoin wallet consists of two agents: %volt, the primary agent that manages channels, payments, invoices, and chain monitoring; and %volt-provider, which connects to an external LND node for handling payments with endpoints outside the Urbit network.
48+
49+All Volt nodes will run %volt, and be able to operate channels with Urbit peers without maintaining an LND instance or depending on another ship that does. To transact with non-Urbit Lightning peers, and in the short term (see below) to perform multi-hop payments inside Urbit, they will need to open a channel with a node that uses %volt-provider to connect with LND. Chain monitoring likewise requires connecting to a local or remote %btc-provider agent (see Wallet above). For more details on the network architecture, see the grant proposal linked below.
50+
51+Volt implements BOLTs 2, 3, 5, and 11, and in the near future will add onion routing (variants on BOLTs 4 and 7). Because %ames, Urbit's networking module, already handles transport, guarantees required message delivery properties, and furnishes persistent, unique 'domain names', implementation of BOLTs 1, 8, and 9 is unnecessary for Volt nodes.
52+
53+Beta release of Volt is planned for the end of 2022, and additional features including multi-hop/onion routing implementation are proposed for completion by mid-2023.
54+
55+**Resources**
56+
57+GitHub: [https://github.com/dasrun-fadben/volt/tree/on-chain](https://github.com/dasrun-fadben/volt/tree/on-chain)
58+
59+Initial grant proposal and spec: [https://urbit.org/grants/volt-lightning-on-urbit](https://urbit.org/grants/volt-lightning-on-urbit)
60+
61+Proposal for further work: [https://urbit.org/grants/lightning-development](https://urbit.org/grants/lightning-development)
62+
63+**Continuing Work**
64+
65+The primary element currently missing from Volt is onion routing of multi-hop payments for full privacy and efficiency of payments to and from nodes that don't also operate LND. This has been partially specified in the second proposal linked above: funding and collaborators are available for this as well as work described there on application integrations, submarine swaps, and more, so interested developers should take a look and get in touch.
66+
67+### Roadmap
68+
69+**2022**
70+
71+- *Volt Beta Release*
72+Final work on channel sweeps, remaining safety checks, and UI development is currently ongoing and expected to be complete by EOY 2022.
73+
74+**2023**
75+
76+- *Initial Ecosystem Integrations*
77+ - Submarine swaps: Implementation of the PeerSwap protocol, fallback use of the Loop API, and UI targeted at merchant use-cases. Funded, expected completion in spring 2023.
78+ - Apps: Integration of Lightning subscription and one-time paygates into Groups, social payments and %volt-provider discovery with %pals, and app tipjar module. Specified and proposed for grant funding.
79+- *Lightning Routing*
80+Payments with multiple intra-Urbit hops and via multiple %volt-provider channels are the last remaining component for full-featured and private payments with Volt. Specified and proposed for grant funding.
81+- *Wallet Improvements*
82+Work on feature, security, and UI upgrades for the Bitcoin wallet and node provider, aimed at making the application a full-featured and BIP157/158-compliant light client, should begin before EOY 2023.
83+
84+**Future**
85+
86+- *Taro Implementation*
87+Implementation of Taproot Asset Representation Overlay, with an initial focus on using nonfungible Taro assets to represent credentials, permissions, and reputation of Urbit identities.
88+- *Atomic Streaming Payments*
89+ - Integration of Volt payments with streaming apps like %radio and the Holium WebRTC client via HTLC-DASH or similar implementation.
90+ - Generic protocol for non-media pay-to-stream data
91+
92+### Grants
93+
94+The Urbit Foundation operates a grant program aimed at new Urbit developers, network public goods, and community development. Work on the Bitcoin wallet, Lightning implementation, and submarine swaps for Volt (Urbit Lightning) users all has received funding from UF, and interesting non-technical proposals with social value to the network are also considered.
95+
96+Grants homepage: [https://urbit.org/grants](https://urbit.org/grants)
97+
98+Past Bitcoin-related grants
99+
100+- [https://urbit.org/grants/bitcoin-full-node-provider-and-wallet](https://urbit.org/grants/bitcoin-full-node-provider-and-wallet)
101+- [https://urbit.org/grants/volt-lightning-on-urbit](https://urbit.org/grants/volt-lightning-on-urbit)
102+- [https://urbit.org/grants/lightning-development](https://urbit.org/grants/lightning-development)
103+
104+## Bitcoiners on Urbit
105+
106+### Groups
107+
108+Urbitcoiners: ~pindet-timmut/urbitcoiners
109+
110+### Providers
111+
112+**Bitcoin Full Nodes**
113+
114+~tirrel
115+~zod
116+
117+**Lightning Nodes**
118+
119+~tondes-sitrym
120+~bosdys
121+
122+### Guides
123+
124+Running %btc-provider with a full node on Raspberry Pi: [https://subject.network/posts/pi-fullnode-urbit/](https://subject.network/posts/pi-fullnode-urbit/)
125+
content/blog/interplanetary_commerce.md+1
@@ -2,6 +2,7 @@
2title = "Interplanetary Commerce"
2title = "Interplanetary Commerce"
3date = "2021-04-08"
3date = "2021-04-08"
4description = "OS-level commercial primitives."
4description = "OS-level commercial primitives."
5+tags = ["bitcoin"]
5
6
6[extra]
7[extra]
7author = "Christian Langalis"
8author = "Christian Langalis"
content/blog/urbit-and-bitcoin.md+1−1
@@ -2,7 +2,7 @@
2title = "Urbit and Bitcoin"
2title = "Urbit and Bitcoin"
3description = "A sound money deserves a sound computer."
3description = "A sound money deserves a sound computer."
4date = "2019-10-16"
4date = "2019-10-16"
5−
5+tags = ["bitcoin"]
6[extra]
6[extra]
7author = "Christian Langalis"
7author = "Christian Langalis"
8ship = "~pindet-timmut"
8ship = "~pindet-timmut"
content/events/2021-04-27-bitcoin-launch.md+1
@@ -9,6 +9,7 @@ description = "Bitcoin is coming to Urbit! With the upcoming release of the Bitc
9timezone = "America/Los_Angeles"
9timezone = "America/Los_Angeles"
10starts = "2021-04-27T20:00:00.000-07:00"
10starts = "2021-04-27T20:00:00.000-07:00"
11dark = true
11dark = true
12+tags = ["bitcoin"]
12
13
13[[hosts]]
14[[hosts]]
14name = "Urbit Foundation"
15name = "Urbit Foundation"
lib/constants.js+1
@@ -24,6 +24,7 @@ export const eventKeys = [
24 "dark",
24 "dark",
25 "timezone",
25 "timezone",
26 "darken_image",
26 "darken_image",
27+ "tags"
27];
28];
28
29
29export const postKeys = [
30export const postKeys = [
package-lock.json+7−7
@@ -12,7 +12,7 @@
12 "@socialgouv/matomo-next": "^1.3.0",
12 "@socialgouv/matomo-next": "^1.3.0",
13 "@tailwindcss/aspect-ratio": "^0.4.0",
13 "@tailwindcss/aspect-ratio": "^0.4.0",
14 "@tlon/sigil-js": "^1.4.5",
14 "@tlon/sigil-js": "^1.4.5",
15− "@urbit/foundation-design-system": "0.6.2",
15+ "@urbit/foundation-design-system": "0.6.3",
16 "@urbit/markdoc": "^0.1.4",
16 "@urbit/markdoc": "^0.1.4",
17 "axios": "^0.26.1",
17 "axios": "^0.26.1",
18 "buffer": "^6.0.3",
18 "buffer": "^6.0.3",
@@ -358,9 +358,9 @@
358 "dev": true
358 "dev": true
359 },
359 },
360 "node_modules/@urbit/foundation-design-system": {
360 "node_modules/@urbit/foundation-design-system": {
361− "version": "0.6.2",
361+ "version": "0.6.3",
362− "resolved": "https://registry.npmjs.org/@urbit/foundation-design-system/-/foundation-design-system-0.6.2.tgz",
362+ "resolved": "https://registry.npmjs.org/@urbit/foundation-design-system/-/foundation-design-system-0.6.3.tgz",
363− "integrity": "sha512-VTQfbM6nkRksaa6IzEhuMDfK+rYuGxcQWK6sovV/J32kQVuJFvO0K11RuI9Ajut1lwlu5C/ngmzo3KIObOBlMw==",
363+ "integrity": "sha512-2OQ4BTxufBYRlHJlnj0JDL+qrqfYoldfQI0/0cDVHgRNzJ0clMBpLLMSDsq90cjuJqqEi3RduYO/DhixYosnPA==",
364 "dependencies": {
364 "dependencies": {
365 "@iarna/toml": "^2.2.5",
365 "@iarna/toml": "^2.2.5",
366 "core-js": "^3.23.1",
366 "core-js": "^3.23.1",
@@ -3323,9 +3323,9 @@
3323 "dev": true
3323 "dev": true
3324 },
3324 },
3325 "@urbit/foundation-design-system": {
3325 "@urbit/foundation-design-system": {
3326− "version": "0.6.2",
3326+ "version": "0.6.3",
3327− "resolved": "https://registry.npmjs.org/@urbit/foundation-design-system/-/foundation-design-system-0.6.2.tgz",
3327+ "resolved": "https://registry.npmjs.org/@urbit/foundation-design-system/-/foundation-design-system-0.6.3.tgz",
3328− "integrity": "sha512-VTQfbM6nkRksaa6IzEhuMDfK+rYuGxcQWK6sovV/J32kQVuJFvO0K11RuI9Ajut1lwlu5C/ngmzo3KIObOBlMw==",
3328+ "integrity": "sha512-2OQ4BTxufBYRlHJlnj0JDL+qrqfYoldfQI0/0cDVHgRNzJ0clMBpLLMSDsq90cjuJqqEi3RduYO/DhixYosnPA==",
3329 "requires": {
3329 "requires": {
3330 "@iarna/toml": "^2.2.5",
3330 "@iarna/toml": "^2.2.5",
3331 "core-js": "^3.23.1",
3331 "core-js": "^3.23.1",
package.json+1−1
@@ -21,7 +21,7 @@
21 "@socialgouv/matomo-next": "^1.3.0",
21 "@socialgouv/matomo-next": "^1.3.0",
22 "@tailwindcss/aspect-ratio": "^0.4.0",
22 "@tailwindcss/aspect-ratio": "^0.4.0",
23 "@tlon/sigil-js": "^1.4.5",
23 "@tlon/sigil-js": "^1.4.5",
24− "@urbit/foundation-design-system": "0.6.2",
24+ "@urbit/foundation-design-system": "0.6.3",
25 "@urbit/markdoc": "^0.1.4",
25 "@urbit/markdoc": "^0.1.4",
26 "axios": "^0.26.1",
26 "axios": "^0.26.1",
27 "buffer": "^6.0.3",
27 "buffer": "^6.0.3",
pages/[slug].js+1−1
@@ -31,7 +31,7 @@ export async function getStaticProps({ params }) {
31}
31}
32
32
33export async function getStaticPaths() {
33export async function getStaticPaths() {
34− const posts = getPostSlugs("/");
34+ const posts = getPostSlugs("/").filter((e) => e !== 'bitcoin.md');
35
35
36 const slugs = posts.map((e) => e.slice(0, -3));
36 const slugs = posts.map((e) => e.slice(0, -3));
37
37
pages/bitcoin.jsnew file+96
@@ -0,0 +1,96 @@
1+import { Container, SingleColumn, Section, IntraNav, TableOfContents, getAllPosts, getPage, TwoUp, Markdown } from "@urbit/foundation-design-system";
2+import Head from "next/head";
3+import Link from "next/link";
4+import Meta from "../components/Meta";
5+import Header from "../components/Header";
6+import PostPreview from "../components/PostPreview"
7+import { join } from 'path';
8+import EventPreview from "../components/EventPreview";
9+import { getAllEvents } from "../lib/lib";
10+
11+import { eventKeys } from "../lib/constants";
12+
13+
14+export default function Bitcoin({ search, posts, data, markdown, events }) {
15+ const post = {
16+ title: "Bitcoin on Urbit: The Sovereign Stack",
17+ description: "Sound digital money and sound computing need each other to achieve a free and prosperous future, and Urbit and Bitcoin's common paradigm of long-termism and self-sovereignty will get us there."
18+ }
19+
20+ return (
21+ <Container>
22+ <Head>
23+ <title>Bitcoin on Urbit: The Sovereign Stack • urbit.org</title>
24+ {Meta(post)}
25+ </Head>
26+ <IntraNav ourSite="https://urbit.org" search={search} />
27+ <SingleColumn>
28+ <Header search={search} />
29+ <Section>
30+ <h1>{post.title}</h1>
31+ </Section>
32+ <Section className="flex space-x-12">
33+ <div className="min-w-0">
34+ <div>
35+ <p>Sound digital money and sound computing need each other to achieve a free and prosperous future, and Urbit and Bitcoin's common paradigm of long-termism and self-sovereignty will get us there. Check out the resources on this page to learn about current Lightning Network development on Urbit and join the Urbitcoiner community.</p>
36+ </div>
37+ <div className="mt-32">
38+ <h2 className="mb-8" id="blog">Blog Posts</h2>
39+ <TwoUp>
40+ <PostPreview post={posts[0]} key={posts[0].slug} />
41+ {posts[1] ? <PostPreview post={posts[1]} key={posts[1].slug} /> : <div />}
42+ </TwoUp>
43+ <Link href="/blog?tag=bitcoin">
44+ <a className="bg-green-400 text-white button-lg w-fit">
45+ View All
46+ </a>
47+ </Link>
48+ </div>
49+ <div className="mt-32">
50+ <h2 className="mb-8" id="events">Events</h2>
51+ <TwoUp>
52+ <EventPreview event={events[0]} className="mb-8" />
53+ {events[1] ? <EventPreview event={events[1]} className="mb-8" /> : <div />}
54+ </TwoUp>
55+ <Link href="/events?tag=bitcoin">
56+ <a className="bg-wall-600 text-white button-lg w-fit">
57+ View All
58+ </a>
59+ </Link>
60+ </div>
61+ <div className="mt-32">
62+ <h2 className="mb-8" id="title">{data.title}</h2>
63+ <div className="markdown">
64+ <Markdown.render content={JSON.parse(markdown)} />
65+ </div>
66+ </div>
67+ </div>
68+ <TableOfContents markdown={false} />
69+ </Section>
70+ </SingleColumn>
71+ </Container>
72+ )
73+}
74+
75+export async function getStaticProps() {
76+ // Latest blog posts
77+ const posts = getAllPosts(
78+ ["title", "slug", "date", "description", "tags", "extra"],
79+ "blog",
80+ "date"
81+ ).filter((e) => e.tags?.includes("bitcoin"));
82+
83+ const events = getAllEvents(eventKeys, "events").filter((e) => e?.tags?.includes("bitcoin"));
84+
85+ const { data, content } = getPage(join(process.cwd(), 'content', 'bitcoin'));
86+
87+ const markdown = JSON.stringify(Markdown.parse({ post: { content } }));
88+ return {
89+ props: {
90+ posts,
91+ data,
92+ markdown,
93+ events
94+ }
95+ }
96+}
pages/blog.js+26−13
@@ -14,8 +14,14 @@ import Footer from "../components/Footer";
14import Header from "../components/Header";
14import Header from "../components/Header";
15import BackgroundImage from "../components/BackgroundImage";
15import BackgroundImage from "../components/BackgroundImage";
16import { contact } from "../lib/constants";
16import { contact } from "../lib/constants";
17+import { useRouter } from "next/router";
17
18
18export default function Blog({ posts, search }) {
19export default function Blog({ posts, search }) {
20+ const router = useRouter();
21+ const { tag } = router.query;
22+
23+ const filteredPosts = tag ? posts.filter((e) => e?.tags?.includes(tag)) : posts;
24+
19 const post = {
25 const post = {
20 title: "Blog",
26 title: "Blog",
21 description:
27 description:
@@ -31,20 +37,27 @@ export default function Blog({ posts, search }) {
31 <SingleColumn>
37 <SingleColumn>
32 <Header search={search} />
38 <Header search={search} />
33 <Section>
39 <Section>
40+ {tag ?
41+ <><h1 className="pb-16">{filteredPosts.length} posts tagged "{tag}"</h1>
42+ <p>
43+ For more posts, return to the <Link href="/blog">blog index</Link>.
44+ </p>
45+ </>
34− <h1 className="pb-16">Blog</h1>
46+ : <><h1 className="pb-16">Blog</h1>
35− <div className="md:columns-2 gap-x-8">
47+ <div className="md:columns-2 gap-x-8">
36− <p className="pb-6 md:pb-0">
48+ <p className="pb-6 md:pb-0">
37− Stories from the broader Urbit community, the Urbit Foundation,
49+ Stories from the broader Urbit community, the Urbit Foundation,
38− and the many people contributing to Urbit.
50+ and the many people contributing to Urbit.
39− </p>
51+ </p>
40− <p>
52+ <p>
41− <a href={contact.newsletter}>Subscribe</a> to the Urbit Newsletter
53+ <a href={contact.newsletter}>Subscribe</a> to the Urbit Newsletter
42− for regular updates, including new blog posts and events.
54+ for regular updates, including new blog posts and events.
43− </p>
55+ </p>
44− </div>
56+ </div>
57+ </>}
45 </Section>
58 </Section>
46 <Section>
59 <Section>
47− {posts.map((post) => {
60+ {filteredPosts.map((post) => {
48 const date = generateDisplayDate(post.date);
61 const date = generateDisplayDate(post.date);
49 return (
62 return (
50 <div key={post.slug} className="mb-20 cursor-pointer">
63 <div key={post.slug} className="mb-20 cursor-pointer">
@@ -107,7 +120,7 @@ export default function Blog({ posts, search }) {
107
120
108export async function getStaticProps() {
121export async function getStaticProps() {
109 const posts = getAllPosts(
122 const posts = getAllPosts(
110− ["title", "slug", "date", "description", "extra"],
123+ ["title", "slug", "date", "description", "tags", "extra"],
111 "blog",
124 "blog",
112 "date"
125 "date"
113 );
126 );
pages/events.js+17−3
@@ -15,11 +15,15 @@ import Footer from "../components/Footer";
15import Header from "../components/Header";
15import Header from "../components/Header";
16import EventPreview from "../components/EventPreview";
16import EventPreview from "../components/EventPreview";
17import { getAllEvents, pair } from "../lib/lib";
17import { getAllEvents, pair } from "../lib/lib";
18+import { useRouter } from "next/router";
18
19
19import { eventKeys } from "../lib/constants";
20import { eventKeys } from "../lib/constants";
20
21
21export default function Events({ events, search }) {
22export default function Events({ events, search }) {
22 const [selected, setSelected] = useState("All");
23 const [selected, setSelected] = useState("All");
24+ const router = useRouter();
25+ const { tag } = router.query;
26+
23 const post = {
27 const post = {
24 title: "Events",
28 title: "Events",
25 description: "In-person, remote, and recorded events about Urbit.",
29 description: "In-person, remote, and recorded events about Urbit.",
@@ -27,6 +31,13 @@ export default function Events({ events, search }) {
27
31
28 const now = DateTime.now();
32 const now = DateTime.now();
29
33
34+ const tagFilter = (event) => {
35+ if (tag) {
36+ return event?.tags?.includes(tag)
37+ } else {
38+ return true;
39+ }
40+ }
30 const selectionFilter = (event) => {
41 const selectionFilter = (event) => {
31 if (selected === "All") {
42 if (selected === "All") {
32 return true;
43 return true;
@@ -38,7 +49,7 @@ export default function Events({ events, search }) {
38 };
49 };
39
50
40 const pastEvents = pair(
51 const pastEvents = pair(
41− events.filter(selectionFilter).filter((event) => {
52+ events.filter(selectionFilter).filter(tagFilter).filter((event) => {
42 const ends = generateRealtimeDate(event.ends);
53 const ends = generateRealtimeDate(event.ends);
43 return ends < now;
54 return ends < now;
44 })
55 })
@@ -47,6 +58,7 @@ export default function Events({ events, search }) {
47 const futureEvents = pair(
58 const futureEvents = pair(
48 events
59 events
49 .filter(selectionFilter)
60 .filter(selectionFilter)
61+ .filter(tagFilter)
50 .filter((event) => {
62 .filter((event) => {
51 const starts = generateRealtimeDate(event.starts);
63 const starts = generateRealtimeDate(event.starts);
52 return starts > now;
64 return starts > now;
@@ -64,7 +76,7 @@ export default function Events({ events, search }) {
64 );
76 );
65
77
66 const happeningNow = pair(
78 const happeningNow = pair(
67− events.filter(selectionFilter).filter((event) => {
79+ events.filter(selectionFilter).filter(tagFilter).filter((event) => {
68 const starts = generateRealtimeDate(event.starts);
80 const starts = generateRealtimeDate(event.starts);
69 const ends = generateRealtimeDate(event.ends);
81 const ends = generateRealtimeDate(event.ends);
70 return starts < DateTime.now() && ends > now;
82 return starts < DateTime.now() && ends > now;
@@ -96,7 +108,9 @@ export default function Events({ events, search }) {
96 <Header search={search} />
108 <Header search={search} />
97 <Section>
109 <Section>
98 <div className="measure">
110 <div className="measure">
111+ {tag
112+ ? <h1 className="pb-16">{events.filter(tagFilter).length} events tagged "{tag}"</h1>
99− <h1 className="pb-16">Events</h1>
113+ : <h1 className="pb-16">Events</h1>}
100 <div className="flex space-x-2">
114 <div className="flex space-x-2">
101 <SelectedButton selection="All" text="All Events" />
115 <SelectedButton selection="All" text="All Events" />
102 <SelectedButton selection="In-person" text="In-person" />
116 <SelectedButton selection="In-person" text="In-person" />