08: Migrate & redesign Blog, Ads, and Dashboard pages

- Blog listing page with hero, responsive grid, tag filters, load more
- Blog post page with markdown rendering, related posts, social share
- Ads landing page with conversion copy, pricing, FAQ, testimonials
- Dashboard shell with sidebar, topbar, stat cards, activity feed
- Dashboard components: Sidebar, TopBar, StatCard, ActivityFeed, QuickActions
- Comprehensive test suite covering all pages and components
This commit is contained in:
2026-05-25 15:26:01 -04:00
parent 25da0cd687
commit 9dc55517b1
11 changed files with 1665 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
import { For, Show } from "solid-js";
import { cn } from "~/lib/utils";
import { Badge } from "~/components/ui";
interface Activity {
id: string;
title: string;
description: string;
timestamp: string;
type: "alert" | "success" | "info" | "warning";
}
interface ActivityFeedProps {
activities: Activity[];
class?: string;
}
const typeVariants: Record<Activity["type"], "error" | "success" | "info" | "warning"> = {
alert: "error",
success: "success",
info: "info",
warning: "warning",
};
function ActivityIcon(props: { type: Activity["type"] }) {
return (
<div class={cn(
"w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0",
props.type === "alert" && "bg-[var(--color-error-bg)] text-[var(--color-error)]",
props.type === "success" && "bg-[var(--color-success-bg)] text-[var(--color-success)]",
props.type === "info" && "bg-[var(--color-info-bg)] text-[var(--color-info)]",
props.type === "warning" && "bg-[var(--color-warning-bg)] text-[var(--color-warning)]",
)}>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<Show when={props.type === "alert"}>
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 5v3M8 10.5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</Show>
<Show when={props.type === "success"}>
<path d="M4 8l3 3 5-5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</Show>
<Show when={props.type === "info"}>
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 7v4M8 5v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</Show>
<Show when={props.type === "warning"}>
<path d="M8 2l6 11H2L8 2z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M8 7v3M8 12v.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</Show>
</svg>
</div>
);
}
export default function ActivityFeed(props: ActivityFeedProps) {
return (
<div class={cn("gradient-card border border-[var(--color-border)]/50 rounded-xl", props.class)}>
<div class="px-5 py-4 border-b border-[var(--color-border)]/50">
<h3 class="text-sm font-semibold text-[var(--color-text-primary)]">Recent Activity</h3>
</div>
<div class="divide-y divide-[var(--color-border)]/50">
<For each={props.activities}>
{(activity) => (
<div class="px-5 py-3.5 flex items-start gap-3">
<ActivityIcon type={activity.type} />
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-0.5">
<span class="text-sm font-medium text-[var(--color-text-primary)]">{activity.title}</span>
<Badge variant={typeVariants[activity.type]}>{activity.type}</Badge>
</div>
<p class="text-xs text-[var(--color-text-secondary)]">{activity.description}</p>
</div>
<span class="text-xs text-[var(--color-text-tertiary)] whitespace-nowrap">{activity.timestamp}</span>
</div>
)}
</For>
</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { For, type JSX } from "solid-js";
import { A } from "@solidjs/router";
import { cn } from "~/lib/utils";
interface QuickAction {
label: string;
href: string;
icon: () => JSX.Element;
}
interface QuickActionsProps {
actions: QuickAction[];
class?: string;
}
export default function QuickActions(props: QuickActionsProps) {
return (
<div class={cn("gradient-card border border-[var(--color-border)]/50 rounded-xl", props.class)}>
<div class="px-5 py-4 border-b border-[var(--color-border)]/50">
<h3 class="text-sm font-semibold text-[var(--color-text-primary)]">Quick Actions</h3>
</div>
<div class="p-5 grid grid-cols-2 gap-3">
<For each={props.actions}>
{(action) => {
const Icon = action.icon;
return (
<A
href={action.href}
class="flex flex-col items-center gap-2 p-3 rounded-xl bg-[var(--color-bg-secondary)] hover:bg-[var(--color-brand-primary)]/10 text-[var(--color-text-secondary)] hover:text-[var(--color-brand-primary)] transition-colors text-center"
>
<Icon />
<span class="text-xs font-medium">{action.label}</span>
</A>
);
}}
</For>
</div>
</div>
);
}

View File

@@ -0,0 +1,146 @@
import { createSignal, Show, type JSX } from "solid-js";
import { A, useLocation } from "@solidjs/router";
import { cn } from "~/lib/utils";
interface SidebarLink {
label: string;
href: string;
icon: () => JSX.Element;
}
function OverviewIcon() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="7" height="7" rx="1" fill="currentColor"/>
<rect x="11" y="2" width="7" height="7" rx="1" fill="currentColor" opacity="0.5"/>
<rect x="2" y="11" width="7" height="7" rx="1" fill="currentColor" opacity="0.5"/>
<rect x="11" y="11" width="7" height="7" rx="1" fill="currentColor" opacity="0.3"/>
</svg>
);
}
function DarkWatchIcon() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="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" fill="currentColor"/>
</svg>
);
}
function VoicePrintIcon() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 2h2v3H9V2zM6 6h8v1.5H6V6zm-1 3h10v1.5H5V9zm0 3h10v1.5H5V12z" fill="currentColor"/>
</svg>
);
}
function SpamShieldIcon() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="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" fill="currentColor"/>
</svg>
);
}
function HomeTitleIcon() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 3L3 8v9h5v-5h4v5h5V8l-7-5zm0 2.5L14 9H6l4-3.5z" fill="currentColor"/>
</svg>
);
}
function RemoveBrokersIcon() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 3h6v2H7V3zm-1 4h8l1 1v1.5H5V8l1-1zm-1 4h10v7H5v-7zm2 2v3h6v-3H7z" fill="currentColor"/>
</svg>
);
}
function SettingsIcon() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="2.5" fill="currentColor"/>
<path d="M10 1.5l2.5 1v2.5L10 5l-2.5-1v-2.5L10 1.5z" fill="currentColor" opacity="0.3"/>
<path d="M18.5 10l-1 2.5H15l-1-2.5 1-2.5h2.5l1 2.5z" fill="currentColor" opacity="0.3"/>
<path d="M10 18.5l-2.5-1V15L10 14l2.5 1v2.5l-2.5 1z" fill="currentColor" opacity="0.3"/>
<path d="M1.5 10l1-2.5H5l1 2.5-1 2.5H2.5l-1-2.5z" fill="currentColor" opacity="0.3"/>
</svg>
);
}
const sidebarLinks: SidebarLink[] = [
{ label: "Overview", href: "/dashboard", icon: OverviewIcon },
{ label: "DarkWatch", href: "/dashboard/darkwatch", icon: DarkWatchIcon },
{ label: "VoicePrint", href: "/dashboard/voiceprint", icon: VoicePrintIcon },
{ label: "SpamShield", href: "/dashboard/spamshield", icon: SpamShieldIcon },
{ label: "HomeTitle", href: "/dashboard/hometitle", icon: HomeTitleIcon },
{ label: "RemoveBrokers", href: "/dashboard/removebrokers", icon: RemoveBrokersIcon },
{ label: "Settings", href: "/dashboard/settings", icon: SettingsIcon },
];
interface SidebarProps {
open: boolean;
onClose: () => void;
}
export default function Sidebar(props: SidebarProps) {
const location = useLocation();
return (
<>
<aside
class={cn(
"fixed lg:static inset-y-0 left-0 z-40 w-64 bg-[var(--color-bg)] border-r border-[var(--color-border)] transform transition-transform duration-300 ease-in-out lg:transform-none overflow-y-auto",
props.open ? "translate-x-0" : "-translate-x-full lg:translate-x-0",
)}
>
<div class="p-6 border-b border-[var(--color-border)]">
<A href="/dashboard" class="flex items-center gap-2">
<svg width="24" height="28" viewBox="0 0 28 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 0L26 6V16C26 24 14 32 14 32S2 24 2 16V6L14 0Z" fill="url(#sidebar-shield-grad)"/>
<path d="M10 16L13 19L19 13" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<defs>
<linearGradient id="sidebar-shield-grad" x1="0" y1="0" x2="28" y2="32" gradientUnits="userSpaceOnUse">
<stop stop-color="var(--color-brand-primary)"/>
<stop offset="1" stop-color="var(--color-brand-accent)"/>
</linearGradient>
</defs>
</svg>
<span class="text-lg font-bold text-[var(--color-text-primary)]">ShieldAI</span>
</A>
</div>
<nav class="p-4 space-y-1">
{sidebarLinks.map((link) => {
const Icon = link.icon;
const isActive = location.pathname === link.href ||
(link.href !== "/dashboard" && location.pathname.startsWith(link.href));
return (
<A
href={link.href}
class={cn(
"flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors",
isActive
? "bg-[var(--color-brand-primary)]/10 text-[var(--color-brand-primary)]"
: "text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)]",
)}
onClick={() => props.onClose()}
>
<Icon />
{link.label}
</A>
);
})}
</nav>
</aside>
<Show when={props.open}>
<div
class="fixed inset-0 z-30 bg-black/50 lg:hidden"
onClick={() => props.onClose()}
/>
</Show>
</>
);
}

