diff --git a/apps/cms/src/migrations/20260910_060000_add_missing_job_task_slugs.ts b/apps/cms/src/migrations/20260910_060000_add_missing_job_task_slugs.ts new file mode 100644 index 00000000..44bca25d --- /dev/null +++ b/apps/cms/src/migrations/20260910_060000_add_missing_job_task_slugs.ts @@ -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 { + 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 { + // 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. +} diff --git a/apps/cms/src/migrations/index.ts b/apps/cms/src/migrations/index.ts index 30bae10d..28ace761 100644 --- a/apps/cms/src/migrations/index.ts +++ b/apps/cms/src/migrations/index.ts @@ -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 = [ { @@ -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', + }, ]; diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 8ee1a16b..8472e705 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -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); +} diff --git a/apps/web/src/components/sections/thank-you/ThankYouContent.tsx b/apps/web/src/components/sections/thank-you/ThankYouContent.tsx index 684d23cb..8384b76d 100644 --- a/apps/web/src/components/sections/thank-you/ThankYouContent.tsx +++ b/apps/web/src/components/sections/thank-you/ThankYouContent.tsx @@ -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"; /** @@ -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 ( -
+
+
+ {/* Bleeds past the 1440 viewport on each side, matching DemoHero. */} +
+ + {/* 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 */} + + +
+ + {/* 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. */} +

+ {content.headline} +

+ +

+ {content.body} +

+
+
+
+ + {/* Lifted out of the band's lower edge. */} -
- +

- - - - {content.eyebrow} - - - {/* tabIndex so the tracker can move focus here after a soft - navigation, which otherwise announces nothing. */} -

- {content.headline} -

- + What happens next +

- {content.body} + {content.whatHappensNext}

-

- WHAT HAPPENS NEXT -

-

- {content.whatHappensNext} -

-
- -
{content.primary.label} + {/* 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. */} {content.secondary.label} + + +
-
+ +
+
); } diff --git a/apps/web/src/lib/thank-you/content.ts b/apps/web/src/lib/thank-you/content.ts index 9db537e3..6f1095b4 100644 --- a/apps/web/src/lib/thank-you/content.ts +++ b/apps/web/src/lib/thank-you/content.ts @@ -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. */ @@ -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",