Files
id/legacy-nextjs/components/MarketingPanel.tsx
zeekayandGitHub 43090cdebf rewrite: Vite + @hanzo/gui monorepo (drops CF Worker + Next.js) (#2)
Replaces the Cloudflare Worker (hanzo.id-worker) and the Next.js portal
with a pnpm monorepo following the ~/work/liquidity/id pattern.

Layout:
  apps/web/        Vite + React 19 SPA, embeds @hanzo/gui shell
    k8s/           Deployment(2) + Service + Ingress (4 hosts, 4 TLS)
  pkgs/shared/     @hanzo/id-shared — TenantConfig, resolveTenant,
                                       loadBrand (browser + node)
  pkgs/auth/       @hanzo/id-auth   — AuthClient (wraps @hanzo/iam REST)
                                       + LoginForm/SignupForm/ForgotForm/OTPForm
  pkgs/idv/        @hanzo/id-idv    — pluggable IDV (stub, persona,
                                       onfido, veriff) behind one
                                       IDVProvider interface
  legacy-nextjs/   Frozen — Next.js predecessor. Delete after v0.1.0 ships.
  Dockerfile       Two-stage: pnpm build → hanzoai/static:0.4.1 serves /spa
  README.md
  LLM.md           Architecture, dev, deploy, cutover plan

Tenant resolution: hostname → TenantConfig (orgId, iamUrl, clientId,
appName, publicOrigin, brandPackage). Built-in defaults for
hanzo.id/lux.id/zoo.id/pars.id; runtime override via
IAM_TENANT_CONFIG_JSON env (served as /config.json at pod startup).

Brand resolution: each per-org brand pkg (@hanzo/brand, @luxfi/brand,
@zooai/brand, @parsdao/brand) ships brand.json. The Vite plugin
brandJsonPlugin emits /brand/<pkg>/brand.json verbatim; the browser
fetches the right one based on the resolved tenant. No bundle bloat.

IDV: stub (default for dev), persona, onfido, veriff. Each adapter
implements `IDVProvider` from pkgs/idv/src/provider.ts. Swap providers
with one registration call at boot — portal code unchanged.

Cutover (separate ops PR — not in this commit):
  1. Tag + push image ghcr.io/hanzoai/id:0.1.0
  2. Apply k8s manifests, cert-manager issues TLS
  3. Remove CF Worker routes for hanzo.id/lux.id/zoo.id/pars.id
  4. CF A records → 129.212.164.5 (hanzo ingress LB)
  5. Archive hanzo.id-worker repo
2026-05-24 06:00:20 -07:00

143 lines
5.2 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import type { BrandingConfig } from '@/lib/branding'
interface MarketingPanelProps {
branding: BrandingConfig
}
export default function MarketingPanel({ branding }: MarketingPanelProps) {
const [currentQuote, setCurrentQuote] = useState(0)
const quotes = branding.content.quotes || []
// Auto-rotate quotes
useEffect(() => {
if (quotes.length <= 1) return
const interval = setInterval(() => {
setCurrentQuote((prev) => (prev + 1) % quotes.length)
}, 5000)
return () => clearInterval(interval)
}, [quotes.length])
return (
<div className="max-w-md space-y-8">
{/* Badge */}
{branding.content.tagline && (
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-zinc-700 text-sm">
<span className="text-yellow-400">✦</span>
<span className="text-zinc-300">{branding.content.tagline}</span>
</div>
)}
{/* Title */}
{branding.content.title && (
<h2 className="text-4xl font-bold text-white">
{branding.content.title}
</h2>
)}
{/* Subtitle */}
{branding.content.subtitle && (
<p className="text-zinc-400 text-lg">
{branding.content.subtitle}
</p>
)}
{/* Interactive prompt (optional feature showcase) */}
{branding.content.features && branding.content.features.length > 0 && (
<div className="bg-zinc-900/50 rounded-xl p-4 border border-zinc-800">
<div className="text-xs text-zinc-500 mb-2 flex items-center gap-1">
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: branding.colors.primary }} />
TRY SOMETHING LIKE
</div>
<p className="text-white">
{branding.content.features[0].description}
</p>
<div className="mt-3 flex items-center gap-2">
<button className="p-2 rounded-lg bg-zinc-800 text-zinc-400 hover:text-white">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
</svg>
</button>
<button className="p-2 rounded-lg bg-zinc-800 text-zinc-400 hover:text-white">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
</svg>
</button>
<div className="flex-1" />
<button
className="flex items-center gap-2 px-4 py-2 rounded-lg font-medium"
style={{
backgroundColor: branding.colors.primary,
color: branding.colors.primaryText
}}
>
<span>✦</span>
Generate
</button>
</div>
</div>
)}
{/* Testimonials/Quotes */}
{quotes.length > 0 && (
<div className="quote-card">
<blockquote className="text-white mb-4">
<span className="text-2xl text-zinc-600">"</span>
{quotes[currentQuote].text}
<span className="text-2xl text-zinc-600">"</span>
</blockquote>
<div className="flex items-center gap-3">
{quotes[currentQuote].avatar ? (
<img
src={quotes[currentQuote].avatar}
alt={quotes[currentQuote].author}
className="w-10 h-10 rounded-full"
/>
) : (
<div
className="w-10 h-10 rounded-full flex items-center justify-center text-white font-medium"
style={{ backgroundColor: branding.colors.primary }}
>
{quotes[currentQuote].author.split(' ').map(n => n[0]).join('').slice(0, 2)}
</div>
)}
<div>
<div className="font-medium text-white">
{quotes[currentQuote].author}
</div>
{(quotes[currentQuote].role || quotes[currentQuote].company) && (
<div className="text-sm text-zinc-500">
{[quotes[currentQuote].role, quotes[currentQuote].company].filter(Boolean).join(', ')}
</div>
)}
</div>
</div>
{/* Quote indicators */}
{quotes.length > 1 && (
<div className="flex justify-center gap-2 mt-4">
{quotes.map((_, i) => (
<button
key={i}
onClick={() => setCurrentQuote(i)}
className={`w-2 h-2 rounded-full transition-colors ${
i === currentQuote ? 'bg-white' : 'bg-zinc-600'
}`}
style={{
backgroundColor: i === currentQuote ? branding.colors.primary : undefined
}}
/>
))}
</div>
)}
</div>
)}
</div>
)
}