Frank hai 1 mes
pai
achega
e146083b73

+ 188 - 0
packages/console/app/src/routes/black/index.css

@@ -131,6 +131,188 @@
         text-decoration: none;
         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"] {
       [data-slot="fine-print"] {
         color: rgba(255, 255, 255, 0.39);
         color: rgba(255, 255, 255, 0.39);
         text-align: center;
         text-align: center;
@@ -138,6 +320,12 @@
         font-style: normal;
         font-style: normal;
         font-weight: 400;
         font-weight: 400;
         line-height: 160%; /* 20.8px */
         line-height: 160%; /* 20.8px */
+        font-style: italic;
+
+        a {
+          color: rgba(255, 255, 255, 0.39);
+          text-decoration: underline;
+        }
       }
       }
     }
     }
   }
   }

+ 103 - 15
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 "./index.css"
 import { Title } from "@solidjs/meta"
 import { Title } from "@solidjs/meta"
 import { github } from "~/lib/github"
 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"
 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() {
 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 githubData = createAsync(() => github())
   const starCount = createMemo(() =>
   const starCount = createMemo(() =>
     githubData()?.stars
     githubData()?.stars
@@ -16,9 +59,6 @@ export default function Black() {
       : config.github.starsFormatted.compact,
       : config.github.starsFormatted.compact,
   )
   )
 
 
-  // TODO: Frank, toggle this based on availability
-  const available = false
-
   return (
   return (
     <div data-page="black">
     <div data-page="black">
       <Title>opencode</Title>
       <Title>opencode</Title>
@@ -148,17 +188,65 @@ export default function Black() {
             <p data-slot="subheading">Including Claude, GPT, Gemini, and more</p>
             <p data-slot="subheading">Including Claude, GPT, Gemini, and more</p>
           </div>
           </div>
           <Switch>
           <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>
-            <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>
             </Match>
           </Switch>
           </Switch>
         </section>
         </section>