Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions app/src/app/bridge/bridge.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* /bridge: the panel on the left at a comfortable reading width, the facts beside it; one column on small screens with the panel first. */
.layout { display: grid; grid-template-columns: minmax(0, 520px) minmax(0, 1fr); align-items: start; gap: 40px 56px; }
.facts { display: grid; gap: 32px; min-width: 0; padding-top: 6px; }
.facts h2 { margin: 0 0 12px; color: var(--color-ink); font-size: 17px; font-weight: 650; letter-spacing: -.02em; line-height: 1.3; }

.networks { display: grid; margin: 0; }
.networks div { display: grid; grid-template-columns: minmax(96px, 132px) minmax(0, 1fr); gap: 12px; padding: 10px 0; border-top: 1px solid var(--color-line); font-size: 14px; line-height: 1.5; }
.networks div:last-child { border-bottom: 1px solid var(--color-line); }
.networks dt { color: var(--color-ink); font-weight: 600; }
.networks dd { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 12px; margin: 0; color: var(--color-body); }
.networks dd span { color: var(--color-muted); font-size: 13px; }

/* a real sequence: each step happens after the one before it */
.steps { display: grid; gap: 14px; margin: 0; padding: 0; list-style: none; counter-reset: step; }
.steps li { display: grid; grid-template-columns: 26px minmax(0, 1fr); column-gap: 12px; counter-increment: step; font-size: 14px; line-height: 1.6; }
.steps li::before { content: counter(step); grid-row: span 2; display: grid; place-items: center; width: 26px; height: 26px; border: 1px solid var(--color-line-strong); border-radius: 50%; color: var(--color-ink); font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums; }
.steps strong { color: var(--color-ink); font-weight: 600; }
.steps span { color: var(--color-body); }

.points { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; color: var(--color-body); font-size: 14px; line-height: 1.6; }
.points li { position: relative; padding-left: 16px; }
.points li::before { content: ""; position: absolute; left: 0; top: .68em; width: 5px; height: 5px; border-radius: 50%; background: var(--color-line-strong); }

@media (max-width: 960px) {
.layout { grid-template-columns: minmax(0, 1fr); }
.layout > :first-child { max-width: 560px; }
}
76 changes: 76 additions & 0 deletions app/src/app/bridge/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { Metadata } from "next";
import { ArrowLeftRight } from "lucide-react";
import { pageMetadata } from "@/lib/seo";
import SectionIntro from "@/components/sections/SectionIntro";
import shell from "@/components/sections/SectionShell.module.css";
import BridgePageSlot from "@/components/bridge/BridgePageSlot";
import { parseBridgeLink } from "@/lib/bridge/link";
import { BRIDGE_ASSETS, BRIDGE_CHAINS, BRIDGE_DESTINATION_IDS, BRIDGE_FUNDING_IDS } from "@/lib/bridge/types";
import styles from "./bridge.module.css";

/**
* The bridge as a page: the same panel the header opens, with room for the facts beside it. The panel is rendered by
* the header's one bridge controller into the slot below (components/bridge/bridge-slot.ts), so this page starts no
* second copy. `?from=…&to=…&asset=…&toAsset=…` preselects a route (lib/bridge/link.ts) and nothing more.
*/
export const metadata: Metadata = pageMetadata({
path: "/bridge",
title: "Bridge",
description: "Move ETH and dollars between Base, Robinhood Chain and Arc, or fund them from Ethereum, Arbitrum and Optimism. Same wallet, no openlaunch fee.",
});

const STEPS = [
["Pick a route and an amount", "A live quote appears with the fee, the gas and the least you will receive. Nothing is asked of your wallet yet."],
["Approve, for tokens only", "USDC and USDG need an approval for the exact amount first. It is its own wallet step and moves nothing. ETH skips it."],
["Confirm the deposit", "Slide the coin across or press the button, then confirm in your wallet. This is the one step that sends funds."],
["Watch it arrive", "The board follows the transfer until the provider confirms delivery to the same address on the other network."],
] as const;

export default async function BridgePage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
const link = parseBridgeLink(await searchParams);
return (
<main className={shell.page}>
<SectionIntro
eyebrow="Bridge"
icon={<ArrowLeftRight />}
title="Move funds to where you launch"
description="Send ETH and dollars between Base, Robinhood Chain and Arc, or bring them in from Ethereum, Arbitrum and Optimism. They arrive at the same wallet address. openlaunch charges nothing for it."
/>
<div className={styles.layout}>
<BridgePageSlot link={link} />
<div className={styles.facts}>
<section aria-labelledby="bridge-networks">
<h2 id="bridge-networks">Networks and tokens</h2>
<dl className={styles.networks}>
{BRIDGE_DESTINATION_IDS.map((id) => <div key={id}><dt>{BRIDGE_CHAINS[id].name}</dt><dd>{BRIDGE_ASSETS[id].join(", ")}<span>Send and receive</span></dd></div>)}
{BRIDGE_FUNDING_IDS.map((id) => <div key={id}><dt>{BRIDGE_CHAINS[id].name}</dt><dd>{BRIDGE_ASSETS[id].join(", ")}<span>Send only</span></dd></div>)}
</dl>
</section>
<section aria-labelledby="bridge-steps">
<h2 id="bridge-steps">How a transfer goes</h2>
<ol className={styles.steps}>
{STEPS.map(([title, body]) => <li key={title}><strong>{title}</strong><span>{body}</span></li>)}
</ol>
</section>
<section aria-labelledby="bridge-costs">
<h2 id="bridge-costs">What it costs</h2>
<ul className={styles.points}>
<li>openlaunch takes no fee on a transfer.</li>
<li>The bridge provider, Relay, charges a fee that is shown before you confirm and is already taken out of the amount you receive.</li>
<li>Gas on the network you send from is extra.</li>
<li>A quote that would cost more than 5% in fees, or lose more than 5% in conversion, is blocked.</li>
</ul>
</section>
<section aria-labelledby="bridge-know">
<h2 id="bridge-know">Good to know</h2>
<ul className={styles.points}>
<li>Funds go from your wallet to Relay&apos;s deposit contract. They never pass through an openlaunch wallet or contract.</li>
<li>Relay is a third-party protocol. openlaunch does not operate it and cannot reverse a transfer.</li>
<li>A transfer is saved in this browser while it is in progress. Reopen the bridge with the same wallet to pick it up.</li>
</ul>
</section>
</div>
</div>
</main>
);
}
1 change: 1 addition & 0 deletions app/src/app/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ test("every indexable page declares its own canonical path and share card", () =
["./feed/page.tsx", "/feed"],
["./agents/page.tsx", "/agents"],
["./about/page.tsx", "/about"],
["./bridge/page.tsx", "/bridge"],
];
for (const [file, path] of pages) {
const src = read(file);
Expand Down
1 change: 1 addition & 0 deletions app/src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import styles from "./Footer.module.css";
const EXPLORE = [
{ href: "/", label: "Launchpad" },
{ href: "/rules", label: "How it works" },
{ href: "/bridge", label: "Bridge" },
{ href: "/about", label: "About openlaunch" },
{ href: "/feed", label: "Community posts" },
{ href: "/me", label: "Your dashboard" },
Expand Down
11 changes: 7 additions & 4 deletions app/src/components/bridge/BridgeDialog.module.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading