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
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { type MigrateUpArgs, sql } from '@payloadcms/db-postgres'

/**
* Adds the five task slugs that were registered in `payload.config.ts` without
* a matching migration, so `enum_payload_jobs_task_slug` never learned them.
*
* Every attempt to queue one failed the INSERT into `payload_jobs` with
* `invalid input value for enum enum_payload_jobs_task_slug`. Payload's
* `scheduleQueueable` catches that and reports the task as `errored`, and
* `defaultAfterSchedule` still advances `lastScheduledRun` in the job-stats
* global — so the scheduler looked healthy while these five had never run once.
*
* Two of them are GDPR retention jobs (consent-log 24-month, deal-registration
* PII 365-day), so this is a compliance fix, not only a dashboard one.
*
* `ADD VALUE` cannot run inside a transaction on Postgres < 12; on 16 it can,
* provided the new label is not used in the same transaction. This migration
* only adds labels, so it is safe. `IF NOT EXISTS` keeps it idempotent and
* lets it no-op on a database built from a later baseline.
*/
export async function up({ db }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
ALTER TYPE "public"."enum_payload_jobs_task_slug" ADD VALUE IF NOT EXISTS 'purgeConsentLog';
ALTER TYPE "public"."enum_payload_jobs_task_slug" ADD VALUE IF NOT EXISTS 'retryDealSync';
ALTER TYPE "public"."enum_payload_jobs_task_slug" ADD VALUE IF NOT EXISTS 'purgeDealRegistrations';
ALTER TYPE "public"."enum_payload_jobs_task_slug" ADD VALUE IF NOT EXISTS 'refreshContentInsights';
ALTER TYPE "public"."enum_payload_jobs_task_slug" ADD VALUE IF NOT EXISTS 'refreshCrux';`)
}

export async function down(): Promise<void> {
// Postgres cannot remove a value from an enum type. The five labels are left
// in place; unused labels are harmless, and dropping the tasks is a
// code-only change.
}
6 changes: 6 additions & 0 deletions apps/cms/src/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import * as migration_20260731_120000_add_legal_role from './20260731_120000_add
import * as migration_20260821_120000_faq_answer_richtext from './20260821_120000_faq_answer_richtext';
import * as migration_20260909_120000_add_form_tel_and_business_email from './20260909_120000_add_form_tel_and_business_email';
import * as migration_20260909_180000_add_submission_attribution from './20260909_180000_add_submission_attribution';
import * as migration_20260910_060000_add_missing_job_task_slugs from './20260910_060000_add_missing_job_task_slugs';

export const migrations = [
{
Expand Down Expand Up @@ -329,4 +330,9 @@ export const migrations = [
down: migration_20260909_180000_add_submission_attribution.down,
name: '20260909_180000_add_submission_attribution',
},
{
up: migration_20260910_060000_add_missing_job_task_slugs.up,
down: migration_20260910_060000_add_missing_job_task_slugs.down,
name: '20260910_060000_add_missing_job_task_slugs',
},
];
31 changes: 31 additions & 0 deletions apps/web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -6335,3 +6335,34 @@ body {
outline: 2px solid #3960f9;
outline-offset: 2px;
}

/* Secondary CTA on a light surface (thank-you card).
`.cs-link-cta` cannot be reused here: it is white with a #33BAEC hover, both
built for the dark bands, so on white it renders invisible and its hover
fails contrast. #3960F9 measures 4.8:1 on white. */
.cs-thank-you-secondary {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: var(--font-sora), ui-sans-serif, system-ui, sans-serif;
font-weight: 600;
letter-spacing: -0.01em;
color: #3960F9;
text-decoration: none;
padding: 4px 0;
transition: color 200ms ease;
}

.cs-thank-you-secondary:hover {
color: #2748D6;
text-decoration: underline;
text-underline-offset: 3px;
}

.cs-thank-you-secondary svg {
transition: transform 200ms ease;
}

.cs-thank-you-secondary:hover svg {
transform: translateX(3px);
}
184 changes: 125 additions & 59 deletions apps/web/src/components/sections/thank-you/ThankYouContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Link from "next/link";

import { Container, Section } from "@/components/layout";
import { Container } from "@/components/layout";
import { HeroReveal } from "@/components/ui/Reveal";
import type { ThankYouContent as Content } from "@/lib/thank-you/content";

/**
Expand All @@ -10,77 +11,127 @@ import type { ThankYouContent as Content } from "@/lib/thank-you/content";
* converted is at peak intent, and the inline banner it replaces vanished after
* five seconds. So it leads with confirmation, says plainly what happens next,
* then offers the two next steps most likely to matter.
*
* Built on the same dark band the originating form pages open with, so arriving
* here reads as the next step in one flow rather than a drop onto a bare page.
* The card overlaps the band's lower edge, which is the site's existing idiom
* for lifting a panel out of a gradient (see the Book a Demo form).
*/
export function ThankYouContent({ content }: { content: Content }): React.ReactElement {
return (
<Section padding="lg">
<div className="bg-white">
<section className="relative w-full overflow-hidden">
{/* Bleeds past the 1440 viewport on each side, matching DemoHero. */}
<div
aria-hidden
className="absolute inset-0 left-1/2 -translate-x-1/2"
style={{
width: "min(1920px, calc(100% + 480px))",
// Fades to transparent at the foot rather than ending on solid
// violet, which is what stops the band cutting a hard line across
// the page where it meets white. Same tail as DemoHero.
background:
"linear-gradient(180deg, rgba(21, 16, 33, 1) 0%, rgba(16, 18, 62, 1) 28%, rgba(19, 30, 143, 1) 52%, rgba(71, 30, 192, 1) 74%, rgba(71, 31, 195, 1) 84%, rgba(70, 30, 191, 0.85) 90%, rgba(66, 30, 188, 0.4) 96%, rgba(66, 30, 188, 0) 100%)",
}}
/>

{/* The asset carries its own edge fade; a CSS-tiled grid would run at
uniform opacity to the section edge and read as graph paper. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/images/cleanstart-images/hero-vector-grid.svg"
alt=""
aria-hidden
loading="lazy"
decoding="async"
className="pointer-events-none select-none absolute inset-x-0 top-0 w-full"
style={{ height: "100%", objectFit: "cover", opacity: 0.5 }}
/>

<div
className="relative mx-auto text-center"
style={{
maxWidth: "var(--container-prose)",
paddingLeft: "24px",
paddingRight: "24px",
// Floor is 104px, not the usual 64px: the header overlays this band
// and the eyebrow pill is the first thing under it, so a smaller
// floor tucks the pill beneath the logo on narrow viewports.
paddingTop: "calc(clamp(104px, 8vw, 128px) + var(--cs-header-extra))",
paddingBottom: "clamp(96px, 11vw, 168px)",
}}
>
<HeroReveal y={40} duration={0.9} lcp>
{/* No status pill above this heading. Nothing on the site renders a
success badge, and `eyebrow` here is a metadata convention, not a
visual one: pages pass it to buildPageMetadata so og.ts can print
a category onto the share card. A pill on the page read as a
generic form-submitted toast. The headline states the outcome. */}
{/* tabIndex so the tracker can move focus here after a soft
navigation, which otherwise announces nothing. */}
<h1
id="thank-you-heading"
tabIndex={-1}
className="text-white outline-none"
style={{
fontFamily: "var(--font-display), sans-serif",
fontSize: "var(--text-hero-utility)",
fontWeight: 600,
lineHeight: 1.08,
letterSpacing: "-0.03em",
}}
>
{content.headline}
</h1>

<p
className="mx-auto mt-5"
style={{
maxWidth: "34em",
color: "rgba(255, 255, 255, 0.76)",
fontSize: "var(--fs-lead)",
lineHeight: 1.55,
}}
>
{content.body}
</p>
</HeroReveal>
</div>
</section>

{/* Lifted out of the band's lower edge. */}
<Container variant="prose">
<div className="flex flex-col items-start gap-5">
<span
className="inline-flex items-center gap-2 rounded-full px-3 py-1"
<div
className="relative z-10 rounded-[20px] bg-white"
style={{
marginTop: "clamp(-120px, -9vw, -72px)",
padding: "clamp(24px, 3.2vw, 40px)",
boxShadow:
"0 24px 60px -24px rgba(9, 6, 63, 0.35), 0 2px 6px rgba(9, 6, 63, 0.06), inset 0 0 0 1px rgba(9, 6, 63, 0.06)",
}}
>
<p
className="font-display text-[#5B6087]"
style={{
background: "rgba(18, 183, 106, 0.10)",
color: "#0E7C4F",
fontFamily: "var(--font-display), 'Manrope', sans-serif",
fontSize: "var(--fs-caption)",
fontWeight: 600,
letterSpacing: "0.01em",
letterSpacing: "0.08em",
textTransform: "uppercase",
}}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" aria-hidden>
<path
d="M20 6L9 17l-5-5"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
{content.eyebrow}
</span>

{/* tabIndex so the tracker can move focus here after a soft
navigation, which otherwise announces nothing. */}
<h1
id="thank-you-heading"
tabIndex={-1}
className="font-display text-[#0F123E] outline-none"
style={{
fontSize: "var(--text-hero-utility)",
fontWeight: 600,
lineHeight: 1.12,
letterSpacing: "-0.02em",
}}
>
{content.headline}
</h1>

What happens next
</p>
<p
className="text-[#3A3F63]"
style={{ fontSize: "var(--fs-lead)", lineHeight: 1.55 }}
className="mt-2.5 text-[#3A3F63]"
style={{ fontSize: "var(--fs-body)", lineHeight: 1.65 }}
>
{content.body}
{content.whatHappensNext}
</p>

<div
className="w-full rounded-[14px] p-5"
style={{
background: "#F5F6FB",
boxShadow: "inset 0 0 0 1px rgba(9,6,63,0.05)",
}}
className="mt-7 flex flex-wrap items-center gap-x-6 gap-y-3 border-t pt-6"
style={{ borderColor: "rgba(9, 6, 63, 0.08)" }}
>
<p
className="mb-1.5 font-display text-[#0F123E]"
style={{ fontSize: "var(--fs-caption)", fontWeight: 600, letterSpacing: "0.04em" }}
>
WHAT HAPPENS NEXT
</p>
<p className="text-[#3A3F63]" style={{ fontSize: "var(--fs-body)", lineHeight: 1.6 }}>
{content.whatHappensNext}
</p>
</div>

<div className="mt-1 flex flex-wrap items-center gap-3">
<Link
href={content.primary.href}
className="cs-btn-blue"
Expand All @@ -94,16 +145,31 @@ export function ThankYouContent({ content }: { content: Content }): React.ReactE
>
<span>{content.primary.label}</span>
</Link>
{/* Not `.cs-link-cta`: that class is white with a #33BAEC hover,
built for the dark bands. On this white card it renders white on
white, and its hover colour measures 2.2:1 here. #3960F9 is the
accessible blue already used for focus rings. */}
<Link
href={content.secondary.href}
className="cs-link-cta"
className="cs-thank-you-secondary"
style={{ fontSize: "var(--fs-button)" }}
>
{content.secondary.label}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden>
<path
d="M5 12h13M12 5l7 7-7 7"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Link>
</div>
</div>
</Container>
</Section>

<div style={{ height: "clamp(64px, 8vw, 112px)" }} />
</div>
);
}
29 changes: 12 additions & 17 deletions apps/web/src/lib/thank-you/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export interface ThankYouCta {
}

export interface ThankYouContent {
eyebrow: string;
headline: string;
body: string;
/** What we will do next, so the visitor knows whether to wait or act. */
Expand Down Expand Up @@ -34,44 +33,40 @@ export interface ThankYouContent {
*/
export const THANK_YOU_CONTENT = {
"book-a-demo": {
eyebrow: "Demo requested",
headline: "Your demo request is in",
body: "A solutions engineer will read what you sent and come back to you directly, usually within one business day.",
body: "One of our solutions specialists will reach out to arrange a time.",
whatHappensNext:
"You will get an email from a real person, not an autoresponder sequence. If your timeline is tight, say so in the reply and we will work to it.",
primary: { label: "See how images are hardened", href: "/clean-images" },
secondary: { label: "Read the SBOM guide", href: "/resource-center" },
"Check your inbox for a confirmation. Reply to it with anything you want the demo to cover: your base images, the CVEs you are dealing with, or an audit you are preparing for.",
primary: { label: "See how our images are built", href: "/cleanstart-images" },
secondary: { label: "Browse the resource center", href: "/resource-center" },
metaTitle: "Demo requested",
metaDescription: "Your CleanStart demo request has been received.",
},
contact: {
eyebrow: "Message sent",
headline: "Thanks, we have your message",
body: "It has gone to the team who can actually answer it, rather than a shared inbox nobody owns.",
body: "It has gone to the team who can answer it.",
whatHappensNext:
"Expect a reply from a named person. If it turns out someone else is better placed to help, we will introduce you rather than forward you.",
primary: { label: "Browse the knowledge hub", href: "/knowledge-hub" },
secondary: { label: "See open roles", href: "/careers" },
"Check your inbox for a confirmation. If your message is urgent, reply to it and it reaches the team directly.",
primary: { label: "Browse the resource center", href: "/resource-center" },
secondary: { label: "Read the blog", href: "/blogs" },
metaTitle: "Message sent",
metaDescription: "Your message to CleanStart has been received.",
},
"deal-registration": {
eyebrow: "Deal registered",
headline: "Your deal registration is logged",
body: "The partnerships team has it, along with the prospect details you entered.",
whatHappensNext:
"We will confirm registration and come back on deal protection within one business day. Nothing is contacted on your prospect's side until you tell us to.",
"Check your inbox for a confirmation. The partner team reviews the registration and confirms next steps with you.",
primary: { label: "Partner resources", href: "/partners" },
secondary: { label: "Compare against alternatives", href: "/compare" },
secondary: { label: "Browse the resource center", href: "/resource-center" },
metaTitle: "Deal registered",
metaDescription: "Your CleanStart deal registration has been received.",
},
"job-application": {
eyebrow: "Application received",
headline: "Thanks for applying",
body: "Your application and CV are with the hiring team. Every one is read by a person.",
body: "Your application and CV are with the hiring team.",
whatHappensNext:
"If there is a fit you will hear from us with next steps. If there is not, you will still hear back rather than being left wondering.",
"Every application is reviewed. If your experience lines up with what the role needs, we will be in touch to arrange a first conversation.",
primary: { label: "See all open roles", href: "/careers" },
secondary: { label: "How we work", href: "/teams" },
metaTitle: "Application received",
Expand Down
Loading