clear old assets, new ci/cd flow

This commit is contained in:
2026-05-26 11:54:41 -04:00
parent 82815009c9
commit 72609755f8
87 changed files with 4132 additions and 7158 deletions

View File

@@ -1,5 +1,5 @@
import { createSignal, onMount, onCleanup, Show } from "solid-js";
import { A } from "@solidjs/router";
import { A, useLocation } from "@solidjs/router";
import { cn } from "~/lib/utils";
import { Button } from "~/components/ui";
import { Typewriter } from "~/components/ui/Typewriter";
@@ -119,11 +119,19 @@ function ThemeToggle() {
);
}
const navLinks = [
const marketingLinks = [
{ label: "Features", href: "/features" },
{ label: "Pricing", href: "/pricing" },
{ label: "Blog", href: "/blog" },
];
const productLinks = [
{ label: "Dashboard", href: "/dashboard" },
{ label: "DarkWatch", href: "/darkwatch" },
{ label: "VoicePrint", href: "/voiceprint" },
{ label: "SpamShield", href: "/spamshield" },
{ label: "HomeTitle", href: "/hometitle" },
{ label: "RemoveBrokers", href: "/removebrokers" },
];
function RealtimeIndicator() {
@@ -174,6 +182,7 @@ function RealtimeIndicator() {
export default function Navbar() {
const [mobileOpen, setMobileOpen] = createSignal(false);
const [scrolled, setScrolled] = createSignal(false);
const location = useLocation();
onMount(() => {
const onScroll = () => {
@@ -183,6 +192,29 @@ export default function Navbar() {
onCleanup(() => window.removeEventListener("scroll", onScroll));
});
const isActive = (href: string) => {
if (href === "/dashboard") return location.pathname === "/dashboard";
return location.pathname.startsWith(href);
};
const NavLink = (props: { href: string; label: string; mobile?: boolean }) => (
<A
href={props.href}
class={cn(
props.mobile
? "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-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
props.mobile && !isActive(props.href) && "hover:bg-[var(--color-bg-secondary)]",
)}
onClick={() => props.mobile && setMobileOpen(false)}
>
{props.label}
</A>
);
return (
<nav
class={cn(
@@ -201,14 +233,12 @@ export default function Navbar() {
</A>
<div class="hidden md:flex items-center gap-6">
{navLinks.map(link => (
<A
href={link.href}
class="text-sm font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
>
{link.label}
</A>
))}
<SignedOut>
{marketingLinks.map(link => <NavLink href={link.href} label={link.label} />)}
</SignedOut>
<SignedIn>
{productLinks.map(link => <NavLink href={link.href} label={link.label} />)}
</SignedIn>
</div>
<div class="hidden md:flex items-center gap-3">
@@ -216,9 +246,6 @@ export default function Navbar() {
<SignedIn>
<UserButton showName />
<RealtimeIndicator />
<Button variant="secondary" size="sm">
<A href="/dashboard">Dashboard</A>
</Button>
</SignedIn>
<SignedOut>
<Button variant="secondary" size="sm">
@@ -276,19 +303,20 @@ export default function Navbar() {
<Show when={mobileOpen()}>
<div class="md:hidden glass border-t border-[var(--color-border)]">
<div class="px-4 py-4 space-y-1">
{navLinks.map(link => (
<A
href={link.href}
class="block px-3 py-2 rounded-lg text-base font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] transition-colors"
onClick={() => setMobileOpen(false)}
>
{link.label}
</A>
))}
<SignedOut>
{marketingLinks.map(link => (
<NavLink href={link.href} label={link.label} mobile />
))}
</SignedOut>
<SignedIn>
{productLinks.map(link => (
<NavLink href={link.href} label={link.label} mobile />
))}
</SignedIn>
<div class="pt-3 flex flex-col gap-2">
<SignedIn>
<Button variant="secondary" class="w-full">
<A href="/dashboard">Dashboard</A>
<A href="/dashboard">Go to Dashboard</A>
</Button>
</SignedIn>
<SignedOut>

View File

@@ -0,0 +1,163 @@
import { createSignal, createEffect, Show } from "solid-js";
import { A, Navigate, useParams } from "@solidjs/router";
import { api } from "~/lib/api";
const ALL_TAGS = ["Identity Theft", "AI Safety", "Privacy", "Deepfakes", "Dark Web", "Scam Alerts", "Product News"];
export default function AdminBlogEdit() {
const params = useParams();
const [post, setPost] = createSignal<any>(null);
const [loading, setLoading] = createSignal(true);
const [title, setTitle] = createSignal("");
const [slug, setSlug] = createSignal("");
const [excerpt, setExcerpt] = createSignal("");
const [content, setContent] = createSignal("");
const [authorName, setAuthorName] = createSignal("");
const [coverImageUrl, setCoverImageUrl] = createSignal("");
const [tags, setTags] = createSignal<string[]>([]);
const [published, setPublished] = createSignal(false);
const [featured, setFeatured] = createSignal(false);
const [saving, setSaving] = createSignal(false);
const [error, setError] = createSignal("");
const [success, setSuccess] = createSignal(false);
createEffect(() => {
api.admin.blogGet.query({ id: params.slug }).then(data => {
if (data) {
setPost(data);
setTitle(data.title);
setSlug(data.slug);
setExcerpt(data.excerpt || "");
setContent(data.content);
setAuthorName(data.authorName || "");
setCoverImageUrl(data.coverImageUrl || "");
setTags(Array.isArray(data.tags) ? data.tags : []);
setPublished(!!data.published);
setFeatured(!!data.featured);
}
setLoading(false);
});
});
const handleSubmit = async (e: Event) => {
e.preventDefault();
setError("");
setSaving(true);
try {
await api.admin.blogUpdate.mutate({
id: params.slug,
title: title() || undefined,
slug: slug() || undefined,
excerpt: excerpt() || undefined,
content: content() || undefined,
authorName: authorName() || undefined,
coverImageUrl: coverImageUrl() || undefined,
tags: tags().join(","),
published: published(),
featured: featured(),
});
setSuccess(true);
} catch (err: any) {
setError(err.message || "Failed to update post");
} finally {
setSaving(false);
}
};
const toggleTag = (tag: string) => {
setTags(tags().includes(tag) ? tags().filter(t => t !== tag) : [...tags(), tag]);
};
if (success()) {
return <Navigate href="/admin/blog" />;
}
return (
<Show when={!loading()} fallback={<p class="text-[var(--color-text-secondary)]">Loading...</p>}>
<div>
<div class="flex items-center gap-4 mb-6">
<A href="/admin/blog" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors">
Back to Posts
</A>
<h2 class="text-2xl font-bold text-[var(--color-text-primary)]">Edit Post</h2>
</div>
<form onSubmit={handleSubmit} class="space-y-6 max-w-4xl">
<div class="bg-[var(--color-bg-secondary)] rounded-xl border border-[var(--color-border)] p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Title</label>
<input type="text" value={title()} onInput={(e) => setTitle(e.currentTarget.value)} required
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]" />
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Slug</label>
<input type="text" value={slug()} onInput={(e) => setSlug(e.currentTarget.value)} required
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]" />
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Excerpt</label>
<textarea value={excerpt()} onInput={(e) => setExcerpt(e.currentTarget.value)} rows={2}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)] resize-none" />
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Author</label>
<input type="text" value={authorName()} onInput={(e) => setAuthorName(e.currentTarget.value)}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]" />
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Cover Image URL</label>
<input type="url" value={coverImageUrl()} onInput={(e) => setCoverImageUrl(e.currentTarget.value)}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]" />
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Tags</label>
<div class="flex flex-wrap gap-2">
{ALL_TAGS.map(tag => (
<button type="button" onClick={() => toggleTag(tag)}
class={[
"px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
tags().includes(tag)
? "bg-[var(--color-brand-primary)] text-white"
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
].join(" ")}
>{tag}</button>
))}
</div>
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Content (Markdown)</label>
<textarea value={content()} onInput={(e) => setContent(e.currentTarget.value)} required rows={16}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm font-mono focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)] resize-y" />
</div>
<div class="flex items-center gap-6 pt-2">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={published()} onChange={(e) => setPublished(e.currentTarget.checked)}
class="rounded border-[var(--color-border)] text-[var(--color-brand-primary)] focus:ring-[var(--color-brand-primary)]" />
<span class="text-sm text-[var(--color-text-primary)]">Published</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={featured()} onChange={(e) => setFeatured(e.currentTarget.checked)}
class="rounded border-[var(--color-border)] text-[var(--color-brand-primary)] focus:ring-[var(--color-brand-primary)]" />
<span class="text-sm text-[var(--color-text-primary)]">Featured</span>
</label>
</div>
</div>
{error() && (
<div class="p-4 rounded-lg bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-400 text-sm">{error()}</div>
)}
<div class="flex items-center gap-3">
<button type="submit" disabled={saving()}
class="px-6 py-2.5 bg-[var(--color-brand-primary)] text-white rounded-lg hover:bg-[var(--color-brand-primary)]/90 transition-colors disabled:opacity-50 text-sm font-medium">
{saving() ? "Saving..." : "Save Changes"}
</button>
<A href="/admin/blog" class="px-6 py-2.5 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors text-sm">
Cancel
</A>
</div>
</form>
</div>
</Show>
);
}

View File

@@ -0,0 +1,126 @@
import { A, Navigate } from "@solidjs/router";
import { For, Show, createSignal, createEffect } from "solid-js";
import { api } from "~/lib/api";
export default function AdminBlog() {
const [posts, setPosts] = createSignal<any[]>([]);
const [loading, setLoading] = createSignal(true);
const [deletingId, setDeletingId] = createSignal<string | null>(null);
const [redirect, setRedirect] = createSignal(false);
const loadPosts = () => {
api.admin.blogList.query().then(setPosts).finally(() => setLoading(false));
};
createEffect(() => {
loadPosts();
});
const handleDelete = async (id: string) => {
if (!confirm("Delete this post? This cannot be undone.")) return;
setDeletingId(id);
try {
await api.admin.blogDelete.mutate({ id });
setPosts(posts().filter(p => p.id !== id));
} catch (err: any) {
alert(err.message || "Failed to delete post");
} finally {
setDeletingId(null);
}
};
if (redirect()) return <Navigate href="/admin/blog/new" />;
return (
<div>
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-[var(--color-text-primary)]">Blog Posts</h2>
<div class="flex items-center gap-3">
<button
type="button"
onClick={loadPosts}
class="px-4 py-2 bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] rounded-lg hover:text-[var(--color-text-primary)] transition-colors text-sm"
>
Refresh
</button>
<button
type="button"
onClick={() => setRedirect(true)}
class="px-4 py-2 bg-[var(--color-brand-primary)] text-white rounded-lg hover:bg-[var(--color-brand-primary)]/90 transition-colors text-sm"
>
New Post
</button>
</div>
</div>
<Show when={!loading()} fallback={<p class="text-[var(--color-text-secondary)]">Loading...</p>}>
<div class="bg-[var(--color-bg-secondary)] rounded-xl border border-[var(--color-border)] overflow-hidden">
<table class="w-full">
<thead>
<tr class="border-b border-[var(--color-border)]">
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Title</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Status</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Featured</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Views</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Date</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Actions</th>
</tr>
</thead>
<tbody>
<For each={posts()}>
{(post) => (
<tr class="border-b border-[var(--color-border)] last:border-0 hover:bg-[var(--color-bg-tertiary)]">
<td class="px-6 py-4">
<div>
<p class="font-medium text-[var(--color-text-primary)]">{post.title}</p>
<p class="text-sm text-[var(--color-text-secondary)]">{post.slug}</p>
</div>
</td>
<td class="px-6 py-4">
<span class={[
"px-2 py-1 rounded-full text-xs font-medium",
post.published
? "bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400"
: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400",
].join(" ")}>
{post.published ? "Published" : "Draft"}
</span>
</td>
<td class="px-6 py-4">
{post.featured ? "⭐" : "—"}
</td>
<td class="px-6 py-4 text-[var(--color-text-secondary)]">{post.viewCount}</td>
<td class="px-6 py-4 text-[var(--color-text-secondary)]">
{post.publishedAt ? new Date(post.publishedAt).toLocaleDateString() : "—"}
</td>
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<A
href={`/admin/blog/${post.id}`}
class="text-sm text-[var(--color-brand-primary)] hover:text-[var(--color-brand-accent)] transition-colors"
>
Edit
</A>
<button
type="button"
onClick={() => handleDelete(post.id)}
disabled={deletingId() === post.id}
class="text-sm text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 transition-colors disabled:opacity-50"
>
{deletingId() === post.id ? "Deleting..." : "Delete"}
</button>
</div>
</td>
</tr>
)}
</For>
</tbody>
</table>
<Show when={posts().length === 0}>
<p class="text-[var(--color-text-secondary)] py-8 text-center">No posts yet</p>
</Show>
</div>
</Show>
</div>
);
}

View File

@@ -0,0 +1,210 @@
import { createSignal, createEffect } from "solid-js";
import { A, Navigate } from "@solidjs/router";
import { api } from "~/lib/api";
const ALL_TAGS = ["Identity Theft", "AI Safety", "Privacy", "Deepfakes", "Dark Web", "Scam Alerts", "Product News"];
function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
}
export default function AdminBlogNew() {
const [title, setTitle] = createSignal("");
const [slug, setSlug] = createSignal("");
const [excerpt, setExcerpt] = createSignal("");
const [content, setContent] = createSignal("");
const [authorName, setAuthorName] = createSignal("Kordant Security Team");
const [coverImageUrl, setCoverImageUrl] = createSignal("");
const [tags, setTags] = createSignal<string[]>([]);
const [published, setPublished] = createSignal(false);
const [featured, setFeatured] = createSignal(false);
const [saving, setSaving] = createSignal(false);
const [error, setError] = createSignal("");
const [success, setSuccess] = createSignal(false);
createEffect(() => {
const t = title();
if (t && !slug()) {
setSlug(slugify(t));
}
});
const handleSubmit = async (e: Event) => {
e.preventDefault();
setError("");
setSaving(true);
try {
await api.admin.blogCreate.mutate({
title: title(),
slug: slug(),
excerpt: excerpt() || undefined,
content: content(),
authorName: authorName() || undefined,
coverImageUrl: coverImageUrl() || undefined,
tags: tags().join(","),
published: published(),
featured: featured(),
});
setSuccess(true);
} catch (err: any) {
setError(err.message || "Failed to create post");
} finally {
setSaving(false);
}
};
const toggleTag = (tag: string) => {
setTags(tags().includes(tag) ? tags().filter(t => t !== tag) : [...tags(), tag]);
};
if (success()) {
return <Navigate href="/admin/blog" />;
}
return (
<div>
<div class="flex items-center gap-4 mb-6">
<A href="/admin/blog" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors">
Back to Posts
</A>
<h2 class="text-2xl font-bold text-[var(--color-text-primary)]">New Blog Post</h2>
</div>
<form onSubmit={handleSubmit} class="space-y-6 max-w-4xl">
<div class="bg-[var(--color-bg-secondary)] rounded-xl border border-[var(--color-border)] p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Title</label>
<input
type="text"
value={title()}
onInput={(e) => setTitle(e.currentTarget.value)}
required
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]"
placeholder="Enter post title..."
/>
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Slug</label>
<input
type="text"
value={slug()}
onInput={(e) => setSlug(e.currentTarget.value)}
required
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]"
placeholder="auto-generated-from-title"
/>
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Excerpt</label>
<textarea
value={excerpt()}
onInput={(e) => setExcerpt(e.currentTarget.value)}
rows={2}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)] resize-none"
placeholder="Brief summary for the blog listing..."
/>
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Author</label>
<input
type="text"
value={authorName()}
onInput={(e) => setAuthorName(e.currentTarget.value)}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]"
placeholder="Author name..."
/>
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Cover Image URL</label>
<input
type="url"
value={coverImageUrl()}
onInput={(e) => setCoverImageUrl(e.currentTarget.value)}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]"
placeholder="https://..."
/>
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Tags</label>
<div class="flex flex-wrap gap-2">
{ALL_TAGS.map(tag => (
<button
type="button"
onClick={() => toggleTag(tag)}
class={[
"px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
tags().includes(tag)
? "bg-[var(--color-brand-primary)] text-white"
: "bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
].join(" ")}
>
{tag}
</button>
))}
</div>
</div>
<div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-1">Content (Markdown)</label>
<textarea
value={content()}
onInput={(e) => setContent(e.currentTarget.value)}
required
rows={16}
class="w-full px-3 py-2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)] text-sm font-mono focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)] resize-y"
placeholder="# Title&#10;&#10;Write your content here..."
/>
</div>
<div class="flex items-center gap-6 pt-2">
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={published()}
onChange={(e) => setPublished(e.currentTarget.checked)}
class="rounded border-[var(--color-border)] text-[var(--color-brand-primary)] focus:ring-[var(--color-brand-primary)]"
/>
<span class="text-sm text-[var(--color-text-primary)]">Published</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={featured()}
onChange={(e) => setFeatured(e.currentTarget.checked)}
class="rounded border-[var(--color-border)] text-[var(--color-brand-primary)] focus:ring-[var(--color-brand-primary)]"
/>
<span class="text-sm text-[var(--color-text-primary)]">Featured</span>
</label>
</div>
</div>
{error() && (
<div class="p-4 rounded-lg bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-400 text-sm">
{error()}
</div>
)}
<div class="flex items-center gap-3">
<button
type="submit"
disabled={saving()}
class="px-6 py-2.5 bg-[var(--color-brand-primary)] text-white rounded-lg hover:bg-[var(--color-brand-primary)]/90 transition-colors disabled:opacity-50 text-sm font-medium"
>
{saving() ? "Creating..." : "Create Post"}
</button>
<A href="/admin/blog" class="px-6 py-2.5 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors text-sm">
Cancel
</A>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,60 @@
import { For, Show, createSignal, createEffect } from "solid-js";
import { api } from "~/lib/api";
function StatCard(props: { label: string; value: string | number; icon: string }) {
return (
<div class="bg-[var(--color-bg-secondary)] rounded-xl p-6 border border-[var(--color-border)]">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-[var(--color-text-secondary)]">{props.label}</p>
<p class="text-3xl font-bold text-[var(--color-text-primary)] mt-2">{props.value}</p>
</div>
<span class="text-2xl">{props.icon}</span>
</div>
</div>
);
}
export default function AdminDashboard() {
const [stats, setStats] = createSignal<any>(null);
const [loading, setLoading] = createSignal(true);
createEffect(() => {
api.admin.stats.query().then(setStats).finally(() => setLoading(false));
});
return (
<div>
<h2 class="text-2xl font-bold text-[var(--color-text-primary)] mb-6">Dashboard</h2>
<Show when={!loading()} fallback={<p class="text-[var(--color-text-secondary)]">Loading...</p>}>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<StatCard label="Total Users" value={stats()?.userCount ?? 0} icon="👥" />
<StatCard label="Blog Posts" value={stats()?.postCount ?? 0} icon="📝" />
<StatCard label="Total Views" value={stats()?.totalViews ?? 0} icon="👁️" />
</div>
<div class="bg-[var(--color-bg-secondary)] rounded-xl border border-[var(--color-border)]">
<div class="p-6 border-b border-[var(--color-border)]">
<h3 class="text-lg font-semibold text-[var(--color-text-primary)]">Recent Posts</h3>
</div>
<div class="p-6">
<For each={stats()?.recentPosts ?? []}>
{(post) => (
<div class="flex items-center justify-between py-3 border-b border-[var(--color-border)] last:border-0">
<span class="text-[var(--color-text-primary)]">{post.title}</span>
<span class="text-sm text-[var(--color-text-secondary)]">
{post.publishedAt ? new Date(post.publishedAt).toLocaleDateString() : "Draft"}
</span>
</div>
)}
</For>
<Show when={(!stats()?.recentPosts || stats()?.recentPosts.length === 0)}>
<p class="text-[var(--color-text-secondary)] py-4">No posts yet</p>
</Show>
</div>
</div>
</Show>
</div>
);
}

View File

@@ -0,0 +1,70 @@
import { A, useLocation } from "@solidjs/router";
import { For, Show, createSignal, createEffect } from "solid-js";
import { useAuth } from "clerk-solidjs";
const adminNavItems = [
{ label: "Dashboard", href: "/admin" },
{ label: "Blog Posts", href: "/admin/blog" },
{ label: "Services", href: "/admin/services" },
{ label: "Users", href: "/admin/users" },
];
export function AdminLayout(props: { children: () => any }) {
const auth = useAuth();
const location = useLocation();
const [isAuthorized, setIsAuthorized] = createSignal(false);
createEffect(() => {
if (auth.isLoaded() && !auth.isSignedIn()) {
window.location.href = "/login";
} else if (auth.isLoaded() && auth.isSignedIn()) {
setIsAuthorized(true);
}
});
return (
<Show when={isAuthorized()}>
<div class="min-h-screen bg-[var(--color-bg)] flex">
<aside class="w-64 bg-[var(--color-bg-secondary)] border-r border-[var(--color-border)] flex flex-col">
<div class="p-6 border-b border-[var(--color-border)]">
<h1 class="text-xl font-bold text-[var(--color-text-primary)]">Admin Panel</h1>
</div>
<nav class="flex-1 p-4">
<ul class="space-y-1">
<For each={adminNavItems}>
{(item) => (
<li>
<A
href={item.href}
class={[
"block px-4 py-2 rounded-lg text-sm font-medium transition-colors",
location.pathname === item.href
? "bg-[var(--color-primary)]/10 text-[var(--color-primary)]"
: "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-tertiary)] hover:text-[var(--color-text-primary)]",
].join(" ")}
>
{item.label}
</A>
</li>
)}
</For>
</ul>
</nav>
<div class="p-4 border-t border-[var(--color-border)]">
<A
href="/"
class="block px-4 py-2 rounded-lg text-sm font-medium text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-tertiary)] hover:text-[var(--color-text-primary)] transition-colors"
>
Back to Site
</A>
</div>
</aside>
<main class="flex-1 overflow-auto">
<div class="p-8 max-w-6xl">
{props.children()}
</div>
</main>
</div>
</Show>
);
}

View File

@@ -0,0 +1,116 @@
import { For, createSignal, createEffect } from "solid-js";
import { api } from "~/lib/api";
interface Service {
name: string;
status: "operational" | "degraded" | "down";
uptime: string;
incident: string | null;
lastChecked: string;
}
// Placeholder services data — replace with real health checks when available
const services: Service[] = [
{ name: "DarkWatch", status: "operational", uptime: "99.98%", incident: null, lastChecked: "2 min ago" },
{ name: "VoicePrint", status: "operational", uptime: "99.95%", incident: null, lastChecked: "1 min ago" },
{ name: "SpamShield", status: "operational", uptime: "99.97%", incident: null, lastChecked: "3 min ago" },
{ name: "HomeTitle", status: "degraded", uptime: "98.50%", incident: "Elevated response times on title lookup API", lastChecked: "5 min ago" },
{ name: "RemoveBrokers", status: "operational", uptime: "99.90%", incident: null, lastChecked: "4 min ago" },
{ name: "Database", status: "operational", uptime: "100.00%", incident: null, lastChecked: "1 min ago" },
{ name: "Clerk Auth", status: "operational", uptime: "99.99%", incident: null, lastChecked: "1 min ago" },
];
function StatusBadge(props: { status: Service["status"] }) {
const config = {
operational: "bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400",
degraded: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400",
down: "bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400",
};
return (
<span class={`px-2 py-1 rounded-full text-xs font-medium ${config[props.status]}`}>
{props.status.charAt(0).toUpperCase() + props.status.slice(1)}
</span>
);
}
export default function AdminServices() {
const [refreshing, setRefreshing] = createSignal(false);
const handleRefresh = () => {
setRefreshing(true);
// Simulate refresh — replace with real health check API call
setTimeout(() => setRefreshing(false), 800);
};
const stats = () => {
const total = services.length;
const operational = services.filter(s => s.status === "operational").length;
const degraded = services.filter(s => s.status === "degraded").length;
const down = services.filter(s => s.status === "down").length;
return { total, operational, degraded, down };
};
return (
<div>
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-[var(--color-text-primary)]">Services</h2>
<button
type="button"
onClick={handleRefresh}
disabled={refreshing()}
class="px-4 py-2 bg-[var(--color-bg-tertiary)] text-[var(--color-text-secondary)] rounded-lg hover:text-[var(--color-text-primary)] transition-colors text-sm disabled:opacity-50"
>
{refreshing() ? "Refreshing..." : "Refresh Status"}
</button>
</div>
{/* Summary Cards */}
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
{[
{ label: "Total Services", value: stats().total, color: "text-[var(--color-text-primary)]" },
{ label: "Operational", value: stats().operational, color: "text-green-600 dark:text-green-400" },
{ label: "Degraded", value: stats().degraded, color: "text-yellow-600 dark:text-yellow-400" },
{ label: "Down", value: stats().down, color: "text-red-600 dark:text-red-400" },
].map(stat => (
<div class="bg-[var(--color-bg-secondary)] rounded-xl border border-[var(--color-border)] p-4">
<p class="text-sm text-[var(--color-text-secondary)] mb-1">{stat.label}</p>
<p class={`text-2xl font-bold ${stat.color}`}>{stat.value}</p>
</div>
))}
</div>
{/* Services Table */}
<div class="bg-[var(--color-bg-secondary)] rounded-xl border border-[var(--color-border)] overflow-hidden">
<table class="w-full">
<thead>
<tr class="border-b border-[var(--color-border)]">
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Service</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Status</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Uptime</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Incident</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Last Checked</th>
</tr>
</thead>
<tbody>
<For each={services}>
{(service) => (
<tr class="border-b border-[var(--color-border)] last:border-0 hover:bg-[var(--color-bg-tertiary)]">
<td class="px-6 py-4 font-medium text-[var(--color-text-primary)]">{service.name}</td>
<td class="px-6 py-4"><StatusBadge status={service.status} /></td>
<td class="px-6 py-4 text-[var(--color-text-secondary)]">{service.uptime}</td>
<td class="px-6 py-4 text-sm">
{service.incident
? <span class="text-yellow-700 dark:text-yellow-400">{service.incident}</span>
: <span class="text-[var(--color-text-tertiary)]"></span>
}
</td>
<td class="px-6 py-4 text-[var(--color-text-secondary)]">{service.lastChecked}</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
);
}

View File

@@ -0,0 +1,90 @@
import { For, Show, createSignal, createEffect } from "solid-js";
import { api } from "~/lib/api";
export default function AdminUsers() {
const [users, setUsers] = createSignal<any[]>([]);
const [loading, setLoading] = createSignal(true);
createEffect(() => {
api.admin.userList.query().then(setUsers).finally(() => setLoading(false));
});
const refresh = () => {
api.admin.userList.query().then(setUsers);
};
const handleRoleChange = async (userId: string, newRole: string) => {
await api.admin.userUpdateRole.mutate({ id: userId, role: newRole });
refresh();
};
return (
<div>
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-[var(--color-text-primary)]">Users</h2>
<button
onclick={refresh}
class="px-4 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 transition-colors"
>
Refresh
</button>
</div>
<Show when={!loading()} fallback={<p class="text-[var(--color-text-secondary)]">Loading...</p>}>
<div class="bg-[var(--color-bg-secondary)] rounded-xl border border-[var(--color-border)] overflow-hidden">
<table class="w-full">
<thead>
<tr class="border-b border-[var(--color-border)]">
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">User</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Role</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Joined</th>
<th class="text-left px-6 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Actions</th>
</tr>
</thead>
<tbody>
<For each={users()}>
{(user) => (
<tr class="border-b border-[var(--color-border)] last:border-0 hover:bg-[var(--color-bg-tertiary)]">
<td class="px-6 py-4">
<div>
<p class="font-medium text-[var(--color-text-primary)]">{user.name || "—"}</p>
<p class="text-sm text-[var(--color-text-secondary)]">{user.email}</p>
</div>
</td>
<td class="px-6 py-4">
<span class={[
"px-2 py-1 rounded-full text-xs font-medium",
user.role === "admin"
? "bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400"
: "bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400",
].join(" ")}>
{user.role}
</span>
</td>
<td class="px-6 py-4 text-[var(--color-text-secondary)]">
{new Date(user.createdAt).toLocaleDateString()}
</td>
<td class="px-6 py-4">
<select
value={user.role}
onchange={(e) => handleRoleChange(user.id, e.currentTarget.value)}
class="px-2 py-1 text-sm rounded border border-[var(--color-border)] bg-[var(--color-bg)] text-[var(--color-text-primary)]"
>
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="support">Support</option>
</select>
</td>
</tr>
)}
</For>
</tbody>
</table>
<Show when={users().length === 0}>
<p class="text-[var(--color-text-secondary)] py-8 text-center">No users found</p>
</Show>
</div>
</Show>
</div>
);
}

View File

@@ -1,105 +1,74 @@
import { createSignal, For, Show } from "solid-js";
import { createSignal, For, Show, createMemo, Suspense } from "solid-js";
import { Title } from "@solidjs/meta";
import { A } from "@solidjs/router";
import { cn } from "~/lib/utils";
import { Badge, Button, Card } from "~/components/ui";
import PageContainer from "~/components/layout/PageContainer";
import { api } from "~/lib/api";
interface BlogPost {
slug: string;
title: string;
excerpt: string;
author: string;
date: string;
readingTime: string;
coverImage: string;
tags: string[];
const POSTS_PER_PAGE = 6;
function readingTime(content: string): string {
const words = content.split(/\s+/).length;
const mins = Math.max(1, Math.ceil(words / 200));
return `${mins} min read`;
}
const allTags = ["Identity Theft", "AI Safety", "Privacy", "Deepfakes", "Dark Web", "Scam Alerts", "Product News"];
const blogPosts: BlogPost[] = [
{
slug: "ai-scam-trends-2026",
title: "AI Scam Trends to Watch in 2026",
excerpt: "As AI technology advances, scammers are finding new ways to exploit it. Here are the top threats to watch and how to protect yourself.",
author: "Sarah Chen",
date: "May 15, 2026",
readingTime: "5 min read",
coverImage: "",
tags: ["AI Safety", "Scam Alerts", "Deepfakes"],
},
{
slug: "dark-web-monitoring-guide",
title: "The Complete Guide to Dark Web Monitoring",
excerpt: "Learn how dark web monitoring works, what data gets exposed, and how Kordant keeps your information safe from cybercriminals.",
author: "Mike Reynolds",
date: "May 10, 2026",
readingTime: "8 min read",
coverImage: "",
tags: ["Dark Web", "Privacy"],
},
{
slug: "protecting-family-identity",
title: "Protecting Your Family's Digital Identity",
excerpt: "Your family's personal data is at risk. Discover the steps you can take to protect everyone in your household from identity theft.",
author: "Emily Torres",
date: "May 5, 2026",
readingTime: "6 min read",
coverImage: "",
tags: ["Identity Theft", "Privacy"],
},
{
slug: "deepfake-voice-scams",
title: "Deepfake Voice Scams: How They Work and How to Spot Them",
excerpt: "AI-generated voice clones are being used to impersonate loved ones. Here's what to listen for and how Kordant's VoicePrint can help.",
author: "Sarah Chen",
date: "April 28, 2026",
readingTime: "7 min read",
coverImage: "",
tags: ["Deepfakes", "AI Safety", "Scam Alerts"],
},
{
slug: "what-is-data-broker",
title: "What Is a Data Broker and How to Remove Your Information",
excerpt: "Data brokers collect and sell your personal information. Find out how to opt out and reclaim your privacy with RemoveBrokers.",
author: "Alex Kim",
date: "April 20, 2026",
readingTime: "4 min read",
coverImage: "",
tags: ["Privacy", "Identity Theft"],
},
{
slug: "kordant-product-update-may-2026",
title: "Kordant Product Update — May 2026",
excerpt: "New features including improved VoicePrint detection, expanded dark web monitoring, and a redesigned dashboard experience.",
author: "Product Team",
date: "April 15, 2026",
readingTime: "3 min read",
coverImage: "",
tags: ["Product News"],
},
];
const POSTS_PER_PAGE = 4;
export default function BlogPage() {
const [selectedTag, setSelectedTag] = createSignal<string | null>(null);
const [visibleCount, setVisibleCount] = createSignal(POSTS_PER_PAGE);
const [loading, setLoading] = createSignal(true);
const filtered = () => {
// Fetch all published posts
const allPosts = createMemo(() => {
return api.blog.list.query({ limit: "100" }).then(res => {
setLoading(false);
return res.posts;
});
});
// Fetch tags
const tagList = createMemo(() => api.blog.tags.query());
// Fetch featured post
const featuredPost = createMemo(() => {
return api.blog.list.query({ limit: "100" }).then(res =>
res.posts.find((p: any) => p.featured) ?? null
);
});
// Filtered + visible posts
const visible = createMemo(() => {
const posts = allPosts();
if (!posts) return [];
const tag = selectedTag();
if (!tag) return blogPosts;
return blogPosts.filter((p) => p.tags.includes(tag));
};
const filtered = tag
? posts.filter((p: any) => {
const tags = p.tags as string[];
return tags?.includes(tag);
})
: posts;
return filtered.slice(0, visibleCount());
});
const filtered = createMemo(() => {
const posts = allPosts();
if (!posts) return [];
const tag = selectedTag();
if (!tag) return posts;
return posts.filter((p: any) => {
const tags = p.tags as string[];
return tags?.includes(tag);
});
});
const visible = () => filtered().slice(0, visibleCount());
const hasMore = () => visibleCount() < filtered().length;
return (
<main>
<Title>Kordant Blog AI-Powered Identity Protection</Title>
{/* Hero */}
<section class="relative py-20 md:py-28 overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-[var(--color-brand-primary)]/5 to-transparent" />
<PageContainer class="relative z-10">
@@ -114,80 +83,126 @@ export default function BlogPage() {
</PageContainer>
</section>
<section class="pb-20">
<PageContainer>
<div class="flex flex-wrap items-center gap-2 mb-10">
<button
type="button"
onClick={() => { setSelectedTag(null); setVisibleCount(POSTS_PER_PAGE); }}
class={cn(
"px-3 py-1.5 rounded-full text-sm font-medium transition-colors",
!selectedTag()
? "bg-[var(--color-brand-primary)] text-white"
: "bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
)}
>
All
</button>
<For each={allTags}>
{(tag) => (
<button
type="button"
onClick={() => { setSelectedTag(tag); setVisibleCount(POSTS_PER_PAGE); }}
class={cn(
"px-3 py-1.5 rounded-full text-sm font-medium transition-colors",
selectedTag() === tag
? "bg-[var(--color-brand-primary)] text-white"
: "bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
)}
>
{tag}
</button>
)}
</For>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<For each={visible()}>
{(post) => (
<A href={`/blog/${post.slug}`}>
<Card class="h-full hover:shadow-lg transition-shadow duration-300">
<div class="h-40 bg-gradient-to-br from-[var(--color-brand-primary)]/20 to-[var(--color-brand-accent)]/20 rounded-lg mb-4 flex items-center justify-center text-[var(--color-text-tertiary)]">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" class="opacity-40">
<rect x="4" y="6" width="24" height="20" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M4 12h24M12 6v20" stroke="currentColor" stroke-width="2"/>
{/* Featured Post */}
<Suspense>
<Show when={featuredPost()}>
{(fp) => (
<section class="py-10">
<PageContainer>
<A href={`/blog/${fp().slug}`}>
<Card class="flex flex-col md:flex-row gap-6 p-6 hover:shadow-lg transition-shadow">
<div class="md:w-64 h-40 md:h-auto bg-gradient-to-br from-[var(--color-brand-primary)]/20 to-[var(--color-brand-accent)]/20 rounded-lg flex-shrink-0 flex items-center justify-center">
<svg width="40" height="40" viewBox="0 0 32 32" fill="none" class="text-[var(--color-brand-primary)] opacity-40">
<path d="M14 2L2 8v8c0 7 6 12 12 13 6-1 12-6 12-13V8L14 2z" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
<div class="flex flex-wrap gap-1.5 mb-3">
<For each={post.tags}>
{(tag) => <Badge variant="default">{tag}</Badge>}
</For>
</div>
<h2 class="text-lg font-semibold text-[var(--color-text-primary)] mb-2 line-clamp-2">
{post.title}
</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4 line-clamp-2">
{post.excerpt}
</p>
<div class="flex items-center justify-between text-xs text-[var(--color-text-tertiary)] mt-auto">
<span>{post.author}</span>
<span>{post.date} · {post.readingTime}</span>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<Badge variant="info" class="text-xs">Featured</Badge>
<span class="text-xs text-[var(--color-text-tertiary)]">
{fp().publishedAt ? new Date(fp().publishedAt).toLocaleDateString() : ""}
</span>
</div>
<h2 class="text-xl font-bold text-[var(--color-text-primary)] mb-2">{fp().title}</h2>
<p class="text-sm text-[var(--color-text-secondary)] line-clamp-2">{fp().excerpt}</p>
</div>
</Card>
</A>
)}
</For>
</div>
</PageContainer>
</section>
)}
</Show>
</Suspense>
<Show when={hasMore()}>
<div class="text-center mt-10">
<Button
variant="secondary"
onClick={() => setVisibleCount((c) => c + POSTS_PER_PAGE)}
{/* Tag Filters + Posts Grid */}
<section class="pb-20">
<PageContainer>
<Suspense fallback={<div class="flex flex-wrap gap-2 mb-10" />}>
<div class="flex flex-wrap items-center gap-2 mb-10">
<button
type="button"
onClick={() => { setSelectedTag(null); setVisibleCount(POSTS_PER_PAGE); }}
class={cn(
"px-3 py-1.5 rounded-full text-sm font-medium transition-colors",
!selectedTag()
? "bg-[var(--color-brand-primary)] text-white"
: "bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
)}
>
Load More Posts
</Button>
All
</button>
<For each={tagList()}>
{({ tag, count }) => (
<button
type="button"
onClick={() => { setSelectedTag(tag); setVisibleCount(POSTS_PER_PAGE); }}
class={cn(
"px-3 py-1.5 rounded-full text-sm font-medium transition-colors",
selectedTag() === tag
? "bg-[var(--color-brand-primary)] text-white"
: "bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]",
)}
>
{tag} ({count})
</button>
)}
</For>
</div>
</Suspense>
<Show when={!loading()}>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<For each={visible()}>
{(post: any) => (
<A href={`/blog/${post.slug}`}>
<Card class="h-full hover:shadow-lg transition-shadow duration-300">
<div class="h-40 bg-gradient-to-br from-[var(--color-brand-primary)]/20 to-[var(--color-brand-accent)]/20 rounded-lg mb-4 flex items-center justify-center text-[var(--color-text-tertiary)]">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" class="opacity-40">
<rect x="4" y="6" width="24" height="20" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M4 12h24M12 6v20" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
<div class="flex flex-wrap gap-1.5 mb-3">
<For each={post.tags as string[]}>
{(tag) => <Badge variant="default">{tag}</Badge>}
</For>
</div>
<h2 class="text-lg font-semibold text-[var(--color-text-primary)] mb-2 line-clamp-2">
{post.title}
</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4 line-clamp-2">
{post.excerpt}
</p>
<div class="flex items-center justify-between text-xs text-[var(--color-text-tertiary)] mt-auto">
<span>{post.authorName || "Kordant"}</span>
<span>
{post.publishedAt ? new Date(post.publishedAt).toLocaleDateString() : ""}
{" · "}
{readingTime(post.content)}
</span>
</div>
</Card>
</A>
)}
</For>
</div>
<Show when={visible().length === 0}>
<div class="text-center py-16">
<p class="text-[var(--color-text-secondary)] text-lg">No posts found{selectedTag() ? ` for "${selectedTag()}"` : ""}</p>
</div>
</Show>
<Show when={hasMore()}>
<div class="text-center mt-10">
<Button
variant="secondary"
onClick={() => setVisibleCount((c) => c + POSTS_PER_PAGE)}
>
Load More Posts
</Button>
</div>
</Show>
</Show>
</PageContainer>
</section>

View File

@@ -1,177 +1,17 @@
import { For, Show, createMemo } from "solid-js";
import { For, Show, createMemo, Suspense } from "solid-js";
import { Title } from "@solidjs/meta";
import { A, useParams } from "@solidjs/router";
import { cn } from "~/lib/utils";
import { Badge, Card, Button } from "~/components/ui";
import PageContainer from "~/components/layout/PageContainer";
import { api } from "~/lib/api";
interface BlogPost {
slug: string;
title: string;
excerpt: string;
content: string;
author: string;
authorRole: string;
date: string;
readingTime: string;
coverImage: string;
tags: string[];
function readingTime(content: string): string {
const words = content.split(/\s+/).length;
const mins = Math.max(1, Math.ceil(words / 200));
return `${mins} min read`;
}
const blogPosts: BlogPost[] = [
{
slug: "ai-scam-trends-2026",
title: "AI Scam Trends to Watch in 2026",
excerpt: "As AI technology advances, scammers are finding new ways to exploit it.",
content: `## The Rise of AI-Powered Scams
Artificial intelligence has become a double-edged sword. While it powers innovation across industries, it also arms bad actors with sophisticated tools for deception.
### Voice Cloning Scams
One of the most alarming trends is the use of AI voice cloning. Scammers need only a few seconds of audio—often scraped from social media videos—to create convincing voice replicas. These are used to impersonate family members in distress, requesting urgent money transfers.
### Deepfake Video Conferencing
In 2025, we saw the first wave of deepfake video calls used in corporate impersonation scams. Attackers use real-time face-swapping technology to pose as executives during Zoom calls, authorizing fraudulent wire transfers.
### Automated Phishing at Scale
AI-generated phishing emails are now nearly indistinguishable from legitimate correspondence. Language models craft personalized messages that reference real events, contacts, and context, dramatically increasing click-through rates.
## How to Protect Yourself
1. **Verify voice requests** — If someone calls asking for money or sensitive information, hang up and call them back on a trusted number.
2. **Use a safe word** — Establish a family safe word that can be used to verify identity in suspicious situations.
3. **Enable Kordant VoicePrint** — Our AI detects voice clones by analyzing acoustic fingerprints that deepfakes cannot replicate.
4. **Stay skeptical of urgency** — Scammers create false urgency to bypass your critical thinking.
## The Kordant Advantage
Kordant's multi-layered protection uses machine learning models trained on millions of scam attempts to identify emerging threats before they reach you. Our DarkWatch service continuously scans the dark web for exposed credentials, while VoicePrint protects against audio deepfakes.`,
author: "Sarah Chen",
authorRole: "Security Researcher",
date: "May 15, 2026",
readingTime: "5 min read",
coverImage: "",
tags: ["AI Safety", "Scam Alerts", "Deepfakes"],
},
{
slug: "dark-web-monitoring-guide",
title: "The Complete Guide to Dark Web Monitoring",
excerpt: "Learn how dark web monitoring works and how Kordant keeps your information safe.",
content: `## What Is Dark Web Monitoring?
The dark web is a hidden part of the internet where cybercriminals trade stolen data. Dark web monitoring services scan these hidden marketplaces, forums, and chat channels for your personal information.
### What Data Gets Exposed
When data breaches occur, the following types of information are commonly stolen and sold:
- **Email addresses and passwords** — The most common credential type traded on dark web markets
- **Social Security numbers** — Often used for identity theft and tax fraud
- **Credit card details** — Sold in bulk with CVV codes and billing addresses
- **Medical records** — Highly valuable on the black market for insurance fraud
- **Phone numbers** — Used for SIM swapping and phishing attacks`,
author: "Mike Reynolds",
authorRole: "Cybersecurity Analyst",
date: "May 10, 2026",
readingTime: "8 min read",
coverImage: "",
tags: ["Dark Web", "Privacy"],
},
{
slug: "protecting-family-identity",
title: "Protecting Your Family's Digital Identity",
excerpt: "Discover steps to protect everyone in your household from identity theft.",
content: `## Why Family Identity Protection Matters
Identity thieves don't discriminate. Children, elderly parents, and everyone in between are potential targets. In fact, child identity theft is particularly insidious because it often goes undetected for years.
### Common Family Identity Threats
- **Child identity theft** — Stolen SSNs used to open credit lines, often discovered when the child applies for college loans
- **Elder financial exploitation** — Scammers target seniors with tech support scams, grandparent scams, and Medicare fraud
- **Family account sharing** — Shared passwords across family streaming services can expose personal data`,
author: "Emily Torres",
authorRole: "Privacy Advocate",
date: "May 5, 2026",
readingTime: "6 min read",
coverImage: "",
tags: ["Identity Theft", "Privacy"],
},
{
slug: "deepfake-voice-scams",
title: "Deepfake Voice Scams: How They Work and How to Spot Them",
excerpt: "AI-generated voice clones are being used to impersonate loved ones.",
content: `## The Mechanics of Voice Cloning
Modern AI voice cloning requires surprisingly little source material. Just 30 seconds of audio—from a voicemail, social media video, or recorded phone call—is enough to generate a convincing voice clone.
### Real Cases
In 2024, a family in Arizona received a frantic call from what sounded like their daughter, claiming she had been kidnapped and demanding ransom. The voice was so convincing that the family almost transferred $50,000 before reaching the real daughter.
### How VoicePrint Detects Clones
VoicePrint analyzes over 200 acoustic features that are nearly impossible for AI to replicate perfectly, including micro-tremors, breathing patterns, and formant transitions unique to each person's vocal tract.`,
author: "Sarah Chen",
authorRole: "Security Researcher",
date: "April 28, 2026",
readingTime: "7 min read",
coverImage: "",
tags: ["Deepfakes", "AI Safety", "Scam Alerts"],
},
{
slug: "what-is-data-broker",
title: "What Is a Data Broker and How to Remove Your Information",
excerpt: "Data brokers collect and sell your personal information.",
content: `## Understanding Data Brokers
Data brokers are companies that collect personal information from various sources—public records, purchasing history, social media activity, and website tracking—then compile and sell this data to third parties.
### Why It Matters
Your data broker profile can include your home address, phone number, email address, income level, purchasing habits, political affiliation, and even health-related interests. This information can be used for targeted scams, identity theft, or unwanted marketing.
### How RemoveBrokers Helps
Kordant's RemoveBrokers service automates the opt-out process for hundreds of data broker sites, sending removal requests on your behalf and verifying that your information has been deleted.`,
author: "Alex Kim",
authorRole: "Data Privacy Specialist",
date: "April 20, 2026",
readingTime: "4 min read",
coverImage: "",
tags: ["Privacy", "Identity Theft"],
},
{
slug: "kordant-product-update-may-2026",
title: "Kordant Product Update — May 2026",
excerpt: "New features including improved VoicePrint detection and redesigned dashboard.",
content: `## What's New in Kordant
We're excited to announce our May 2026 product update, packed with new features and improvements based on your feedback.
### Enhanced VoicePrint Detection
Our voice clone detection model has been retrained on a dataset 3x larger than before, improving detection accuracy to 99.7% while reducing false positives by 40%.
### Redesigned Dashboard
The dashboard has been completely redesigned for faster access to critical information. Key metrics are now displayed at a glance, and each service has its own dedicated section with detailed analytics.
### Expanded Dark Web Coverage
We've added monitoring for 50 additional dark web forums and marketplaces, bringing our total coverage to over 200 sources.`,
author: "Product Team",
authorRole: "Kordant",
date: "April 15, 2026",
readingTime: "3 min read",
coverImage: "",
tags: ["Product News"],
},
];
function contentToHtml(markdown: string): string {
const lines = markdown.split("\n");
let html = "";
@@ -205,24 +45,13 @@ function contentToHtml(markdown: string): string {
return html;
}
function getRelatedPosts(current: BlogPost, all: BlogPost[], count: number): BlogPost[] {
const shared = all
.filter((p) => p.slug !== current.slug)
.map((p) => ({
post: p,
sharedTags: p.tags.filter((t) => current.tags.includes(t)).length,
}))
.filter((p) => p.sharedTags > 0)
.sort((a, b) => b.sharedTags - a.sharedTags)
.slice(0, count);
return shared.map((s) => s.post);
}
export default function BlogPostPage() {
const params = useParams();
const post = createMemo(() => blogPosts.find((p) => p.slug === params.slug));
const data = createMemo(() => api.blog.bySlug.query({ slug: params.slug }));
const post = createMemo(() => data()?.post ?? null);
const related = createMemo(() => data()?.related ?? []);
const contentHtml = createMemo(() => post() ? contentToHtml(post()!.content) : "");
const relatedPosts = createMemo(() => post() ? getRelatedPosts(post()!, blogPosts, 2) : []);
return (
<Show
@@ -242,122 +71,124 @@ export default function BlogPostPage() {
</main>
}
>
<main>
<Title>{post()!.title} Kordant Blog</Title>
{(p) => (
<main>
<Title>{p().title} Kordant Blog</Title>
<article>
<section class="relative py-16 md:py-20 overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-[var(--color-brand-primary)]/5 to-transparent" />
<PageContainer class="relative z-10">
<A href="/blog" class="inline-flex items-center gap-1.5 text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] mb-6 transition-colors">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 12L6 8l4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back to Blog
</A>
<article>
<section class="relative py-16 md:py-20 overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-[var(--color-brand-primary)]/5 to-transparent" />
<PageContainer class="relative z-10">
<A href="/blog" class="inline-flex items-center gap-1.5 text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] mb-6 transition-colors">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 12L6 8l4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Back to Blog
</A>
<div class="flex flex-wrap gap-2 mb-4">
<For each={post()!.tags}>
{(tag) => <Badge variant="info">{tag}</Badge>}
</For>
</div>
<h1 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4 max-w-3xl">
{post()!.title}
</h1>
<div class="flex items-center gap-4 text-sm text-[var(--color-text-tertiary)] mb-8">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-full bg-[var(--color-brand-primary)]/20 flex items-center justify-center text-[var(--color-brand-primary)] text-xs font-bold">
{post()!.author.split(" ").map(n => n[0]).join("")}
</div>
<div>
<p class="text-sm font-medium text-[var(--color-text-primary)]">{post()!.author}</p>
<p class="text-xs text-[var(--color-text-tertiary)]">{post()!.authorRole}</p>
</div>
<div class="flex flex-wrap gap-2 mb-4">
<For each={p().tags as string[]}>
{(tag) => <Badge variant="info">{tag}</Badge>}
</For>
</div>
<span class="text-[var(--color-text-tertiary)]">·</span>
<span>{post()!.date}</span>
<span class="text-[var(--color-text-tertiary)]">·</span>
<span>{post()!.readingTime}</span>
</div>
</PageContainer>
</section>
<section class="pb-16">
<PageContainer>
<div class="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-10">
<div class="prose-custom" innerHTML={contentHtml()} />
<h1 class="text-3xl md:text-4xl lg:text-5xl font-bold text-[var(--color-text-primary)] mb-4 max-w-3xl">
{p().title}
</h1>
<aside class="space-y-6">
<Card>
<div class="text-center">
<div class="w-16 h-16 rounded-full bg-[var(--color-brand-primary)]/20 flex items-center justify-center text-[var(--color-brand-primary)] text-xl font-bold mx-auto mb-3">
{post()!.author.split(" ").map(n => n[0]).join("")}
</div>
<h3 class="text-sm font-semibold text-[var(--color-text-primary)]">{post()!.author}</h3>
<p class="text-xs text-[var(--color-text-tertiary)] mb-3">{post()!.authorRole}</p>
<p class="text-xs text-[var(--color-text-secondary)]">Security researcher and writer covering digital identity protection and AI safety.</p>
<div class="flex items-center gap-4 text-sm text-[var(--color-text-tertiary)] mb-8">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-full bg-[var(--color-brand-primary)]/20 flex items-center justify-center text-[var(--color-brand-primary)] text-xs font-bold">
{(p().authorName || "K").split(" ").map(n => n[0]).join("")}
</div>
</Card>
<div>
<h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-3">Share this article</h3>
<div class="flex gap-2">
<button
type="button"
class="p-2 rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
aria-label="Share on Twitter"
onClick={() => window.open(`https://twitter.com/intent/tweet?text=${encodeURIComponent(post()!.title)}&url=${encodeURIComponent(window.location.href)}`, "_blank")}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</button>
<button
type="button"
class="p-2 rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
aria-label="Share on LinkedIn"
onClick={() => window.open(`https://linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(window.location.href)}`, "_blank")}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
</button>
<button
type="button"
class="p-2 rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
aria-label="Copy link"
onClick={() => navigator.clipboard.writeText(window.location.href)}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
</button>
<div>
<p class="text-sm font-medium text-[var(--color-text-primary)]">{p().authorName || "Kordant"}</p>
<p class="text-xs text-[var(--color-text-tertiary)]">Security Team</p>
</div>
</div>
<span class="text-[var(--color-text-tertiary)]">·</span>
<span>{p().publishedAt ? new Date(p().publishedAt).toLocaleDateString() : ""}</span>
<span class="text-[var(--color-text-tertiary)]">·</span>
<span>{readingTime(p().content)}</span>
</div>
</PageContainer>
</section>
<section class="pb-16">
<PageContainer>
<div class="grid grid-cols-1 lg:grid-cols-[1fr_300px] gap-10">
<div class="prose-custom" innerHTML={contentHtml()} />
<aside class="space-y-6">
<Card>
<div class="text-center">
<div class="w-16 h-16 rounded-full bg-[var(--color-brand-primary)]/20 flex items-center justify-center text-[var(--color-brand-primary)] text-xl font-bold mx-auto mb-3">
{(p().authorName || "K").split(" ").map(n => n[0]).join("")}
</div>
<h3 class="text-sm font-semibold text-[var(--color-text-primary)]">{p().authorName || "Kordant"}</h3>
<p class="text-xs text-[var(--color-text-tertiary)] mb-3">Security Team</p>
<p class="text-xs text-[var(--color-text-secondary)]">Research and insights on digital identity protection and AI safety.</p>
</div>
</Card>
<Show when={relatedPosts().length > 0}>
<div>
<h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-3">Related Posts</h3>
<div class="space-y-3">
<For each={relatedPosts()}>
{(rp) => (
<A href={`/blog/${rp.slug}`}>
<Card class="hover:shadow-md transition-shadow">
<p class="text-sm font-medium text-[var(--color-text-primary)] mb-1">{rp.title}</p>
<div class="flex flex-wrap gap-1">
<For each={rp.tags}>
{(tag) => <Badge>{tag}</Badge>}
</For>
</div>
</Card>
</A>
)}
</For>
<h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-3">Share this article</h3>
<div class="flex gap-2">
<button
type="button"
class="p-2 rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
aria-label="Share on Twitter"
onClick={() => window.open(`https://twitter.com/intent/tweet?text=${encodeURIComponent(p().title)}&url=${encodeURIComponent(window.location.href)}`, "_blank")}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</button>
<button
type="button"
class="p-2 rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
aria-label="Share on LinkedIn"
onClick={() => window.open(`https://linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(window.location.href)}`, "_blank")}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
</button>
<button
type="button"
class="p-2 rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
aria-label="Copy link"
onClick={() => navigator.clipboard.writeText(window.location.href)}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
</button>
</div>
</div>
</Show>
</aside>
</div>
</PageContainer>
</section>
</article>
</main>
<Show when={related().length > 0}>
<div>
<h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-3">Related Posts</h3>
<div class="space-y-3">
<For each={related()}>
{(rp: any) => (
<A href={`/blog/${rp.slug}`}>
<Card class="hover:shadow-md transition-shadow">
<p class="text-sm font-medium text-[var(--color-text-primary)] mb-1">{rp.title}</p>
<div class="flex flex-wrap gap-1">
<For each={rp.tags as string[]}>
{(tag) => <Badge>{tag}</Badge>}
</For>
</div>
</Card>
</A>
)}
</For>
</div>
</div>
</Show>
</aside>
</div>
</PageContainer>
</section>
</article>
</main>
)}
</Show>
);
}

218
web/src/routes/features.tsx Normal file
View File

@@ -0,0 +1,218 @@
import { For } from "solid-js";
import { Title } from "@solidjs/meta";
import { A } from "@solidjs/router";
import { Badge, Card } from "~/components/ui";
import PageContainer from "~/components/layout/PageContainer";
interface FeatureSection {
title: string;
description: string;
iconD: string;
benefits: string[];
link: string;
linkText: string;
badge: string;
}
const features: FeatureSection[] = [
{
title: "DarkWatch",
description: "Continuous dark web monitoring that scans forums, marketplaces, and data dumps for your exposed credentials and personal information.",
iconD: "M10 2L2 6v6c0 4.4 3.6 8.4 8 9 4.4-.6 8-4.6 8-9V6l-8-4zm0 1.7L16 7.5v4.5c0 3.7-2.6 7-6 8-3.4-1-6-4.3-6-8V7.5l6-3.8z",
benefits: [
"200+ dark web sources monitored in real-time",
"Instant alerts when your data appears online",
"Detailed breach reports with remediation steps",
"Credential leak detection across all accounts",
"Family-wide monitoring with centralized alerts",
],
link: "/darkwatch",
linkText: "Open DarkWatch",
badge: "Dark Web",
},
{
title: "VoicePrint",
description: "AI-powered voice clone detection that analyzes acoustic fingerprints to identify deepfake voices before they can harm you or your family.",
iconD: "M9 2h2v3H9V2zM6 6h8v1.5H6V6zm-1 3h10v1.5H5V9zm0 3h10v1.5H5V12z",
benefits: [
"99.7% accuracy in detecting AI voice clones",
"Real-time analysis of incoming calls",
"200+ acoustic feature comparison",
"Micro-tremor and breathing pattern detection",
"Works with any phone call or voice message",
],
link: "/voiceprint",
linkText: "Open VoicePrint",
badge: "AI Safety",
},
{
title: "SpamShield",
description: "Intelligent spam and scam call blocking that uses machine learning to identify and filter malicious calls before they reach you.",
iconD: "M10 2l7 3.5v5c0 5.2-3.5 9.5-7 10.5-3.5-1-7-5.3-7-10.5v-5L10 2zm0 2.1L5 7.2v3.3c0 4.2 2.8 7.8 5 8.6 2.2-.8 5-4.4 5-8.6V7.2l-5-3.1z",
benefits: [
"Blocks 99.2% of spam and scam calls",
"Real-time call risk scoring",
"Customizable blocking rules",
"Detailed call analytics and history",
"Family-wide protection with shared rules",
],
link: "/spamshield",
linkText: "Open SpamShield",
badge: "Call Protection",
},
{
title: "HomeTitle",
description: "Property fraud detection that monitors your home title for unauthorized changes, ensuring your biggest asset stays protected.",
iconD: "M10 3L3 8v9h5v-5h4v5h5V8l-7-5zm0 2.5L14 9H6l4-3.5z",
benefits: [
"Instant alerts on title changes or transfers",
"Monitoring for fraudulent mortgage applications",
"Property boundary and ownership verification",
"Historical title change tracking",
"Legal documentation and support resources",
],
link: "/hometitle",
linkText: "Open HomeTitle",
badge: "Property",
},
{
title: "RemoveBrokers",
description: "Automated data broker removal that sends opt-out requests to 200+ data broker sites and verifies your information has been deleted.",
iconD: "M7 3h6v2H7V3zm-1 4h8l1 1v1.5H5V8l1-1zm-1 4h10v7H5v-7zm2 2v3h6v-3H7z",
benefits: [
"200+ data broker sites covered",
"Automated opt-out request submission",
"Removal verification and re-scanning",
"New listing detection and removal",
"Family-wide data reclamation",
],
link: "/removebrokers",
linkText: "Open RemoveBrokers",
badge: "Privacy",
},
{
title: "Family Plans",
description: "Unified family protection that lets you monitor and manage security for all household members from a single, intuitive dashboard.",
iconD: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15h-2v-2h2v2zm0-4h-2V7h2v6zm4 4h-2v-2h2v2zm0-4h-2V7h2v6z",
benefits: [
"Up to 5 members on Plus, unlimited on Premium",
"Centralized alert management",
"Individual member dashboards",
"Family-wide threat correlation",
"Parental controls and child protection",
],
link: "/signup",
linkText: "Start Family Plan",
badge: "Family",
},
];
function FeatureIcon(props: { d: string }) {
return (
<svg width="32" height="32" viewBox="0 0 20 20" fill="var(--color-brand-primary)" class="flex-shrink-0">
<path d={props.d} />
</svg>
);
}
export default function FeaturesPage() {
return (
<main>
<Title>Kordant Features Comprehensive Identity Protection</Title>
<section class="relative py-20 md:py-28 overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-[var(--color-brand-primary)]/5 to-transparent" />
<PageContainer class="relative z-10">
<div class="text-center max-w-3xl mx-auto">
<Badge variant="info" class="mb-4">6-in-1 Protection</Badge>
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-text-primary)] mb-6">
Complete Identity{" "}
<span class="text-gradient-primary">Protection Suite</span>
</h1>
<p class="text-xl text-[var(--color-text-secondary)] mb-8 max-w-2xl mx-auto">
Six powerful tools working together to protect you, your family, and your digital life from modern threats.
</p>
</div>
</PageContainer>
</section>
<section class="py-20 md:py-28">
<PageContainer>
<div class="space-y-20 md:space-y-32">
<For each={features}>
{(feature, index) => {
const isEven = () => index() % 2 === 0;
return (
<div class={
"grid grid-cols-1 lg:grid-cols-2 gap-12 items-center" +
(isEven() ? "" : " lg:direction-rtl")
}>
<div class={isEven() ? "" : "lg:order-2"}>
<Badge variant="default" class="mb-4">{feature.badge}</Badge>
<h2 class="text-3xl md:text-4xl font-bold text-[var(--color-text-primary)] mb-4">
{feature.title}
</h2>
<p class="text-lg text-[var(--color-text-secondary)] mb-6">
{feature.description}
</p>
<ul class="space-y-3 mb-8">
<For each={feature.benefits}>
{(benefit) => (
<li class="flex items-start gap-3 text-[var(--color-text-secondary)]">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" class="flex-shrink-0 mt-0.5">
<circle cx="10" cy="10" r="10" fill="var(--color-brand-primary)" opacity="0.1"/>
<path d="M7 10.5L8.5 12L13 8" stroke="var(--color-brand-primary)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>{benefit}</span>
</li>
)}
</For>
</ul>
<A href={feature.link}>
<span class="inline-flex items-center gap-2 text-sm font-medium text-[var(--color-brand-primary)] hover:text-[var(--color-brand-accent)] transition-colors">
{feature.linkText}
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
</A>
</div>
<div class={isEven() ? "lg:order-2" : ""}>
<Card class="p-8 flex items-center justify-center min-h-[240px] bg-gradient-to-br from-[var(--color-brand-primary)]/5 to-[var(--color-brand-accent)]/5">
<div class="flex flex-col items-center gap-4">
<FeatureIcon d={feature.iconD} />
<span class="text-sm font-medium text-[var(--color-text-secondary)]">{feature.title}</span>
</div>
</Card>
</div>
</div>
);
}}
</For>
</div>
</PageContainer>
</section>
<section class="py-16 bg-[var(--color-bg-secondary)]">
<PageContainer>
<div class="text-center max-w-3xl mx-auto">
<h2 class="text-3xl md:text-4xl font-bold text-[var(--color-text-primary)] mb-4">
All Tools, One Dashboard
</h2>
<p class="text-lg text-[var(--color-text-secondary)] mb-8 max-w-2xl mx-auto">
Monitor all your security tools from a single, unified dashboard. Get real-time alerts, detailed analytics, and actionable insights.
</p>
<A href="/signup">
<span class="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-[var(--color-brand-primary)] text-white font-medium hover:bg-[var(--color-brand-primary)]/90 transition-colors shadow-lg">
Start Your Free Trial
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
</A>
</div>
</PageContainer>
</section>
</main>
);
}

View File

@@ -1,60 +1,498 @@
import { For, Show, onMount } from "solid-js";
import { Title } from "@solidjs/meta";
import {
ColorWaveBackground,
HeroSection,
HowItWorksSection,
FeaturesGridSection,
ForUsersSection,
WhyKordantSection,
CTABannerSection,
} from "~/components/landing";
import { A } from "@solidjs/router";
import { cn } from "~/lib/utils";
import { Button, Badge, Card } from "~/components/ui";
import { Typewriter } from "~/components/ui/Typewriter";
import { ColorWaveBackground } from "~/components/landing/ColorWaveBackground";
import PageContainer from "~/components/layout/PageContainer";
/* ── SVG Icon Helpers ── */
function IconPath(props: { d: string; class?: string }) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class={cn("w-6 h-6 text-[var(--color-brand-primary)]", props.class)}
>
<path d={props.d} stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
);
}
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>
);
}
/* ── Typed Data Arrays ── */
interface Step {
number: number;
title: string;
description: string;
iconD: string;
}
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.",
iconD: "M12 4a4 4 0 100 8 4 4 0 000-8zM6 21v-2a4 4 0 014-4h4a4 4 0 014 4v2M17 8l2 2 4-4",
},
{
number: 2,
title: "We Monitor 24/7",
description:
"Our system runs continuous dark web scans, voiceprint detection, and spam filtering to catch threats early.",
iconD: "M21 12a9 9 0 11-18 0 9 9 0 0118 0zM12 8v4l3 3M3 12h3m15 0h3M12 3v3m0 15v3",
},
{
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.",
iconD: "M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9M12 22a2 2 0 01-2-2h4a2 2 0 01-2 2zM12 11v3m0-6v1",
},
];
interface Feature {
title: string;
description: string;
iconD: string;
}
const features: Feature[] = [
{
title: "DarkWatch",
description:
"Continuous dark web monitoring to detect your exposed credentials and personal data.",
iconD: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8zM12 9a3 3 0 100 6 3 3 0 000-6z",
},
{
title: "VoicePrint",
description:
"AI-powered voice clone detection to protect against deepfake voice scams.",
iconD: "M12 4a4 4 0 00-4 4v8a4 4 0 008 0V8a4 4 0 00-4-4zM4 11v2m16-2v2M8 18.5A6 6 0 0016 18.5",
},
{
title: "SpamShield",
description:
"Intelligent spam and scam call blocking that learns your patterns over time.",
iconD: "M12 2l9 4v6c0 5.55-3.84 10.74-9 12-5.16-1.26-9-6.45-9-12V6l9-4zM10 10l4 4m0-4l-4 4",
},
{
title: "HomeTitle",
description:
"Property fraud alerts that notify you of unauthorized changes to your home records.",
iconD: "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",
},
{
title: "RemoveBrokers",
description:
"Automatic data broker removal to minimize your personal data footprint online.",
iconD: "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",
},
{
title: "Family Plans",
description:
"Protect your whole household with shared monitoring, alerts, and management tools.",
iconD: "M12 12a4 4 0 100-8 4 4 0 000 8zM5 22v-2a4 4 0 014-4h6a4 4 0 014 4v2M20 9a3 3 0 100-6 3 3 0 000 6zM16 22v-2a3 3 0 00-3-3h-2a3 3 0 00-3 3v2",
},
];
interface AudiencePanel {
title: string;
description: string;
items: string[];
iconType: "individual" | "family";
}
const audiencePanels: AudiencePanel[] = [
{
title: "For Individuals",
description: "Personal identity protection tailored to your digital footprint.",
iconType: "individual",
items: [
"Monitor personal email and phone numbers",
"Dark web credential scanning",
"Voiceprint clone detection",
"Spam and scam call filtering",
"Data broker opt-out service",
],
},
{
title: "For Families",
description: "Group management tools to keep every household member safe.",
iconType: "family",
items: [
"Add unlimited family members",
"Shared alert dashboard",
"Child account monitoring",
"Family-wide dark web scans",
"Centralized threat notifications",
],
},
];
interface ValueProp {
title: string;
description: string;
items: string[];
iconD: string;
}
const valueProps: ValueProp[] = [
{
title: "Proactive, Not Reactive",
description:
"We detect threats before they cause damage, so you can act early.",
iconD: "M13 3l-2 6h5l-3 8M4 14l5-5m0 0l5 5m-5-5v12",
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.",
iconD: "M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3zM8 16l2 2-2 2M16 16l-2 2 2 2",
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.",
iconD: "M12 2l9 4v6c0 5.55-3.84 10.74-9 12-5.16-1.26-9-6.45-9-12V6l9-4zM9 12l2 2 4-4",
items: [
"End-to-end encrypted data",
"GDPR and CCPA compliant",
"Zero data selling policy",
],
},
];
/* ── Inline Icon Components ── */
function StepIcon(props: { d: string }) {
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={props.d} stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
);
}
function AudienceIcon(props: { type: "individual" | "family" }) {
if (props.type === "individual") {
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>
);
}
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>
);
}
/* ── Page ── */
export default function Home() {
let heroRef: HTMLDivElement | undefined;
onMount(() => {
if (heroRef) {
heroRef.style.opacity = "1";
heroRef.style.transform = "translateY(0)";
}
});
return (
<main class="overflow-hidden" style="--cut: clamp(16px, 2.5vw, 40px)">
<Title>Kordant AI-Powered Identity Protection</Title>
{/* Hero */}
<ColorWaveBackground yOffset={-0.1} scale={0.65} speed={0.5} />
<div class="relative z-10">
<HeroSection />
<section>
<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">
<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>
</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"><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"><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>
</div>
{/* How It Works */}
<div
class="bg-dot-grid relative z-10"
style={{
"clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)",
}}
style={{ "clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)" }}
>
<HowItWorksSection />
<section id="how-it-works" class="py-20 md:py-28 scroll-mt-16">
<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) => {
const isEven = index() % 2 === 0;
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">
<StepIcon d={step.iconD} />
</div>
<div>
<span class="text-sm font-semibold text-[var(--color-brand-primary)]">
Step {step.number}
</span>
<h3 class="text-xl md:text-2xl font-bold text-[var(--color-text-primary)] mb-2">
{step.title}
</h3>
<p class="text-base text-[var(--color-text-secondary)] leading-relaxed">
{step.description}
</p>
</div>
</div>
</div>
<div class="flex-1 hidden md:block" />
</div>
);
}}
</For>
</div>
</PageContainer>
</section>
</div>
{/* Platform Features */}
<div
class="relative z-10 backdrop-blur-2xl bg-bg/40 py-8 -my-10"
style={{
"clip-path":
"polygon(0 0, 100% 0, 100% calc(100% - var(--cut)), 0 100%)",
}}
style={{ "clip-path": "polygon(0 0, 100% 0, 100% calc(100% - var(--cut)), 0 100%)" }}
>
<FeaturesGridSection />
<section id="features" class="py-20 md:py-28 scroll-mt-16">
<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) => (
<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)]">
<IconPath d={feature.iconD} />
</div>
<div>
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-1">
{feature.title}
</h3>
<p class="text-[var(--color-text-secondary)] leading-relaxed">
{feature.description}
</p>
</div>
</div>
</Card>
)}
</For>
</div>
</PageContainer>
</section>
</div>
{/* For Everyone */}
<div
class="bg-dot-grid"
style={{
"clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)",
}}
style={{ "clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)" }}
>
<ForUsersSection />
<section id="for-users" class="py-20 md:py-28 scroll-mt-16">
<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">
<For each={audiencePanels}>
{(panel) => (
<Card class="h-full">
<div class="flex flex-col h-full">
<div class="mb-4">
<AudienceIcon type={panel.iconType} />
</div>
<h3 class="text-xl font-semibold text-[var(--color-text-primary)] mb-2">
{panel.title}
</h3>
<p class="text-[var(--color-text-secondary)] mb-6">
{panel.description}
</p>
<ul class="space-y-3 flex-1">
<For each={panel.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>
)}
</For>
</div>
</PageContainer>
</section>
</div>
{/* Why Kordant + CTA */}
<div
class="relative z-10 backdrop-blur-2xl bg-bg/40 pt-8 -mt-10"
style={{
"clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)",
}}
style={{ "clip-path": "polygon(0 var(--cut), 100% 0, 100% 100%, 0 100%)" }}
>
<WhyKordantSection />
<CTABannerSection />
<section id="why-kordant" class="py-20 md:py-28 scroll-mt-16">
<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) => (
<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">
<IconPath d={prop.iconD} />
</div>
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-2">
{prop.title}
</h3>
<p class="text-[var(--color-text-secondary)] mb-4 leading-relaxed">
{prop.description}
</p>
<ul class="space-y-2 flex-1">
<For each={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>
)}
</For>
</div>
</PageContainer>
</section>
<section id="cta" class="py-20 md:py-28 scroll-mt-16">
<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>
</div>
</main>
);

