fix stripe configuration
This commit is contained in:
79
web/src/components/EmbeddedCheckout.tsx
Normal file
79
web/src/components/EmbeddedCheckout.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { createSignal, onMount, Show } from "solid-js";
|
||||
import type { Stripe, StripeEmbeddedCheckout } from "@stripe/stripe-js";
|
||||
|
||||
interface EmbeddedCheckoutProps {
|
||||
clientSecret: string;
|
||||
onCheckoutComplete?: () => void;
|
||||
}
|
||||
|
||||
export default function EmbeddedCheckout(props: EmbeddedCheckoutProps) {
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
const [loading, setLoading] = createSignal(true);
|
||||
let container: HTMLDivElement | undefined;
|
||||
|
||||
onMount(async () => {
|
||||
let embeddedCheckout: StripeEmbeddedCheckout | null = null;
|
||||
|
||||
try {
|
||||
const mod = await import("@stripe/stripe-js");
|
||||
const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
|
||||
if (!publishableKey) {
|
||||
setError("Stripe publishable key not configured");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const stripe = await mod.loadStripe(publishableKey);
|
||||
if (!stripe) {
|
||||
setError("Failed to load Stripe");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
embeddedCheckout = await stripe.createEmbeddedCheckoutPage({
|
||||
clientSecret: props.clientSecret,
|
||||
onComplete: () => {
|
||||
props.onCheckoutComplete?.();
|
||||
},
|
||||
});
|
||||
|
||||
if (container) {
|
||||
embeddedCheckout.mount(container);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load checkout");
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
embeddedCheckout?.destroy();
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="w-full">
|
||||
<Show when={loading()}>
|
||||
<div class="flex items-center justify-center min-h-[400px]">
|
||||
<div class="text-center">
|
||||
<div class="animate-spin h-8 w-8 border-2 border-[var(--color-brand-primary)] border-t-transparent rounded-full mx-auto mb-3" />
|
||||
<p class="text-sm text-[var(--color-text-secondary)]">Loading checkout...</p>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={error() && !loading()}>
|
||||
<div class="text-center py-8">
|
||||
<p class="text-[var(--color-error)] mb-4">{error()}</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div
|
||||
ref={container}
|
||||
class={loading() ? "hidden" : ""}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { Button } from "~/components/ui";
|
||||
import PageContainer from "~/components/layout/PageContainer";
|
||||
|
||||
interface CTABannerSectionProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export default function CTABannerSection(props: CTABannerSectionProps) {
|
||||
return (
|
||||
<section id="cta" class={cn("py-20 md:py-28 scroll-mt-16", props.class)}>
|
||||
<PageContainer py="py-8">
|
||||
<div class="gradient-card border border-(--color-border)/50 rounded-2xl p-10 md:p-16 text-center">
|
||||
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
|
||||
Ready to protect your identity?
|
||||
</h2>
|
||||
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto mb-10">
|
||||
Join thousands of users who trust Kordant to keep their digital
|
||||
identity safe from emerging threats.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<A href="/signup">
|
||||
<Button variant="primary" size="lg">
|
||||
Create Account
|
||||
</Button>
|
||||
</A>
|
||||
<A href="/login">
|
||||
<Button variant="secondary" size="lg">
|
||||
Sign In
|
||||
</Button>
|
||||
</A>
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
import { For } from "solid-js";
|
||||
import type { JSX } from "solid-js";
|
||||
import { cn } from "~/lib/utils";
|
||||
import Card from "~/components/ui/Card";
|
||||
import PageContainer from "~/components/layout/PageContainer";
|
||||
|
||||
interface Feature {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: () => JSX.Element;
|
||||
}
|
||||
|
||||
function DarkWatchIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-[var(--color-brand-primary)]"
|
||||
>
|
||||
<path
|
||||
d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function VoicePrintIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-[var(--color-brand-primary)]"
|
||||
>
|
||||
<path
|
||||
d="M12 4a4 4 0 00-4 4v8a4 4 0 008 0V8a4 4 0 00-4-4z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M4 11v2m16-2v2M8 18.5A6 6 0 0016 18.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function SpamShieldIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-[var(--color-brand-primary)]"
|
||||
>
|
||||
<path
|
||||
d="M12 2l9 4v6c0 5.55-3.84 10.74-9 12-5.16-1.26-9-6.45-9-12V6l9-4z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10 10l4 4m0-4l-4 4"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeTitleIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-[var(--color-brand-primary)]"
|
||||
>
|
||||
<path
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function RemoveBrokersIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-[var(--color-brand-primary)]"
|
||||
>
|
||||
<path
|
||||
d="M3 6h18M8 6V4a1 1 0 011-1h6a1 1 0 011 1v2m-4 4v6m-4-6v6m-3-8v10a2 2 0 002 2h10a2 2 0 002-2V8H8z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function FamilyPlansIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-[var(--color-brand-primary)]"
|
||||
>
|
||||
<path
|
||||
d="M12 12a4 4 0 100-8 4 4 0 000 8zM5 22v-2a4 4 0 014-4h6a4 4 0 014 4v2"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M20 9a3 3 0 100-6 3 3 0 000 6zM16 22v-2a3 3 0 00-3-3h-2a3 3 0 00-3 3v2"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
opacity="0.6"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const features: Feature[] = [
|
||||
{
|
||||
title: "DarkWatch",
|
||||
description:
|
||||
"Continuous dark web monitoring to detect your exposed credentials and personal data.",
|
||||
icon: DarkWatchIcon,
|
||||
},
|
||||
{
|
||||
title: "VoicePrint",
|
||||
description:
|
||||
"AI-powered voice clone detection to protect against deepfake voice scams.",
|
||||
icon: VoicePrintIcon,
|
||||
},
|
||||
{
|
||||
title: "SpamShield",
|
||||
description:
|
||||
"Intelligent spam and scam call blocking that learns your patterns over time.",
|
||||
icon: SpamShieldIcon,
|
||||
},
|
||||
{
|
||||
title: "HomeTitle",
|
||||
description:
|
||||
"Property fraud alerts that notify you of unauthorized changes to your home records.",
|
||||
icon: HomeTitleIcon,
|
||||
},
|
||||
{
|
||||
title: "RemoveBrokers",
|
||||
description:
|
||||
"Automatic data broker removal to minimize your personal data footprint online.",
|
||||
icon: RemoveBrokersIcon,
|
||||
},
|
||||
{
|
||||
title: "Family Plans",
|
||||
description:
|
||||
"Protect your whole household with shared monitoring, alerts, and management tools.",
|
||||
icon: FamilyPlansIcon,
|
||||
},
|
||||
];
|
||||
|
||||
interface FeatureCardProps {
|
||||
feature: Feature;
|
||||
}
|
||||
|
||||
function FeatureCard(props: FeatureCardProps) {
|
||||
const Icon = props.feature.icon;
|
||||
return (
|
||||
<Card class="hover:shadow-lg transition-shadow duration-300">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 p-2 rounded-lg bg-[var(--color-bg-secondary)]">
|
||||
<Icon />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-1">
|
||||
{props.feature.title}
|
||||
</h3>
|
||||
<p class="text-[var(--color-text-secondary)] leading-relaxed">
|
||||
{props.feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
interface FeaturesGridSectionProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export default function FeaturesGridSection(props: FeaturesGridSectionProps) {
|
||||
return (
|
||||
<section
|
||||
id="features"
|
||||
class={cn("py-20 md:py-28 scroll-mt-16", props.class)}
|
||||
>
|
||||
<PageContainer py="py-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
|
||||
Platform Features
|
||||
</h2>
|
||||
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
|
||||
Comprehensive protection powered by AI and real-time monitoring
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<For each={features}>
|
||||
{(feature) => <FeatureCard feature={feature} />}
|
||||
</For>
|
||||
</div>
|
||||
</PageContainer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import { For } from "solid-js";
|
||||
import type { JSX } from "solid-js";
|
||||
import { cn } from "~/lib/utils";
|
||||
import Card from "~/components/ui/Card";
|
||||
import PageContainer from "~/components/layout/PageContainer";
|
||||
|
||||
function CheckIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="flex-shrink-0"
|
||||
>
|
||||
<path
|
||||
d="M4 9l3 3 7-7"
|
||||
stroke="var(--color-success)"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IndividualIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 40 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-10 h-10 text-[var(--color-brand-primary)]"
|
||||
>
|
||||
<circle cx="20" cy="14" r="6" fill="currentColor" />
|
||||
<path
|
||||
d="M8 32c0-6.6 5.4-12 12-12s12 5.4 12 12H8z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function FamilyIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 40 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-10 h-10 text-[var(--color-brand-primary)]"
|
||||
>
|
||||
<circle cx="14" cy="12" r="5" fill="currentColor" />
|
||||
<circle cx="26" cy="12" r="4" fill="currentColor" opacity="0.7" />
|
||||
<path
|
||||
d="M2 30c0-5 4-9 9-9 1.5 0 3 .4 4.2 1.1C16.5 21.5 18 21 20 21s3.5.5 4.8 1.1C26 21.4 27.5 21 29 21c5 0 9 4 9 9H2z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
interface PanelProps {
|
||||
title: string;
|
||||
description: string;
|
||||
items: string[];
|
||||
icon: () => JSX.Element;
|
||||
}
|
||||
|
||||
function Panel(props: PanelProps) {
|
||||
const Icon = props.icon;
|
||||
return (
|
||||
<Card class="h-full">
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="mb-4">
|
||||
<Icon />
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-[var(--color-text-primary)] mb-2">
|
||||
{props.title}
|
||||
</h3>
|
||||
<p class="text-[var(--color-text-secondary)] mb-6">
|
||||
{props.description}
|
||||
</p>
|
||||
<ul class="space-y-3 flex-1">
|
||||
<For each={props.items}>
|
||||
{(item) => (
|
||||
<li class="flex items-start gap-2.5">
|
||||
<CheckIcon />
|
||||
<span class="text-[var(--color-text-secondary)] text-sm">
|
||||
{item}
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
interface ForUsersSectionProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export default function ForUsersSection(props: ForUsersSectionProps) {
|
||||
return (
|
||||
<section
|
||||
id="for-users"
|
||||
class={cn("py-20 md:py-28 scroll-mt-16", props.class)}
|
||||
>
|
||||
<PageContainer py="py-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
|
||||
For Everyone
|
||||
</h2>
|
||||
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
|
||||
Whether you're protecting yourself or your whole family
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Panel
|
||||
title="For Individuals"
|
||||
description="Personal identity protection tailored to your digital footprint."
|
||||
icon={IndividualIcon}
|
||||
items={[
|
||||
"Monitor personal email and phone numbers",
|
||||
"Dark web credential scanning",
|
||||
"Voiceprint clone detection",
|
||||
"Spam and scam call filtering",
|
||||
"Data broker opt-out service",
|
||||
]}
|
||||
/>
|
||||
<Panel
|
||||
title="For Families"
|
||||
description="Group management tools to keep every household member safe."
|
||||
icon={FamilyIcon}
|
||||
items={[
|
||||
"Add unlimited family members",
|
||||
"Shared alert dashboard",
|
||||
"Child account monitoring",
|
||||
"Family-wide dark web scans",
|
||||
"Centralized threat notifications",
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</PageContainer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { onMount } from "solid-js";
|
||||
import { A } from "@solidjs/router";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { Button } from "~/components/ui";
|
||||
import { Typewriter } from "~/components/ui/Typewriter";
|
||||
import PageContainer from "~/components/layout/PageContainer";
|
||||
|
||||
function ShieldIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-12 h-12 md:w-16 md:h-16"
|
||||
>
|
||||
<circle cx="24" cy="24" r="24" fill="var(--color-brand-primary)" />
|
||||
<path
|
||||
d="M24 10L16 14v6.5c0 5.1 3.4 9.9 8 11.5 4.6-1.6 8-6.4 8-11.5V14l-8-4z"
|
||||
fill="white"
|
||||
fill-opacity="0.9"
|
||||
/>
|
||||
<path
|
||||
d="M20 24l3 2.5 5-5"
|
||||
stroke="var(--color-brand-primary)"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
interface HeroSectionProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export default function HeroSection(props: HeroSectionProps) {
|
||||
let heroRef: HTMLDivElement | undefined;
|
||||
|
||||
onMount(() => {
|
||||
if (heroRef) {
|
||||
heroRef.style.opacity = "1";
|
||||
heroRef.style.transform = "translateY(0)";
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<section class={cn("relative", props.class)}>
|
||||
<PageContainer>
|
||||
<div
|
||||
ref={heroRef}
|
||||
class="flex flex-col items-center text-center py-20 md:py-32 transition-all duration-700 ease-out"
|
||||
style="opacity: 0; transform: translateY(20px);"
|
||||
>
|
||||
<div class="mb-6 shadow-glow-primary rounded-full p-3">
|
||||
<ShieldIcon />
|
||||
</div>
|
||||
|
||||
<h1 class="text-4xl md:text-6xl lg:text-7xl font-bold tracking-tight mb-6 max-w-4xl">
|
||||
<Typewriter speed={50} delay={400} keepAlive={false}>
|
||||
<span class="text-text-primary">AI-Powered </span>
|
||||
<span class="text-gradient-primary">Identity Protection</span>
|
||||
<br />
|
||||
<span class="text-text-primary">for Everyone</span>
|
||||
</Typewriter>
|
||||
</h1>
|
||||
|
||||
<p class="text-xl md:text-2xl text-text-secondary max-w-2xl mb-10 leading-relaxed">
|
||||
Threat actors are using AI in multifaceted attacks. Kordant evens
|
||||
the playing field using advanced AI to monitor, detect, and prevent
|
||||
identity threats in real-time.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 mb-8">
|
||||
<A href="/signup">
|
||||
<Button variant="primary" size="lg">
|
||||
Get Started
|
||||
</Button>
|
||||
</A>
|
||||
<A href="#features">
|
||||
<Button variant="ghost" size="lg">
|
||||
Learn More
|
||||
</Button>
|
||||
</A>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-text-tertiary">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.5 11.5L3 8L4.1 6.9L6.5 9.3L12.4 3.4L13.5 4.5L6.5 11.5Z"
|
||||
fill="var(--color-success)"
|
||||
/>
|
||||
</svg>
|
||||
No credit card required
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.5 11.5L3 8L4.1 6.9L6.5 9.3L12.4 3.4L13.5 4.5L6.5 11.5Z"
|
||||
fill="var(--color-success)"
|
||||
/>
|
||||
</svg>
|
||||
Free tier available
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
import { For } from "solid-js";
|
||||
import type { JSX } from "solid-js";
|
||||
import { cn } from "~/lib/utils";
|
||||
import PageContainer from "~/components/layout/PageContainer";
|
||||
|
||||
interface Step {
|
||||
number: number;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: () => JSX.Element;
|
||||
}
|
||||
|
||||
function EnrollIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-8 h-8 text-white"
|
||||
>
|
||||
<path
|
||||
d="M12 4a4 4 0 100 8 4 4 0 000-8zM6 21v-2a4 4 0 014-4h4a4 4 0 014 4v2"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M17 8l2 2 4-4"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MonitorIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-8 h-8 text-white"
|
||||
>
|
||||
<path
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M12 8v4l3 3"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3 12h3m15 0h3M12 3v3m0 15v3"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
opacity="0.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-8 h-8 text-white"
|
||||
>
|
||||
<path
|
||||
d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 22a2 2 0 01-2-2h4a2 2 0 01-2 2z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M12 11v3m0-6v1"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
number: 1,
|
||||
title: "Enroll Your Identity",
|
||||
description:
|
||||
"Sign up and add your emails, phone numbers, and family members to create your protection profile.",
|
||||
icon: EnrollIcon,
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
title: "We Monitor 24/7",
|
||||
description:
|
||||
"Our system runs continuous dark web scans, voiceprint detection, and spam filtering to catch threats early.",
|
||||
icon: MonitorIcon,
|
||||
},
|
||||
{
|
||||
number: 3,
|
||||
title: "Get Instant Alerts",
|
||||
description:
|
||||
"Receive real-time notifications the moment a threat is detected, with clear guidance on what to do next.",
|
||||
icon: AlertIcon,
|
||||
},
|
||||
];
|
||||
|
||||
interface StepBlockProps {
|
||||
step: Step;
|
||||
index: number;
|
||||
}
|
||||
|
||||
function StepBlock(props: StepBlockProps) {
|
||||
const isEven = props.index % 2 === 0;
|
||||
const Icon = props.step.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"flex gap-8 md:flex-row flex-col",
|
||||
isEven ? "" : "md:flex-row-reverse",
|
||||
)}
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-start gap-5">
|
||||
<div class="w-14 h-14 rounded-full gradient-primary shadow-glow-primary flex items-center justify-center shrink-0">
|
||||
<Icon />
|
||||
</div>
|
||||
<div>
|
||||
<div class="inline-flex items-center gap-2 mb-1.5">
|
||||
<span class="text-sm font-semibold text-[var(--color-brand-primary)]">
|
||||
Step {props.step.number}
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="text-xl md:text-2xl font-bold text-[var(--color-text-primary)] mb-2">
|
||||
{props.step.title}
|
||||
</h3>
|
||||
<p class="text-base text-[var(--color-text-secondary)] leading-relaxed">
|
||||
{props.step.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 hidden md:block" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface HowItWorksSectionProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export default function HowItWorksSection(props: HowItWorksSectionProps) {
|
||||
return (
|
||||
<section
|
||||
id="how-it-works"
|
||||
class={cn("py-20 md:py-28 scroll-mt-16", props.class)}
|
||||
>
|
||||
<PageContainer py="py-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
|
||||
How It Works
|
||||
</h2>
|
||||
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
|
||||
Three simple steps to full identity protection
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-12 md:gap-16">
|
||||
<For each={steps}>
|
||||
{(step, index) => <StepBlock step={step} index={index()} />}
|
||||
</For>
|
||||
</div>
|
||||
</PageContainer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
import { For } from "solid-js";
|
||||
import type { JSX } from "solid-js";
|
||||
import { cn } from "~/lib/utils";
|
||||
import Card from "~/components/ui/Card";
|
||||
import PageContainer from "~/components/layout/PageContainer";
|
||||
|
||||
function CheckIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="flex-shrink-0"
|
||||
>
|
||||
<path
|
||||
d="M4 9l3 3 7-7"
|
||||
stroke="var(--color-success)"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ProactiveIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-[var(--color-brand-primary)]"
|
||||
>
|
||||
<path
|
||||
d="M13 3l-2 6h5l-3 8"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M4 14l5-5m0 0l5 5m-5-5v12"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function AIIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-[var(--color-brand-primary)]"
|
||||
>
|
||||
<path
|
||||
d="M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8 16l2 2-2 2M16 16l-2 2 2 2"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function PrivacyIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-[var(--color-brand-primary)]"
|
||||
>
|
||||
<path
|
||||
d="M12 2l9 4v6c0 5.55-3.84 10.74-9 12-5.16-1.26-9-6.45-9-12V6l9-4z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9 12l2 2 4-4"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
interface ValueProp {
|
||||
title: string;
|
||||
description: string;
|
||||
items: string[];
|
||||
icon: () => JSX.Element;
|
||||
}
|
||||
|
||||
const valueProps: ValueProp[] = [
|
||||
{
|
||||
title: "Proactive, Not Reactive",
|
||||
description:
|
||||
"We detect threats before they cause damage, so you can act early.",
|
||||
icon: ProactiveIcon,
|
||||
items: [
|
||||
"Real-time dark web scanning",
|
||||
"Pre-breach alerts and warnings",
|
||||
"Automated threat response",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "AI-Powered Detection",
|
||||
description:
|
||||
"Machine learning models trained on real scam data to catch the latest threats.",
|
||||
icon: AIIcon,
|
||||
items: [
|
||||
"Deepfake voice identification",
|
||||
"Pattern-based scam detection",
|
||||
"Continuous model improvement",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Privacy First",
|
||||
description:
|
||||
"Your data stays encrypted and private. We never sell your information.",
|
||||
icon: PrivacyIcon,
|
||||
items: [
|
||||
"End-to-end encrypted data",
|
||||
"GDPR and CCPA compliant",
|
||||
"Zero data selling policy",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface ValueCardProps {
|
||||
prop: ValueProp;
|
||||
}
|
||||
|
||||
function ValueCard(props: ValueCardProps) {
|
||||
const Icon = props.prop.icon;
|
||||
return (
|
||||
<Card class="backdrop-blur-2xl">
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="mb-3 p-2 rounded-lg bg-[var(--color-bg-secondary)] w-fit">
|
||||
<Icon />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-2">
|
||||
{props.prop.title}
|
||||
</h3>
|
||||
<p class="text-[var(--color-text-secondary)] mb-4 leading-relaxed">
|
||||
{props.prop.description}
|
||||
</p>
|
||||
<ul class="space-y-2 flex-1">
|
||||
<For each={props.prop.items}>
|
||||
{(item) => (
|
||||
<li class="flex items-start gap-2.5">
|
||||
<CheckIcon />
|
||||
<span class="text-[var(--color-text-secondary)] text-sm">
|
||||
{item}
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
interface WhyKordantSectionProps {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export default function WhyKordantSection(props: WhyKordantSectionProps) {
|
||||
return (
|
||||
<section
|
||||
id="why-kordant"
|
||||
class={cn("py-20 md:py-28 scroll-mt-16", props.class)}
|
||||
>
|
||||
<PageContainer py="py-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4">
|
||||
Why Kordant
|
||||
</h2>
|
||||
<p class="text-lg text-[var(--color-text-secondary)] max-w-2xl mx-auto">
|
||||
Built on cutting-edge technology with your privacy at the core
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<For each={valueProps}>
|
||||
{(prop) => <ValueCard prop={prop} />}
|
||||
</For>
|
||||
</div>
|
||||
</PageContainer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render } from "solid-js/web";
|
||||
import type { JSX } from "solid-js";
|
||||
|
||||
vi.mock("@solidjs/router", () => ({
|
||||
A: (props: { href?: string; children?: JSX.Element }) => {
|
||||
const href = props.href || "#";
|
||||
return (
|
||||
<a href={href}>
|
||||
{props.children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
import HeroSection from "./HeroSection";
|
||||
|
||||
function mount(comp: () => JSX.Element): HTMLDivElement {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
render(() => comp(), container);
|
||||
return container;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
describe("HeroSection", () => {
|
||||
it("renders the headline with AI-Powered Identity Protection", () => {
|
||||
mount(() => <HeroSection />);
|
||||
expect(document.body.textContent).toContain("AI-Powered");
|
||||
expect(document.body.textContent).toContain("Identity Protection");
|
||||
expect(document.body.textContent).toContain("for Everyone");
|
||||
});
|
||||
|
||||
it("renders the subheadline", () => {
|
||||
mount(() => <HeroSection />);
|
||||
expect(document.body.textContent).toContain("Kordant uses advanced AI");
|
||||
});
|
||||
|
||||
it("renders the Get Started CTA", () => {
|
||||
mount(() => <HeroSection />);
|
||||
expect(document.body.textContent).toContain("Get Started");
|
||||
const primaryBtn = document.querySelector("button.gradient-primary");
|
||||
expect(primaryBtn).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders the Learn More CTA", () => {
|
||||
mount(() => <HeroSection />);
|
||||
expect(document.body.textContent).toContain("Learn More");
|
||||
});
|
||||
|
||||
it("renders trust indicators", () => {
|
||||
mount(() => <HeroSection />);
|
||||
expect(document.body.textContent).toContain("No credit card required");
|
||||
expect(document.body.textContent).toContain("Free tier available");
|
||||
});
|
||||
|
||||
it("renders the shield icon SVG", () => {
|
||||
mount(() => <HeroSection />);
|
||||
const svg = document.querySelector("svg");
|
||||
expect(svg).toBeTruthy();
|
||||
});
|
||||
|
||||
it("wraps content in PageContainer", () => {
|
||||
mount(() => <HeroSection />);
|
||||
const container = document.querySelector(".max-w-7xl");
|
||||
expect(container).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders two buttons for CTAs", () => {
|
||||
mount(() => <HeroSection />);
|
||||
const buttons = document.querySelectorAll("button");
|
||||
expect(buttons.length).toBe(2);
|
||||
});
|
||||
|
||||
it("has Get Started button wrapped in link to /signup", () => {
|
||||
mount(() => <HeroSection />);
|
||||
const links = document.querySelectorAll("a");
|
||||
const signupLink = Array.from(links).find(
|
||||
(a) => a.getAttribute("href") === "/signup",
|
||||
);
|
||||
expect(signupLink).toBeTruthy();
|
||||
expect(signupLink!.textContent).toContain("Get Started");
|
||||
});
|
||||
|
||||
it("has Learn More button wrapped in link to #features", () => {
|
||||
mount(() => <HeroSection />);
|
||||
const links = document.querySelectorAll("a");
|
||||
const featuresLink = Array.from(links).find(
|
||||
(a) => a.getAttribute("href") === "#features",
|
||||
);
|
||||
expect(featuresLink).toBeTruthy();
|
||||
expect(featuresLink!.textContent).toContain("Learn More");
|
||||
});
|
||||
|
||||
it("applies custom class prop", () => {
|
||||
mount(() => <HeroSection class="custom-hero" />);
|
||||
const section = document.querySelector("section.custom-hero");
|
||||
expect(section).toBeTruthy();
|
||||
});
|
||||
|
||||
it("has centered text layout", () => {
|
||||
mount(() => <HeroSection />);
|
||||
const inner = document.querySelector(".text-center");
|
||||
expect(inner).toBeTruthy();
|
||||
});
|
||||
|
||||
it("has responsive vertical padding", () => {
|
||||
mount(() => <HeroSection />);
|
||||
const inner = document.querySelector(".py-20");
|
||||
expect(inner).toBeTruthy();
|
||||
expect(inner!.className).toContain("md:py-32");
|
||||
});
|
||||
});
|
||||
@@ -1,379 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render } from "solid-js/web";
|
||||
import type { JSX } from "solid-js";
|
||||
|
||||
vi.mock("@solidjs/router", () => ({
|
||||
A: (props: { href?: string; children?: JSX.Element }) => {
|
||||
const href = props.href || "#";
|
||||
return (
|
||||
<a href={href}>
|
||||
{props.children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
import HowItWorksSection from "./HowItWorksSection";
|
||||
import FeaturesGridSection from "./FeaturesGridSection";
|
||||
import ForUsersSection from "./ForUsersSection";
|
||||
import WhyKordantSection from "./WhyKordantSection";
|
||||
import CTABannerSection from "./CTABannerSection";
|
||||
|
||||
function mount(comp: () => JSX.Element): HTMLDivElement {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
render(() => comp(), container);
|
||||
return container;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
describe("HowItWorksSection", () => {
|
||||
it("renders the section heading", () => {
|
||||
mount(() => <HowItWorksSection />);
|
||||
expect(document.body.textContent).toContain("How It Works");
|
||||
});
|
||||
|
||||
it("renders the section subheading", () => {
|
||||
mount(() => <HowItWorksSection />);
|
||||
expect(document.body.textContent).toContain(
|
||||
"Three simple steps to full identity protection",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders all 3 steps", () => {
|
||||
mount(() => <HowItWorksSection />);
|
||||
expect(document.body.textContent).toContain("Enroll Your Identity");
|
||||
expect(document.body.textContent).toContain("We Monitor 24/7");
|
||||
expect(document.body.textContent).toContain("Get Instant Alerts");
|
||||
});
|
||||
|
||||
it("renders step descriptions", () => {
|
||||
mount(() => <HowItWorksSection />);
|
||||
expect(document.body.textContent).toContain(
|
||||
"Sign up and add your emails",
|
||||
);
|
||||
expect(document.body.textContent).toContain("dark web scans");
|
||||
expect(document.body.textContent).toContain("real-time notifications");
|
||||
});
|
||||
|
||||
it("renders 3 numbered circles with gradient-primary", () => {
|
||||
mount(() => <HowItWorksSection />);
|
||||
const circles = document.querySelectorAll(".gradient-primary");
|
||||
expect(circles.length).toBe(3);
|
||||
});
|
||||
|
||||
it("has the anchor ID for smooth scrolling", () => {
|
||||
mount(() => <HowItWorksSection />);
|
||||
const section = document.querySelector('#how-it-works');
|
||||
expect(section).toBeTruthy();
|
||||
});
|
||||
|
||||
it("applies custom class prop", () => {
|
||||
mount(() => <HowItWorksSection class="custom-how" />);
|
||||
const section = document.querySelector("section.custom-how");
|
||||
expect(section).toBeTruthy();
|
||||
});
|
||||
|
||||
it("wraps content in PageContainer", () => {
|
||||
mount(() => <HowItWorksSection />);
|
||||
const container = document.querySelector(".max-w-7xl");
|
||||
expect(container).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("FeaturesGridSection", () => {
|
||||
it("renders the section heading", () => {
|
||||
mount(() => <FeaturesGridSection />);
|
||||
expect(document.body.textContent).toContain("Platform Features");
|
||||
});
|
||||
|
||||
it("renders the section subheading", () => {
|
||||
mount(() => <FeaturesGridSection />);
|
||||
expect(document.body.textContent).toContain("Comprehensive protection");
|
||||
});
|
||||
|
||||
it("renders all 6 feature cards", () => {
|
||||
mount(() => <FeaturesGridSection />);
|
||||
expect(document.body.textContent).toContain("DarkWatch");
|
||||
expect(document.body.textContent).toContain("VoicePrint");
|
||||
expect(document.body.textContent).toContain("SpamShield");
|
||||
expect(document.body.textContent).toContain("HomeTitle");
|
||||
expect(document.body.textContent).toContain("RemoveBrokers");
|
||||
expect(document.body.textContent).toContain("Family Plans");
|
||||
});
|
||||
|
||||
it("renders 6 Card components", () => {
|
||||
mount(() => <FeaturesGridSection />);
|
||||
const cards = document.querySelectorAll(".gradient-card");
|
||||
expect(cards.length).toBe(6);
|
||||
});
|
||||
|
||||
it("renders feature descriptions", () => {
|
||||
mount(() => <FeaturesGridSection />);
|
||||
expect(document.body.textContent).toContain("dark web monitoring");
|
||||
expect(document.body.textContent).toContain("voice clone detection");
|
||||
expect(document.body.textContent).toContain("scam call blocking");
|
||||
});
|
||||
|
||||
it("has the anchor ID for smooth scrolling", () => {
|
||||
mount(() => <FeaturesGridSection />);
|
||||
const section = document.querySelector('#features');
|
||||
expect(section).toBeTruthy();
|
||||
});
|
||||
|
||||
it("applies custom class prop", () => {
|
||||
mount(() => <FeaturesGridSection class="custom-features" />);
|
||||
const section = document.querySelector("section.custom-features");
|
||||
expect(section).toBeTruthy();
|
||||
});
|
||||
|
||||
it("uses responsive grid layout", () => {
|
||||
mount(() => <FeaturesGridSection />);
|
||||
const grid = document.querySelector(".grid-cols-1");
|
||||
expect(grid).toBeTruthy();
|
||||
expect(grid!.className).toContain("md:grid-cols-2");
|
||||
expect(grid!.className).toContain("lg:grid-cols-3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ForUsersSection", () => {
|
||||
it("renders the section heading", () => {
|
||||
mount(() => <ForUsersSection />);
|
||||
expect(document.body.textContent).toContain("For Everyone");
|
||||
});
|
||||
|
||||
it("renders the section subheading", () => {
|
||||
mount(() => <ForUsersSection />);
|
||||
expect(document.body.textContent).toContain(
|
||||
"Whether you're protecting yourself",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders both panels", () => {
|
||||
mount(() => <ForUsersSection />);
|
||||
expect(document.body.textContent).toContain("For Individuals");
|
||||
expect(document.body.textContent).toContain("For Families");
|
||||
});
|
||||
|
||||
it("renders individual panel description", () => {
|
||||
mount(() => <ForUsersSection />);
|
||||
expect(document.body.textContent).toContain(
|
||||
"Personal identity protection",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders family panel description", () => {
|
||||
mount(() => <ForUsersSection />);
|
||||
expect(document.body.textContent).toContain("Group management tools");
|
||||
});
|
||||
|
||||
it("renders bullet items for individuals", () => {
|
||||
mount(() => <ForUsersSection />);
|
||||
expect(document.body.textContent).toContain(
|
||||
"Monitor personal email and phone numbers",
|
||||
);
|
||||
expect(document.body.textContent).toContain(
|
||||
"Dark web credential scanning",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders bullet items for families", () => {
|
||||
mount(() => <ForUsersSection />);
|
||||
expect(document.body.textContent).toContain(
|
||||
"Add unlimited family members",
|
||||
);
|
||||
expect(document.body.textContent).toContain("Shared alert dashboard");
|
||||
});
|
||||
|
||||
it("renders checkmark icons", () => {
|
||||
mount(() => <ForUsersSection />);
|
||||
const checkmarks = document.querySelectorAll(
|
||||
'svg path[fill="var(--color-success)"]',
|
||||
);
|
||||
expect(checkmarks.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders 2 Card components for panels", () => {
|
||||
mount(() => <ForUsersSection />);
|
||||
const cards = document.querySelectorAll(".gradient-card");
|
||||
expect(cards.length).toBe(2);
|
||||
});
|
||||
|
||||
it("has the anchor ID for smooth scrolling", () => {
|
||||
mount(() => <ForUsersSection />);
|
||||
const section = document.querySelector('#for-users');
|
||||
expect(section).toBeTruthy();
|
||||
});
|
||||
|
||||
it("applies custom class prop", () => {
|
||||
mount(() => <ForUsersSection class="custom-users" />);
|
||||
const section = document.querySelector("section.custom-users");
|
||||
expect(section).toBeTruthy();
|
||||
});
|
||||
|
||||
it("uses two-column grid on desktop", () => {
|
||||
mount(() => <ForUsersSection />);
|
||||
const grid = document.querySelector(".grid-cols-1");
|
||||
expect(grid).toBeTruthy();
|
||||
expect(grid!.className).toContain("md:grid-cols-2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("WhyKordantSection", () => {
|
||||
it("renders the section heading", () => {
|
||||
mount(() => <WhyKordantSection />);
|
||||
expect(document.body.textContent).toContain("Why Kordant");
|
||||
});
|
||||
|
||||
it("renders the section subheading", () => {
|
||||
mount(() => <WhyKordantSection />);
|
||||
expect(document.body.textContent).toContain(
|
||||
"Built on cutting-edge technology",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders all 3 value prop cards", () => {
|
||||
mount(() => <WhyKordantSection />);
|
||||
expect(document.body.textContent).toContain("Proactive, Not Reactive");
|
||||
expect(document.body.textContent).toContain("AI-Powered Detection");
|
||||
expect(document.body.textContent).toContain("Privacy First");
|
||||
});
|
||||
|
||||
it("renders value prop descriptions", () => {
|
||||
mount(() => <WhyKordantSection />);
|
||||
expect(document.body.textContent).toContain(
|
||||
"detect threats before they cause damage",
|
||||
);
|
||||
expect(document.body.textContent).toContain(
|
||||
"Machine learning models trained",
|
||||
);
|
||||
expect(document.body.textContent).toContain("encrypted and private");
|
||||
});
|
||||
|
||||
it("renders bullet items for each card", () => {
|
||||
mount(() => <WhyKordantSection />);
|
||||
expect(document.body.textContent).toContain(
|
||||
"Real-time dark web scanning",
|
||||
);
|
||||
expect(document.body.textContent).toContain(
|
||||
"Deepfake voice identification",
|
||||
);
|
||||
expect(document.body.textContent).toContain("End-to-end encrypted data");
|
||||
});
|
||||
|
||||
it("renders 3 Card components", () => {
|
||||
mount(() => <WhyKordantSection />);
|
||||
const cards = document.querySelectorAll(".gradient-card");
|
||||
expect(cards.length).toBe(3);
|
||||
});
|
||||
|
||||
it("has the anchor ID for smooth scrolling", () => {
|
||||
mount(() => <WhyKordantSection />);
|
||||
const section = document.querySelector('#why-kordant');
|
||||
expect(section).toBeTruthy();
|
||||
});
|
||||
|
||||
it("applies custom class prop", () => {
|
||||
mount(() => <WhyKordantSection class="custom-why" />);
|
||||
const section = document.querySelector("section.custom-why");
|
||||
expect(section).toBeTruthy();
|
||||
});
|
||||
|
||||
it("uses three-column grid on desktop", () => {
|
||||
mount(() => <WhyKordantSection />);
|
||||
const grid = document.querySelector(".grid-cols-1");
|
||||
expect(grid).toBeTruthy();
|
||||
expect(grid!.className).toContain("md:grid-cols-3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("CTABannerSection", () => {
|
||||
it("renders the CTA headline", () => {
|
||||
mount(() => <CTABannerSection />);
|
||||
expect(document.body.textContent).toContain(
|
||||
"Ready to protect your identity?",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders the CTA subtext", () => {
|
||||
mount(() => <CTABannerSection />);
|
||||
expect(document.body.textContent).toContain(
|
||||
"Join thousands of users",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders Create Account button", () => {
|
||||
mount(() => <CTABannerSection />);
|
||||
expect(document.body.textContent).toContain("Create Account");
|
||||
const primaryBtn = document.querySelector("button.gradient-primary");
|
||||
expect(primaryBtn).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders Sign In button", () => {
|
||||
mount(() => <CTABannerSection />);
|
||||
expect(document.body.textContent).toContain("Sign In");
|
||||
});
|
||||
|
||||
it("has Create Account link to /signup", () => {
|
||||
mount(() => <CTABannerSection />);
|
||||
const links = document.querySelectorAll("a");
|
||||
const signupLink = Array.from(links).find(
|
||||
(a) => a.getAttribute("href") === "/signup",
|
||||
);
|
||||
expect(signupLink).toBeTruthy();
|
||||
expect(signupLink!.textContent).toContain("Create Account");
|
||||
});
|
||||
|
||||
it("has Sign In link to /login", () => {
|
||||
mount(() => <CTABannerSection />);
|
||||
const links = document.querySelectorAll("a");
|
||||
const loginLink = Array.from(links).find(
|
||||
(a) => a.getAttribute("href") === "/login",
|
||||
);
|
||||
expect(loginLink).toBeTruthy();
|
||||
expect(loginLink!.textContent).toContain("Sign In");
|
||||
});
|
||||
|
||||
it("renders 2 buttons", () => {
|
||||
mount(() => <CTABannerSection />);
|
||||
const buttons = document.querySelectorAll("button");
|
||||
expect(buttons.length).toBe(2);
|
||||
});
|
||||
|
||||
it("has the anchor ID for smooth scrolling", () => {
|
||||
mount(() => <CTABannerSection />);
|
||||
const section = document.querySelector('#cta');
|
||||
expect(section).toBeTruthy();
|
||||
});
|
||||
|
||||
it("applies custom class prop", () => {
|
||||
mount(() => <CTABannerSection class="custom-cta" />);
|
||||
const section = document.querySelector("section.custom-cta");
|
||||
expect(section).toBeTruthy();
|
||||
});
|
||||
|
||||
it("uses centered text layout", () => {
|
||||
mount(() => <CTABannerSection />);
|
||||
const inner = document.querySelector(".text-center");
|
||||
expect(inner).toBeTruthy();
|
||||
});
|
||||
|
||||
it("wraps content in PageContainer", () => {
|
||||
mount(() => <CTABannerSection />);
|
||||
const container = document.querySelector(".max-w-7xl");
|
||||
expect(container).toBeTruthy();
|
||||
});
|
||||
|
||||
it("uses gradient card for CTA banner", () => {
|
||||
mount(() => <CTABannerSection />);
|
||||
const card = document.querySelector(".gradient-card");
|
||||
expect(card).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -143,7 +143,7 @@ function RealtimeIndicator() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearUnread}
|
||||
class="relative flex items-center justify-center w-6 h-6 rounded-full bg-[var(--color-error)] text-white text-[10px] font-bold leading-none transition-transform hover:scale-110"
|
||||
class="relative flex items-center justify-center w-6 h-6 rounded-full bg-(--color-error) text-white text-[10px] font-bold leading-none transition-transform hover:scale-110"
|
||||
aria-label={`${unreadCount()} unread alerts`}
|
||||
>
|
||||
{unreadCount() > 99 ? "99+" : unreadCount()}
|
||||
@@ -152,17 +152,24 @@ function RealtimeIndicator() {
|
||||
<Show
|
||||
when={connectionStatus() === "connected"}
|
||||
fallback={
|
||||
connectionStatus() === "reconnecting" || connectionStatus() === "connecting" ? (
|
||||
<div class="flex items-center gap-1 text-[10px] text-[var(--color-warning)]" aria-label="Reconnecting">
|
||||
connectionStatus() === "reconnecting" ||
|
||||
connectionStatus() === "connecting" ? (
|
||||
<div
|
||||
class="flex items-center gap-1 text-[10px] text-(--color-warning)"
|
||||
aria-label="Reconnecting"
|
||||
>
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--color-warning)] opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-[var(--color-warning)]" />
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-(--color-warning) opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-(--color-warning)" />
|
||||
</span>
|
||||
<span class="hidden sm:inline">Reconnecting</span>
|
||||
</div>
|
||||
) : (
|
||||
<div class="flex items-center gap-1 text-[10px] text-[var(--color-text-muted)]" aria-label="Offline">
|
||||
<span class="inline-flex rounded-full h-2 w-2 bg-[var(--color-text-muted)]" />
|
||||
<div
|
||||
class="flex items-center gap-1 text-[10px] text-(--color-text-muted)"
|
||||
aria-label="Offline"
|
||||
>
|
||||
<span class="inline-flex rounded-full h-2 w-2 bg-(--color-text-muted)" />
|
||||
<span class="hidden sm:inline">Offline</span>
|
||||
</div>
|
||||
)
|
||||
@@ -170,8 +177,8 @@ function RealtimeIndicator() {
|
||||
>
|
||||
<div class="flex items-center gap-1" aria-label="Connected">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--color-success)] opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-[var(--color-success)]" />
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-(--color-success) opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-(--color-success)" />
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -197,7 +204,11 @@ export default function Navbar() {
|
||||
return location.pathname.startsWith(href);
|
||||
};
|
||||
|
||||
const NavLink = (props: { href: string; label: string; mobile?: boolean }) => (
|
||||
const NavLink = (props: {
|
||||
href: string;
|
||||
label: string;
|
||||
mobile?: boolean;
|
||||
}) => (
|
||||
<A
|
||||
href={props.href}
|
||||
class={cn(
|
||||
@@ -205,9 +216,11 @@ export default function Navbar() {
|
||||
? "block px-3 py-2 rounded-lg text-base font-medium transition-colors"
|
||||
: "text-sm font-medium transition-colors",
|
||||
isActive(props.href)
|
||||
? "text-[var(--color-text-primary)]"
|
||||
? "text-text-primary"
|
||||
: "text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
|
||||
props.mobile && !isActive(props.href) && "hover:bg-[var(--color-bg-secondary)]",
|
||||
props.mobile &&
|
||||
!isActive(props.href) &&
|
||||
"hover:bg-[var(--color-bg-secondary)]",
|
||||
)}
|
||||
onClick={() => props.mobile && setMobileOpen(false)}
|
||||
>
|
||||
@@ -234,10 +247,14 @@ export default function Navbar() {
|
||||
|
||||
<div class="hidden md:flex items-center gap-6">
|
||||
<SignedOut>
|
||||
{marketingLinks.map(link => <NavLink href={link.href} label={link.label} />)}
|
||||
{marketingLinks.map((link) => (
|
||||
<NavLink href={link.href} label={link.label} />
|
||||
))}
|
||||
</SignedOut>
|
||||
<SignedIn>
|
||||
{productLinks.map(link => <NavLink href={link.href} label={link.label} />)}
|
||||
{productLinks.map((link) => (
|
||||
<NavLink href={link.href} label={link.label} />
|
||||
))}
|
||||
</SignedIn>
|
||||
</div>
|
||||
|
||||
@@ -263,7 +280,7 @@ export default function Navbar() {
|
||||
type="button"
|
||||
aria-label="Toggle menu"
|
||||
class="p-2 rounded-lg text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]"
|
||||
onClick={() => setMobileOpen(v => !v)}
|
||||
onClick={() => setMobileOpen((v) => !v)}
|
||||
>
|
||||
<Show
|
||||
when={mobileOpen()}
|
||||
@@ -304,12 +321,12 @@ export default function Navbar() {
|
||||
<div class="md:hidden glass border-t border-[var(--color-border)]">
|
||||
<div class="px-4 py-4 space-y-1">
|
||||
<SignedOut>
|
||||
{marketingLinks.map(link => (
|
||||
{marketingLinks.map((link) => (
|
||||
<NavLink href={link.href} label={link.label} mobile />
|
||||
))}
|
||||
</SignedOut>
|
||||
<SignedIn>
|
||||
{productLinks.map(link => (
|
||||
{productLinks.map((link) => (
|
||||
<NavLink href={link.href} label={link.label} mobile />
|
||||
))}
|
||||
</SignedIn>
|
||||
|
||||
Reference in New Issue
Block a user