View File

@@ -0,0 +1,54 @@
import { Show, type JSX } from "solid-js";
import { cn } from "~/lib/utils";
interface StatCardProps {
label: string;
value: string;
trend?: "up" | "down";
trendLabel?: string;
icon: () => JSX.Element;
class?: string;
}
export default function StatCard(props: StatCardProps) {
const Icon = props.icon;
return (
<div class={cn("gradient-card border border-[var(--color-border)]/50 rounded-xl p-5", props.class)}>
<div class="flex items-start justify-between mb-3">
<span class="text-sm font-medium text-[var(--color-text-secondary)]">{props.label}</span>
<div class="p-2 rounded-lg bg-[var(--color-brand-primary)]/10 text-[var(--color-brand-primary)]">
<Icon />
</div>
</div>
<div class="text-2xl font-bold text-[var(--color-text-primary)] mb-1">{props.value}</div>
<Show when={props.trend}>
<div class="flex items-center gap-1 text-xs">
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class={cn(
props.trend === "up" ? "text-[var(--color-success)]" : "text-[var(--color-error)]",
)}
>
<path
d={props.trend === "up" ? "M6 9V3M6 3l-3 3M6 3l3 3" : "M6 3v6M6 9l-3-3M6 9l3-3"}
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<span class={cn(
"font-medium",
props.trend === "up" ? "text-[var(--color-success)]" : "text-[var(--color-error)]",
)}>
{props.trendLabel}
</span>
</div>
</Show>
</div>
);
}

