SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
11.9 KB · 185 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import { Check, KeyRound, LogIn, Mail } from "lucide-react";4import { PLAN_LIMITS } from "@fetcha/core";5import { Button } from "@/components/ui/button";6import { Badge } from "@/components/ui/badge";7import { formatNumber } from "@/lib/format";89export const metadata: Metadata = {10  title: "Access",11  description: "Fetcha is a private, invitation-only web access platform. One plan, no meters: unlimited requests, 200 concurrent, all network classes, managed browser and crawl jobs.",12  alternates: { canonical: "/pricing" },13};1415const REQUEST_ACCESS = "mailto:hello@fetcha.co?subject=Fetcha%20access";1617function seconds(ms: number) {18  return `${ms / 1000} s`;19}2021export default function AccessPage() {22  const p = PLAN_LIMITS.unlimited;23  const networks = p.networks.map((n) => n[0]!.toUpperCase() + n.slice(1)).join(", ");2425  const included: Array<{ label: string; value: string; hint: string }> = [26    { label: "Requests", value: "Unlimited", hint: "No monthly quota, no overage, no request fee." },27    { label: "Concurrency", value: formatNumber(p.concurrency), hint: "Requests in flight at once, per organization." },28    { label: "Timeout", value: `up to ${seconds(p.max_timeout_ms)}`, hint: "One budget for the whole request, retries included." },29    { label: "Retries", value: `up to ${p.max_retries}`, hint: "New exit IP and alternate route on every attempt." },30    { label: "Network classes", value: "All", hint: networks + ", chosen automatically or pinned per request." },31    { label: "Managed browser", value: `${p.browser_concurrency} concurrent`, hint: "Real Chromium, same networks, automatic fallback on JS challenges." },32    { label: "Crawl jobs", value: `${formatNumber(p.crawl_max_pages)} pages / job`, hint: `${p.crawl_concurrent_jobs} jobs in parallel, sitemap discovery, Markdown output.` },33    { label: "Sessions and geo", value: "Included", hint: "Sticky sessions up to 30 minutes; country, region and city targeting." },34    { label: "Request logs", value: `${p.retention_days} days`, hint: "Every attempt, route alias, timing and cost, in the dashboard and the API." },35  ];3637  const features = ["POST /v1/fetch with HTML, text, Markdown, JSON or raw output", "Managed headless browser with screenshots and wait conditions", "Crawl and map endpoints for whole sites", "Sticky sessions and geo targeting", "Playground, request explorer and usage analytics", "JavaScript and Python SDKs", "Optional spending limits as guard-rails"];3839  return (40    <div className="container-page py-14 sm:py-20">41      <header className="mx-auto max-w-2xl text-center">42        <p className="text-[11.5px] font-semibold uppercase tracking-[0.12em] text-accent">Access</p>43        <h1 className="mt-2 text-[34px] font-semibold leading-[1.1] tracking-tight sm:text-[44px]">Private access. No plans, no meters.</h1>44        <p className="mt-4 text-[15.5px] leading-relaxed text-fg-muted">45          Fetcha is an invitation-only platform. There is a single, unlimited plan for everyone who is let in, and there is nothing to buy: no tiers, no quotas, no invoices. Accounts are created46          from an access list managed by a Fetcha administrator.47        </p>48        <div className="mt-8 flex flex-wrap items-center justify-center gap-3">49          <Button asChild size="lg">50            <Link href="/login">51              <LogIn /> Log in52            </Link>53          </Button>54          <Button asChild size="lg" variant="outline">55            <a href={REQUEST_ACCESS}>56              <Mail /> Request access57            </a>58          </Button>59        </div>60        <p className="mt-4 text-[12.5px] text-fg-subtle">Already invited? Sign up with the email address your administrator approved.</p>61      </header>6263      {/* Single plan */}64      <section aria-labelledby="plan-title" className="mt-16">65        <div className="overflow-hidden rounded-xl border border-border bg-bg">66          <div className="flex flex-col gap-4 border-b border-border bg-bg-subtle/60 px-6 py-6 sm:flex-row sm:items-center sm:justify-between sm:px-8">67            <div>68              <div className="flex items-center gap-2">69                <h2 id="plan-title" className="text-[22px] font-semibold tracking-tight">70                  {p.label}71                </h2>72                <Badge variant="solid">private platform</Badge>73              </div>74              <p className="mt-1 text-[14px] text-fg-muted">The only plan. Same limits for every organization, enforced by the API from the configuration below.</p>75            </div>76            <div className="text-left sm:text-right">77              <div className="font-mono text-[30px] font-semibold tabular tracking-tight">$0</div>78              <div className="text-[12.5px] text-fg-subtle">no billing, by invitation</div>79            </div>80          </div>81          <dl className="grid divide-y divide-border sm:grid-cols-2 sm:divide-y-0 lg:grid-cols-3 [&>div]:border-border sm:[&>div:nth-child(n+3)]:border-t lg:[&>div:nth-child(n+3)]:border-t-0 lg:[&>div:nth-child(n+4)]:border-t sm:[&>div:nth-child(odd)]:border-r lg:[&>div:nth-child(odd)]:border-r-0 lg:[&>div:not(:nth-child(3n))]:border-r">82            {included.map((row) => (83              <div key={row.label} className="px-6 py-5 sm:px-8">84                <dt className="text-[11.5px] font-medium uppercase tracking-wide text-fg-subtle">{row.label}</dt>85                <dd className="mt-1 font-mono text-[18px] font-semibold tabular tracking-tight">{row.value}</dd>86                <dd className="mt-1 text-[12.5px] leading-snug text-fg-muted">{row.hint}</dd>87              </div>88            ))}89          </dl>90        </div>91      </section>9293      {/* What is included + how to get access */}94      <section className="mt-16 grid gap-8 lg:grid-cols-2 lg:gap-12">95        <div>96          <h2 className="text-[24px] font-semibold tracking-tight">Everything is included</h2>97          <p className="mt-2 text-[14px] text-fg-muted">No feature is gated behind a tier. When a capability ships, every account gets it the same day.</p>98          <ul className="mt-5 grid gap-2.5 text-[14px]">99            {features.map((f) => (100              <li key={f} className="flex items-start gap-2.5">101                <Check className="mt-0.5 size-4 shrink-0 text-success" aria-hidden />102                <span>{f}</span>103              </li>104            ))}105          </ul>106        </div>107        <div className="rounded-xl border border-border bg-bg-subtle/60 p-6 sm:p-8">108          <div className="flex items-center gap-2">109            <KeyRound className="size-4 text-accent" aria-hidden />110            <h2 className="text-[20px] font-semibold tracking-tight">How to get access</h2>111          </div>112          <ol className="mt-4 grid gap-4 text-[14px]">113            {[114              { title: "Ask for an invitation", body: <>Write to <a className="text-accent underline-offset-4 hover:underline" href={REQUEST_ACCESS}>hello@fetcha.co</a> with the email address you want to use and a sentence about what you plan to fetch.</> },115              { title: "Your email is added to the access list", body: <>A Fetcha administrator adds it and sends you an invitation. Only listed addresses can create an account; anyone else is refused at signup.</> },116              { title: "Create your account", body: <>Follow the link in the invitation (or open <Link className="text-accent underline-offset-4 hover:underline" href="/signup">/signup</Link>) using the same address. Your workspace, a default project and the Playground are ready immediately.</> },117            ].map((s, i) => (118              <li key={s.title} className="flex gap-3">119                <span className="mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full bg-fg font-mono text-[12px] font-semibold text-bg">{i + 1}</span>120                <div>121                  <div className="font-medium">{s.title}</div>122                  <p className="mt-0.5 text-[13.5px] leading-relaxed text-fg-muted">{s.body}</p>123                </div>124              </li>125            ))}126          </ol>127          <div className="mt-6 flex flex-wrap gap-2">128            <Button asChild>129              <Link href="/login">Log in</Link>130            </Button>131            <Button asChild variant="outline">132              <a href={REQUEST_ACCESS}>Request access</a>133            </Button>134          </div>135        </div>136      </section>137138      {/* Notes */}139      <section aria-labelledby="notes-title" className="mt-16 grid gap-8 lg:grid-cols-[280px_minmax(0,1fr)] lg:gap-16">140        <div>141          <h2 id="notes-title" className="text-[24px] font-semibold tracking-tight">142            Good to know143          </h2>144          <p className="mt-2 text-[14px] text-fg-muted">145            Questions? Write to <a className="text-accent underline-offset-4 hover:underline" href="mailto:support@fetcha.co">support@fetcha.co</a>.146          </p>147        </div>148        <div className="divide-y divide-border rounded-lg border border-border">149          {[150            { q: "Why is there no pricing?", a: "Fetcha runs as a private platform for invited teams. Access is granted by an administrator rather than sold, so there is no checkout, no card on file and no invoice. The dashboard's “Plan & access” page shows your usage for information only." },151            { q: "Are the limits really unlimited?", a: `There is no monthly request quota and no overage. Operational limits still apply to keep the platform healthy: ${formatNumber(p.concurrency)} concurrent requests per organization, a ${seconds(p.max_timeout_ms)} maximum timeout, up to ${p.max_retries} retries, ${p.browser_concurrency} concurrent browser renders and ${formatNumber(p.crawl_max_pages)} pages per crawl job. Burst rate limits protect the API from runaway loops.` },152            { q: "What are spending limits, then?", a: "Optional guard-rails you set yourself, per organization or per project. They are based on an internal cost estimate and stop requests with USAGE_LIMIT_REACHED when a hard limit is reached. Nothing is charged." },153            { q: "Can I invite my teammates?", a: "Ask your Fetcha administrator to add their email addresses to the access list; each of them receives an invitation and creates their own account. Shared organizations and roles are not available yet." },154            { q: "What happens if I sign up with an email that was not invited?", a: "Signup is refused with the message “This email is not on the access list. Ask your Fetcha administrator to invite you.” No account is created." },155          ].map((f) => (156            <details key={f.q} className="group px-5 py-4 open:bg-bg-subtle/40">157              <summary className="flex cursor-pointer list-none items-center justify-between gap-4 text-[15px] font-medium tracking-tight marker:content-none [&::-webkit-details-marker]:hidden">158                {f.q}159                <span className="shrink-0 font-mono text-fg-subtle transition-transform group-open:rotate-45" aria-hidden>160                  +161                </span>162              </summary>163              <div className="mt-3 max-w-2xl text-[14px] leading-relaxed text-fg-muted">{f.a}</div>164            </details>165          ))}166        </div>167      </section>168169      <section className="mt-20 rounded-xl border border-border bg-bg-subtle/60 px-6 py-12 text-center">170        <h2 className="text-[26px] font-semibold tracking-tight">Invited? Your workspace is waiting.</h2>171        <p className="mx-auto mt-3 max-w-lg text-[14.5px] text-fg-muted">Log in with the address your administrator approved, or ask us for access.</p>172        <div className="mt-6 flex flex-wrap items-center justify-center gap-3">173          <Button asChild size="lg">174            <Link href="/login">Log in</Link>175          </Button>176          <Button asChild size="lg" variant="outline">177            <a href={REQUEST_ACCESS}>Request access</a>178          </Button>179        </div>180        <p className="mt-4 text-[12.5px] text-fg-subtle">Private platform. No plans, no meters, no billing.</p>181      </section>182    </div>183  );184}185