Skip to content

Commit 0ffe608

Browse files
authored
Merge pull request #2407 from urbit/tlat/jan-release
January Urbit.org Feature Release
2 parents 19f6861 + 2ce53b2 commit 0ffe608

13 files changed

Lines changed: 256 additions & 72 deletions

File tree

.claude/settings.local.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@
88
"Bash(for file in /Users/tlat/dev/urbit-foundation/urbit.org-reloaded/app/content/overview/running-urbit/*.md)",
99
"Bash(head:*)",
1010
"Bash(done)",
11-
"Bash(curl:*)"
11+
"Bash(curl:*)",
12+
"Bash(gh issue view:*)"
1213
],
1314
"deny": [],
1415
"ask": []
16+
},
17+
"enabledPlugins": {
18+
"github@claude-plugins-official": true
1519
}
1620
}

app/blog/page.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ export default async function BlogHome() {
107107
</SidebarElement>
108108
</SidebarSlot>
109109

110+
<Link href="/">
110111
<img src="/icons/digi-logo-1.svg" className="hidden md:block pb-4" />
112+
</Link>
111113
<div className="mb-32 text-xlarge gap-y-2 leading-[100%] max-w-[1200px] mx-auto">
112114
{Object.entries(yearGroups)
113115
.sort(([yearA], [yearB]) => yearB - yearA)

app/components/HeroSection.js

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"use client";
22

3+
import { useState } from "react";
34
import Link from "next/link";
45
import Image from "next/image";
6+
import { Modal } from "./Modal";
57

68
/**
79
* HeroSection - Full viewport width hero section
@@ -19,6 +21,8 @@ import Image from "next/image";
1921
* @param {Object} hero - Hero configuration object with all content
2022
*/
2123
export function HeroSection({ hero }) {
24+
const [isModalOpen, setIsModalOpen] = useState(false);
25+
2226
if (!hero) return null;
2327

2428
const {
@@ -65,7 +69,7 @@ export function HeroSection({ hero }) {
6569

6670
return (
6771
<section
68-
className="relative flex items-start md:pt-[15vh] min-h-screen md:min-h-[calc(100vh+300px)] z-0 hero-background"
72+
className="relative flex items-center justify-center md:items-start md:justify-start md:pt-[15vh] min-h-dvh md:min-h-[calc(100vh+300px)] z-0 hero-background"
6973
{...(backgroundImage && {
7074
style: {
7175
backgroundImage: getResponsiveBackgroundImage(backgroundImage),
@@ -122,7 +126,7 @@ export function HeroSection({ hero }) {
122126

123127

124128
{/* Content Container */}
125-
<div className="relative z-20 mx-[15px] md:ml-[5%] lg:ml-[10%] sm:mx-auto md:px-16 flex flex-col max-w-4xl xl:max-w-[60vw]">
129+
<div className="relative z-20 mx-[15px] md:ml-[5%] lg:ml-[10%] md:px-16 flex flex-col max-w-4xl xl:max-w-[60vw] py-[10vh] md:py-0">
126130
<div className="hidden md:block">
127131
<Image
128132
src="/icons/urbit-digi-accent-2.svg"
@@ -147,7 +151,7 @@ export function HeroSection({ hero }) {
147151
alt="urbit digi logo"
148152
width={90}
149153
height={90}
150-
className="mt-24 md:hidden"
154+
className="md:hidden"
151155
/>
152156
</div>
153157
)}
@@ -182,14 +186,25 @@ export function HeroSection({ hero }) {
182186

183187
{/* Secondary Mobile CTA */}
184188
{secondaryMobileCta && (
185-
<Link
186-
href={secondaryMobileCta.link}
187-
className="font-sans text-2xl flex w-fit items-center justify-center my-2 px-2 py-1
188-
bg-background text-accent-1 border border-accent-1 rounded-lg
189-
hover:bg-primary hover:text-secondary transition-all transform"
190-
>
191-
{secondaryMobileCta.label}
192-
</Link>
189+
secondaryMobileCta.link.startsWith('http') ? (
190+
<button
191+
onClick={() => setIsModalOpen(true)}
192+
className="font-sans text-2xl flex w-fit items-center justify-center my-2 px-2 py-1
193+
bg-background text-accent-1 border border-accent-1 rounded-lg
194+
hover:bg-primary hover:text-secondary transition-all transform"
195+
>
196+
{secondaryMobileCta.label}
197+
</button>
198+
) : (
199+
<Link
200+
href={secondaryMobileCta.link}
201+
className="font-sans text-2xl flex w-fit items-center justify-center my-2 px-2 py-1
202+
bg-background text-accent-1 border border-accent-1 rounded-lg
203+
hover:bg-primary hover:text-secondary transition-all transform"
204+
>
205+
{secondaryMobileCta.label}
206+
</Link>
207+
)
193208
)}
194209
</div>
195210

@@ -237,18 +252,52 @@ export function HeroSection({ hero }) {
237252

238253
{/* Desktop Tertiary Link */}
239254
{tertiaryLink && (
255+
tertiaryLink.link.startsWith('http') ? (
256+
<button
257+
onClick={() => setIsModalOpen(true)}
258+
className="hidden md:block font-mono text-sm text-contrast-2 hover:text-primary transition-colors text-left"
259+
>
260+
{tertiaryLink.label}
261+
</button>
262+
) : (
263+
<Link
264+
href={tertiaryLink.link}
265+
className="hidden md:block font-mono text-sm text-contrast-2 hover:text-primary transition-colors"
266+
>
267+
{tertiaryLink.label}
268+
</Link>
269+
)
270+
)}
271+
</div>
272+
273+
{/* Leaving Site Modal */}
274+
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
275+
<h2 className="text-2xl font-serif italic font-semibold text-primary mb-4">
276+
Quickstart with Tlon Messenger
277+
</h2>
278+
<p className="font-sans text-large leading-120 text-primary mb-6">
279+
Tlon will onboard you to Urbit without needing to run your own node. They provide free hosting and a free Urbit ID with their mobile app.</p>
280+
<p className="font-sans text-large leading-120 text-primary mb-6">
281+
The link below will get you set up and added to the Urbit Foundation public group; say hello and someone will show you around!</p>
282+
<div className="flex gap-3 justify-end">
240283
<Link
241-
href={tertiaryLink.link}
242-
className="hidden md:block font-mono text-sm text-contrast-2 hover:text-primary transition-colors"
243-
{...(tertiaryLink.link.startsWith('http') && {
244-
target: "_blank",
245-
rel: "noopener noreferrer",
246-
})}
284+
href="/overview/running-urbit"
285+
onClick={() => setIsModalOpen(false)}
286+
className="font-sans text-lg flex items-center py-1 px-3 rounded-lg text-contrast-2 hover:text-primary font-[600]"
247287
>
248-
{tertiaryLink.label}
288+
Help me self-host
249289
</Link>
250-
)}
251-
</div>
290+
<a
291+
href={tertiaryLink?.link || tertiaryMobileLink?.link}
292+
target="_blank"
293+
rel="noopener noreferrer"
294+
className="font-sans text-lg flex items-center py-1 px-3 rounded-lg text-background bg-foreground hover:text-contrast-1 font-[600]"
295+
onClick={() => setIsModalOpen(false)}
296+
>
297+
Onboard via Tlon
298+
</a>
299+
</div>
300+
</Modal>
252301
</section>
253302
);
254303
}

app/components/Modal.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"use client";
2+
3+
import { useEffect, useCallback, useState } from "react";
4+
import { createPortal } from "react-dom";
5+
6+
/**
7+
* Modal - A reusable modal component
8+
*
9+
* Displays content in a centered overlay with:
10+
* - Semi-transparent backdrop (click to close)
11+
* - X button to close
12+
* - Escape key to close
13+
* - Body scroll lock when open
14+
*
15+
* @param {boolean} isOpen - Controls modal visibility
16+
* @param {function} onClose - Called when modal is dismissed
17+
* @param {ReactNode} children - Custom content slot
18+
*/
19+
export function Modal({ isOpen, onClose, children }) {
20+
const [mounted, setMounted] = useState(false);
21+
22+
// Handle escape key
23+
const handleEscape = useCallback(
24+
(e) => {
25+
if (e.key === "Escape") {
26+
onClose();
27+
}
28+
},
29+
[onClose]
30+
);
31+
32+
// Set mounted state for portal
33+
useEffect(() => {
34+
setMounted(true);
35+
}, []);
36+
37+
// Handle body scroll lock and escape key
38+
useEffect(() => {
39+
if (isOpen) {
40+
// Lock scroll on both html and body to ensure it works
41+
document.documentElement.style.overflow = "hidden";
42+
document.body.style.overflow = "hidden";
43+
document.addEventListener("keydown", handleEscape);
44+
} else {
45+
document.documentElement.style.overflow = "";
46+
document.body.style.overflow = "";
47+
}
48+
49+
return () => {
50+
document.documentElement.style.overflow = "";
51+
document.body.style.overflow = "";
52+
document.removeEventListener("keydown", handleEscape);
53+
};
54+
}, [isOpen, handleEscape]);
55+
56+
if (!isOpen || !mounted) return null;
57+
58+
return createPortal(
59+
<div
60+
className="fixed inset-0 z-[9999] flex items-center justify-center p-4"
61+
role="dialog"
62+
aria-modal="true"
63+
onClick={onClose}
64+
>
65+
<div
66+
className="relative bg-contrast-1 border border-gray-87 rounded-16px shadow-lg max-w-md w-full max-h-[90vh] overflow-y-auto animate-fadeIn"
67+
onClick={(e) => e.stopPropagation()}
68+
>
69+
{/* Close button */}
70+
<button
71+
onClick={onClose}
72+
className="absolute top-4 right-4 text-contrast-2 hover:text-primary transition-colors"
73+
aria-label="Close modal"
74+
>
75+
<svg
76+
width="24"
77+
height="24"
78+
viewBox="0 0 24 24"
79+
fill="none"
80+
stroke="currentColor"
81+
strokeWidth="2"
82+
strokeLinecap="round"
83+
strokeLinejoin="round"
84+
>
85+
<line x1="18" y1="6" x2="6" y2="18" />
86+
<line x1="6" y1="6" x2="18" y2="18" />
87+
</svg>
88+
</button>
89+
90+
{/* Content */}
91+
<div className="p-6 pt-12">{children}</div>
92+
</div>
93+
</div>,
94+
document.body
95+
);
96+
}

app/components/NewsletterSignup.js

Lines changed: 52 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -43,53 +43,57 @@ export const NewsletterSignup = () => {
4343
document.body.removeChild(script);
4444
};
4545

46-
return (
47-
<React.Fragment>
48-
<form
49-
onSubmit={handleSubmit}
50-
id="mc-embedded-subscribe-form"
51-
name="mc-embedded-subscribe-form"
52-
className="validate form"
53-
noValidate
54-
>
55-
<div className="input-group relative font-small" id="mc_embed_signup_scroll">
56-
<div className={classNames(
57-
"w-full max-w-[700px] mc-field-group h-max relative")}>
58-
<input
59-
className={classNames(
60-
email.length > 0 && !isSuccess && "text-primary border-white",
61-
isSuccess ? "bg-[#878787] text-secondary cursor-default border-none" : 'text-gray-87 bg-transparent ',
62-
"text-large border-[.0875rem] pt-[.1rem] pb-[.2rem] appearance-none font-[300] placeholder:font-[300] placeholder:text-contrast-2 outline-none border-contrast-2 rounded-[.3125rem] pb-[.05em] pl-[.3em] pr-1 w-full leading-[1cap]",
63-
email.length > 0 && !isSuccess && "pr-[5.5rem]",
64-
)
65-
}
66-
disabled={isSuccess}
67-
type="email"
68-
name="EMAIL"
69-
id="mce-EMAIL"
70-
placeholder="Get email updates"
71-
required
72-
value={email}
73-
onChange={(e) => setEmail(e.target.value)} // Update state on input change
74-
/>
75-
{email.length > 0 && !isSuccess && ( // Only show the button if input length > 0
76-
<div id="subscribe" className="flex font-[300] items-center justify-center absolute h-full top-0 right-0">
77-
<button
78-
id="mc-embedded-subscribe"
79-
className={classNames(
80-
email.length > 0 && "text-contrast-2 hover:text-primary",
81-
"body-lg text-contrast-2 hover:text-primary leading-[1cap] bg-transparent pr-[.4em]"
82-
)}
83-
type="submit"
84-
name="subscribe"
85-
>
86-
Subscribe
87-
</button>
88-
</div>
46+
return (
47+
<React.Fragment>
48+
<form
49+
onSubmit={handleSubmit}
50+
id="mc-embedded-subscribe-form"
51+
name="mc-embedded-subscribe-form"
52+
className="validate form"
53+
noValidate
54+
>
55+
<div className="input-group relative font-small" id="mc_embed_signup_scroll">
56+
<div className={classNames(
57+
"w-full max-w-[700px] mc-field-group h-max relative")}>
58+
<input
59+
className={classNames(
60+
email.length > 0 && !isSuccess && "text-primary border-white",
61+
isSuccess ? "bg-[#878787] text-secondary cursor-default border-none" : 'text-gray-87 bg-transparent ',
62+
"text-large border-[.0875rem] pt-[.1rem] pb-[.2rem] appearance-none font-[300] placeholder:font-[300] placeholder:text-contrast-2 outline-none border-contrast-2 rounded-[.3125rem] pb-[.05em] pl-[.3em] pr-1 w-full leading-[1cap]",
63+
email.length > 0 && !isSuccess && "pr-[5.5rem]",
8964
)}
90-
</div>
65+
disabled={isSuccess}
66+
type="email"
67+
name="EMAIL"
68+
id="mce-EMAIL"
69+
placeholder="Get email updates"
70+
required
71+
autoComplete="off"
72+
data-1p-ignore
73+
data-lpignore="true"
74+
data-bwignore
75+
data-form-type="other"
76+
value={email}
77+
onChange={(e) => setEmail(e.target.value)}
78+
/>
79+
{email.length > 0 && !isSuccess && (
80+
<div id="subscribe" className="flex font-[300] items-center justify-center absolute h-full top-0 right-0">
81+
<button
82+
id="mc-embedded-subscribe"
83+
className={classNames(
84+
email.length > 0 && "text-contrast-2 hover:text-primary",
85+
"body-lg text-contrast-2 hover:text-primary leading-[1cap] bg-transparent pr-[.4em]"
86+
)}
87+
type="submit"
88+
name="subscribe"
89+
>
90+
Subscribe
91+
</button>
92+
</div>
93+
)}
9194
</div>
92-
</form>
93-
</React.Fragment>
94-
);
95-
};
95+
</div>
96+
</form>
97+
</React.Fragment>
98+
);
99+
}

app/content/blog/subassembly-hackathon-2024.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ description = "Use Login with Urbit ID in your app and win Urbit Stars"
55

66
[extra]
77
ship = "~sarlev-sarsen"
8-
image = "https://sfo3.digitaloceanspaces.com/sarlev-sarsen/sarlev-sarsen/2024.9.13..21.40.44..25a1.cac0.8312.6e97-image.png"
8+
image = "https://s3.us-east-1.amazonaws.com/urbit.orgcontent/imagery/Subassembly-image.png"
99
+++
1010

11-
![Image](https://sfo3.digitaloceanspaces.com/sarlev-sarsen/sarlev-sarsen/2024.9.13..21.40.44..25a1.cac0.8312.6e97-image.png)
11+
![Image](https://s3.us-east-1.amazonaws.com/urbit.orgcontent/imagery/Subassembly-image.png)
1212

1313
The Subassembly event series is focusing on identity and reputation for [their inaugural event](https://urbit.org/events/2024-10-20-Subassembly-PNW), and in concert with the IRL gathering, the Urbit Foundation is sponsoring a a hackathon focused around the development of applications that use Urbit ID as the authentication and identity layer.
1414

app/content/events/2024-10-20-subassembly-pnw.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ starts = "2024-10-20T14:00:00"
44
ends = "2024-10-22T11:00:00"
55
timezone = "America/Los_Angeles"
66
location = "Wellspring Spa & Woodland Retreat - 154922 Kernahan Rd E, Ashford, WA 98304"
7-
image = "https://sfo3.digitaloceanspaces.com/sarlev-sarsen/sarlev-sarsen/2024.5.16..5.20.19..6f5c.28f5.c28f.5c28-image.png"
7+
image = "https://s3.us-east-1.amazonaws.com/urbit.orgcontent/imagery/Subassembly-image.png"
88
links = [
99
{ label = "Register", url = "https://docs.google.com/forms/d/e/1FAIpQLSfbHcFLRgGKBW91qINxhZ6TAgSDgsY_ikG1ATauHL7AVeLqDA/viewform" },
1010
{ label = "Event Details", url = "https://subassembly.tocwexsyndicate.com/details.html" },

0 commit comments

Comments
 (0)