View File

@@ -0,0 +1,91 @@
import { createSignal, Show } from "solid-js";
import { A } from "@solidjs/router";
import { cn } from "~/lib/utils";
interface TopBarProps {
onMenuToggle: () => void;
}
export default function TopBar(props: TopBarProps) {
const [showDropdown, setShowDropdown] = createSignal(false);
return (
<header class="h-16 border-b border-[var(--color-border)] bg-[var(--color-bg)] flex items-center justify-between px-4 lg:px-6">
<div class="flex items-center gap-4">
<button
type="button"
class="lg:hidden p-2 rounded-lg text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)]"
onClick={() => props.onMenuToggle()}
aria-label="Toggle menu"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 5h14M3 10h14M3 15h14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
<div class="relative hidden sm:block">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-text-tertiary)]">
<circle cx="7" cy="7" r="3" stroke="currentColor" stroke-width="1.5"/>
<path d="M11 11l3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<input
type="search"
placeholder="Search..."
class="pl-9 pr-4 py-2 text-sm rounded-lg bg-[var(--color-bg-secondary)] border border-[var(--color-border)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-primary)]/30 w-64"
/>
</div>
</div>
<div class="flex items-center gap-3">
<button
type="button"
class="relative p-2 rounded-lg text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)]"
aria-label="Notifications"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 2.5a5.5 5.5 0 00-5.5 5.5v3l-1.5 2v1h14v-1l-1.5-2V8a5.5 5.5 0 00-5.5-5.5z" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 15.5a2 2 0 004 0" stroke="currentColor" stroke-width="1.5"/>
</svg>
<span class="absolute top-1.5 right-1.5 w-2 h-2 rounded-full bg-[var(--color-error)]" />
</button>
<div class="relative">
<button
type="button"
class="flex items-center gap-2 p-1.5 rounded-lg hover:bg-[var(--color-bg-secondary)] transition-colors"
onClick={() => setShowDropdown(v => !v)}
aria-label="User menu"
>
<div class="w-8 h-8 rounded-full bg-[var(--color-brand-primary)] flex items-center justify-center text-white text-sm font-medium">
JD
</div>
</button>
<Show when={showDropdown()}>
<>
<div
class="fixed inset-0 z-40"
onClick={() => setShowDropdown(false)}
/>
<div class="absolute right-0 top-full mt-2 z-50 w-48 rounded-xl bg-[var(--color-bg)] border border-[var(--color-border)] shadow-lg py-1">
<div class="px-4 py-2 border-b border-[var(--color-border)]">
<p class="text-sm font-medium text-[var(--color-text-primary)]">John Doe</p>
<p class="text-xs text-[var(--color-text-tertiary)]">john@shieldai.app</p>
</div>
<A href="/dashboard/settings" class="block px-4 py-2 text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)]" onClick={() => setShowDropdown(false)}>
Settings
</A>
<button
type="button"
class="w-full text-left px-4 py-2 text-sm text-[var(--color-error)] hover:bg-[var(--color-bg-secondary)]"
onClick={() => setShowDropdown(false)}
>
Sign out
</button>
</div>
</>
</Show>
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,5 @@
export { default as Sidebar } from "./Sidebar";
export { default as TopBar } from "./TopBar";
export { default as StatCard } from "./StatCard";
export { default as ActivityFeed } from "./ActivityFeed";
export { default as QuickActions } from "./QuickActions";