TenantAtlas/apps/website/src/components/pages/EvaluationPage.astro
ahmido 2e6504618c 409: add evaluation, procurement and rollout website surface (#408)
## Summary
- add the localized evaluation-readiness route pair at `/evaluierung` and `/en/evaluation` with a shared page component
- wire homepage, platform, trust, review-pack, use-case, footer, and locale-switcher discovery paths into the new evaluation surface
- add smoke coverage plus full Spec Kit artifacts for the evaluation, procurement, and rollout readiness feature

## Validation
- `corepack pnpm --filter @tenantatlas/website build`
- `WEBSITE_PORT=4322 corepack pnpm --filter @tenantatlas/website test tests/smoke/public-routes.spec.ts`
- `WEBSITE_PORT=4323 corepack pnpm --filter @tenantatlas/website test tests/smoke/interaction.spec.ts`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #408
2026-05-30 18:09:16 +00:00

531 lines
18 KiB
Plaintext

---
import MainLayout from '@/layouts/MainLayout.astro';
import HeroSection from '@components/sections/landing/HeroSection.astro';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import Icon from '@components/ui/icons/Icon.astro';
import AccordionItem from '@components/ui/blocks/AccordionItem.astro';
import heroImage from '@images/tenantial-rollout-plan.avif';
import { SITE } from '@data/constants';
import { siteCopy } from '@data/site-copy';
import {
localeHtmlLang,
localizeHref,
localizedPath,
type Locale,
} from '@/i18n';
const { locale } = Astro.props;
interface Props {
locale: Locale;
}
const copy = siteCopy[locale].evaluation;
const siteDescription = siteCopy[locale].site.description;
const canonicalPath = localizedPath(copy.routePath, locale);
---
<MainLayout
lang={locale}
title={copy.pageTitle}
customDescription={copy.metaDescription}
customOgTitle={copy.pageTitle}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
'@id': `${SITE.url}${canonicalPath}`,
url: `${SITE.url}${canonicalPath}`,
name: copy.pageTitle,
description: copy.metaDescription,
isPartOf: {
'@type': 'WebSite',
url: SITE.url,
name: SITE.title,
description: siteDescription,
},
inLanguage: localeHtmlLang[locale],
}}
>
<HeroSection
title={copy.heroTitle}
subTitle={copy.heroSubtitle}
primaryBtn={copy.primaryCta.label}
primaryBtnURL={localizeHref(copy.primaryCta.href, locale)}
secondaryBtn={copy.secondaryCta.label}
secondaryBtnURL={localizeHref(copy.secondaryCta.href, locale)}
withReview={false}
supportingLine={copy.supportingLine}
src={heroImage}
alt={copy.heroTitle}
/>
{/* Evaluation path */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionRoute" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.pathTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.pathIntro}
</p>
</div>
</div>
<ol class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.evaluationSteps.map((step: any, index: number) => (
<li class="rounded-3xl border border-neutral-300 bg-white p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<span class="inline-flex rounded-full bg-yellow-400 px-3 py-1 text-xs font-semibold tracking-[0.18em] text-neutral-900 uppercase">
{String(index + 1).padStart(2, '0')}
</span>
<h3 class="mt-4 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{step.title}
</h3>
<p class="mt-3 text-pretty text-neutral-700 dark:text-neutral-300">
{step.content}
</p>
</li>
))
}
</ol>
</div>
</section>
{/* Preparation */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionPrep" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.preparationTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.preparationIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.preparationCards.map((card: any) => (
<article class="rounded-2xl bg-white p-5 shadow-xs dark:bg-neutral-900/70">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{card.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{card.content}
</p>
</article>
))
}
</div>
</div>
</section>
{/* Pilot scenarios */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionPilot" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.pilotTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.pilotIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-6 md:grid-cols-2 xl:grid-cols-3">
{
copy.pilotScenarios.map((scenario: any) => (
<article class="rounded-3xl border border-neutral-300 bg-white p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{scenario.title}
</h3>
<p class="mt-3 text-pretty text-neutral-700 dark:text-neutral-300">
{scenario.content}
</p>
</article>
))
}
</div>
</div>
</section>
{/* Stakeholders */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionStakeholders" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.stakeholderTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.stakeholderIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.stakeholderCards.map((card: any) => (
<article class="rounded-2xl border border-neutral-300 bg-white p-5 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{card.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{card.content}
</p>
</article>
))
}
</div>
</section>
{/* Security & procurement */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/20 text-yellow-600 ring-1 ring-yellow-400/40 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionSecurity" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.securityTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.securityIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.securityChecklist.map((item: any) => (
<article class="rounded-2xl bg-white p-5 shadow-xs dark:bg-neutral-900/70">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{item.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{item.content}
</p>
</article>
))
}
</div>
<div
class="mt-8 border-t border-neutral-300 pt-6 dark:border-neutral-700"
>
<h3
class="text-lg font-semibold text-neutral-900 dark:text-neutral-100"
>
{copy.securityHandoffTitle}
</h3>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 dark:text-neutral-300"
>
{copy.securityHandoffText}
</p>
<div class="mt-5">
<PrimaryCTA
title={copy.securityHandoffCta.label}
url={localizeHref(copy.securityHandoffCta.href, locale)}
/>
</div>
</div>
</div>
</section>
{/* Microsoft 365 access principles */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionAccess" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.accessTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.accessIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-6 md:grid-cols-2">
{
copy.accessPrinciples.map((principle: any) => (
<article class="rounded-3xl border border-neutral-300 bg-white p-6 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{principle.title}
</h3>
<p class="mt-3 text-pretty text-neutral-700 dark:text-neutral-300">
{principle.content}
</p>
</article>
))
}
</div>
</section>
{/* Non-requirements */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionLimits" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.nonRequirementTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.nonRequirementIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{
copy.nonRequirementCards.map((card: any) => (
<article class="rounded-2xl border border-neutral-300 bg-white p-5 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<h3 class="text-base font-semibold text-neutral-900 dark:text-neutral-100">
{card.title}
</h3>
<p class="mt-3 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{card.content}
</p>
</article>
))
}
</div>
</div>
</section>
{/* Example timeline */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionTimeline" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.timelineTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.timelineIntro}
</p>
</div>
</div>
<ol class="mt-8 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{
copy.timelineEntries.map((entry: any, index: number) => (
<li class="rounded-3xl border border-neutral-300 bg-white p-5 shadow-xs dark:border-neutral-700 dark:bg-white/[0.04]">
<span class="inline-flex rounded-full bg-yellow-400 px-3 py-1 text-xs font-semibold tracking-[0.18em] text-neutral-900 uppercase">
{String(index + 1).padStart(2, '0')}
</span>
<h3 class="mt-4 text-base font-semibold text-neutral-900 dark:text-neutral-100">
{entry.title}
</h3>
<p class="mt-2 text-sm leading-6 text-neutral-700 dark:text-neutral-300">
{entry.content}
</p>
</li>
))
}
</ol>
</section>
{/* Buyer FAQ */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-neutral-100/80 p-6 md:p-10 dark:border-neutral-700 dark:bg-white/[0.05]"
>
<div class="flex max-w-(--breakpoint-md) items-start gap-x-4">
<span
class="inline-flex size-12 shrink-0 items-center justify-center rounded-2xl bg-yellow-400/15 text-yellow-600 ring-1 ring-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:ring-yellow-400/20"
>
<Icon name="sectionFaq" />
</span>
<div>
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.faqTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.faqIntro}
</p>
</div>
</div>
<div class="mt-8 grid gap-x-12 md:grid-cols-2">
{
[0, 1].map((column: number) => (
<div class="hs-accordion-group divide-y divide-neutral-300 dark:divide-neutral-700">
{copy.faqItems
.filter((_item: any, index: number) => index % 2 === column)
.map((item: any, index: number) => {
const itemIndex = index * 2 + column;
return (
<AccordionItem
id={`evaluation-faq-heading-${itemIndex}`}
collapseId={`evaluation-faq-collapse-${itemIndex}`}
question={item.question}
answer={item.answer}
first={index === 0}
/>
);
})}
</div>
))
}
</div>
</div>
</section>
{/* Final CTA */}
<section
class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div
class="rounded-[2rem] border border-neutral-300 bg-linear-to-br from-neutral-100 to-yellow-100/60 p-6 md:p-10 dark:border-neutral-700 dark:from-neutral-900 dark:to-neutral-800"
>
<div class="max-w-(--breakpoint-md)">
<h2
class="text-2xl font-bold text-balance text-neutral-900 md:text-3xl dark:text-neutral-100"
>
{copy.finalCtaTitle}
</h2>
<p
class="mt-3 max-w-prose text-pretty text-neutral-700 md:text-lg dark:text-neutral-300"
>
{copy.finalCtaSubtitle}
</p>
</div>
<div class="mt-6 flex flex-wrap gap-4">
{
copy.finalCtas.map((cta: any, index: number) =>
index === 0 ? (
<PrimaryCTA
title={cta.label}
url={localizeHref(cta.href, locale)}
/>
) : (
<SecondaryCTA
title={cta.label}
url={localizeHref(cta.href, locale)}
/>
)
)
}
</div>
</div>
</section>
</MainLayout>