Skip to content
Merged
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
15 changes: 10 additions & 5 deletions site/src/components/EmailAction.astro
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const action = endpoint === "/portal" ? "billing_portal" : "resend_license";
<script>
import { LICENSE_SERVER_URL, SUPPORT_EMAIL } from "../lib/license-server";
import { track } from "../lib/analytics";
import { showDone } from "../lib/forms";

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

Expand Down Expand Up @@ -65,13 +66,17 @@ const action = endpoint === "/portal" ? "billing_portal" : "resend_license";
body: JSON.stringify({ email }),
});
if (!res.ok) throw new Error(`request failed (${res.status})`);
const data = (await res.json()) as { message?: string };
say(
"ok",
data.message ??
"If that email matches an active license, we've sent you an email. Check your inbox (and spam).",
// The server answers the same whether or not the address has a
// license, so the page says what happens next, not whether it did.
showDone(
form,
"Check your inbox.",
endpoint === "/portal"
? "If this address has a subscription, the billing portal link arrives within a minute; look in the spam folder too."
: "If this address has a license, it arrives within a minute; look in the spam folder too.",
);
track("license_action_sent", { action });
return;
} catch {
say("error", `Something went wrong. Please try again, or email ${SUPPORT_EMAIL}.`);
track("license_action_failed", { action });
Expand Down
1 change: 1 addition & 0 deletions site/src/components/HeaderLinks.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const inChangelog = path.startsWith("/docs/changelog/");
const links = [
{ href: "/docs/", label: "Docs", active: path.startsWith("/docs/") && !inChangelog },
{ href: "/pricing/", label: "Pricing", active: false },
{ href: "/license/", label: "License", active: false },
{ href: "/docs/changelog/", label: "Changelog", active: inChangelog },
];
---
Expand Down
5 changes: 2 additions & 3 deletions site/src/components/NewsletterForm.astro
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const { placement } = Astro.props;

<script>
import { track } from "../lib/analytics";
import { showDone } from "../lib/forms";

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const GENERIC = "Something went wrong. Please try again later.";
Expand Down Expand Up @@ -76,10 +77,8 @@ const { placement } = Astro.props;
// Non-JSON body: the status decides below.
}
if (res.ok && data.success) {
say("ok", "You're in. Release notes and tips, straight to your inbox.");
showDone(form, "You're in.", "Release notes and tips, straight to your inbox. Unsubscribe with one click, any time.");
track("newsletter_success", { placement });
// Stays disabled: the address is on the list.
button.textContent = "Subscribed";
return;
}
say("error", data.error ?? GENERIC);
Expand Down
16 changes: 16 additions & 0 deletions site/src/components/PricingPlans.astro
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ const PRO_FEATURES = [
<div class="cta">
<BuyProButton />
<a class="chui-btn" href="#trial" data-cta="pricing_trial_jump">Try free for 30 days</a>
<p class="consent">
Secure checkout via Stripe. By buying you agree to the <a href="/terms/">Terms</a> and the
<a href="/privacy/">Privacy Policy</a>.
</p>
</div>
<ul>
{PRO_FEATURES.map((f) => <li>{f}</li>)}
Expand Down Expand Up @@ -147,11 +151,23 @@ const PRO_FEATURES = [
.cta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin-top: 1.1rem;
padding-bottom: 1.4rem;
border-bottom: 1px solid var(--chui-edge-subtle);
}
.consent {
flex-basis: 100%;
margin: 0.25rem 0 0;
font-size: 0.72rem;
line-height: 1.5;
color: var(--chui-fg-4);
}
.consent a {
color: var(--chui-fg-3);
text-underline-offset: 2px;
}
ul {
display: grid;
gap: 0.5rem;
Expand Down
45 changes: 33 additions & 12 deletions site/src/components/TrialForm.astro
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
---
// Self-serve 30-day trial. Posts to the license server's /trial from the
// browser; the license arrives by email. A page may carry several of these.
// The fields sit in one row and wrap when the container is narrow.
// The fields sit in one row and wrap when the container is narrow, or one
// under the other with layout="stack".
interface Props {
layout?: "row" | "stack";
}
const { layout = "row" } = Astro.props;
---

<form class="chui-trial not-content" data-trial-form novalidate>
<form class="chui-trial not-content" data-trial-form data-layout={layout} novalidate>
<div class="fields">
<input class="chui-input" type="text" name="name" maxlength="80" placeholder="Name (optional)" aria-label="Name (optional)" autocomplete="name" />
<input class="chui-input" type="email" name="email" required maxlength="320" placeholder="you@company.com" aria-label="Email" autocomplete="email" />
<button class="chui-btn" data-variant="solid" type="submit">Start free trial</button>
</div>
<p class="chui-status" role="status" hidden></p>
<p class="consent">
By requesting a trial you agree to the <a href="/terms/">Terms</a> and the
<a href="/privacy/">Privacy Policy</a>.
</p>
</form>

<style>
Expand All @@ -29,11 +38,29 @@
.fields button {
flex: 0 0 auto;
}
[data-layout="stack"] .fields {
flex-direction: column;
}
[data-layout="stack"] .fields > * {
flex: 0 0 auto;
width: 100%;
}
.consent {
margin: 0;
font-size: 0.72rem;
line-height: 1.5;
color: var(--chui-fg-4);
}
.consent a {
color: var(--chui-fg-3);
text-underline-offset: 2px;
}
</style>

<script>
import { LICENSE_SERVER_URL, SUPPORT_EMAIL } from "../lib/license-server";
import { track } from "../lib/analytics";
import { showDone } from "../lib/forms";

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const GENERIC = `Something went wrong. Please try again, or email ${SUPPORT_EMAIL}.`;
Expand Down Expand Up @@ -87,17 +114,11 @@
}
if (res.ok && body.success) {
track("trial_started");
say(
"ok",
[
body.message,
"Trial license sent to your email. Activate it in Settings → License of your CH-UI instance.",
]
.filter(Boolean)
.join(" "),
showDone(
form,
"Check your inbox.",
"The email arrives within a minute; look in the spam folder too. If this address already had a license, that is what you get, not a second trial. Upload the file in Settings → License.",
);
// Stays disabled: one trial per submit, the email is on its way.
button.textContent = "Trial sent";
return;
}
const limited = res.status === 429;
Expand Down
1 change: 1 addition & 0 deletions site/src/layouts/Landing.astro
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const path = Astro.url.pathname;
const nav = [
{ href: "/docs/", label: "Docs" },
{ href: "/pricing/", label: "Pricing" },
{ href: "/license/", label: "License" },
{ href: "/docs/changelog/", label: "Changelog" },
];
---
Expand Down
18 changes: 18 additions & 0 deletions site/src/lib/forms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Shared by the forms that email something: once the request went through,
// the fields are replaced by a short panel. The server answers the same
// whatever it decided to send (a new license, an existing one, nothing
// inside a cooldown), so the panel only says what to do next.
export function showDone(form: HTMLFormElement, title: string, text: string): void {
const fields = form.querySelector<HTMLElement>(".fields");
if (!fields) return;
const done = document.createElement("div");
done.className = "chui-done";
done.setAttribute("role", "status");
const strong = document.createElement("strong");
strong.textContent = title;
const span = document.createElement("span");
span.textContent = text;
done.append(strong, span);
fields.replaceWith(done);
form.querySelector<HTMLElement>(".chui-status")?.setAttribute("hidden", "");
}
33 changes: 30 additions & 3 deletions site/src/pages/license/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,26 @@ const trialPoints = [
<h2>Buy a license</h2>
<p class="price"><strong>$1,199</strong> per instance, per year</p>
<BuyProButton />
<p class="consent">
Secure checkout via Stripe. By buying you agree to the <a href="/terms/">Terms</a> and the
<a href="/privacy/">Privacy Policy</a>.
</p>
<ul class="checks">
{buyPoints.map((p) => <li>{p}</li>)}
</ul>
<p class="fineprint">Secure checkout via Stripe. Email receipt included.</p>
</div>
<div class="cell">
<h2>Start a free 30-day trial</h2>
<p class="price"><strong>Free</strong> no card required</p>
<TrialForm />
<TrialForm layout="stack" />
<ul class="checks">
{trialPoints.map((p) => <li>{p}</li>)}
</ul>
</div>
</div>

<h2 class="sub-head">Already a customer?</h2>
<div class="frame">
<div class="frame stacked">
<div class="cell">
<h3>Re-send my license</h3>
<p>Enter the email you purchased with and we'll re-send your license.json.</p>
Expand Down Expand Up @@ -90,6 +93,20 @@ const trialPoints = [
</Landing>

<style>
/* One row per action, the whole width, instead of two side by side. */
.stacked {
grid-template-columns: minmax(0, 1fr);
}
.stacked .cell {
border-inline-start: 0;
border-top: 1px solid var(--chui-edge-subtle);
}
.stacked .cell:first-child {
border-top: 0;
}
.stacked :global(.chui-email .fields) {
max-width: 28rem;
}
.featured {
box-shadow: inset 0 2px 0 var(--chui-accent);
}
Expand All @@ -111,6 +128,16 @@ const trialPoints = [
.checks {
margin-top: 1.4rem;
}
.consent {
margin: 0.5rem 0 0;
font-size: 0.72rem;
line-height: 1.5;
color: var(--chui-fg-4);
}
.consent a {
color: var(--chui-fg-3);
text-underline-offset: 2px;
}
.cell .fineprint {
margin-top: 1.1rem;
}
Expand Down
10 changes: 10 additions & 0 deletions site/src/styles/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,20 @@ starlight-toc a[aria-current="true"]:hover {
color: var(--sl-color-accent-high);
}

/* Previous and next: a compact pair of links, not two billboards. */
.pagination-links a {
padding: 0.7rem 0.9rem;
border-radius: 6px;
border-color: var(--chui-edge);
box-shadow: none;
font-size: 0.8125rem;
}
.pagination-links a .link-title {
font-size: 0.9375rem;
font-weight: 560;
}
.pagination-links a svg {
font-size: 1.1rem;
}
.pagination-links a:hover {
border-color: color-mix(in srgb, var(--sl-color-accent) 50%, transparent);
Expand Down
34 changes: 33 additions & 1 deletion site/src/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,39 @@
color: var(--chui-danger);
}
.chui-status[data-tone="ok"] {
color: var(--chui-success);
color: var(--chui-fg-2);
}
/* What a form turns into once its request went through: a title and one
line that is true whatever the server decided to send. */
.chui-done {
display: grid;
gap: 0.3rem;
padding: 0.9rem 1rem;
border: 1px solid var(--chui-edge);
border-radius: var(--chui-radius);
background: var(--chui-hover);
}
.chui-done strong {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 560;
color: var(--chui-fg);
}
.chui-done strong::before {
content: "";
width: 0.3rem;
height: 0.6rem;
margin: 0 0.3rem 0.15rem 0.15rem;
border: solid var(--chui-accent);
border-width: 0 1.5px 1.5px 0;
transform: rotate(45deg);
}
.chui-done span {
font-size: 0.8125rem;
line-height: 1.55;
color: var(--chui-fg-3);
}
/* The Pro mark: a small outlined tag in the accent. */
.chui-pro {
Expand Down
Loading