diff options
| -rw-r--r-- | packages/console/app/src/routes/black/index.css | 188 | ||||
| -rw-r--r-- | packages/console/app/src/routes/black/index.tsx | 118 |
2 files changed, 291 insertions, 15 deletions
diff --git a/packages/console/app/src/routes/black/index.css b/packages/console/app/src/routes/black/index.css index eb0ec87d3..418598792 100644 --- a/packages/console/app/src/routes/black/index.css +++ b/packages/console/app/src/routes/black/index.css @@ -131,6 +131,188 @@ text-decoration: none; } + [data-slot="pricing"] { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + max-width: 540px; + padding: 0 20px; + + @media (min-width: 768px) { + padding: 0; + } + } + + [data-slot="pricing-card"] { + display: flex; + flex-direction: column; + gap: 12px; + padding: 20px; + border: 1px solid rgba(255, 255, 255, 0.17); + border-radius: 4px; + text-decoration: none; + transition: border-color 0.15s ease; + background: transparent; + cursor: pointer; + text-align: left; + + &:hover { + border-color: rgba(255, 255, 255, 0.35); + } + + [data-slot="icon"] { + color: rgba(255, 255, 255, 0.59); + } + + [data-slot="price"] { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px; + } + + [data-slot="amount"] { + color: rgba(255, 255, 255, 0.92); + font-size: 24px; + font-weight: 500; + } + + [data-slot="period"] { + color: rgba(255, 255, 255, 0.59); + font-size: 14px; + } + + [data-slot="multiplier"] { + color: rgba(255, 255, 255, 0.39); + font-size: 14px; + + &::before { + content: "·"; + margin-right: 8px; + } + } + } + + [data-slot="selected-plan"] { + display: flex; + flex-direction: column; + gap: 32px; + width: fit-content; + max-width: calc(100% - 40px); + margin: 0 auto; + } + + [data-slot="selected-card"] { + display: flex; + flex-direction: column; + gap: 16px; + padding: 20px; + border: 1px solid rgba(255, 255, 255, 0.17); + border-radius: 4px; + width: fit-content; + + [data-slot="icon"] { + color: rgba(255, 255, 255, 0.59); + } + + [data-slot="price"] { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px; + } + + [data-slot="amount"] { + color: rgba(255, 255, 255, 0.92); + font-size: 24px; + font-weight: 500; + } + + [data-slot="period"] { + color: rgba(255, 255, 255, 0.59); + font-size: 14px; + } + + [data-slot="multiplier"] { + color: rgba(255, 255, 255, 0.39); + font-size: 14px; + + &::before { + content: "·"; + margin-right: 8px; + } + } + + [data-slot="terms"] { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 12px; + text-align: left; + + li { + color: rgba(255, 255, 255, 0.59); + font-size: 13px; + line-height: 1.5; + padding-left: 16px; + position: relative; + white-space: nowrap; + + &::before { + content: "▪"; + position: absolute; + left: 0; + color: rgba(255, 255, 255, 0.39); + } + } + } + + [data-slot="actions"] { + display: flex; + gap: 16px; + margin-top: 8px; + + button, + a { + flex: 1; + display: inline-flex; + height: 48px; + padding: 0 16px; + justify-content: center; + align-items: center; + border-radius: 4px; + font-family: var(--font-mono); + font-size: 16px; + font-weight: 400; + text-decoration: none; + cursor: pointer; + } + + [data-slot="cancel"] { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.17); + color: rgba(255, 255, 255, 0.92); + + &:hover { + border-color: rgba(255, 255, 255, 0.35); + } + } + + [data-slot="continue"] { + background: rgba(255, 255, 255, 0.17); + border: 1px solid rgba(255, 255, 255, 0.17); + color: rgba(255, 255, 255, 0.59); + + &:hover { + background: rgba(255, 255, 255, 0.25); + } + } + } + } + [data-slot="fine-print"] { color: rgba(255, 255, 255, 0.39); text-align: center; @@ -138,6 +320,12 @@ font-style: normal; font-weight: 400; line-height: 160%; /* 20.8px */ + font-style: italic; + + a { + color: rgba(255, 255, 255, 0.39); + text-decoration: underline; + } } } } diff --git a/packages/console/app/src/routes/black/index.tsx b/packages/console/app/src/routes/black/index.tsx index c83ccd251..f5a375adf 100644 --- a/packages/console/app/src/routes/black/index.tsx +++ b/packages/console/app/src/routes/black/index.tsx @@ -1,11 +1,54 @@ -import { A, createAsync } from "@solidjs/router" +import { A, createAsync, useSearchParams } from "@solidjs/router" import "./index.css" import { Title } from "@solidjs/meta" import { github } from "~/lib/github" -import { createMemo, Match, Switch } from "solid-js" +import { createMemo, createSignal, For, Match, Show, Switch } from "solid-js" import { config } from "~/config" +const plans = [ + { id: "20", amount: 20, multiplier: null }, + { id: "100", amount: 100, multiplier: "6x more usage than Black 20" }, + { id: "200", amount: 200, multiplier: "21x more usage than Black 20" }, +] as const + +function PlanIcon(props: { plan: string }) { + return ( + <Switch> + <Match when={props.plan === "20"}> + <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <rect x="4" y="4" width="16" height="16" rx="2" stroke="currentColor" stroke-width="1.5" /> + </svg> + </Match> + <Match when={props.plan === "100"}> + <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <rect x="3" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" /> + <rect x="14" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" /> + <rect x="3" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" /> + <rect x="14" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" /> + </svg> + </Match> + <Match when={props.plan === "200"}> + <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <rect x="2" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" /> + <rect x="10" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" /> + <rect x="18" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" /> + <rect x="2" y="10" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" /> + <rect x="10" y="10" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" /> + <rect x="18" y="10" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" /> + <rect x="2" y="18" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" /> + <rect x="10" y="18" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" /> + <rect x="18" y="18" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" /> + </svg> + </Match> + </Switch> + ) +} + export default function Black() { + const [params] = useSearchParams() + const [selected, setSelected] = createSignal<string | null>(params.plan as string | null) + const selectedPlan = createMemo(() => plans.find((p) => p.id === selected())) + const githubData = createAsync(() => github()) const starCount = createMemo(() => githubData()?.stars @@ -16,9 +59,6 @@ export default function Black() { : config.github.starsFormatted.compact, ) - // TODO: Frank, toggle this based on availability - const available = false - return ( <div data-page="black"> <Title>opencode</Title> @@ -148,17 +188,65 @@ export default function Black() { <p data-slot="subheading">Including Claude, GPT, Gemini, and more</p> </div> <Switch> - <Match when={available}> - <a href="/black/subscribe" data-slot="button"> - Subscribe $200/mo - </a> - <p data-slot="fine-print">Fair usage limits apply</p> + <Match when={!selected()}> + <div data-slot="pricing"> + <For each={plans}> + {(plan) => ( + <button type="button" onClick={() => setSelected(plan.id)} data-slot="pricing-card"> + <div data-slot="icon"> + <PlanIcon plan={plan.id} /> + </div> + <p data-slot="price"> + <span data-slot="amount">${plan.amount}</span> <span data-slot="period">per month</span> + <Show when={plan.multiplier}> + <span data-slot="multiplier">{plan.multiplier}</span> + </Show> + </p> + </button> + )} + </For> + </div> + <p data-slot="fine-print"> + Prices shown don't include applicable tax · <A href="/legal/terms-of-service">Terms of Service</A> + </p> </Match> - <Match when={!available}> - <p data-slot="back-soon">We’ll be back soon with more availability.</p> - <a data-slot="follow-us" href="https://x.com/opencode" target="_blank"> - Follow @opencode - </a> + <Match when={selectedPlan()}> + {(plan) => ( + <div data-slot="selected-plan"> + <div data-slot="selected-card"> + <div data-slot="icon"> + <PlanIcon plan={plan().id} /> + </div> + <p data-slot="price"> + <span data-slot="amount">${plan().amount}</span>{" "} + <span data-slot="period">per person billed monthly</span> + <Show when={plan().multiplier}> + <span data-slot="multiplier">{plan().multiplier}</span> + </Show> + </p> + <ul data-slot="terms"> + <li>Your subscription will not start immediately</li> + <li>You will be added to the waitlist and activated soon</li> + <li>Your card will be only charged when your subscription is activated</li> + <li>Usage limits apply, heavily automated use may reach limits sooner</li> + <li>Subscriptions for individuals, contact Enterprise for teams</li> + <li>Limits may be adjusted and plans may be discontinued in the future</li> + <li>Cancel your subscription at anytime</li> + </ul> + <div data-slot="actions"> + <button type="button" onClick={() => setSelected(null)} data-slot="cancel"> + Cancel + </button> + <a href={`/black/subscribe?plan=${plan().id}`} data-slot="continue"> + Continue + </a> + </div> + </div> + <p data-slot="fine-print"> + Prices shown don't include applicable tax · <A href="/legal/terms-of-service">Terms of Service</A> + </p> + </div> + )} </Match> </Switch> </section> |