297
web/src/routes/pricing.tsx Normal file
View File

@@ -0,0 +1,297 @@
import { createSignal, For, Show } from "solid-js";
import { Title } from "@solidjs/meta";
import { A, useSearchParams } from "@solidjs/router";
import { cn } from "~/lib/utils";
import { Badge, Button, Card } from "~/components/ui";
import PageContainer from "~/components/layout/PageContainer";
interface Plan {
name: string;
price: string;
period: string;
description: string;
features: string[];
cta: string;
popular: boolean;
}
interface FAQ {
q: string;
a: string;
}
const plans: Plan[] = [
{
name: "Basic",
price: "$9",
period: "/month",
description: "Essential identity protection for individuals",
features: ["Dark web monitoring", "Email breach alerts", "Basic scam call blocking", "Monthly reports"],
cta: "Start Free Trial",
popular: false,
},
{
name: "Plus",
price: "$19",
period: "/month",
description: "Advanced protection for you and your family",
features: ["Everything in Basic", "VoicePrint AI detection", "HomeTitle fraud alerts", "RemoveBrokers automation", "Family sharing (up to 5)"],
cta: "Start Free Trial",
popular: true,
},
{
name: "Premium",
price: "$39",
period: "/month",
description: "Maximum security for the whole household",
features: ["Everything in Plus", "Unlimited family members", "Priority support 24/7", "Real-time alert correlation", "Advanced analytics dashboard", "Data broker suppression"],
cta: "Start Free Trial",
popular: false,
},
];
const faqs: FAQ[] = [
{
q: "How does Kordant detect voice clones?",
a: "VoicePrint analyzes over 200 acoustic features in real-time, including micro-tremors and breathing patterns that AI clones can't replicate accurately.",
},
{
q: "Is my data encrypted?",
a: "Yes. All data is encrypted at rest using AES-256 and in transit using TLS 1.3. We never share or sell your personal information.",
},
{
q: "Can I protect my whole family?",
a: "Absolutely. Plus and Premium plans include family sharing with centralized monitoring and alert management for all household members.",
},
{
q: "How does dark web monitoring work?",
a: "DarkWatch continuously scans dark web forums, marketplaces, and data dumps for your email addresses, phone numbers, and other personal data.",
},
{
q: "What happens after my free trial?",
a: "Your trial includes full access to your selected plan for 14 days. You can cancel anytime before the trial ends with no charge.",
},
{
q: "Can I remove my data from brokers?",
a: "Yes. RemoveBrokers automates opt-out requests to over 200 data broker sites and verifies removal on your behalf.",
},
];
const comparisonFeatures = [
{ feature: "Dark web monitoring", basic: true, plus: true, premium: true },
{ feature: "Email breach alerts", basic: true, plus: true, premium: true },
{ feature: "Basic scam call blocking", basic: true, plus: true, premium: true },
{ feature: "Monthly reports", basic: true, plus: true, premium: true },
{ feature: "VoicePrint AI detection", basic: false, plus: true, premium: true },
{ feature: "HomeTitle fraud alerts", basic: false, plus: true, premium: true },
{ feature: "RemoveBrokers automation", basic: false, plus: true, premium: true },
{ feature: "Family sharing", basic: false, plus: "Up to 5", premium: "Unlimited" },
{ feature: "Priority support 24/7", basic: false, plus: false, premium: true },
{ feature: "Real-time alert correlation", basic: false, plus: false, premium: true },
{ feature: "Advanced analytics", basic: false, plus: false, premium: true },
{ feature: "Data broker suppression", basic: false, plus: false, premium: true },
];
function CheckIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" class="flex-shrink-0">
<path d="M6.5 11.5L3 8l1.1-1.1L6.5 9.3l5.9-5.9L13.5 4.5l-7 7z" fill="var(--color-success)"/>
</svg>
);
}
function XIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" class="flex-shrink-0">
<path d="M4 4l8 8M12 4l-8 8" stroke="var(--color-text-muted)" stroke-width="1.5" stroke-linecap="round"/>
</svg>
);
}
export default function PricingPage() {
const [searchParams] = useSearchParams();
const [openFaq, setOpenFaq] = createSignal<string | null>(null);
const signupUrl = () => `/signup${searchParams.utm_source ? `?utm_source=${searchParams.utm_source}&utm_medium=${searchParams.utm_medium || ""}&utm_campaign=${searchParams.utm_campaign || ""}` : ""}`;
return (
<main>
<Title>Kordant Pricing AI-Powered Identity Protection Plans</Title>
<section class="relative py-20 md:py-28 overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-[var(--color-brand-primary)]/5 to-transparent" />
<PageContainer class="relative z-10">
<div class="text-center max-w-3xl mx-auto">
<Badge variant="info" class="mb-4">Simple Pricing</Badge>
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-[var(--color-text-primary)] mb-6">
Protection That Fits{" "}
<span class="text-gradient-primary">Your Budget</span>
</h1>
<p class="text-xl text-[var(--color-text-secondary)] mb-8 max-w-2xl mx-auto">
Start with a 14-day free trial. No credit card required. Cancel anytime.
</p>
<div class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-[var(--color-text-tertiary)]">
<span class="flex items-center gap-1.5">
<CheckIcon />14-day free trial
</span>
<span class="flex items-center gap-1.5">
<CheckIcon />No credit card required
</span>
<span class="flex items-center gap-1.5">
<CheckIcon />Cancel anytime
</span>
</div>
</div>
</PageContainer>
</section>
<section class="py-20 md:py-28">
<PageContainer>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto">
<For each={plans}>
{(plan) => (
<Card
class={cn(
"relative flex flex-col",
plan.popular && "ring-2 ring-[var(--color-brand-primary)] shadow-glow-primary",
)}
>
<Show when={plan.popular}>
<div class="absolute -top-3 left-1/2 -translate-x-1/2">
<Badge variant="info">Most Popular</Badge>
</div>
</Show>
<div class="mb-6">
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-1">{plan.name}</h3>
<p class="text-sm text-[var(--color-text-secondary)] mb-4">{plan.description}</p>
<div class="flex items-baseline gap-0.5">
<span class="text-4xl font-bold text-[var(--color-text-primary)]">{plan.price}</span>
<span class="text-sm text-[var(--color-text-tertiary)]">{plan.period}</span>
</div>
</div>
<ul class="space-y-3 mb-8 flex-1">
<For each={plan.features}>
{(feature) => (
<li class="flex items-start gap-2 text-sm text-[var(--color-text-secondary)]">
<CheckIcon />
{feature}
</li>
)}
</For>
</ul>
<A href={signupUrl()}>
<Button variant={plan.popular ? "primary" : "secondary"} class="w-full">
{plan.cta}
</Button>
</A>
</Card>
)}
</For>
</div>
</PageContainer>
</section>
<section class="py-16 bg-[var(--color-bg-secondary)]">
<PageContainer>
<div class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold text-[var(--color-text-primary)] mb-4">
Compare Plans
</h2>
</div>
<div class="max-w-4xl mx-auto overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b-2 border-[var(--color-border)]">
<th class="text-left px-4 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Feature</th>
<th class="text-center px-4 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Basic</th>
<th class="text-center px-4 py-3 text-sm font-semibold text-[var(--color-brand-primary)]">Plus</th>
<th class="text-center px-4 py-3 text-sm font-medium text-[var(--color-text-secondary)]">Premium</th>
</tr>
</thead>
<tbody>
<For each={comparisonFeatures}>
{(row) => (
<tr class="border-b border-[var(--color-border)]">
<td class="px-4 py-3 text-sm text-[var(--color-text-primary)]">{row.feature}</td>
<td class="text-center px-4 py-3">
{row.basic === true ? <CheckIcon /> : row.basic === false ? <XIcon /> : <span class="text-xs text-[var(--color-text-secondary)]">{row.basic}</span>}
</td>
<td class="text-center px-4 py-3">
{row.plus === true ? <CheckIcon /> : row.plus === false ? <XIcon /> : <span class="text-xs text-[var(--color-text-secondary)]">{row.plus}</span>}
</td>
<td class="text-center px-4 py-3">
{row.premium === true ? <CheckIcon /> : row.premium === false ? <XIcon /> : <span class="text-xs text-[var(--color-text-secondary)]">{row.premium}</span>}
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</PageContainer>
</section>
<section class="py-20 md:py-28">
<PageContainer>
<div class="max-w-3xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold text-[var(--color-text-primary)] mb-4">
Frequently Asked Questions
</h2>
</div>
<div class="space-y-3">
<For each={faqs}>
{(faq) => {
const isOpen = () => openFaq() === faq.q;
return (
<div class="border border-[var(--color-border)] rounded-xl overflow-hidden">
<button
type="button"
class="w-full flex items-center justify-between px-5 py-4 text-left text-sm font-medium text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] transition-colors"
onClick={() => setOpenFaq(isOpen() ? null : faq.q)}
>
{faq.q}
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
class={cn("transition-transform duration-200", isOpen() && "rotate-180")}
>
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<Show when={isOpen()}>
<div class="px-5 pb-4 text-sm text-[var(--color-text-secondary)] leading-relaxed">
{faq.a}
</div>
</Show>
</div>
);
}}
</For>
</div>
</div>
</PageContainer>
</section>
<section class="py-16 bg-[var(--color-brand-primary)]">
<PageContainer>
<div class="text-center">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
Ready to protect your identity?
</h2>
<p class="text-lg text-white/80 mb-8 max-w-2xl mx-auto">
Join 50,000+ users who trust Kordant for AI-powered identity protection.
</p>
<A href={signupUrl()}>
<Button variant="primary" size="lg" class="bg-white text-[var(--color-brand-primary)] hover:bg-white/90 shadow-lg">
Get Started Free
</Button>
</A>
</div>
</PageContainer>
</section>
</main>
);
}

View File

@@ -11,6 +11,8 @@ import { correlationRouter } from "./routers/correlation";
import { reportsRouter } from "./routers/reports";
import { schedulerRouter } from "./routers/scheduler";
import { extensionRouter } from "./routers/extension";
import { blogRouter } from "./routers/blog";
import { adminRouter } from "./routers/admin";
import { createTRPCRouter } from "./utils";
export const appRouter = createTRPCRouter({
@@ -27,6 +29,8 @@ export const appRouter = createTRPCRouter({
reports: reportsRouter,
scheduler: schedulerRouter,
extension: extensionRouter,
blog: blogRouter,
admin: adminRouter,
});
export type AppRouter = typeof appRouter;

View File

@@ -0,0 +1,151 @@
import { object, string, boolean, minLength, optional } from "valibot";
import { wrap } from "@typeschema/valibot";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, adminProcedure } from "~/server/api/utils";
import { blogPosts } from "~/server/db/schema/marketing";
import { users } from "~/server/db/schema/auth";
import { eq, desc, count, sql } from "drizzle-orm";
const CreateBlogInput = wrap(
object({
title: string([minLength(1)]),
slug: string([minLength(1)]),
excerpt: optional(string()),
content: string([minLength(1)]),
authorName: optional(string()),
coverImageUrl: optional(string()),
tags: optional(string()),
published: optional(boolean()),
featured: optional(boolean()),
})
);
const UpdateBlogInput = wrap(
object({
id: string(),
title: optional(string([minLength(1)])),
slug: optional(string([minLength(1)])),
excerpt: optional(string()),
content: optional(string([minLength(1)])),
authorName: optional(string()),
coverImageUrl: optional(string()),
tags: optional(string()),
published: optional(boolean()),
featured: optional(boolean()),
})
);
export const adminRouter = createTRPCRouter({
// --- Dashboard ---
stats: adminProcedure.query(async ({ ctx }) => {
const [{ userCount }] = await ctx.db.select({ userCount: count() }).from(users);
const [{ postCount }] = await ctx.db
.select({ postCount: count() })
.from(blogPosts)
.where(eq(blogPosts.published, true));
const [{ totalViews }] = await ctx.db
.select({ totalViews: sql<number>`${count()}` })
.from(blogPosts);
const recentPosts = await ctx.db
.select({ id: blogPosts.id, title: blogPosts.title, publishedAt: blogPosts.publishedAt })
.from(blogPosts)
.orderBy(desc(blogPosts.createdAt))
.limit(5);
return { userCount, postCount, totalViews, recentPosts };
}),
// --- Blog ---
blogList: adminProcedure.query(async ({ ctx }) => {
return await ctx.db.select().from(blogPosts).orderBy(desc(blogPosts.createdAt));
}),
blogGet: adminProcedure
.input(wrap(object({ id: string() })))
.query(async ({ ctx, input }) => {
const post = await ctx.db
.select().from(blogPosts)
.where(eq(blogPosts.id, input.id)).limit(1);
return post[0] ?? null;
}),
blogCreate: adminProcedure
.input(CreateBlogInput)
.mutation(async ({ ctx, input }) => {
const existing = await ctx.db
.select({ id: blogPosts.id }).from(blogPosts)
.where(eq(blogPosts.slug, input.slug)).limit(1);
if (existing.length > 0) {
throw new TRPCError({ code: "CONFLICT", message: "Slug already exists" });
}
const tags = input.tags
? input.tags.split(",").map((t: string) => t.trim()).filter(Boolean)
: [];
const [newPost] = await ctx.db
.insert(blogPosts)
.values({
title: input.title,
slug: input.slug,
excerpt: input.excerpt,
content: input.content,
authorName: input.authorName,
coverImageUrl: input.coverImageUrl,
tags,
published: input.published ?? false,
featured: input.featured ?? false,
publishedAt: input.published ? new Date() : undefined,
}).returning();
return newPost;
}),
blogUpdate: adminProcedure
.input(UpdateBlogInput)
.mutation(async ({ ctx, input }) => {
const { id, ...updates } = input;
const existing = await ctx.db
.select().from(blogPosts)
.where(eq(blogPosts.id, id)).limit(1);
if (existing.length === 0) {
throw new TRPCError({ code: "NOT_FOUND", message: "Post not found" });
}
const set: Record<string, unknown> = {};
for (const [key, value] of Object.entries(updates)) {
if (value !== undefined) {
if (key === "tags" && typeof value === "string") {
set[key] = value.split(",").map((t) => t.trim()).filter(Boolean);
} else {
set[key] = value;
}
}
}
if (set.published) {
set.publishedAt = new Date();
}
const [updated] = await ctx.db
.update(blogPosts).set(set)
.where(eq(blogPosts.id, id)).returning();
return updated;
}),
blogDelete: adminProcedure
.input(wrap(object({ id: string() })))
.mutation(async ({ ctx, input }) => {
await ctx.db.delete(blogPosts).where(eq(blogPosts.id, input.id));
return { success: true };
}),
// --- Users ---
userList: adminProcedure.query(async ({ ctx }) => {
return await ctx.db
.select({ id: users.id, email: users.email, name: users.name, role: users.role, createdAt: users.createdAt })
.from(users).orderBy(desc(users.createdAt));
}),
userUpdateRole: adminProcedure
.input(wrap(object({ id: string(), role: string() })))
.mutation(async ({ ctx, input }) => {
const [updated] = await ctx.db
.update(users).set({ role: input.role })
.where(eq(users.id, input.id)).returning();
return updated;
}),
});

View File

@@ -0,0 +1,95 @@
import { object, string, optional } from "valibot";
import { wrap } from "@typeschema/valibot";
import { createTRPCRouter, publicProcedure } from "~/server/api/utils";
import { blogPosts } from "~/server/db/schema/marketing";
import { eq, and, desc, count, sql } from "drizzle-orm";
export const blogRouter = createTRPCRouter({
list: publicProcedure
.input(
wrap(
object({
tag: optional(string()),
limit: optional(string()),
offset: optional(string()),
})
)
)
.query(async ({ ctx, input }) => {
const { tag, limit, offset } = input ?? {};
const lim = limit ? parseInt(limit, 10) : 12;
const off = offset ? parseInt(offset, 10) : 0;
const conditions = [eq(blogPosts.published, true)];
if (tag) {
conditions.push(sql`${blogPosts.tags} LIKE ${`%${tag}%`}`);
}
const where = conditions.length > 1 ? and(...conditions) : conditions[0];
const posts = await ctx.db
.select()
.from(blogPosts)
.where(where)
.orderBy(desc(blogPosts.publishedAt))
.limit(lim)
.offset(off);
const [{ total }] = await ctx.db
.select({ total: count() })
.from(blogPosts)
.where(where);
return { posts, total };
}),
bySlug: publicProcedure
.input(wrap(object({ slug: string() })))
.query(async ({ ctx, input }) => {
const post = await ctx.db
.select()
.from(blogPosts)
.where(and(eq(blogPosts.slug, input.slug), eq(blogPosts.published, true)))
.limit(1);
if (post.length === 0) return null;
await ctx.db
.update(blogPosts)
.set({ viewCount: sql`${blogPosts.viewCount} + 1` })
.where(eq(blogPosts.id, post[0].id));
const currentTags = post[0].tags as string[];
const related = await ctx.db
.select()
.from(blogPosts)
.where(
and(
eq(blogPosts.published, true),
sql`${blogPosts.id} != ${post[0].id}`,
sql`${blogPosts.tags} LIKE ${`%${currentTags[0]}%`}`,
),
)
.orderBy(desc(blogPosts.publishedAt))
.limit(2);
return { post: post[0], related };
}),
tags: publicProcedure.query(async ({ ctx }) => {
const posts = await ctx.db
.select({ tags: blogPosts.tags })
.from(blogPosts)
.where(eq(blogPosts.published, true));
const tagCounts = new Map<string, number>();
for (const row of posts) {
const tags = row.tags as string[];
for (const tag of tags) {
tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
}
}
return Array.from(tagCounts.entries())
.map(([tag, count]) => ({ tag, count }))
.sort((a, b) => b.count - a.count);
}),
});

View File

@@ -31,6 +31,7 @@ export const blogPosts = sqliteTable("blog_posts", {
coverImageUrl: text("cover_image_url"),
tags: text("tags", { mode: "json" }).notNull().$defaultFn(() => []),
published: integer("published", { mode: "boolean" }).default(false).notNull(),
featured: integer("featured", { mode: "boolean" }).default(false).notNull(),
publishedAt: integer("published_at", { mode: "timestamp_ms" }),
viewCount: integer("view_count").default(0).notNull(),
createdAt: integer("created_at", { mode: "timestamp_ms" }).defaultNow().notNull(),

View File

@@ -199,37 +199,210 @@ export async function seed() {
await db.insert(blogPosts).values([
{
slug: "what-is-dark-web-monitoring",
title: "What Is Dark Web Monitoring and Why You Need It",
excerpt: "Learn how dark web monitoring protects your personal information from cybercriminals.",
content: "The dark web is a hidden part of the internet where cybercriminals buy and sell stolen data. Kordant helps you monitor your digital footprint...",
authorName: "Kordant Team",
tags: ["dark-web", "monitoring", "security"],
slug: "ai-scam-trends-2026",
title: "AI Scam Trends to Watch in 2026",
excerpt: "As AI technology advances, scammers are finding new ways to exploit it. Here are the top threats to watch and how to protect yourself.",
content: `## The Rise of AI-Powered Scams
Artificial intelligence has become a double-edged sword. While it powers innovation across industries, it also arms bad actors with sophisticated tools for deception.
### Voice Cloning Scams
One of the most alarming trends is the use of AI voice cloning. Scammers need only a few seconds of audio—often scraped from social media videos—to create convincing voice replicas. These are used to impersonate family members in distress, requesting urgent money transfers.
### Deepfake Video Conferencing
In 2025, we saw the first wave of deepfake video calls used in corporate impersonation scams. Attackers use real-time face-swapping technology to pose as executives during Zoom calls, authorizing fraudulent wire transfers.
### Automated Phishing at Scale
AI-generated phishing emails are now nearly indistinguishable from legitimate correspondence. Language models craft personalized messages that reference real events, contacts, and context, dramatically increasing click-through rates.
## How to Protect Yourself
1. **Verify voice requests** — If someone calls asking for money or sensitive information, hang up and call them back on a trusted number.
2. **Use a safe word** — Establish a family safe word that can be used to verify identity in suspicious situations.
3. **Enable Kordant VoicePrint** — Our AI detects voice clones by analyzing acoustic fingerprints that deepfakes cannot replicate.
4. **Stay skeptical of urgency** — Scammers create false urgency to bypass your critical thinking.
## The Kordant Advantage
Kordant's multi-layered protection uses machine learning models trained on millions of scam attempts to identify emerging threats before they reach you. Our DarkWatch service continuously scans the dark web for exposed credentials, while VoicePrint protects against audio deepfakes.`,
authorName: "Sarah Chen",
tags: ["AI Safety", "Scam Alerts", "Deepfakes"],
published: true,
publishedAt: pastDate(60),
featured: true,
publishedAt: pastDate(10),
viewCount: 1250,
},
{
slug: "protect-your-family-online",
title: "5 Tips to Protect Your Family's Online Privacy",
excerpt: "Simple steps to keep your family's personal information safe from data brokers.",
content: "In today's digital age, protecting your family's privacy is more important than ever. Here are five actionable tips...",
authorName: "Kordant Team",
tags: ["family", "privacy", "tips"],
slug: "dark-web-monitoring-guide",
title: "The Complete Guide to Dark Web Monitoring",
excerpt: "Learn how dark web monitoring works, what data gets exposed, and how Kordant keeps your information safe from cybercriminals.",
content: `## What Is Dark Web Monitoring?
The dark web is a hidden part of the internet where cybercriminals trade stolen data. Dark web monitoring services scan these hidden marketplaces, forums, and chat channels for your personal information.
### What Data Gets Exposed
When data breaches occur, the following types of information are commonly stolen and sold:
- **Email addresses and passwords** — The most common credential type traded on dark web markets
- **Social Security numbers** — Often used for identity theft and tax fraud
- **Credit card details** — Sold in bulk with CVV codes and billing addresses
- **Medical records** — Highly valuable on the black market for insurance fraud
- **Phone numbers** — Used for SIM swapping and phishing attacks
### How DarkWatch Works
DarkWatch continuously monitors over 200 dark web sources including forums, marketplaces, paste sites, and data dumps. When your information is detected, you receive an instant alert with full details about the exposure and recommended remediation steps.
### What to Do When Your Data Is Found
1. **Change affected passwords immediately** — Use unique passwords for every account
2. **Enable two-factor authentication** — Prefer hardware keys or authenticator apps over SMS
3. **Monitor financial accounts** — Check for unauthorized transactions or new accounts
4. **Consider a credit freeze** — Lock your credit files at all three bureaus
5. **File an identity theft report** — Report to IdentityTheft.gov for documentation`,
authorName: "Mike Reynolds",
tags: ["Dark Web", "Privacy"],
published: true,
publishedAt: pastDate(30),
publishedAt: pastDate(15),
viewCount: 820,
},
{
slug: "understanding-data-brokers",
title: "Understanding Data Brokers: Who Has Your Information?",
excerpt: "A comprehensive guide to data brokers and how to opt out of their databases.",
content: "Data brokers collect and sell personal information. This guide explains how they operate and how you can remove your data...",
authorName: "Kordant Team",
tags: ["data-brokers", "privacy", "opt-out"],
slug: "protecting-family-identity",
title: "Protecting Your Family's Digital Identity",
excerpt: "Your family's personal data is at risk. Discover the steps you can take to protect everyone in your household from identity theft.",
content: `## Why Family Identity Protection Matters
Identity thieves don't discriminate. Children, elderly parents, and everyone in between are potential targets. In fact, child identity theft is particularly insidious because it often goes undetected for years.
### Common Family Identity Threats
- **Child identity theft** — Stolen SSNs used to open credit lines, often discovered when the child applies for college loans
- **Elder financial exploitation** — Scammers target seniors with tech support scams, grandparent scams, and Medicare fraud
- **Family account sharing** — Shared passwords across family streaming services can expose personal data
### Steps to Protect Your Family
1. **Monitor every family member's data** — Use a family plan to track all household members from one dashboard
2. **Teach safe online habits** — Help children understand phishing, oversharing, and digital footprints
3. **Secure elderly relatives** — Set up fraud alerts with banks and review Medicare statements regularly
4. **Use unique passwords** — Never share passwords between family members' accounts
5. **Freeze credit for minors** — Contact all three credit bureaus to freeze your child's credit file
### Family Plans Made Simple
Kordant's family plans let you monitor up to 5 members on Plus and unlimited members on Premium. Each member gets individual protection while you maintain centralized control over alerts and settings.`,
authorName: "Emily Torres",
tags: ["Identity Theft", "Privacy"],
published: true,
publishedAt: pastDate(7),
viewCount: 340,
publishedAt: pastDate(20),
viewCount: 640,
},
{
slug: "deepfake-voice-scams",
title: "Deepfake Voice Scams: How They Work and How to Spot Them",
excerpt: "AI-generated voice clones are being used to impersonate loved ones. Here's what to listen for and how Kordant's VoicePrint can help.",
content: `## The Mechanics of Voice Cloning
Modern AI voice cloning requires surprisingly little source material. Just 30 seconds of audio—from a voicemail, social media video, or recorded phone call—is enough to generate a convincing voice clone.
### Real Cases
In 2024, a family in Arizona received a frantic call from what sounded like their daughter, claiming she had been kidnapped and demanding ransom. The voice was so convincing that the family almost transferred $50,000 before reaching the real daughter.
### How VoicePrint Detects Clones
VoicePrint analyzes over 200 acoustic features that are nearly impossible for AI to replicate perfectly, including micro-tremors, breathing patterns, and formant transitions unique to each person's vocal tract.
### Warning Signs of a Voice Clone
- **Unnatural pauses** — AI voices often pause at odd intervals or between syllables
- **Monotone delivery** — Lack of emotional variation even in distressing situations
- **Background silence** — No ambient noise, even when the caller claims to be somewhere specific
- **Repetition issues** — AI may struggle to repeat phrases naturally
### What to Do If You Suspect a Voice Clone
1. Hang up immediately
2. Call the person back on a known, trusted number
3. Use your family safe word to verify identity
4. Report the incident to authorities`,
authorName: "Sarah Chen",
tags: ["Deepfakes", "AI Safety", "Scam Alerts"],
published: true,
publishedAt: pastDate(25),
viewCount: 980,
},
{
slug: "what-is-data-broker",
title: "What Is a Data Broker and How to Remove Your Information",
excerpt: "Data brokers collect and sell your personal information. Find out how to opt out and reclaim your privacy with RemoveBrokers.",
content: `## Understanding Data Brokers
Data brokers are companies that collect personal information from various sources—public records, purchasing history, social media activity, and website tracking—then compile and sell this data to third parties.
### Why It Matters
Your data broker profile can include your home address, phone number, email address, income level, purchasing habits, political affiliation, and even health-related interests. This information can be used for targeted scams, identity theft, or unwanted marketing.
### How RemoveBrokers Helps
Kordant's RemoveBrokers service automates the opt-out process for hundreds of data broker sites, sending removal requests on your behalf and verifying that your information has been deleted.
### Top Data Brokers to Opt Out Of
- **Whitepages** — One of the most well-known people search sites
- **Spokeo** — Aggregates data from public records and social media
- **BeenVerified** — Offers background checks and people search
- **Intelius** — Provides detailed personal profiles
- **Pipl** — Used by both consumers and law enforcement
### The Ongoing Battle
Data brokers re-list information constantly. RemoveBrokers continuously monitors for new listings and automatically submits opt-out requests, ensuring your data stays off these platforms.`,
authorName: "Alex Kim",
tags: ["Privacy", "Identity Theft"],
published: true,
publishedAt: pastDate(30),
viewCount: 530,
},
{
slug: "kordant-product-update-may-2026",
title: "Kordant Product Update — May 2026",
excerpt: "New features including improved VoicePrint detection, expanded dark web monitoring, and a redesigned dashboard experience.",
content: `## What's New in Kordant
We're excited to announce our May 2026 product update, packed with new features and improvements based on your feedback.
### Enhanced VoicePrint Detection
Our voice clone detection model has been retrained on a dataset 3x larger than before, improving detection accuracy to 99.7% while reducing false positives by 40%.
### Redesigned Dashboard
The dashboard has been completely redesigned for faster access to critical information. Key metrics are now displayed at a glance, and each service has its own dedicated section with detailed analytics.
### Expanded Dark Web Coverage
We've added monitoring for 50 additional dark web forums and marketplaces, bringing our total coverage to over 200 sources.
### New Family Features
- **Individual member dashboards** — Each family member can now view their own protection status
- **Shared threat alerts** — When one member's data is compromised, all admins are notified
- **Child protection mode** — Enhanced monitoring for minors with parental controls
### Coming Soon
Stay tuned for our upcoming SpamShield 2.0 with real-time call interception and HomeTitle property fraud alerts.`,
authorName: "Product Team",
tags: ["Product News"],
published: true,
publishedAt: pastDate(35),
viewCount: 410,
},
]).onConflictDoNothing();
console.log("[seed] Blog posts created");

View File

@@ -0,0 +1,11 @@
// Scheduler entry point — runs BullMQ worker + cron scheduler on pan server
import "dotenv/config";
import { initialize, shutdown } from "~/server/jobs";
await initialize();
process.on("SIGTERM", () => shutdown());
process.on("SIGINT", () => shutdown());
console.log("[scheduler] Running on pan — connected to Turso + Redis");

80
web/src/theme/tokens.ts Normal file
View File

@@ -0,0 +1,80 @@
// Auto-generated from design-tokens/*.json — DO NOT EDIT MANUALLY
// Run: node scripts/generate-tokens.mjs
export const tokenColors = {
brand: {
primary: "#4F46E5",
primary_light: "#818CF8",
primary_dark: "#4338CA",
accent: "#06B6D4",
accent_light: "#67E8F9",
accent_dark: "#0891B2",
},
semantic: {
success_bg: { light: "#ECFEFF", dark: "#0C4A6E" },
warning_bg: { light: "#FFFBEB", dark: "#78350F" },
error_bg: { light: "#FEF2F2", dark: "#7F1D1D" },
info_bg: { light: "#EEF2FF", dark: "#1E1B4B" },
},
background: {
bg: { light: "#FAFBFC", dark: "#111827" },
bg_secondary: { light: "#F3F4F6", dark: "#1F2937" },
bg_tertiary: { light: "#E5E7EB", dark: "#374151" },
},
text: {
text_primary: { light: "#111827", dark: "#F9FAFB" },
text_secondary: { light: "#6B7280", dark: "#D1D5DB" },
text_tertiary: { light: "#9CA3AF", dark: "#9CA3AF" },
},
border: {
border: { light: "#E5E7EB", dark: "#374151" },
border_dark: { light: "#D1D5DB", dark: "#4B5563" },
},
};
export const tokenTypography = {
fontFamily: "Inter",
fallback: "-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif",
scale: {
caption: { size: "12px", lineHeight: "16px" },
body: { size: "16px", lineHeight: "24px" },
body_large: { size: "18px", lineHeight: "28px" },
headline: { size: "20px", lineHeight: "28px" },
title: { size: "24px", lineHeight: "32px" },
large_title: { size: "32px", lineHeight: "40px" },
display: { size: "48px", lineHeight: "56px" },
},
weights: {
regular: 400,
medium: 500,
semibold: 600,
bold: 700,
},
};
export const tokenSpacing = {
0: "0px",
xs: "4px",
sm: "8px",
md: "16px",
lg: "24px",
xl: "32px",
xxl: "48px",
xxxl: "64px",
};
export const tokenShadows = {
sm: "0px 1px 2px 0px rgba(0, 0, 0, 0.05)",
md: "0px 4px 6px -1px rgba(0, 0, 0, 0.1)",
lg: "0px 10px 15px -3px rgba(0, 0, 0, 0.1)",
xl: "0px 20px 25px -5px rgba(0, 0, 0, 0.15)",
};
export const tokenRadius = {
none: "0px",
sm: "4px",
md: "8px",
lg: "12px",
xl: "16px",
full: "9999px",
};