mirror of
https://github.com/hanzoai/agency.git
synced 2026-09-30 20:39:59 +00:00
Agency plans check out on the catalog's plans, leads go to the CRM, white-label sells at $999 a month, and the mock account pages are gone.
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -1,383 +0,0 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
} from "../src/components/ui/navigation-menu";
|
||||
import { cn } from "../src/lib/utils";
|
||||
import { navigationItems } from "../src/data/navigationItems";
|
||||
import { ArrowUpRight, ExternalLink, FileImage, Book, HelpCircle, Palette, Coffee } from "lucide-react";
|
||||
import React from "react";
|
||||
import "../src/components/NavigationFixes.css";
|
||||
import "../src/components/NavigationMenuFix.css";
|
||||
|
||||
const NewHeader = () => {
|
||||
const [showLogoMenu, setShowLogoMenu] = useState(false);
|
||||
const logoMenuRef = useRef<HTMLDivElement>(null);
|
||||
const logoRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
// Close the context menu when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (logoMenuRef.current && !logoMenuRef.current.contains(event.target as Node)) {
|
||||
setShowLogoMenu(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Custom context menu for the logo
|
||||
const handleLogoContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setShowLogoMenu(true);
|
||||
};
|
||||
|
||||
// Links for the logo context menu
|
||||
const logoMenuLinks = [
|
||||
{
|
||||
title: "Hanzo.ai Main Site",
|
||||
href: "https://hanzo.ai",
|
||||
icon: <ArrowUpRight className="h-4 w-4" />
|
||||
},
|
||||
{
|
||||
title: "Branding Kit",
|
||||
href: "https://hanzo.ai/brand",
|
||||
icon: <FileImage className="h-4 w-4" />
|
||||
},
|
||||
{
|
||||
title: "Press Page",
|
||||
href: "https://hanzo.ai/press",
|
||||
icon: <Book className="h-4 w-4" />
|
||||
},
|
||||
{
|
||||
title: "FAQ",
|
||||
href: "/faq",
|
||||
icon: <HelpCircle className="h-4 w-4" />
|
||||
},
|
||||
{
|
||||
title: "Brand Page",
|
||||
href: "https://hanzo.ai/brand",
|
||||
icon: <Palette className="h-4 w-4" />
|
||||
},
|
||||
{
|
||||
title: "Zen of Hanzo",
|
||||
href: "https://hanzo.ai/zen",
|
||||
icon: <Coffee className="h-4 w-4" />
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full bg-black fixed top-0 z-50 border-b border-border/40" role="banner">
|
||||
<div className="header-container w-full max-w-full px-4 mx-auto flex h-16 items-center">
|
||||
<div className="logo-container relative mr-6">
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
<img
|
||||
ref={logoRef}
|
||||
src="/images/logo/logo.png"
|
||||
alt="Hanzo"
|
||||
className="h-6 w-auto object-contain"
|
||||
onContextMenu={handleLogoContextMenu}
|
||||
/>
|
||||
<span className="font-bold text-lg text-white whitespace-nowrap">Hanzo</span>
|
||||
</Link>
|
||||
|
||||
{/* Logo context menu */}
|
||||
{showLogoMenu && (
|
||||
<div
|
||||
ref={logoMenuRef}
|
||||
className="absolute top-full left-0 mt-2 w-60 bg-black border border-gray-800 rounded-md shadow-lg z-50 overflow-hidden"
|
||||
style={{ boxShadow: '0 10px 25px rgba(0, 0, 0, 0.2)' }}
|
||||
>
|
||||
<div className="py-2">
|
||||
<div className="px-4 py-2 text-xs text-gray-500 uppercase">Hanzo Resources</div>
|
||||
{logoMenuLinks.map((link, index) => (
|
||||
<a
|
||||
key={index}
|
||||
href={link.href}
|
||||
target={link.href.startsWith('http') ? "_blank" : undefined}
|
||||
rel={link.href.startsWith('http') ? "noopener noreferrer" : undefined}
|
||||
className="flex items-center px-4 py-2 text-sm text-white hover:bg-gray-800"
|
||||
onClick={() => setShowLogoMenu(false)}
|
||||
>
|
||||
<span className="mr-2 text-gray-400">{link.icon}</span>
|
||||
{link.title}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<NavigationMenu className="flex-1 bg-black text-white">
|
||||
<NavigationMenuList className="justify-center">
|
||||
{navigationItems.map((item) => (
|
||||
<NavigationMenuItem key={item.title}>
|
||||
{item.href && !item.children && !item.capabilities && !item.industries && !item.categories ? (
|
||||
<Link to={item.href}>
|
||||
<NavigationMenuLink className={navigationMenuTriggerStyle()}>
|
||||
{item.title}
|
||||
</NavigationMenuLink>
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<NavigationMenuTrigger>{item.title}</NavigationMenuTrigger>
|
||||
|
||||
{/* Solutions Layout with Capabilities & Industries */}
|
||||
{(item.capabilities || item.industries) && (
|
||||
<NavigationMenuContent>
|
||||
<div className="p-6 w-full mx-auto">
|
||||
<div className="grid gap-8 grid-cols-3">
|
||||
{/* Capabilities Column */}
|
||||
{item.capabilities && (
|
||||
<div className="col-span-1">
|
||||
<h3 className="font-medium text-white text-sm mb-4">Capabilities</h3>
|
||||
<ul className="space-y-2">
|
||||
{item.capabilities.map((capability) => (
|
||||
<ListItem
|
||||
key={capability.title}
|
||||
title={capability.title}
|
||||
href={capability.href}
|
||||
icon={capability.icon}
|
||||
className="w-full"
|
||||
isExternal={capability.isExternal}
|
||||
compact
|
||||
>
|
||||
{capability.description}
|
||||
</ListItem>
|
||||
))}
|
||||
</ul>
|
||||
<Link to="/solutions?category=capabilities" className="mt-4 text-xs text-gray-400 hover:text-white inline-flex items-center">
|
||||
View all
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="ml-1">
|
||||
<path d="M9 5L16 12L9 19" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Industries Column */}
|
||||
{item.industries && (
|
||||
<div className="col-span-1">
|
||||
<h3 className="font-medium text-white text-sm mb-4">Industries</h3>
|
||||
<ul className="space-y-2">
|
||||
{item.industries.map((industry) => (
|
||||
<ListItem
|
||||
key={industry.title}
|
||||
title={industry.title}
|
||||
href={industry.href}
|
||||
icon={industry.icon}
|
||||
className="w-full"
|
||||
isExternal={industry.isExternal}
|
||||
compact
|
||||
>
|
||||
{industry.description}
|
||||
</ListItem>
|
||||
))}
|
||||
</ul>
|
||||
<Link to="/solutions?category=industries" className="mt-4 text-xs text-gray-400 hover:text-white inline-flex items-center">
|
||||
View all
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="ml-1">
|
||||
<path d="M9 5L16 12L9 19" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Featured Column */}
|
||||
{item.featured && (
|
||||
<div className="col-span-1">
|
||||
{item.featured.map((feature, index) => (
|
||||
<div
|
||||
key={feature.title}
|
||||
className={`bg-gray-800/50 rounded-md p-5 ${index > 0 ? 'mt-4' : ''}`}
|
||||
>
|
||||
{feature.icon && (
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="bg-gray-700 text-white w-6 h-6 rounded flex items-center justify-center text-xs font-bold">
|
||||
{feature.icon}
|
||||
</div>
|
||||
<span className="text-sm font-medium text-white">{feature.title}</span>
|
||||
</div>
|
||||
)}
|
||||
{!feature.icon && (
|
||||
<h3 className="text-sm font-medium text-white mb-2">{feature.title}</h3>
|
||||
)}
|
||||
<p className="text-xs text-gray-400 mb-3">{feature.description}</p>
|
||||
{feature.cta && (
|
||||
<a
|
||||
href={feature.href}
|
||||
target={feature.isExternal ? "_blank" : undefined}
|
||||
rel={feature.isExternal ? "noopener noreferrer" : undefined}
|
||||
className="text-xs text-white inline-flex items-center hover:underline"
|
||||
>
|
||||
{feature.cta}
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="ml-1">
|
||||
<path d="M5 12H19M19 12L12 5M19 12L12 19" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</NavigationMenuContent>
|
||||
)}
|
||||
|
||||
{/* Services Categories Layout */}
|
||||
{item.categories && (
|
||||
<NavigationMenuContent>
|
||||
<div className="p-6 w-full mx-auto">
|
||||
<div className="grid gap-8 grid-cols-4">
|
||||
{/* Categories Columns */}
|
||||
{item.categories.map((category, categoryIndex) => (
|
||||
<div key={category.title} className={categoryIndex === 3 ? "col-span-1" : "col-span-1"}>
|
||||
<a
|
||||
href={category.href}
|
||||
className="font-medium text-white text-sm mb-4 flex items-center hover:underline group"
|
||||
>
|
||||
{category.title}
|
||||
<svg
|
||||
className="ml-1 h-4 w-4 group-hover:translate-x-1 transition-transform"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
</a>
|
||||
<ul className="space-y-2 mt-3">
|
||||
{category.items.map((item) => (
|
||||
<ListItem
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
href={item.href}
|
||||
icon={item.icon}
|
||||
className="w-full"
|
||||
isExternal={item.isExternal}
|
||||
compact
|
||||
isNew={item.isNew}
|
||||
>
|
||||
{item.description}
|
||||
</ListItem>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Featured Column */}
|
||||
{item.featured && (
|
||||
<div className="col-span-1">
|
||||
{item.featured.map((feature, index) => (
|
||||
<div
|
||||
key={feature.title}
|
||||
className={`bg-gray-800/50 rounded-md p-5 ${index > 0 ? 'mt-4' : ''}`}
|
||||
>
|
||||
{feature.icon && (
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="bg-gray-700 text-white w-6 h-6 rounded flex items-center justify-center text-xs font-bold">
|
||||
{feature.icon}
|
||||
</div>
|
||||
<span className="text-sm font-medium text-white">{feature.title}</span>
|
||||
</div>
|
||||
)}
|
||||
{!feature.icon && (
|
||||
<h3 className="text-sm font-medium text-white mb-2">{feature.title}</h3>
|
||||
)}
|
||||
<p className="text-xs text-gray-400 mb-3">{feature.description}</p>
|
||||
{feature.cta && (
|
||||
<a href={feature.href} className="text-xs text-white inline-flex items-center hover:underline">
|
||||
{feature.cta}
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="ml-1">
|
||||
<path d="M5 12H19M19 12L12 5M19 12L12 19" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</NavigationMenuContent>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</NavigationMenuItem>
|
||||
))}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
|
||||
<div className="ml-auto flex items-center space-x-3">
|
||||
<a
|
||||
href="https://cloud.hanzo.ai"
|
||||
className="border border-white text-white hover:bg-white hover:text-black transition-colors duration-200 px-4 sm:px-5 py-1.5 sm:py-2 rounded-full text-sm font-medium"
|
||||
>
|
||||
Console
|
||||
</a>
|
||||
<Link
|
||||
to="/onboarding"
|
||||
className="bg-white text-black border border-white hover:bg-transparent hover:text-white transition-colors duration-200 px-4 sm:px-5 py-1.5 sm:py-2 rounded-full text-sm font-medium"
|
||||
>
|
||||
Sign Up
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ListItem = React.forwardRef<
|
||||
React.ElementRef<"a">,
|
||||
React.ComponentPropsWithoutRef<"a"> & {
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
isExternal?: boolean;
|
||||
compact?: boolean;
|
||||
isNew?: boolean;
|
||||
}
|
||||
>(({ className, title, children, icon: Icon, isExternal, compact, isNew, ...props }, ref) => {
|
||||
return (
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<a
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"block select-none rounded-md leading-none no-underline outline-none transition-colors hover:bg-gray-800 text-white focus:bg-gray-800",
|
||||
compact ? "py-1 px-2" : "p-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
target={isExternal ? "_blank" : undefined}
|
||||
rel={isExternal ? "noopener noreferrer" : undefined}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{Icon && <Icon className="h-4 w-4 text-gray-400" />}
|
||||
<div className="text-sm font-medium leading-none flex items-center">
|
||||
{title}
|
||||
{isNew && <span className="ml-2 bg-green-500 text-white text-[10px] px-1 py-0.5 rounded">New</span>}
|
||||
</div>
|
||||
{isExternal && (
|
||||
<ExternalLink className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-gray-400">
|
||||
{children}
|
||||
</p>
|
||||
</a>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
ListItem.displayName = "ListItem";
|
||||
|
||||
export default NewHeader;
|
||||
@@ -4,33 +4,17 @@ import { navigationItems } from "../src/data/navigationItems";
|
||||
import { ArrowUpRight, ExternalLink, FileImage, Book, HelpCircle, Palette, Coffee, ChevronDown, Menu, X } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Wordmark } from "../src/components/Wordmark";
|
||||
import { app } from "../src/data/contact";
|
||||
import "./HeaderDropdown.css";
|
||||
|
||||
const NewHeader = () => {
|
||||
const [showLogoMenu, setShowLogoMenu] = useState(false);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const [mobileExpandedItems, setMobileExpandedItems] = useState<string[]>([]);
|
||||
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
||||
const logoMenuRef = useRef<HTMLDivElement>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Check authentication status
|
||||
useEffect(() => {
|
||||
const checkAuth = () => {
|
||||
const authToken = localStorage.getItem('authToken');
|
||||
setIsAuthenticated(!!authToken);
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
// Listen for storage changes
|
||||
window.addEventListener('storage', checkAuth);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('storage', checkAuth);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Close the context menu when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@@ -420,29 +404,18 @@ const NewHeader = () => {
|
||||
>
|
||||
Hanzo AI
|
||||
</a>
|
||||
{isAuthenticated ? (
|
||||
<Link
|
||||
to="/dashboard"
|
||||
className="bg-white text-black border border-white hover:bg-transparent hover:text-white transition-colors duration-200 px-4 sm:px-5 py-1.5 sm:py-2 rounded-full text-sm font-medium"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
to="/login"
|
||||
className="border border-white text-white hover:bg-white hover:text-black transition-colors duration-200 px-4 sm:px-5 py-1.5 sm:py-2 rounded-full text-sm font-medium"
|
||||
>
|
||||
Log In
|
||||
</Link>
|
||||
<Link
|
||||
to="/signup"
|
||||
className="bg-white text-black border border-white hover:bg-transparent hover:text-white transition-colors duration-200 px-4 sm:px-5 py-1.5 sm:py-2 rounded-full text-sm font-medium"
|
||||
>
|
||||
Sign Up
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<a
|
||||
href={app.login}
|
||||
className="border border-white text-white hover:bg-white hover:text-black transition-colors duration-200 px-4 sm:px-5 py-1.5 sm:py-2 rounded-full text-sm font-medium"
|
||||
>
|
||||
Log in
|
||||
</a>
|
||||
<Link
|
||||
to="/contact"
|
||||
className="bg-white text-black border border-white hover:bg-transparent hover:text-white transition-colors duration-200 px-4 sm:px-5 py-1.5 sm:py-2 rounded-full text-sm font-medium"
|
||||
>
|
||||
Talk to us
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
@@ -560,32 +533,19 @@ const NewHeader = () => {
|
||||
>
|
||||
Hanzo AI
|
||||
</a>
|
||||
{isAuthenticated ? (
|
||||
<Link
|
||||
to="/dashboard"
|
||||
className="block w-full text-center bg-white text-black border border-white hover:bg-transparent hover:text-white transition-colors duration-200 px-4 py-3 rounded-full text-sm font-medium"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
to="/login"
|
||||
className="block w-full text-center border border-white text-white hover:bg-white hover:text-black transition-colors duration-200 px-4 py-3 rounded-full text-sm font-medium"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
Log In
|
||||
</Link>
|
||||
<Link
|
||||
to="/signup"
|
||||
className="block w-full text-center bg-white text-black border border-white hover:bg-transparent hover:text-white transition-colors duration-200 px-4 py-3 rounded-full text-sm font-medium"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
Sign Up
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<a
|
||||
href={app.login}
|
||||
className="block w-full text-center border border-white text-white hover:bg-white hover:text-black transition-colors duration-200 px-4 py-3 rounded-full text-sm font-medium"
|
||||
>
|
||||
Log in
|
||||
</a>
|
||||
<Link
|
||||
to="/contact"
|
||||
className="block w-full text-center bg-white text-black border border-white hover:bg-transparent hover:text-white transition-colors duration-200 px-4 py-3 rounded-full text-sm font-medium"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
Talk to us
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+16
-27
@@ -22,24 +22,22 @@ import Subscribed from "./pages/Subscribed";
|
||||
import Enterprise from "./pages/Enterprise";
|
||||
import ServicesPage from "./pages/ServicesPage";
|
||||
import SolutionsPage from "./pages/SolutionsPage";
|
||||
// Credit System Pages
|
||||
import Login from "./pages/Login";
|
||||
import Signup from "./pages/Signup";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import PurchaseCredits from "./pages/PurchaseCredits";
|
||||
import RedeemService from "./pages/RedeemService";
|
||||
import History from "./pages/History";
|
||||
import AddPaymentMethod from "./pages/AddPaymentMethod";
|
||||
import AdminDashboard from "./pages/AdminDashboard";
|
||||
import EmailInvitation from "./pages/EmailInvitation";
|
||||
import PaymentSuccess from "./pages/PaymentSuccess";
|
||||
import PaymentCancel from "./pages/PaymentCancel";
|
||||
import Platform from "./pages/Platform";
|
||||
import WhiteLabel from "./pages/WhiteLabel";
|
||||
import { app } from "./data/contact";
|
||||
import { Analytics } from "./analytics";
|
||||
import { AIChatWidget } from "./components/AIChatWidget";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
// An address that lives on another site: signing in and the client's workspace
|
||||
// are the Hanzo app's, at hanzo.ai.
|
||||
function Away({ to }: { to: string }) {
|
||||
useEffect(() => {
|
||||
window.location.replace(to);
|
||||
}, [to]);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Scroll position, and nothing else. Counting the navigation is <Analytics>'s
|
||||
// job, and GA4's own history listener's (index.html) — a page_view sent from here
|
||||
// would count every route twice.
|
||||
@@ -85,20 +83,11 @@ const App = () => (
|
||||
<Route path="/payment-success" element={<Subscribed />} />
|
||||
<Route path="/instant-site-form" element={<Navigate to="/pricing" replace />} />
|
||||
<Route path="/enterprise" element={<Enterprise />} />
|
||||
<Route path="/platform" element={<Platform />} />
|
||||
|
||||
{/* Credit System Routes */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/signup" element={<Signup />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/admin" element={<AdminDashboard />} />
|
||||
<Route path="/purchase-credits" element={<PurchaseCredits />} />
|
||||
<Route path="/redeem/:serviceId" element={<RedeemService />} />
|
||||
<Route path="/history" element={<History />} />
|
||||
<Route path="/add-payment-method" element={<AddPaymentMethod />} />
|
||||
<Route path="/admin-invite" element={<EmailInvitation />} />
|
||||
<Route path="/success" element={<PaymentSuccess />} />
|
||||
<Route path="/cancel" element={<PaymentCancel />} />
|
||||
<Route path="/platform" element={<Away to="https://hanzo.ai/platform" />} />
|
||||
<Route path="/white-label" element={<WhiteLabel />} />
|
||||
<Route path="/login" element={<Away to={app.login} />} />
|
||||
<Route path="/signup" element={<Away to={app.signup} />} />
|
||||
<Route path="/dashboard" element={<Away to={app.home} />} />
|
||||
|
||||
<Route path="/terms" element={<Terms />} />
|
||||
<Route path="/privacy" element={<Privacy />} />
|
||||
|
||||
@@ -133,7 +133,7 @@ const AICapabilities = () => {
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden md:block flex-shrink-0">
|
||||
<a href="https://calendar.app.google/z1YsZQrqR4s6jQqD8">
|
||||
<a href="/capabilities">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
@@ -148,7 +148,7 @@ const AICapabilities = () => {
|
||||
|
||||
{/* Mobile button */}
|
||||
<div className="mb-8 md:hidden">
|
||||
<a href="https://calendar.app.google/z1YsZQrqR4s6jQqD8">
|
||||
<a href="/capabilities">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
|
||||
+105
-427
@@ -1,467 +1,145 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useState, type FormEvent } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { HanzoLogo } from '@hanzo/logo';
|
||||
import {
|
||||
Calendar,
|
||||
X,
|
||||
Send,
|
||||
Sparkles,
|
||||
ArrowUpRight,
|
||||
Bot,
|
||||
User,
|
||||
Clock,
|
||||
Phone,
|
||||
Mail,
|
||||
CheckCircle2,
|
||||
ChevronRight,
|
||||
ExternalLink,
|
||||
Minimize2,
|
||||
MessageSquare
|
||||
} from 'lucide-react';
|
||||
import { ArrowUpRight, Calendar, CheckCircle2, Send, X } from 'lucide-react';
|
||||
import { contact } from '@/data/contact';
|
||||
import { plans, planHref, priceLabel } from '@/data/plans';
|
||||
import { EMAIL, fileLead, mailto } from '@/lib/leads';
|
||||
|
||||
const CALENDAR_URL = 'https://calendar.app.google/z1YsZQrqR4s6jQqD8';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
sender: 'bot' | 'user';
|
||||
text: string;
|
||||
timestamp: Date;
|
||||
actions?: {
|
||||
type: 'calendar' | 'pricing' | 'contact' | 'lead_captured';
|
||||
data?: any;
|
||||
};
|
||||
}
|
||||
|
||||
const QUICK_PROMPTS = [
|
||||
'📅 Book a discovery call',
|
||||
'💰 Pricing & Agency Plans',
|
||||
'🤖 Custom AI & Agents',
|
||||
'🎨 Creative & Design services',
|
||||
'🔗 Web3 & Blockchain solutions',
|
||||
];
|
||||
type Sent = 'idle' | 'busy' | 'filed' | 'missed';
|
||||
|
||||
/**
|
||||
* The corner "Talk to us" panel: a short note and a work email, filed as a lead
|
||||
* with Hanzo's CRM (lib/leads, the same endpoint as /contact), beside the booking
|
||||
* page and the plans.
|
||||
*/
|
||||
export function AIChatWidget() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [hasInteracted, setHasInteracted] = useState(false);
|
||||
const [input, setInput] = useState('');
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const [leadCaptured, setLeadCaptured] = useState<{ email?: string; phone?: string } | null>(null);
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
id: 'welcome-1',
|
||||
sender: 'bot',
|
||||
text: "Hi! I'm the Hanzo AI sales & strategy partner. We're Techstars '17 alumni engineering frontier AI systems, high-performance blockchains, and world-class design for high-growth teams.",
|
||||
timestamp: new Date(),
|
||||
},
|
||||
{
|
||||
id: 'welcome-2',
|
||||
sender: 'bot',
|
||||
text: "Are you looking to launch a new AI product, scale your design team, or schedule an executive discovery call?",
|
||||
timestamp: new Date(),
|
||||
actions: {
|
||||
type: 'calendar',
|
||||
},
|
||||
},
|
||||
]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
const [note, setNote] = useState('');
|
||||
const [wrong, setWrong] = useState('');
|
||||
const [sent, setSent] = useState<Sent>('idle');
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
scrollToBottom();
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [isOpen, messages, isTyping]);
|
||||
|
||||
const handleOpen = () => {
|
||||
setIsOpen(true);
|
||||
setHasInteracted(true);
|
||||
};
|
||||
|
||||
const generateAIResponse = (userText: string) => {
|
||||
const text = userText.toLowerCase();
|
||||
|
||||
// Check for email address in user text to capture lead
|
||||
const emailMatch = userText.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/);
|
||||
const phoneMatch = userText.match(/(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/);
|
||||
|
||||
if (emailMatch || phoneMatch) {
|
||||
const email = emailMatch ? emailMatch[0] : undefined;
|
||||
const phone = phoneMatch ? phoneMatch[0] : undefined;
|
||||
setLeadCaptured({ email, phone });
|
||||
return {
|
||||
text: `Got it! I've noted ${email || phone}. A partner from our executive team will reach out directly. You can also lock in an exact slot right now on our calendar below:`,
|
||||
actions: { type: 'calendar' as const },
|
||||
};
|
||||
}
|
||||
|
||||
// Call / scheduling intent
|
||||
if (
|
||||
text.includes('call') ||
|
||||
text.includes('calendar') ||
|
||||
text.includes('schedule') ||
|
||||
text.includes('meet') ||
|
||||
text.includes('demo') ||
|
||||
text.includes('talk') ||
|
||||
text.includes('book')
|
||||
) {
|
||||
return {
|
||||
text: "Let's get a call on the calendar! Our founding team runs 15-minute discovery and technical architecture sessions. Pick a time that suits you directly through Google Calendar:",
|
||||
actions: { type: 'calendar' as const },
|
||||
};
|
||||
}
|
||||
|
||||
// Pricing / Cost / Plans intent
|
||||
if (
|
||||
text.includes('price') ||
|
||||
text.includes('cost') ||
|
||||
text.includes('pricing') ||
|
||||
text.includes('plan') ||
|
||||
text.includes('rate') ||
|
||||
text.includes('subscription') ||
|
||||
text.includes('how much')
|
||||
) {
|
||||
return {
|
||||
text: `Three plans. Anything larger is a conversation.\n\n${plans
|
||||
.map((p) => `• ${p.name}: ${priceLabel(p)}${p.priceMonthly === null ? '' : '/month'}. ${p.description}`)
|
||||
.join('\n')}\n\nWhich one fits your roadmap?`,
|
||||
actions: { type: 'pricing' as const },
|
||||
};
|
||||
}
|
||||
|
||||
// AI & Technology intent
|
||||
if (
|
||||
text.includes('ai') ||
|
||||
text.includes('agent') ||
|
||||
text.includes('llm') ||
|
||||
text.includes('rag') ||
|
||||
text.includes('model') ||
|
||||
text.includes('fine tun') ||
|
||||
text.includes('pipeline') ||
|
||||
text.includes('autonomous')
|
||||
) {
|
||||
return {
|
||||
text: "We build production-grade AI systems, not proof-of-concepts:\n\n• **Custom AI Agents & Workflows**: Multi-agent reasoning, tool use, and automated orchestration.\n• **LLM Fine-Tuning & Custom RAG**: Enterprise-grade retrieval augmented generation over proprietary knowledge bases.\n• **AI Pipeline Infrastructure**: High-throughput inference, low-latency microservices, and continuous evaluation.\n\nWould you like to schedule a 15-minute architecture discussion with our AI leads?",
|
||||
actions: { type: 'calendar' as const },
|
||||
};
|
||||
}
|
||||
|
||||
// Design & Creative intent
|
||||
if (
|
||||
text.includes('design') ||
|
||||
text.includes('brand') ||
|
||||
text.includes('creative') ||
|
||||
text.includes('ui') ||
|
||||
text.includes('ux') ||
|
||||
text.includes('video') ||
|
||||
text.includes('3d') ||
|
||||
text.includes('motion')
|
||||
) {
|
||||
return {
|
||||
text: "Our design team transforms brands from stealth to market leadership:\n\n• **Brand Identity & Systems**: Complete visual guidelines, wordmarks, and design systems.\n• **High-Impact Web & UI/UX**: Conversion-engineered interfaces built in React & Tailwind.\n• **3D, AR & Motion Graphics**: Cinematic visuals and interactive 3D assets.\n\nWe can start delivering within 48 hours of onboarding. Would you like to see relevant case studies or talk to a creative director?",
|
||||
actions: { type: 'calendar' as const },
|
||||
};
|
||||
}
|
||||
|
||||
// Blockchain / Web3 / Lux
|
||||
if (
|
||||
text.includes('blockchain') ||
|
||||
text.includes('web3') ||
|
||||
text.includes('crypto') ||
|
||||
text.includes('lux') ||
|
||||
text.includes('consensus') ||
|
||||
text.includes('quantum')
|
||||
) {
|
||||
return {
|
||||
text: "Hanzo has deep cryptography & consensus engineering pedigree. We architected Lux Network — sub-second finality with post-quantum security algorithms, high-throughput smart contract VMs, and multi-validator infrastructure.\n\nLet's discuss your blockchain or cryptography requirements on a call with our core engineers:",
|
||||
actions: { type: 'calendar' as const },
|
||||
};
|
||||
}
|
||||
|
||||
// Background / Credibility / Clients
|
||||
if (
|
||||
text.includes('who are you') ||
|
||||
text.includes('clients') ||
|
||||
text.includes('portfolio') ||
|
||||
text.includes('work') ||
|
||||
text.includes('techstars') ||
|
||||
text.includes('experience')
|
||||
) {
|
||||
return {
|
||||
text: "Hanzo AI is a Techstars '17 agency with over a decade of engineering and design excellence. We've built and scaled solutions for leaders including Damon Motorcycles, Lux Network, Zoo Foundation, BellaBeat, Triller, and Casper.\n\nReady to elevate your project? Let's connect for 15 minutes:",
|
||||
actions: { type: 'calendar' as const },
|
||||
};
|
||||
}
|
||||
|
||||
// Default sales response
|
||||
return {
|
||||
text: "We can help you execute that with speed and precision. Whether it's end-to-end AI agent development, enterprise design, or dedicated engineering, our team is ready.\n\nCan we set up a quick 15-minute discovery call to review your scope?",
|
||||
actions: { type: 'calendar' as const },
|
||||
};
|
||||
};
|
||||
|
||||
const handleSend = (textToSend?: string) => {
|
||||
const userMessageText = textToSend || input.trim();
|
||||
if (!userMessageText) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
id: `user-${Date.now()}`,
|
||||
sender: 'user',
|
||||
text: userMessageText,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setInput('');
|
||||
setIsTyping(true);
|
||||
|
||||
// Simulate realistic AI thought and response delay
|
||||
setTimeout(() => {
|
||||
const response = generateAIResponse(userMessageText);
|
||||
const botMessage: Message = {
|
||||
id: `bot-${Date.now()}`,
|
||||
sender: 'bot',
|
||||
text: response.text,
|
||||
timestamp: new Date(),
|
||||
actions: response.actions,
|
||||
};
|
||||
setMessages((prev) => [...prev, botMessage]);
|
||||
setIsTyping(false);
|
||||
}, 600);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
const send = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!EMAIL.test(email.trim())) {
|
||||
setWrong('Enter your work email.');
|
||||
return;
|
||||
}
|
||||
setWrong('');
|
||||
setSent('busy');
|
||||
const lead = await fileLead({ email: email.trim(), need: note, source: 'hanzo.agency/chat' });
|
||||
setSent(lead ? 'filed' : 'missed');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Floating Bottom-Right Trigger Button */}
|
||||
<div className="fixed bottom-6 right-6 z-50 flex items-center select-none">
|
||||
<button
|
||||
onClick={() => (isOpen ? setIsOpen(false) : handleOpen())}
|
||||
aria-label="Hanzo AI Assistant — Sales & Scheduling"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
aria-label={open ? 'Close' : 'Talk to us'}
|
||||
aria-expanded={open}
|
||||
className="relative group p-2 flex items-center justify-center transition-all duration-300 hover:scale-110 active:scale-95 focus:outline-none cursor-pointer"
|
||||
>
|
||||
{isOpen ? (
|
||||
{open ? (
|
||||
<div className="w-10 h-10 rounded-lg bg-zinc-900/90 border border-white/20 flex items-center justify-center text-white shadow-xl">
|
||||
<X className="w-5 h-5 text-white transition-transform group-hover:rotate-90" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative flex items-center justify-center">
|
||||
<HanzoLogo size={36} className="[&>svg]:w-9 [&>svg]:h-9 text-white drop-shadow-[0_4px_14px_rgba(0,0,0,0.9)] transition-transform group-hover:scale-110" />
|
||||
</div>
|
||||
<HanzoLogo size={36} className="[&>svg]:w-9 [&>svg]:h-9 text-white drop-shadow-[0_4px_14px_rgba(0,0,0,0.9)] transition-transform group-hover:scale-110" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Interactive AI Chat Modal */}
|
||||
{isOpen && (
|
||||
{open && (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-label="Hanzo AI Sales and Calendaring Chat"
|
||||
className="fixed bottom-24 right-4 sm:right-6 z-50 w-[94vw] sm:w-[420px] max-w-[440px] h-[600px] max-h-[calc(100vh-7.5rem)] rounded-2xl border border-white/15 bg-zinc-950/95 backdrop-blur-2xl shadow-2xl flex flex-col overflow-hidden animate-in fade-in slide-in-from-bottom-5 duration-200"
|
||||
style={{
|
||||
boxShadow: '0 25px 60px -15px rgba(0, 0, 0, 0.9), 0 0 35px rgba(255, 255, 255, 0.05)',
|
||||
}}
|
||||
aria-label="Talk to us"
|
||||
className="fixed bottom-24 right-4 sm:right-6 z-50 w-[calc(100vw-2rem)] sm:w-[380px] rounded-2xl border border-white/15 bg-zinc-950/95 backdrop-blur-2xl shadow-2xl overflow-hidden"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-5 py-4 border-b border-white/10 bg-zinc-900/60 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-black border border-white/20 flex items-center justify-center p-1.5 flex-shrink-0">
|
||||
<HanzoLogo size={22} className="[&>svg]:w-full [&>svg]:h-full text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-sm text-white tracking-tight">Hanzo AI</span>
|
||||
<span className="text-[10px] uppercase font-semibold tracking-wider px-1.5 py-0.5 rounded bg-white/10 text-white/80 border border-white/10">
|
||||
Techstars '17
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-white/60">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
|
||||
<span>Sales & Strategy Partner</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4 border-b border-white/10 bg-zinc-900/60 flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-black border border-white/20 flex items-center justify-center p-1.5 flex-shrink-0">
|
||||
<HanzoLogo size={22} className="[&>svg]:w-full [&>svg]:h-full text-white" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<a
|
||||
href={CALENDAR_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Book Google Calendar Meeting"
|
||||
className="p-1.5 rounded-lg text-white/70 hover:text-white hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<Calendar className="w-4 h-4" />
|
||||
</a>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="p-1.5 rounded-lg text-white/70 hover:text-white hover:bg-white/10 transition-colors cursor-pointer"
|
||||
aria-label="Close Chat"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
<div>
|
||||
<p className="font-semibold text-sm text-white tracking-tight">Hanzo Agency</p>
|
||||
<p className="text-xs text-white/60">We reply by email.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Persistent Calendar Callout Banner */}
|
||||
<div className="bg-gradient-to-r from-emerald-950/40 via-zinc-900 to-zinc-900 px-4 py-2 border-b border-white/5 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 text-xs text-white/90">
|
||||
<Calendar className="w-3.5 h-3.5 text-emerald-400 flex-shrink-0" />
|
||||
<span>Ready to scale? Book a 15-min call:</span>
|
||||
</div>
|
||||
<a
|
||||
href={CALENDAR_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-semibold bg-white text-black hover:bg-white/90 transition-colors shadow-sm flex-shrink-0"
|
||||
>
|
||||
<span>Schedule</span>
|
||||
<ArrowUpRight className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Messages Area */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4 text-sm scrollbar-thin scrollbar-thumb-white/10">
|
||||
{messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={`flex gap-2.5 ${msg.sender === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
{msg.sender === 'bot' && (
|
||||
<div className="w-7 h-7 rounded-lg bg-zinc-900 border border-white/15 flex items-center justify-center p-1 flex-shrink-0 mt-0.5">
|
||||
<HanzoLogo size={16} className="[&>svg]:w-full [&>svg]:h-full text-white" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-w-[82%] space-y-2">
|
||||
<div
|
||||
className={`p-3.5 rounded-2xl text-[13px] leading-relaxed ${
|
||||
msg.sender === 'user'
|
||||
? 'bg-zinc-800 text-white rounded-tr-sm ml-auto'
|
||||
: 'bg-zinc-900/90 text-zinc-200 border border-white/10 rounded-tl-sm shadow-md'
|
||||
}`}
|
||||
<div className="p-5 space-y-4">
|
||||
{sent === 'filed' ? (
|
||||
<div role="status" className="space-y-3 text-sm text-white/90">
|
||||
<p className="flex items-center gap-2 font-semibold text-white">
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-400" />
|
||||
Thanks. We have your note.
|
||||
</p>
|
||||
<p>Want to talk sooner? Pick a time.</p>
|
||||
</div>
|
||||
) : sent === 'missed' ? (
|
||||
<div role="alert" className="space-y-3 text-sm text-white/90">
|
||||
<p>Send it by email, or pick a time. Your note goes with the email.</p>
|
||||
<a
|
||||
href={mailto('Hanzo Agency', `${email}\n\n${note}`)}
|
||||
className="inline-flex items-center gap-1 px-4 py-2 rounded-full text-sm font-semibold bg-white text-black hover:bg-white/90"
|
||||
>
|
||||
Email us
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={send} noValidate className="space-y-3">
|
||||
<p className="text-sm text-white/90">Tell us what you are building.</p>
|
||||
<textarea
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
rows={3}
|
||||
placeholder="A launch, an AI agent, a brand, a team for the quarter…"
|
||||
className="w-full rounded-lg border border-white/15 bg-white/5 px-3 py-2 text-sm text-white placeholder:text-white/40 focus:border-white/40 focus:outline-none"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
placeholder="[email protected]"
|
||||
aria-label="Work email"
|
||||
className="min-w-0 flex-1 rounded-lg border border-white/15 bg-white/5 px-3 py-2 text-sm text-white placeholder:text-white/40 focus:border-white/40 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={sent === 'busy'}
|
||||
aria-label="Send"
|
||||
className="px-3 rounded-lg bg-white text-black hover:bg-white/90 disabled:opacity-60 flex items-center justify-center"
|
||||
>
|
||||
<p className="whitespace-pre-line">{msg.text}</p>
|
||||
</div>
|
||||
|
||||
{/* Interactive Calendar Card */}
|
||||
{msg.actions?.type === 'calendar' && (
|
||||
<div className="p-3.5 rounded-xl bg-gradient-to-br from-zinc-900 to-black border border-white/15 space-y-2.5 shadow-lg animate-in fade-in duration-300">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-white font-medium text-xs">
|
||||
<Clock className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>15-Min Executive Discovery</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-white/50">Google Meet</span>
|
||||
</div>
|
||||
<p className="text-xs text-white/70 leading-normal">
|
||||
Direct consultation with our engineering & design partners to scope your timeline, architecture, and team fit.
|
||||
</p>
|
||||
<a
|
||||
href={CALENDAR_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full inline-flex items-center justify-center gap-1.5 px-4 py-2.5 rounded-lg text-xs font-semibold bg-white text-black hover:bg-white/90 transition-all shadow-md group"
|
||||
>
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
<span>Select Time on Google Calendar</span>
|
||||
<ArrowUpRight className="w-3.5 h-3.5 transition-transform group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Interactive Pricing Card */}
|
||||
{msg.actions?.type === 'pricing' && (
|
||||
<div className="grid gap-2 pt-1 animate-in fade-in duration-300">
|
||||
{plans.map((p) => (
|
||||
<div key={p.id} className="p-3 rounded-xl bg-zinc-900 border border-white/10 flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-[11px] text-white/60 uppercase tracking-wider font-semibold">{p.name}</div>
|
||||
<div className="text-base font-bold text-white mt-0.5">
|
||||
{priceLabel(p)}
|
||||
{p.priceMonthly !== null && <span className="text-xs font-normal text-white/60">/mo</span>}
|
||||
</div>
|
||||
</div>
|
||||
<Link
|
||||
to={planHref(p)}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex-shrink-0 inline-flex items-center justify-center px-2.5 py-1.5 rounded-md text-[11px] font-medium bg-white text-black hover:bg-white/90 transition-colors"
|
||||
>
|
||||
{p.cta}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Send className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isTyping && (
|
||||
<div className="flex gap-2.5 justify-start items-center">
|
||||
<div className="w-7 h-7 rounded-lg bg-zinc-900 border border-white/15 flex items-center justify-center p-1 flex-shrink-0">
|
||||
<HanzoLogo size={16} className="[&>svg]:w-full [&>svg]:h-full text-white" />
|
||||
</div>
|
||||
<div className="px-3.5 py-2.5 rounded-2xl bg-zinc-900/80 border border-white/10 rounded-tl-sm flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-white/60 animate-bounce" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-white/60 animate-bounce [animation-delay:0.2s]" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-white/60 animate-bounce [animation-delay:0.4s]" />
|
||||
</div>
|
||||
</div>
|
||||
{wrong ? (
|
||||
<p role="alert" className="text-xs text-red-400">
|
||||
{wrong}
|
||||
</p>
|
||||
) : null}
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Quick Prompts */}
|
||||
<div className="px-3 py-2 border-t border-white/5 bg-zinc-950 flex gap-1.5 overflow-x-auto no-scrollbar">
|
||||
{QUICK_PROMPTS.map((prompt) => (
|
||||
<button
|
||||
key={prompt}
|
||||
onClick={() => handleSend(prompt)}
|
||||
className="px-2.5 py-1 rounded-full text-[11px] font-medium bg-white/5 hover:bg-white/10 border border-white/10 text-white/80 whitespace-nowrap transition-colors flex-shrink-0 cursor-pointer"
|
||||
<div className="flex flex-wrap gap-2 pt-1">
|
||||
<a
|
||||
href={contact.booking}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold border border-white/20 text-white hover:bg-white/10"
|
||||
>
|
||||
{prompt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Input Box */}
|
||||
<div className="p-3 border-t border-white/10 bg-zinc-900/80">
|
||||
<div className="relative flex items-center">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask about AI, design, pricing, or book a call..."
|
||||
className="w-full bg-zinc-950 border border-white/15 rounded-xl pl-3.5 pr-10 py-2.5 text-xs text-white placeholder-white/40 focus:outline-none focus:border-white/40 transition-colors"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleSend()}
|
||||
disabled={!input.trim()}
|
||||
aria-label="Send message"
|
||||
className="absolute right-1.5 p-1.5 rounded-lg bg-white text-black hover:bg-white/90 disabled:opacity-30 disabled:hover:bg-white transition-opacity cursor-pointer"
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
Book a 15-minute call
|
||||
</a>
|
||||
<Link
|
||||
to="/pricing"
|
||||
onClick={() => setOpen(false)}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-semibold border border-white/20 text-white hover:bg-white/10"
|
||||
>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between text-[10px] text-white/40 px-1">
|
||||
<span>Direct scheduling via Google Calendar</span>
|
||||
<span>San Francisco • {contact.email}</span>
|
||||
See plans
|
||||
<ArrowUpRight className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ArrowUpRight, Instagram, Facebook, Twitter, Github, MessageSquare, Mail, MapPin, Phone, ArrowRight, ChevronRight } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { contact } from '@/data/contact';
|
||||
import { app, contact } from '@/data/contact';
|
||||
import { Wordmark } from './Wordmark';
|
||||
import Newsletter from './Newsletter';
|
||||
|
||||
@@ -50,14 +50,14 @@ const Footer = () => {
|
||||
</p>
|
||||
|
||||
<div className="pt-4 flex flex-col gap-3 w-fit">
|
||||
<a href="https://calendar.app.google/z1YsZQrqR4s6jQqD8"
|
||||
<a href={contact.booking}
|
||||
className="group bg-white text-black border border-gray-700 px-8 py-3.5 rounded-full font-medium hover:bg-gray-100 transition-all duration-200 ease-in-out inline-flex items-center justify-start whitespace-nowrap">
|
||||
Schedule a Consultation
|
||||
<ArrowUpRight className="ml-2 h-5 w-5 transition-transform duration-300 ease-in-out group-hover:translate-x-1 group-hover:-translate-y-1" />
|
||||
</a>
|
||||
<a href="https://hanzo.id"
|
||||
<a href={app.login}
|
||||
className="group bg-transparent text-white border border-gray-700 hover:border-white px-8 py-3.5 rounded-full font-medium hover:bg-black/20 transition-all duration-200 ease-in-out inline-flex items-center justify-between whitespace-nowrap">
|
||||
Login / Sign Up
|
||||
Log in to Hanzo
|
||||
<ArrowRight className="h-5 w-5 transition-transform duration-300 ease-in-out group-hover:translate-x-1" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/* FORCE DROPDOWN VISIBILITY - TEMPORARY DEBUG */
|
||||
|
||||
/* Force all dropdown content to be visible */
|
||||
nav * {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Force NavigationMenuContent to show */
|
||||
nav div[role="group"] {
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
position: absolute !important;
|
||||
top: 100% !important;
|
||||
left: 0 !important;
|
||||
z-index: 9999 !important;
|
||||
background: black !important;
|
||||
border: 2px solid white !important;
|
||||
min-width: 800px !important;
|
||||
min-height: 400px !important;
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
/* Show viewport */
|
||||
nav > div > div[style*="position"] {
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
position: absolute !important;
|
||||
top: 100% !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
/* Force all navigation content to be visible */
|
||||
button[aria-haspopup="menu"] ~ * {
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Override any hiding styles */
|
||||
nav [style*="opacity: 0"],
|
||||
nav [style*="visibility: hidden"],
|
||||
nav [style*="display: none"] {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Specific target for shadcn NavigationMenuContent */
|
||||
.absolute.left-0.top-full {
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
position: absolute !important;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
/* Force content divs to show */
|
||||
nav button ~ div,
|
||||
nav button ~ * > div {
|
||||
display: block !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
border: 1px solid red !important;
|
||||
min-height: 50px !important;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { contact } from '@/data/contact';
|
||||
import { Link } from 'react-router-dom';
|
||||
import './Hero.css';
|
||||
|
||||
const clientLogos = [
|
||||
'Triller', 'Damon', 'Bellabeat', 'Unikrn', 'Cover', 'Casper', 'Myle', 'Drumpants', 'Cove', 'Aura', 'KANOA', 'SKULLY', 'LUX', 'ZOO'
|
||||
'Triller', 'Damon', 'Bellabeat', 'Unikrn', 'Cover', 'Casper', 'Myle', 'Drumpants', 'Cove', 'Aura', 'KANOA', 'SKULLY'
|
||||
];
|
||||
|
||||
const Hero = () => {
|
||||
@@ -52,12 +52,12 @@ const Hero = () => {
|
||||
>
|
||||
<span>See pricing</span> <ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
<a
|
||||
href={`mailto:${contact.email}`}
|
||||
<Link
|
||||
to="/contact"
|
||||
className="bg-purple-600/30 hover:bg-purple-600/40 border border-purple-500/40 px-6 sm:px-8 py-3 sm:py-3.5 rounded-full text-purple-200 font-semibold transition text-center w-full sm:w-auto text-sm cursor-pointer"
|
||||
>
|
||||
<span>Talk to us</span>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,581 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Menu, X, ArrowUpRight } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { buttonModifiers } from '@/lib/button-utils';
|
||||
import './NavigationDropdownFix.css';
|
||||
import './navbar-dropdown-final-fix.css';
|
||||
|
||||
const Navbar = () => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [isTop, setIsTop] = useState(true);
|
||||
const [isBannerVisible, setIsBannerVisible] = useState(false);
|
||||
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 20);
|
||||
setIsTop(window.scrollY <= 20);
|
||||
};
|
||||
|
||||
// Toggle body class when menu state changes
|
||||
if (isMenuOpen) {
|
||||
document.body.classList.add('menu-open');
|
||||
} else {
|
||||
document.body.classList.remove('menu-open');
|
||||
}
|
||||
|
||||
const checkBannerVisible = () => {
|
||||
const bannerShown = localStorage.getItem('bannerShown');
|
||||
const bannerExpiry = localStorage.getItem('bannerExpiry');
|
||||
const discountUsed = localStorage.getItem('discountUsed');
|
||||
|
||||
if (discountUsed === 'true') {
|
||||
setIsBannerVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bannerShown === 'true' && bannerExpiry) {
|
||||
const expiryTime = parseInt(bannerExpiry, 10);
|
||||
const currentTime = new Date().getTime();
|
||||
|
||||
setIsBannerVisible(expiryTime > currentTime);
|
||||
} else {
|
||||
setIsBannerVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for banner visibility changes
|
||||
const handleBannerVisibilityChanged = (e: Event) => {
|
||||
const customEvent = e as CustomEvent;
|
||||
setIsBannerVisible(customEvent.detail.visible);
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
window.addEventListener('bannerVisibilityChanged', handleBannerVisibilityChanged);
|
||||
checkBannerVisible();
|
||||
|
||||
const handleStorageChange = () => {
|
||||
checkBannerVisible();
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
window.removeEventListener('bannerVisibilityChanged', handleBannerVisibilityChanged);
|
||||
};
|
||||
}, [isMenuOpen]);
|
||||
|
||||
const handleNavLinkClick = (e: React.MouseEvent<HTMLAnchorElement>, target: string) => {
|
||||
e.preventDefault();
|
||||
|
||||
// If we're on the homepage
|
||||
if (window.location.pathname === '/') {
|
||||
const targetElement = document.querySelector(target);
|
||||
if (targetElement) {
|
||||
window.scrollTo({
|
||||
top: targetElement.getBoundingClientRect().top + window.scrollY - 100,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
setIsMenuOpen(false);
|
||||
document.body.classList.remove('menu-open');
|
||||
}
|
||||
} else {
|
||||
// If we're on a different page, navigate to homepage first and then scroll
|
||||
window.location.href = `/${target}`;
|
||||
}
|
||||
};
|
||||
|
||||
// Close the mobile menu when clicking any link
|
||||
const handleMobileLinkClick = () => {
|
||||
setIsMenuOpen(false);
|
||||
document.body.classList.remove('menu-open');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="fixed top-0 left-0 right-0 z-[100] transition-all duration-300 h-16 flex items-center bg-black border-b border-gray-800">
|
||||
<div className="container-custom">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<Link to="/" className="flex items-center gap-2 py-1 flex-shrink-0">
|
||||
<img src="/images/logo/logo.png" alt="Hanzo" className="h-6 w-auto" />
|
||||
<span className="text-lg font-semibold tracking-tight whitespace-nowrap">Hanzo</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex-1 flex justify-center px-4">
|
||||
<nav className="hidden lg:flex items-center justify-center gap-6">
|
||||
<Link to="/our-work" className="text-foreground/90 hover:text-foreground font-medium transition">
|
||||
Our Work
|
||||
</Link>
|
||||
|
||||
<div className="relative group">
|
||||
<Link to="/services" className="text-foreground/90 hover:text-foreground font-medium transition flex items-center gap-1">
|
||||
Services
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="transition-transform duration-200 group-hover:rotate-180">
|
||||
<path d="m6 9 6 6 6-6"/>
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 top-full z-50 w-screen max-w-full bg-black border-t border-gray-800 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
|
||||
<div className="container-custom py-8">
|
||||
<div className="grid grid-cols-4 gap-8">
|
||||
{/* Column 1: Creative design services */}
|
||||
<div>
|
||||
<a href="/services/creative-design" className="text-lime-400 font-medium hover:underline flex items-center gap-1 mb-4">
|
||||
Creative design services
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M7 7h10v10"/><path d="M7 17 17 7"/></svg>
|
||||
</a>
|
||||
<ul className="space-y-4">
|
||||
<li>
|
||||
<a href="/services/ad-creative" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">Ad creative</h3>
|
||||
<p className="text-sm text-foreground/70">Eye-catching designs that perform</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">🎯</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/services/social-media-creative" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">Social media creative</h3>
|
||||
<p className="text-sm text-foreground/70">Engaging assets for all platforms</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">📱</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/services/presentation-design" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">Presentation design</h3>
|
||||
<p className="text-sm text-foreground/70">Captivating slides that tell your story</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">📊</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/services/illustration-design" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">Illustration design</h3>
|
||||
<p className="text-sm text-foreground/70">Visual storytelling for your brand</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">🎨</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/services/branding-services" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">Branding services</h3>
|
||||
<p className="text-sm text-foreground/70">Expertise & custom design services</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">⭐</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/services/email-creation" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">Email creation</h3>
|
||||
<p className="text-sm text-foreground/70">Click-worthy emails that drive engagement</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">📧</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Column 2: Creative design services continued + Specialized production services */}
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<ul className="space-y-4">
|
||||
<li>
|
||||
<a href="/services/web-design" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">Web design</h3>
|
||||
<p className="text-sm text-foreground/70">Stunning websites built to engage</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">🖥️</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/services/ebooks-report-design" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">eBooks & report design</h3>
|
||||
<p className="text-sm text-foreground/70">Your digital content supercharged</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">📄</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/services/concept-creation" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">Concept creation</h3>
|
||||
<p className="text-sm text-foreground/70">Big ideas crafted for maximum impact</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">💡</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/services/print-design" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">Print design</h3>
|
||||
<p className="text-sm text-foreground/70">Tangible designs that leave a lasting impression</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">📚</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/services/packaging-merchandise-design" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">Packaging & merchandise design</h3>
|
||||
<p className="text-sm text-foreground/70">Bring your brand to life</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">👕</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<a href="/services/specialized-production" className="text-emerald-700 font-medium hover:underline flex items-center gap-1 mb-4">
|
||||
Specialized production services
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M7 7h10v10"/><path d="M7 17 17 7"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Column 3: Specialized production services continued + AI services */}
|
||||
<div>
|
||||
<ul className="space-y-4 mb-8">
|
||||
<li>
|
||||
<a href="/services/video-production" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">Video production</h3>
|
||||
<p className="text-sm text-foreground/70">Effortless video production at scale</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">🎬</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/services/motion-design" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">Motion design</h3>
|
||||
<p className="text-sm text-foreground/70">For websites, ads, and presentations</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">✨</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/services/3d-ar-design" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">3D & AR design</h3>
|
||||
<p className="text-sm text-foreground/70">Innovative solutions for 3D design services</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">🔍</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<a href="/services/ai-services" className="text-blue-700 font-medium hover:underline flex items-center gap-1 mb-4">
|
||||
AI services
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M7 7h10v10"/><path d="M7 17 17 7"/></svg>
|
||||
</a>
|
||||
<ul className="space-y-4">
|
||||
<li>
|
||||
<a href="/services/ai-enhanced-creative" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">AI enhanced creative</h3>
|
||||
<p className="text-sm text-foreground/70">Human brilliance powered by AI</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">🤖</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/services/ai-consulting" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium hover:text-accent">AI consulting</h3>
|
||||
<p className="text-sm text-foreground/70">Maximize AI with tailored strategies</p>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">💬</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Column 4: Marketing services */}
|
||||
<div>
|
||||
<a href="/services/marketing-services" className="text-amber-500 font-medium hover:underline flex items-center gap-1 mb-4">
|
||||
Marketing services
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M7 7h10v10"/><path d="M7 17 17 7"/></svg>
|
||||
</a>
|
||||
<ul className="space-y-4">
|
||||
<li>
|
||||
<a href="/services/marketing-strategy" className="group/item block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium hover:text-accent">Marketing strategy</h3>
|
||||
<span className="bg-lime-400 text-black text-xs px-2 py-0.5 rounded">New</span>
|
||||
</div>
|
||||
<span className="text-foreground/30 text-lg">📈</span>
|
||||
</div>
|
||||
<p className="text-sm text-foreground/70">Grow your brand with expert consultants</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group dropdown-container">
|
||||
<Link to="/solutions" className="text-foreground/90 hover:text-foreground font-medium transition flex items-center gap-1">
|
||||
Solutions
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="transition-transform duration-200 group-hover:rotate-180">
|
||||
<path d="m6 9 6 6 6-6"/>
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 top-full z-50 w-screen max-w-full bg-black border-t border-gray-800 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
|
||||
<div className="container-custom py-8 grid grid-cols-3 gap-6">
|
||||
<div>
|
||||
<h3 className="font-bold text-lg mb-4">Capabilities</h3>
|
||||
<ul className="space-y-3">
|
||||
<li><a href="/capabilities/cloud" className="text-foreground/80 hover:text-foreground flex items-center gap-2"><span className="w-5 h-5 bg-foreground/10 rounded-full flex items-center justify-center">☁️</span> Cloud</a></li>
|
||||
<li><a href="/capabilities/cybersecurity" className="text-foreground/80 hover:text-foreground flex items-center gap-2"><span className="w-5 h-5 bg-foreground/10 rounded-full flex items-center justify-center">🔒</span> Cybersecurity</a></li>
|
||||
<li><a href="/capabilities/data-ai" className="text-foreground/80 hover:text-foreground flex items-center gap-2"><span className="w-5 h-5 bg-foreground/10 rounded-full flex items-center justify-center">🧠</span> Data and Artificial Intelligence</a></li>
|
||||
<li><a href="/capabilities/digital-engineering" className="text-foreground/80 hover:text-foreground flex items-center gap-2"><span className="w-5 h-5 bg-foreground/10 rounded-full flex items-center justify-center">⚙️</span> Digital Engineering</a></li>
|
||||
<li><a href="/capabilities/emerging-tech" className="text-foreground/80 hover:text-foreground flex items-center gap-2"><span className="w-5 h-5 bg-foreground/10 rounded-full flex items-center justify-center">🚀</span> Emerging Technology</a></li>
|
||||
<li><a href="/capabilities/finance-risk" className="text-foreground/80 hover:text-foreground flex items-center gap-2"><span className="w-5 h-5 bg-foreground/10 rounded-full flex items-center justify-center">💰</span> Finance and Risk Management</a></li>
|
||||
<li><a href="https://sensei.group" target="_blank" className="text-foreground/80 hover:text-foreground flex items-center gap-2"><span className="w-5 h-5 bg-foreground/10 rounded-full flex items-center justify-center">👑</span> Fractional CXO</a></li>
|
||||
</ul>
|
||||
<a href="/capabilities" className="text-accent hover:underline mt-4 inline-block">View all</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-bold text-lg mb-4">Industries</h3>
|
||||
<ul className="space-y-3">
|
||||
<li><a href="/industries/aerospace" className="text-foreground/80 hover:text-foreground flex items-center gap-2"><span className="w-5 h-5 bg-foreground/10 rounded-full flex items-center justify-center">✈️</span> Aerospace and Defense</a></li>
|
||||
<li><a href="/industries/automotive" className="text-foreground/80 hover:text-foreground flex items-center gap-2"><span className="w-5 h-5 bg-foreground/10 rounded-full flex items-center justify-center">🚗</span> Automotive</a></li>
|
||||
<li><a href="/industries/banking" className="text-foreground/80 hover:text-foreground flex items-center gap-2"><span className="w-5 h-5 bg-foreground/10 rounded-full flex items-center justify-center">🏦</span> Banking</a></li>
|
||||
<li><a href="/industries/capital-markets" className="text-foreground/80 hover:text-foreground flex items-center gap-2"><span className="w-5 h-5 bg-foreground/10 rounded-full flex items-center justify-center">📈</span> Capital Markets</a></li>
|
||||
<li><a href="/industries/chemicals" className="text-foreground/80 hover:text-foreground flex items-center gap-2"><span className="w-5 h-5 bg-foreground/10 rounded-full flex items-center justify-center">🧪</span> Chemicals</a></li>
|
||||
<li><a href="/industries/communications" className="text-foreground/80 hover:text-foreground flex items-center gap-2"><span className="w-5 h-5 bg-foreground/10 rounded-full flex items-center justify-center">📱</span> Communications and Media</a></li>
|
||||
<li><a href="/industries/consumer" className="text-foreground/80 hover:text-foreground flex items-center gap-2"><span className="w-5 h-5 bg-foreground/10 rounded-full flex items-center justify-center">🛍️</span> Consumer Goods and Services</a></li>
|
||||
</ul>
|
||||
<a href="/industries" className="text-accent hover:underline mt-4 inline-block">View all</a>
|
||||
</div>
|
||||
|
||||
<div className="bg-black/40 p-4 rounded-lg">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<div className="w-10 h-10 bg-emerald-800/30 rounded-lg flex items-center justify-center text-white">SG</div>
|
||||
<h3 className="font-bold">Sensei Group</h3>
|
||||
</div>
|
||||
<p className="text-sm text-foreground/80 mb-4">Accelerate enterprise transformation with our elite collective of CXOs and technology experts. Strategic implementation for digital evolution.</p>
|
||||
<a href="https://sensei.group" target="_blank" className="text-accent hover:underline flex items-center gap-1 text-sm">
|
||||
Learn more
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M7 7h10v10"/><path d="M7 17 17 7"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link to="/faq" className="text-foreground/90 hover:text-foreground font-medium transition">
|
||||
FAQ
|
||||
</Link>
|
||||
|
||||
<Link to="/pricing" className="text-foreground/90 hover:text-foreground font-medium transition">
|
||||
Pricing
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:flex items-center gap-3">
|
||||
<Link to="/dashboard" className="text-foreground/90 hover:text-white font-medium transition px-3 py-2 text-sm">
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link to="/login" className="border border-white/40 hover:border-white px-5 py-2 rounded-full text-foreground/90 hover:text-white font-medium transition text-sm">
|
||||
Sign In
|
||||
</Link>
|
||||
<Link to="/pricing" className="bg-white text-black px-6 py-2 rounded-full font-medium hover:bg-white/90 inline-flex items-center whitespace-nowrap text-sm">
|
||||
Get Started
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="lg:hidden p-2 text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-white/30 rounded-md"
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
aria-label="Toggle navigation menu"
|
||||
>
|
||||
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
{isMenuOpen && (
|
||||
<div className="lg:hidden fixed inset-0 top-0 left-0 right-0 bottom-0 w-full h-full bg-black/95 backdrop-blur-lg z-[999] overflow-auto transition-all duration-300">
|
||||
<div className="container-custom py-20 min-h-screen">
|
||||
<nav className="flex flex-col space-y-6 divide-y divide-white/10 pb-20">
|
||||
<div className="py-4">
|
||||
<details className="group mobile-nav-details">
|
||||
<summary className="text-foreground hover:text-foreground font-medium transition list-none flex items-center justify-between cursor-pointer py-2">
|
||||
<span>Services</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="transition-transform duration-200 group-open:rotate-180">
|
||||
<path d="m6 9 6 6 6-6"/>
|
||||
</svg>
|
||||
</summary>
|
||||
<div className="mt-2 ml-4 space-y-4">
|
||||
{/* Creative design services */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-2 text-lime-400">Creative design services</h4>
|
||||
<ul className="space-y-2 ml-2">
|
||||
<li><a href="/services/ad-creative" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Ad creative</a></li>
|
||||
<li><a href="/services/social-media-creative" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Social media creative</a></li>
|
||||
<li><a href="/services/presentation-design" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Presentation design</a></li>
|
||||
<li><a href="/services/illustration-design" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Illustration design</a></li>
|
||||
<li><a href="/services/branding-services" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Branding services</a></li>
|
||||
<li><a href="/services/email-creation" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Email creation</a></li>
|
||||
<li><a href="/services/web-design" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Web design</a></li>
|
||||
<li><a href="/services/ebooks-report-design" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>eBooks & report design</a></li>
|
||||
<li><a href="/services/concept-creation" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Concept creation</a></li>
|
||||
<li><a href="/services/print-design" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Print design</a></li>
|
||||
<li><a href="/services/packaging-merchandise-design" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Packaging & merchandise design</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Specialized production services */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-2 text-emerald-700">Specialized production services</h4>
|
||||
<ul className="space-y-2 ml-2">
|
||||
<li><a href="/services/video-production" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Video production</a></li>
|
||||
<li><a href="/services/motion-design" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Motion design</a></li>
|
||||
<li><a href="/services/3d-ar-design" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>3D & AR design</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* AI services */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-2 text-blue-700">AI services</h4>
|
||||
<ul className="space-y-2 ml-2">
|
||||
<li><a href="/services/ai-enhanced-creative" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>AI enhanced creative</a></li>
|
||||
<li><a href="/services/ai-consulting" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>AI consulting</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Marketing services */}
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-2 text-amber-500">Marketing services</h4>
|
||||
<ul className="space-y-2 ml-2">
|
||||
<li>
|
||||
<a href="/services/marketing-strategy" className="text-foreground/80 hover:text-foreground text-sm flex items-center gap-2" onClick={handleMobileLinkClick}>
|
||||
Marketing strategy
|
||||
<span className="bg-lime-400 text-black text-xs px-1 py-0.5 rounded">New</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div className="py-4">
|
||||
<details className="group mobile-nav-details">
|
||||
<summary className="text-foreground hover:text-foreground font-medium transition list-none flex items-center justify-between cursor-pointer py-2">
|
||||
<span>Solutions</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="transition-transform duration-200 group-open:rotate-180">
|
||||
<path d="m6 9 6 6 6-6"/>
|
||||
</svg>
|
||||
</summary>
|
||||
<div className="mt-2 ml-4 space-y-4">
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-2">Capabilities</h4>
|
||||
<ul className="space-y-2 ml-2">
|
||||
<li><a href="/capabilities/cloud" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Cloud</a></li>
|
||||
<li><a href="/capabilities/cybersecurity" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Cybersecurity</a></li>
|
||||
<li><a href="/capabilities/data-ai" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Data and Artificial Intelligence</a></li>
|
||||
<li><a href="/capabilities/digital-engineering" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Digital Engineering</a></li>
|
||||
<li><a href="/capabilities/emerging-tech" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Emerging Technology</a></li>
|
||||
<li><a href="/capabilities/finance-risk" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Finance and Risk Management</a></li>
|
||||
<li><a href="https://sensei.group" target="_blank" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Fractional CXO</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold text-sm mb-2">Industries</h4>
|
||||
<ul className="space-y-2 ml-2">
|
||||
<li><a href="/industries/aerospace" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Aerospace and Defense</a></li>
|
||||
<li><a href="/industries/automotive" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Automotive</a></li>
|
||||
<li><a href="/industries/banking" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Banking</a></li>
|
||||
<li><a href="/industries/capital-markets" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Capital Markets</a></li>
|
||||
<li><a href="/industries/chemicals" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Chemicals</a></li>
|
||||
<li><a href="/industries/communications" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Communications and Media</a></li>
|
||||
<li><a href="/industries/consumer" className="text-foreground/80 hover:text-foreground text-sm" onClick={handleMobileLinkClick}>Consumer Goods and Services</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-black/20 p-3 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="w-8 h-8 bg-emerald-800/30 rounded-lg flex items-center justify-center text-white text-xs">SG</div>
|
||||
<h4 className="font-semibold text-sm">Sensei Group</h4>
|
||||
</div>
|
||||
<p className="text-xs text-foreground/80 mb-2">Accelerate enterprise transformation with our elite collective of CXOs and technology experts.</p>
|
||||
<a href="https://sensei.group" target="_blank" className="text-accent hover:underline text-xs flex items-center gap-1" onClick={handleMobileLinkClick}>
|
||||
Learn more
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M7 7h10v10"/><path d="M7 17 17 7"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<Link to="/faq" className="text-foreground/90 hover:text-foreground py-2 font-medium transition" onClick={() => setIsMenuOpen(false)}>FAQ</Link>
|
||||
<Link to="/pricing" className="text-foreground/90 hover:text-foreground py-2 font-medium transition" onClick={() => setIsMenuOpen(false)}>Pricing</Link>
|
||||
<Link to="/login" className="text-foreground/90 hover:text-white py-2 font-medium transition" onClick={() => setIsMenuOpen(false)}>Log In</Link>
|
||||
<a href="https://cloud.hanzo.ai" className="text-foreground/90 hover:text-white py-2 font-medium transition border border-white/60 hover:border-white rounded-full px-6 py-2.5 inline-block mt-4" onClick={handleMobileLinkClick}>Console</a>
|
||||
<Link to="/onboarding" className="bg-white text-black px-7 py-2.5 rounded-full font-medium hover:bg-white/90 inline-flex items-center whitespace-nowrap mt-4" onClick={() => setIsMenuOpen(false)}>
|
||||
Sign Up
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
@@ -1,105 +0,0 @@
|
||||
/* Navigation Dropdown Fixes */
|
||||
|
||||
/* Ensure the header has proper z-index stacking */
|
||||
header {
|
||||
z-index: 100 !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Fix for dropdown visibility with important flags */
|
||||
nav .group:hover > div[class*="opacity-0"] {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Ensure dropdowns can receive hover events */
|
||||
nav .group > div[class*="absolute"] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
nav .group:hover > div[class*="absolute"] {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* Additional fixes for dropdown positioning */
|
||||
nav .group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Ensure the dropdown stays visible when hovering over it */
|
||||
nav .group > div[class*="absolute"]:hover {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* Fix for Safari/Chrome transform issues */
|
||||
nav .group > div[class*="absolute"] {
|
||||
-webkit-transform: translateX(-50%);
|
||||
transform: translateX(-50%);
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
/* Ensure smooth transitions */
|
||||
nav .group > div[class*="absolute"] {
|
||||
transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Make sure the dropdown content is above other elements */
|
||||
nav .group > div[class*="absolute"] > div {
|
||||
position: relative;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
/* Fix for edge cases where dropdown might be cut off */
|
||||
nav {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
header > div {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Delay the hiding of dropdown to allow mouse to move to it */
|
||||
nav .group:not(:hover) > div[class*="absolute"] {
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
/* Specific fixes for the dropdowns */
|
||||
nav .group > .absolute {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Fix container overflow issues */
|
||||
.container-custom {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Additional specificity for Tailwind classes */
|
||||
.group:hover .opacity-0.group-hover\:opacity-100 {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.group:hover .invisible.group-hover\:visible {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Fix z-index layering */
|
||||
nav .group > div.absolute {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
/* Ensure parent containers don't clip */
|
||||
.flex-1 {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Debug - temporarily make dropdowns visible to check if they exist */
|
||||
/* Uncomment the following to debug: */
|
||||
/*
|
||||
nav .group > div[class*="absolute"] {
|
||||
opacity: 0.5 !important;
|
||||
visibility: visible !important;
|
||||
border: 2px solid red !important;
|
||||
}
|
||||
*/
|
||||
@@ -1,34 +0,0 @@
|
||||
/* Navigation spacing fixes */
|
||||
.navigation-menu-list {
|
||||
gap: 1.5rem !important; /* Increase spacing between nav items */
|
||||
}
|
||||
|
||||
/* Ensure logo doesn't get cut off */
|
||||
.logo-container {
|
||||
flex-shrink: 0;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
/* Better responsive handling */
|
||||
@media (max-width: 1280px) {
|
||||
.navigation-menu-list {
|
||||
gap: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.navigation-menu-list {
|
||||
gap: 0.75rem !important;
|
||||
}
|
||||
|
||||
/* Reduce font size slightly on smaller screens */
|
||||
.navigation-menu-trigger {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure header content doesn't overflow */
|
||||
.header-container {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
/* Navigation Menu Dropdown Fixes for shadcn/ui */
|
||||
|
||||
/* Force the navigation content to be visible on hover */
|
||||
[data-radix-collection-item] [data-state="open"] > [data-radix-popper-content-wrapper] {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* Ensure navigation content wrapper allows interaction */
|
||||
[data-radix-popper-content-wrapper] {
|
||||
pointer-events: auto !important;
|
||||
z-index: 50 !important;
|
||||
}
|
||||
|
||||
/* Navigation menu content styling */
|
||||
[role="navigation"] [data-radix-collection-item] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Ensure dropdowns appear correctly */
|
||||
.NavigationMenuContent,
|
||||
[data-radix-popper-content-wrapper] > div {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
background-color: rgb(0, 0, 0) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 10px 50px rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
|
||||
/* Fix viewport positioning */
|
||||
.NavigationMenuViewport,
|
||||
[data-radix-viewport] {
|
||||
position: absolute !important;
|
||||
top: 100% !important;
|
||||
left: 0 !important;
|
||||
width: var(--radix-navigation-menu-viewport-width) !important;
|
||||
pointer-events: auto !important;
|
||||
z-index: 50 !important;
|
||||
}
|
||||
|
||||
/* Ensure content is interactive */
|
||||
.NavigationMenuContent {
|
||||
pointer-events: auto !important;
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Remove any overflow hidden that might clip dropdowns */
|
||||
header,
|
||||
header > div,
|
||||
nav,
|
||||
[role="navigation"],
|
||||
.NavigationMenu {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Ensure triggers work properly */
|
||||
.NavigationMenuTrigger {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Style the dropdown content container */
|
||||
.NavigationMenuContent > div {
|
||||
background-color: black;
|
||||
min-width: 600px;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
/* Ensure list items are properly styled */
|
||||
.NavigationMenuList {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Debug styles - uncomment to see dropdown positions */
|
||||
/*
|
||||
[data-radix-popper-content-wrapper] {
|
||||
border: 2px solid red !important;
|
||||
background: rgba(255, 0, 0, 0.1) !important;
|
||||
}
|
||||
*/
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Check, ArrowUpRight } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card';
|
||||
import { plans, planHref, priceLabel, ladder } from '@/data/plans';
|
||||
import { plans, priceLabel, ladder } from '@/data/plans';
|
||||
import { checkoutUrl } from '@/lib/commerce';
|
||||
|
||||
/** The plans as cards. The home page, /pricing and /services all render this one grid. */
|
||||
export const PlanCards = () => (
|
||||
@@ -16,7 +17,7 @@ export const PlanCards = () => (
|
||||
<CardContent className="p-8 flex-grow">
|
||||
<div className="flex justify-center items-baseline mb-2">
|
||||
<span className="text-4xl font-bold text-white tracking-tight">{priceLabel(plan)}</span>
|
||||
{plan.priceMonthly !== null && <span className="ml-2 text-white/70">/month</span>}
|
||||
<span className="ml-2 text-white/70">/month</span>
|
||||
</div>
|
||||
<p className="text-center text-white/60 text-sm min-h-5 mb-6">{plan.terms}</p>
|
||||
|
||||
@@ -31,10 +32,10 @@ export const PlanCards = () => (
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="p-6 pt-0">
|
||||
<Link to={planHref(plan)} className="lets-talk-btn w-full justify-center text-lg py-3">
|
||||
<a href={checkoutUrl(plan)} className="lets-talk-btn w-full justify-center text-lg py-3">
|
||||
{plan.cta}
|
||||
<ArrowUpRight size={20} className="ml-2" />
|
||||
</Link>
|
||||
</a>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
@@ -50,7 +51,7 @@ export const NeedMore = () => (
|
||||
</p>
|
||||
<p className="font-semibold mb-6">Let's build the engagement around you.</p>
|
||||
<Link to="/contact" className="lets-talk-btn">
|
||||
Contact us
|
||||
Talk to us
|
||||
<ArrowUpRight size={16} className="ml-1" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
/* Comprehensive Navigation Dropdown Fix for Radix UI Navigation Menu */
|
||||
|
||||
/* 1. Force viewport to be visible and properly positioned */
|
||||
[data-radix-viewport] {
|
||||
position: absolute !important;
|
||||
top: 100% !important;
|
||||
left: 0 !important;
|
||||
width: var(--radix-navigation-menu-viewport-width) !important;
|
||||
height: var(--radix-navigation-menu-viewport-height) !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
pointer-events: auto !important;
|
||||
z-index: 999 !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* 2. Ensure content wrapper is visible */
|
||||
[data-radix-popper-content-wrapper] {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
pointer-events: auto !important;
|
||||
z-index: 999 !important;
|
||||
position: absolute !important;
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
/* 3. Force navigation content to be visible when state is open */
|
||||
[data-state="open"] > [data-radix-popper-content-wrapper],
|
||||
[data-state="open"] + [data-radix-viewport] {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* 4. Ensure the navigation menu content is styled and visible */
|
||||
[data-radix-collection-item] > div[role="group"] {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
background-color: black !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 10px 50px rgba(0, 0, 0, 0.8) !important;
|
||||
min-width: 600px !important;
|
||||
}
|
||||
|
||||
/* 5. Remove all overflow hidden from parent elements */
|
||||
body,
|
||||
#root,
|
||||
header,
|
||||
header > div,
|
||||
nav,
|
||||
[role="navigation"],
|
||||
[data-radix-collection-item] {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* 6. Ensure triggers have proper z-index */
|
||||
button[data-state="open"],
|
||||
button[data-state="closed"] {
|
||||
position: relative !important;
|
||||
z-index: 10 !important;
|
||||
}
|
||||
|
||||
/* 7. Force content to show on state open */
|
||||
[data-state="open"] div[style*="pointer-events"] {
|
||||
pointer-events: auto !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* 8. Alternative method using aria-expanded */
|
||||
button[aria-expanded="true"] ~ * {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* 9. Specific fixes for NavigationMenuContent component */
|
||||
.NavigationMenuContent,
|
||||
div[data-radix-collection-item] div[style*="position: absolute"] {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
display: block !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* 10. Animation overrides to prevent hiding */
|
||||
[data-motion],
|
||||
[data-state] {
|
||||
animation-duration: 0s !important;
|
||||
animation-delay: 0s !important;
|
||||
}
|
||||
|
||||
/* 11. Override any transform that might hide content */
|
||||
[data-radix-popper-content-wrapper] > div,
|
||||
[data-radix-viewport] > div {
|
||||
transform: none !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* 12. Debug mode - uncomment to see all dropdown elements */
|
||||
/* ACTIVE DEBUG MODE - THIS WILL SHOW ALL DROPDOWN ELEMENTS */
|
||||
[data-radix-viewport],
|
||||
[data-radix-popper-content-wrapper],
|
||||
[data-radix-collection-item] > div,
|
||||
[data-state="open"] ~ div,
|
||||
.NavigationMenuContent {
|
||||
border: 2px solid red !important;
|
||||
background: rgba(255, 0, 0, 0.1) !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
min-height: 100px !important;
|
||||
}
|
||||
|
||||
/* 13. Ensure the dropdown content minimum dimensions */
|
||||
[role="navigation"] [data-radix-collection-item] > div > div {
|
||||
min-width: 900px !important;
|
||||
background-color: black !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
/* 14. Fix for Services dropdown specifically */
|
||||
[role="navigation"] [data-radix-collection-item]:nth-child(2) > div > div {
|
||||
min-width: 1200px !important;
|
||||
}
|
||||
|
||||
/* 15. Make sure list items don't hide content */
|
||||
[role="navigation"] ul {
|
||||
position: static !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/* Navigation Dropdown Final Fix */
|
||||
|
||||
/* Remove all overflow hidden from parent elements */
|
||||
html, body, #root {
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
header, header > div, nav, .container-custom {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Fix the group hover state */
|
||||
.group:hover .opacity-0 {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.group:hover .invisible {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Ensure dropdowns are above everything */
|
||||
.group > .absolute {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
/* Fix pointer events */
|
||||
.group > .absolute {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.group:hover > .absolute {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* Keep dropdown open when hovering over it */
|
||||
.group > .absolute:hover {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Fix the width issue - use max width instead of full screen */
|
||||
.group > .absolute {
|
||||
width: max-content !important;
|
||||
max-width: 90vw !important;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
}
|
||||
|
||||
/* Add a small delay to prevent flicker */
|
||||
.group > .absolute {
|
||||
transition: opacity 0.3s ease, visibility 0.3s ease !important;
|
||||
}
|
||||
|
||||
/* Create invisible bridge between trigger and dropdown */
|
||||
.group::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: -50px;
|
||||
right: -50px;
|
||||
height: 20px;
|
||||
z-index: 9998;
|
||||
}
|
||||
|
||||
/* Debug mode - uncomment to visualize */
|
||||
/*
|
||||
.group > .absolute {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
background: rgba(255, 0, 0, 0.2) !important;
|
||||
border: 2px solid yellow !important;
|
||||
}
|
||||
|
||||
.group::before {
|
||||
background: rgba(0, 255, 0, 0.2) !important;
|
||||
}
|
||||
*/
|
||||
+15
-3
@@ -1,9 +1,21 @@
|
||||
// One home for the agency's contact facts. The footer and the Contact page
|
||||
// both render these, so they cannot drift apart again.
|
||||
// One home for the agency's contact facts. The footer, the Contact page, the
|
||||
// chat widget and every "book a call" button render these, so they cannot drift
|
||||
// apart again.
|
||||
export const contact = {
|
||||
entity: 'Hanzo AI, Inc.',
|
||||
address: ['995 Market St', 'San Francisco, CA 94103'],
|
||||
email: 'hi@hanzo.agency',
|
||||
// hanzo.agency publishes no MX record, so mail to an @hanzo.agency address
|
||||
// bounces. This is the address hanzo.ai/contact publishes.
|
||||
email: '[email protected]',
|
||||
phone: '+1 415 373 2496',
|
||||
phoneHref: 'tel:+14153732496',
|
||||
/** The agency's booking page: 15-minute calls with the team. */
|
||||
booking: 'https://calendar.app.google/z1YsZQrqR4s6jQqD8',
|
||||
} as const;
|
||||
|
||||
/** Where an agency client works and signs in: the Hanzo app. */
|
||||
export const app = {
|
||||
home: 'https://hanzo.ai',
|
||||
login: 'https://hanzo.ai/login',
|
||||
signup: 'https://hanzo.ai/signup',
|
||||
} as const;
|
||||
|
||||
@@ -296,7 +296,7 @@ export const navigationItems: NavigationItem[] = [
|
||||
featured: [
|
||||
{
|
||||
title: "AI Cloud Platform",
|
||||
href: "https://cloud.hanzo.ai",
|
||||
href: "https://hanzo.ai/cloud",
|
||||
description: "Our enterprise AI platform for scalable, secure solutions.",
|
||||
cta: "Explore platform",
|
||||
isExternal: true
|
||||
|
||||
@@ -5,6 +5,8 @@ export const formSections: FormSection[] = [
|
||||
{
|
||||
title: "Business Information",
|
||||
fields: [
|
||||
{ id: "email", label: "Your work email", type: "text", placeholder: "[email protected]", required: true },
|
||||
{ id: "name", label: "Your name", type: "text", required: true },
|
||||
{ id: "companyName", label: "What's your company's official name?", type: "text", required: true },
|
||||
{ id: "businessDescription", label: "Provide a short description of your business and core offering.", type: "textarea", required: true },
|
||||
{ id: "targetAudience", label: "Describe your target audience in detail (industry, demographics, preferences).", type: "textarea", required: true },
|
||||
@@ -37,8 +39,6 @@ export const formSections: FormSection[] = [
|
||||
{ id: "timeline", label: "Preferred timeline or deadlines?", type: "text", required: false },
|
||||
{ id: "competitors", label: "List competitors' websites or businesses you wish to differentiate from.", type: "textarea", required: false },
|
||||
{ id: "additionalRequests", label: "Any additional requests or inspiration you want incorporated into your project?", type: "textarea", required: false },
|
||||
{ id: "designBrief", label: "Upload Design Brief, branding Document, pitch decks, any content with a visual representation of your brand", type: "file", accept: ".jpg,.png,.jpeg,.svg,.pdf", required: false },
|
||||
{ id: "logo", label: "Upload your logo", type: "file", accept: ".jpg,.png,.svg", required: false },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
+48
-23
@@ -1,29 +1,39 @@
|
||||
// What hanzo.agency sells, in one file.
|
||||
//
|
||||
// Every surface that shows a plan or a price reads it from here: /pricing, the
|
||||
// pricing section on the home page, /services, the FAQ, the Enterprise page,
|
||||
// the chat assistant, the client dashboard, and the checkout summary.
|
||||
// pricing section on the home page, /services, the FAQ, the chat widget, the
|
||||
// checkout return page and /white-label.
|
||||
//
|
||||
// Field names, and dollars as the unit, are taken from @hanzo/plans
|
||||
// (plan.schema.json): id, name, description, priceMonthly, features. Same names,
|
||||
// same units, so these entries can move into that package's catalog without
|
||||
// being rewritten.
|
||||
// Each plan names the commerce plan the checkout charges (`slug`, a row of
|
||||
// GET api.hanzo.ai/v1/billing/plans). The price and features here are that
|
||||
// row's, so the card a buyer clicks and the cart hanzo.ai/pay opens agree:
|
||||
//
|
||||
// agency -> advisory $4,999 / month
|
||||
// forward -> dedicated $9,999 / month
|
||||
// pod -> pod $24,999 / month
|
||||
// white -> white-label $999 / month (sold only on /white-label)
|
||||
//
|
||||
// `id` is this site's own name for a plan, used in its URLs. The commerce slug
|
||||
// `agency` is a different, private plan; never send an id to the checkout.
|
||||
|
||||
export interface Plan {
|
||||
id: string;
|
||||
/** The commerce plan the checkout charges. */
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string;
|
||||
/** USD per month, whole units. Null when the price is set per engagement. */
|
||||
priceMonthly: number | null;
|
||||
/** USD per month, whole units: the commerce row's price. */
|
||||
priceMonthly: number;
|
||||
features: string[];
|
||||
/** Commitment, in the words we say it to a customer. */
|
||||
terms?: string;
|
||||
terms: string;
|
||||
cta: string;
|
||||
}
|
||||
|
||||
export const plans: Plan[] = [
|
||||
{
|
||||
id: 'agency',
|
||||
slug: 'advisory',
|
||||
name: 'Agency',
|
||||
description: 'Send us the work. We ship it.',
|
||||
priceMonthly: 4999,
|
||||
@@ -42,6 +52,7 @@ export const plans: Plan[] = [
|
||||
},
|
||||
{
|
||||
id: 'forward',
|
||||
slug: 'dedicated',
|
||||
name: 'Forward Deployed',
|
||||
description: 'We join your team.',
|
||||
priceMonthly: 9999,
|
||||
@@ -63,6 +74,7 @@ export const plans: Plan[] = [
|
||||
},
|
||||
{
|
||||
id: 'pod',
|
||||
slug: 'pod',
|
||||
name: 'Embedded Pod',
|
||||
description: 'We bring the team.',
|
||||
priceMonthly: 24999,
|
||||
@@ -84,25 +96,38 @@ export const plans: Plan[] = [
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* The partner plan: your own branded agency, resold to your clients. Paid by
|
||||
* card, monthly, up front. It is sold on /white-label only, so it is not in
|
||||
* `plans` and no pricing grid draws it.
|
||||
*/
|
||||
export const whiteLabel: Plan = {
|
||||
id: 'white-label',
|
||||
slug: 'white-label',
|
||||
name: 'White-label',
|
||||
description: 'Your own AI agency, under your brand, on your domain.',
|
||||
priceMonthly: 999,
|
||||
terms: 'Monthly, paid up front by card',
|
||||
features: [
|
||||
'Your own agency site on your domain',
|
||||
'Your name, logo, colors and theme',
|
||||
'Resell to your own clients at your own prices',
|
||||
'Your clients see your brand, only yours',
|
||||
'Set up for you by the Hanzo team',
|
||||
],
|
||||
cta: 'Start white-label',
|
||||
};
|
||||
|
||||
/** The line under the homepage headline. */
|
||||
export const ladder =
|
||||
'Agency from $4,999. Forward Deployed from $9,999. Embedded teams from $24,999.';
|
||||
|
||||
/**
|
||||
* A plan by id, or undefined.
|
||||
*
|
||||
* Undefined rather than a default. An id that is not in the catalogue has no
|
||||
* price, and a lookup that supplies one anyway decides on the caller's behalf.
|
||||
* Every caller here would rather ask again than guess, so each handles the
|
||||
* absence itself.
|
||||
* A plan by this site's id or by its commerce slug, or undefined. The checkout
|
||||
* returns with the slug; this site's own links carry the id.
|
||||
*/
|
||||
export const planById = (id: string | null | undefined): Plan | undefined =>
|
||||
plans.find((p) => p.id === id);
|
||||
[...plans, whiteLabel].find((p) => p.id === id || p.slug === id);
|
||||
|
||||
/** "$4,999", or "Talk" when the price is set in a conversation. USD only. */
|
||||
export const priceLabel = (p: Plan): string =>
|
||||
p.priceMonthly === null ? 'Talk' : `$${p.priceMonthly.toLocaleString('en-US')}`;
|
||||
|
||||
/** Where a plan's call to action goes: checkout for a priced plan, the contact page otherwise. */
|
||||
export const planHref = (p: Plan): string =>
|
||||
p.priceMonthly === null ? '/contact' : `/payment?plan=${p.id}`;
|
||||
/** "$4,999". USD only. */
|
||||
export const priceLabel = (p: Plan): string => `$${p.priceMonthly.toLocaleString('en-US')}`;
|
||||
|
||||
+13
-55
@@ -1,67 +1,25 @@
|
||||
import { EVENTS } from '@hanzo/event';
|
||||
import { analytics } from '@/analytics';
|
||||
import type { Plan } from '@/data/plans';
|
||||
|
||||
/**
|
||||
* Where money changes hands: the pay site, which is the one host that answers.
|
||||
* Where money changes hands: the checkout at hanzo.ai/pay (pay.hanzo.ai
|
||||
* redirects there).
|
||||
*
|
||||
* Its cart takes `?plan=` and `?returnUrl=`, charges the plan the billing
|
||||
* catalog prices under that slug, and comes back to the return address with
|
||||
* Its cart takes `?plan=` and `?returnUrl=`, charges the commerce plan under that
|
||||
* slug by card, and comes back to the return address with
|
||||
* `?checkout=<status>&plan=<slug>` appended. The return must be a host commerce
|
||||
* allows: `GET api.hanzo.ai/v1/commerce/org` publishes that allowlist, and
|
||||
* hanzo.agency is on it.
|
||||
*/
|
||||
const PAY = 'https://pay.hanzo.ai';
|
||||
const PAY = 'https://hanzo.ai/pay';
|
||||
|
||||
/** The public commerce read a checkout page boots from. Needs no credential. */
|
||||
const COMMERCE = 'https://api.hanzo.ai/v1/commerce';
|
||||
|
||||
/** The brand this site sells under. */
|
||||
const ORG = 'hanzo';
|
||||
/** Where a paid buyer lands: the page that confirms the plan. */
|
||||
const BACK = '/payment-success';
|
||||
|
||||
/**
|
||||
* The checkout address for a plan.
|
||||
*
|
||||
* ONE writer for this address. A plan id only selects the card a reader clicked
|
||||
* if every writer spells the query the same way.
|
||||
* The checkout address for a plan. ONE writer for this address, so the plan a
|
||||
* reader clicked is the plan the cart opens on.
|
||||
*/
|
||||
export function checkoutUrl(planId?: string, returnPath = '/payment-success'): string {
|
||||
if (!planId) return PAY;
|
||||
const back = new URL(returnPath, window.location.origin).toString();
|
||||
return `${PAY}/cart?plan=${encodeURIComponent(planId)}&returnUrl=${encodeURIComponent(back)}`;
|
||||
export function checkoutUrl(plan: Plan): string {
|
||||
const back = new URL(BACK, window.location.origin).toString();
|
||||
return `${PAY}/cart?plan=${encodeURIComponent(plan.slug)}&returnUrl=${encodeURIComponent(back)}`;
|
||||
}
|
||||
|
||||
export type PaymentProvider = { name: string; enabled: boolean };
|
||||
|
||||
export type CheckoutConfig = {
|
||||
brand: { displayName: string; legalName: string; termsUrl: string; privacyUrl: string };
|
||||
providers: PaymentProvider[];
|
||||
};
|
||||
|
||||
/**
|
||||
* What the pay site will offer, read from commerce rather than assumed.
|
||||
*
|
||||
* The provider list is the deployment's, so a method this site advertises and a
|
||||
* method checkout accepts cannot disagree. Callers that only need a link do not
|
||||
* need this.
|
||||
*/
|
||||
export async function checkoutConfig(): Promise<CheckoutConfig> {
|
||||
const res = await fetch(`${COMMERCE}/org?org=${ORG}`);
|
||||
if (!res.ok) throw new Error(`commerce org config: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** Cents to a display string. */
|
||||
export const formatCurrency = (amount: number): string =>
|
||||
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount / 100);
|
||||
|
||||
/**
|
||||
* The moment money changed hands. ONE event; the pack is a PROPERTY, never part
|
||||
* of the name.
|
||||
*/
|
||||
export const trackPurchaseSuccess = (orderId: string, products: unknown[], totalAmount: number) => {
|
||||
analytics.capture(
|
||||
EVENTS.ORDER_COMPLETED,
|
||||
{ orderId, items: products },
|
||||
{ revenue: totalAmount, currency: 'USD', quantity: products.length },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { contact } from '@/data/contact';
|
||||
|
||||
/**
|
||||
* The sales form's endpoint: POST api.hanzo.ai/v1/marketing/leads files a lead in
|
||||
* Hanzo's CRM (cloud apps/marketing). It is the endpoint hanzo.ai/contact-sales
|
||||
* posts to; every form on this site that asks for someone's details posts here.
|
||||
* It takes no credential and admits hanzo.agency's origin.
|
||||
*/
|
||||
const LEADS = 'https://api.hanzo.ai/v1/marketing/leads';
|
||||
|
||||
/** The fields the endpoint reads. `source` names the form, e.g. hanzo.agency/contact. */
|
||||
export interface Lead {
|
||||
email: string;
|
||||
name?: string;
|
||||
company?: string;
|
||||
role?: string;
|
||||
need?: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
/** The endpoint clips the free-text answer at 4 KiB. */
|
||||
const NEED = 4000;
|
||||
|
||||
export const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
/**
|
||||
* File a lead. Resolves to the CRM's reference, or undefined when nothing was
|
||||
* filed, so the caller can offer the booking page and email instead.
|
||||
*/
|
||||
export async function fileLead(lead: Lead): Promise<string | undefined> {
|
||||
try {
|
||||
const r = await fetch(LEADS, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...lead,
|
||||
need: lead.need?.slice(0, NEED),
|
||||
visit: { url: window.location.href, userAgent: navigator.userAgent },
|
||||
}),
|
||||
});
|
||||
if (!r.ok) return undefined;
|
||||
return ((await r.json()) as { lead?: string }).lead;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** A mail to the team carrying what the form collected, for when filing fails. */
|
||||
export function mailto(subject: string, body: string): string {
|
||||
const q = new URLSearchParams({ subject, body: body.slice(0, 1800) });
|
||||
return `mailto:${contact.email}?${q.toString().replace(/\+/g, '%20')}`;
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import Footer from '@/components/Footer';
|
||||
import { ArrowLeft, CreditCard, Lock } from 'lucide-react';
|
||||
|
||||
const AddPaymentMethod = () => {
|
||||
const navigate = useNavigate();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
cardNumber: '',
|
||||
expiry: '',
|
||||
cvc: '',
|
||||
name: ''
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
// Simulate adding payment method
|
||||
setTimeout(() => {
|
||||
localStorage.setItem('hasPaymentMethod', 'true');
|
||||
navigate('/purchase-credits');
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="pt-32 pb-20 bg-black text-white min-h-screen">
|
||||
<div className="container-custom max-w-md">
|
||||
<div className="mb-12">
|
||||
<Link to="/purchase-credits" className="inline-flex items-center gap-2 text-gray-400 hover:text-white mb-6">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back
|
||||
</Link>
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4">Add Payment Method</h1>
|
||||
<p className="text-gray-400">
|
||||
Add a card to purchase credits for design services
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-8">
|
||||
<div className="flex items-center gap-3 mb-6 pb-6 border-b border-gray-800">
|
||||
<CreditCard className="h-6 w-6 text-gray-400" />
|
||||
<h2 className="text-xl font-semibold">Card Information</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<Label htmlFor="cardNumber" className="text-gray-300 mb-2 block">
|
||||
Card Number
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="cardNumber"
|
||||
type="text"
|
||||
required
|
||||
value={formData.cardNumber}
|
||||
onChange={(e) => setFormData({...formData, cardNumber: e.target.value})}
|
||||
className="bg-gray-900/50 border-gray-700 text-white pl-4 pr-12 py-3 rounded-lg w-full"
|
||||
placeholder="1234 5678 9012 3456"
|
||||
/>
|
||||
<Lock className="absolute right-4 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="expiry" className="text-gray-300 mb-2 block">
|
||||
Expiry Date
|
||||
</Label>
|
||||
<Input
|
||||
id="expiry"
|
||||
type="text"
|
||||
required
|
||||
value={formData.expiry}
|
||||
onChange={(e) => setFormData({...formData, expiry: e.target.value})}
|
||||
className="bg-gray-900/50 border-gray-700 text-white px-4 py-3 rounded-lg"
|
||||
placeholder="MM/YY"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="cvc" className="text-gray-300 mb-2 block">
|
||||
CVC
|
||||
</Label>
|
||||
<Input
|
||||
id="cvc"
|
||||
type="text"
|
||||
required
|
||||
value={formData.cvc}
|
||||
onChange={(e) => setFormData({...formData, cvc: e.target.value})}
|
||||
className="bg-gray-900/50 border-gray-700 text-white px-4 py-3 rounded-lg"
|
||||
placeholder="123"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="name" className="text-gray-300 mb-2 block">
|
||||
Name on Card
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
required
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
||||
className="bg-gray-900/50 border-gray-700 text-white px-4 py-3 rounded-lg w-full"
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-gray-800/30 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Lock className="h-4 w-4" />
|
||||
<span>Your payment information is encrypted and secure</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full bg-white hover:bg-gray-100 text-black py-3 text-lg font-semibold rounded-lg transition-colors mt-8"
|
||||
>
|
||||
{isLoading ? 'Adding card...' : 'Add Payment Method'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPaymentMethod;
|
||||
@@ -1,393 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Footer from '@/components/Footer';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Download,
|
||||
Mail,
|
||||
Calendar,
|
||||
User,
|
||||
CreditCard,
|
||||
Package,
|
||||
Filter,
|
||||
RefreshCw
|
||||
} from 'lucide-react';
|
||||
|
||||
interface Order {
|
||||
id: string;
|
||||
type: 'purchase' | 'redemption';
|
||||
userEmail: string;
|
||||
userName: string;
|
||||
credits: number;
|
||||
amount?: number;
|
||||
service?: string;
|
||||
description: string;
|
||||
date: string;
|
||||
status?: 'completed' | 'pending' | 'active';
|
||||
}
|
||||
|
||||
const AdminDashboard = () => {
|
||||
const navigate = useNavigate();
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [filter, setFilter] = useState<'all' | 'purchases' | 'redemptions'>('all');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [stats, setStats] = useState({
|
||||
totalRevenue: 0,
|
||||
totalCreditsIssued: 0,
|
||||
totalRedemptions: 0,
|
||||
activeUsers: 0
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user is admin
|
||||
const userEmail = localStorage.getItem('userEmail');
|
||||
const adminEmails = ['[email protected]', '[email protected]'];
|
||||
|
||||
if (!userEmail || !adminEmails.includes(userEmail)) {
|
||||
navigate('/dashboard');
|
||||
return;
|
||||
}
|
||||
|
||||
loadAllOrders();
|
||||
}, [navigate]);
|
||||
|
||||
const loadAllOrders = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
// Simulate loading all orders from all users
|
||||
// In production, this would be an API call to your backend
|
||||
const allOrders: Order[] = [];
|
||||
const userEmails = new Set<string>();
|
||||
|
||||
// Get all stored data (in production, this would come from a database)
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key?.includes('creditHistory_')) {
|
||||
const email = key.replace('creditHistory_', '');
|
||||
const history = JSON.parse(localStorage.getItem(key) || '[]');
|
||||
|
||||
history.forEach((item: any) => {
|
||||
allOrders.push({
|
||||
...item,
|
||||
userEmail: email,
|
||||
userName: localStorage.getItem(`userName_${email}`) || email
|
||||
});
|
||||
});
|
||||
|
||||
userEmails.add(email);
|
||||
}
|
||||
}
|
||||
|
||||
// Also get current user's history
|
||||
const currentHistory = JSON.parse(localStorage.getItem('creditHistory') || '[]');
|
||||
const currentEmail = localStorage.getItem('userEmail') || '';
|
||||
const currentName = localStorage.getItem('userName') || '';
|
||||
|
||||
currentHistory.forEach((item: any) => {
|
||||
allOrders.push({
|
||||
...item,
|
||||
userEmail: currentEmail,
|
||||
userName: currentName
|
||||
});
|
||||
});
|
||||
|
||||
if (currentEmail) userEmails.add(currentEmail);
|
||||
|
||||
// Sort by date (newest first)
|
||||
allOrders.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
|
||||
// Calculate stats
|
||||
const revenue = allOrders
|
||||
.filter(order => order.type === 'purchase')
|
||||
.reduce((sum, order) => sum + (order.amount || 0), 0);
|
||||
|
||||
const creditsIssued = allOrders
|
||||
.filter(order => order.type === 'purchase')
|
||||
.reduce((sum, order) => sum + order.credits, 0);
|
||||
|
||||
const redemptions = allOrders
|
||||
.filter(order => order.type === 'redemption')
|
||||
.length;
|
||||
|
||||
setStats({
|
||||
totalRevenue: revenue,
|
||||
totalCreditsIssued: creditsIssued,
|
||||
totalRedemptions: redemptions,
|
||||
activeUsers: userEmails.size
|
||||
});
|
||||
|
||||
setOrders(allOrders);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const filteredOrders = orders.filter(order => {
|
||||
if (filter === 'all') return true;
|
||||
if (filter === 'purchases') return order.type === 'purchase';
|
||||
if (filter === 'redemptions') return order.type === 'redemption';
|
||||
return true;
|
||||
});
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusBadge = (status?: string) => {
|
||||
const statusClasses = {
|
||||
completed: 'bg-green-900/30 text-green-400 border-green-800',
|
||||
active: 'bg-blue-900/30 text-blue-400 border-blue-800',
|
||||
pending: 'bg-yellow-900/30 text-yellow-400 border-yellow-800'
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium border ${statusClasses[status as keyof typeof statusClasses] || ''}`}>
|
||||
{status || 'N/A'}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const exportToCSV = () => {
|
||||
const headers = ['Date', 'User Email', 'User Name', 'Type', 'Service', 'Credits', 'Amount', 'Status'];
|
||||
const csvData = [
|
||||
headers.join(','),
|
||||
...filteredOrders.map(order => [
|
||||
formatDate(order.date),
|
||||
order.userEmail,
|
||||
order.userName,
|
||||
order.type,
|
||||
order.service || 'N/A',
|
||||
order.credits,
|
||||
order.amount || 0,
|
||||
order.status || 'N/A'
|
||||
].join(','))
|
||||
].join('\n');
|
||||
|
||||
const blob = new Blob([csvData], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `orders_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
a.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="pt-32 pb-20 bg-black text-white min-h-screen">
|
||||
<div className="container-custom max-w-7xl">
|
||||
{/* Header */}
|
||||
<div className="mb-12">
|
||||
<Link to="/dashboard" className="inline-flex items-center gap-2 text-gray-400 hover:text-white mb-6">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Dashboard
|
||||
</Link>
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-2">Admin Dashboard</h1>
|
||||
<p className="text-gray-400 text-lg">
|
||||
Manage orders and monitor platform activity
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={loadAllOrders}
|
||||
className="bg-gray-800 hover:bg-gray-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid md:grid-cols-4 gap-6 mb-12">
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<CreditCard className="h-8 w-8 text-green-400" />
|
||||
<span className="text-xs text-gray-400">Total Revenue</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold">${stats.totalRevenue.toLocaleString()}</p>
|
||||
<p className="text-sm text-gray-400 mt-1">All time</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Package className="h-8 w-8 text-blue-400" />
|
||||
<span className="text-xs text-gray-400">Credits Issued</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold">{stats.totalCreditsIssued.toLocaleString()}</p>
|
||||
<p className="text-sm text-gray-400 mt-1">Total credits</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Download className="h-8 w-8 text-purple-400" />
|
||||
<span className="text-xs text-gray-400">Redemptions</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold">{stats.totalRedemptions}</p>
|
||||
<p className="text-sm text-gray-400 mt-1">Services redeemed</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<User className="h-8 w-8 text-yellow-400" />
|
||||
<span className="text-xs text-gray-400">Active Users</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold">{stats.activeUsers}</p>
|
||||
<p className="text-sm text-gray-400 mt-1">Total users</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters and Export */}
|
||||
<div className="flex flex-wrap justify-between items-center mb-8 gap-4">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<button
|
||||
onClick={() => setFilter('all')}
|
||||
className={`px-4 py-2 rounded-lg transition-colors flex items-center gap-2 ${
|
||||
filter === 'all'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-gray-800 text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Filter className="h-4 w-4" />
|
||||
All Orders ({orders.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('purchases')}
|
||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
||||
filter === 'purchases'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-gray-800 text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Purchases
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('redemptions')}
|
||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
||||
filter === 'redemptions'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-gray-800 text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Redemptions
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={exportToCSV}
|
||||
className="bg-gray-800 hover:bg-gray-700 text-white px-4 py-2 rounded-lg flex items-center gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Export CSV
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Orders Table */}
|
||||
{isLoading ? (
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-12 text-center">
|
||||
<p className="text-gray-400">Loading orders...</p>
|
||||
</div>
|
||||
) : filteredOrders.length === 0 ? (
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-12 text-center">
|
||||
<p className="text-gray-400">No orders found</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-800">
|
||||
<th className="text-left p-4 text-sm font-medium text-gray-400">Date</th>
|
||||
<th className="text-left p-4 text-sm font-medium text-gray-400">User</th>
|
||||
<th className="text-left p-4 text-sm font-medium text-gray-400">Type</th>
|
||||
<th className="text-left p-4 text-sm font-medium text-gray-400">Service/Package</th>
|
||||
<th className="text-right p-4 text-sm font-medium text-gray-400">Credits</th>
|
||||
<th className="text-right p-4 text-sm font-medium text-gray-400">Amount</th>
|
||||
<th className="text-center p-4 text-sm font-medium text-gray-400">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredOrders.map((order) => (
|
||||
<tr key={order.id} className="border-b border-gray-800 hover:bg-gray-900/50 transition-colors">
|
||||
<td className="p-4 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4 text-gray-500" />
|
||||
{formatDate(order.date)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium">{order.userName}</p>
|
||||
<p className="text-xs text-gray-400">{order.userEmail}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<span className={`text-sm font-medium ${
|
||||
order.type === 'purchase' ? 'text-green-400' : 'text-blue-400'
|
||||
}`}>
|
||||
{order.type === 'purchase' ? 'Purchase' : 'Redemption'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4 text-sm">
|
||||
{order.service || order.description}
|
||||
</td>
|
||||
<td className="p-4 text-right">
|
||||
<span className={`text-sm font-bold ${
|
||||
order.type === 'purchase' ? 'text-green-400' : 'text-white'
|
||||
}`}>
|
||||
{order.type === 'purchase' ? '+' : ''}{order.credits.toLocaleString()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4 text-right">
|
||||
{order.amount ? (
|
||||
<span className="text-sm font-semibold">${order.amount.toLocaleString()}</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-500">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
{getStatusBadge(order.status)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email Notification Info */}
|
||||
<div className="mt-12 bg-gray-900/30 border border-gray-800 rounded-xl p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<Mail className="h-6 w-6 text-gray-400 mt-1" />
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">Email Notifications</h3>
|
||||
<p className="text-sm text-gray-400 mb-3">
|
||||
Email notifications are sent to admin accounts when:
|
||||
</p>
|
||||
<ul className="space-y-1 text-sm text-gray-400">
|
||||
<li>• New user signs up</li>
|
||||
<li>• Credits are purchased</li>
|
||||
<li>• Services are redeemed</li>
|
||||
<li>• Services are started (booking made)</li>
|
||||
</ul>
|
||||
<p className="text-xs text-gray-500 mt-3">
|
||||
Admin emails: a@hanzo.ai, z@hanzo.ai
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminDashboard;
|
||||
+133
-49
@@ -1,66 +1,150 @@
|
||||
|
||||
|
||||
import { useState, type FormEvent } from 'react';
|
||||
import Footer from '@/components/Footer';
|
||||
import { ArrowUpRight, Instagram, Facebook, Twitter, Linkedin } from 'lucide-react';
|
||||
import { ArrowUpRight, Calendar, Instagram, Facebook, Twitter, Linkedin } from 'lucide-react';
|
||||
import { contact } from '@/data/contact';
|
||||
import { EMAIL, fileLead, mailto } from '@/lib/leads';
|
||||
|
||||
const field =
|
||||
'w-full rounded-lg border border-white/15 bg-white/5 px-4 py-3 text-white placeholder:text-white/40 focus:border-white/40 focus:outline-none';
|
||||
|
||||
type Sent = 'idle' | 'busy' | 'filed' | 'missed';
|
||||
|
||||
/**
|
||||
* /contact: the agency's one sales page. The form files a lead with Hanzo's CRM
|
||||
* (lib/leads), then offers the booking page. Every "Talk to us" on the site
|
||||
* lands here.
|
||||
*/
|
||||
const Contact = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [company, setCompany] = useState('');
|
||||
const [need, setNeed] = useState('');
|
||||
const [wrong, setWrong] = useState('');
|
||||
const [sent, setSent] = useState<Sent>('idle');
|
||||
|
||||
const send = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!EMAIL.test(email.trim())) {
|
||||
setWrong('Enter your work email.');
|
||||
return;
|
||||
}
|
||||
setWrong('');
|
||||
setSent('busy');
|
||||
const lead = await fileLead({ email: email.trim(), name, company, need, source: 'hanzo.agency/contact' });
|
||||
setSent(lead ? 'filed' : 'missed');
|
||||
};
|
||||
|
||||
const note = [name, company, email, need].filter(Boolean).join('\n');
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* <Navbar /> removed - using global NewHeader */}
|
||||
|
||||
<main className="flex-grow container-custom py-32">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<h1 className="text-4xl md:text-6xl font-black uppercase tracking-tighter mb-8">GET IN TOUCH</h1>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-12 mb-16">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Contact Information</h2>
|
||||
<div className="space-y-3 mb-8 text-primary/80">
|
||||
<p>
|
||||
{contact.entity}<br/>
|
||||
{contact.address[0]}<br/>
|
||||
{contact.address[1]}<br/>
|
||||
United States
|
||||
</p>
|
||||
<p>
|
||||
<a href={`mailto:${contact.email}`} className="hover:text-primary transition-colors">{contact.email}</a><br/>
|
||||
<a href={contact.phoneHref} className="hover:text-primary transition-colors">{contact.phone}</a>
|
||||
</p>
|
||||
</div>
|
||||
<a href="https://calendar.app.google/z1YsZQrqR4s6jQqD8" className="lets-talk-btn">
|
||||
Send an email
|
||||
<ArrowUpRight size={16} className="ml-1" />
|
||||
<div className="max-w-5xl mx-auto grid md:grid-cols-2 gap-12">
|
||||
<div>
|
||||
<h1 className="text-4xl md:text-6xl font-black uppercase tracking-tighter mb-6">Talk to us</h1>
|
||||
<p className="text-lg text-primary/80 mb-8">
|
||||
Tell us what you are building. We reply by email, or pick a time for a 15-minute call.
|
||||
</p>
|
||||
<a href={contact.booking} className="lets-talk-btn mb-10">
|
||||
<Calendar size={16} className="mr-2" />
|
||||
Book a call
|
||||
<ArrowUpRight size={16} className="ml-1" />
|
||||
</a>
|
||||
|
||||
<div className="space-y-3 mt-10 text-primary/80">
|
||||
<p>
|
||||
{contact.entity}
|
||||
<br />
|
||||
{contact.address[0]}
|
||||
<br />
|
||||
{contact.address[1]}
|
||||
</p>
|
||||
<p>
|
||||
<a href={`mailto:${contact.email}`} className="hover:text-primary transition-colors">
|
||||
{contact.email}
|
||||
</a>
|
||||
<br />
|
||||
<a href={contact.phoneHref} className="hover:text-primary transition-colors">
|
||||
{contact.phone}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4 mt-8">
|
||||
<a href="https://www.instagram.com/hanzoai" target="_blank" rel="noopener noreferrer" aria-label="Instagram" className="geometric-circle border border-white/20 aspect-square w-12 h-12 flex items-center justify-center hover:bg-accent/20 transition-colors">
|
||||
<Instagram size={20} />
|
||||
</a>
|
||||
<a href="https://www.facebook.com/hanzo-inc" target="_blank" rel="noopener noreferrer" aria-label="Facebook" className="geometric-circle border border-white/20 aspect-square w-12 h-12 flex items-center justify-center hover:bg-accent/20 transition-colors">
|
||||
<Facebook size={20} />
|
||||
</a>
|
||||
<a href="https://x.com/hanzoai" target="_blank" rel="noopener noreferrer" aria-label="X" className="geometric-circle border border-white/20 aspect-square w-12 h-12 flex items-center justify-center hover:bg-accent/20 transition-colors">
|
||||
<Twitter size={20} />
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/hanzoai" target="_blank" rel="noopener noreferrer" aria-label="LinkedIn" className="geometric-circle border border-white/20 aspect-square w-12 h-12 flex items-center justify-center hover:bg-accent/20 transition-colors">
|
||||
<Linkedin size={20} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Office Hours</h2>
|
||||
<p className="text-primary/80 mb-4">
|
||||
Monday - Friday: 9:00 AM - 6:00 PM (PST)<br/>
|
||||
Saturday - Sunday: Closed
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold mt-8 mb-4">Follow Us</h2>
|
||||
<div className="flex space-x-4">
|
||||
<a href="https://www.instagram.com/hanzoai" target="_blank" rel="noopener noreferrer" className="geometric-circle border border-black/20 aspect-square w-12 h-12 flex items-center justify-center hover:bg-accent/20 transition-colors">
|
||||
<Instagram size={20} />
|
||||
</a>
|
||||
<a href="https://www.facebook.com/hanzo-inc" target="_blank" rel="noopener noreferrer" className="geometric-circle border border-black/20 aspect-square w-12 h-12 flex items-center justify-center hover:bg-accent/20 transition-colors">
|
||||
<Facebook size={20} />
|
||||
</a>
|
||||
<a href="https://x.com/hanzoai" target="_blank" rel="noopener noreferrer" className="geometric-circle border border-black/20 aspect-square w-12 h-12 flex items-center justify-center hover:bg-accent/20 transition-colors">
|
||||
<Twitter size={20} />
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/hanzoai" target="_blank" rel="noopener noreferrer" className="geometric-circle border border-black/20 aspect-square w-12 h-12 flex items-center justify-center hover:bg-accent/20 transition-colors">
|
||||
<Linkedin size={20} />
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] p-6 md:p-8">
|
||||
{sent === 'filed' ? (
|
||||
<div role="status" className="space-y-4">
|
||||
<h2 className="text-2xl font-bold">Thanks. We have your note.</h2>
|
||||
<p className="text-primary/80">We reply by email. Want to talk sooner? Pick a time.</p>
|
||||
<a href={contact.booking} className="lets-talk-btn">
|
||||
Book a call
|
||||
<ArrowUpRight size={16} className="ml-1" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : sent === 'missed' ? (
|
||||
<div role="alert" className="space-y-4">
|
||||
<h2 className="text-2xl font-bold">Send it by email, or pick a time</h2>
|
||||
<p className="text-primary/80">Your note goes with the email.</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<a href={mailto('Hanzo Agency', note)} className="lets-talk-btn">
|
||||
Email us
|
||||
</a>
|
||||
<a href={contact.booking} className="lets-talk-btn">
|
||||
Book a call
|
||||
<ArrowUpRight size={16} className="ml-1" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={send} noValidate className="space-y-4">
|
||||
<h2 className="text-2xl font-bold mb-2">What do you need?</h2>
|
||||
<label className="block">
|
||||
<span className="text-sm text-primary/70">Work email *</span>
|
||||
<input className={field} type="email" name="email" autoComplete="email" required value={email} onChange={(e) => setEmail(e.target.value)} placeholder="[email protected]" />
|
||||
</label>
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<label className="block">
|
||||
<span className="text-sm text-primary/70">Name</span>
|
||||
<input className={field} name="name" autoComplete="name" value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-sm text-primary/70">Company</span>
|
||||
<input className={field} name="company" autoComplete="organization" value={company} onChange={(e) => setCompany(e.target.value)} />
|
||||
</label>
|
||||
</div>
|
||||
<label className="block">
|
||||
<span className="text-sm text-primary/70">The work</span>
|
||||
<textarea className={field} name="need" rows={4} value={need} onChange={(e) => setNeed(e.target.value)} placeholder="A product launch, an AI agent, a brand, a team for the quarter…" />
|
||||
</label>
|
||||
{wrong ? (
|
||||
<p role="alert" className="text-sm text-red-400">
|
||||
{wrong}
|
||||
</p>
|
||||
) : null}
|
||||
<button type="submit" className="lets-talk-btn w-full justify-center" disabled={sent === 'busy'}>
|
||||
{sent === 'busy' ? 'Sending…' : 'Send'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,628 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Footer from '@/components/Footer';
|
||||
import { HanzoLogo } from '@hanzo/logo';
|
||||
import {
|
||||
CreditCard,
|
||||
History,
|
||||
LogOut,
|
||||
Plus,
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Palette,
|
||||
FileText,
|
||||
Globe,
|
||||
Briefcase,
|
||||
Image,
|
||||
Shield,
|
||||
ExternalLink,
|
||||
Calendar,
|
||||
Bot,
|
||||
Terminal,
|
||||
MessageSquare,
|
||||
Sparkles,
|
||||
PhoneCall,
|
||||
Send,
|
||||
Code2,
|
||||
Layers,
|
||||
Zap,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import { plans, planHref, priceLabel } from '@/data/plans';
|
||||
|
||||
const HANZO_ID_LOGIN = 'https://hanzo.id/login?redirect=https%3A%2F%2Fhanzo.agency%2Fdashboard';
|
||||
|
||||
const Dashboard = () => {
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'builder' | 'services' | 'chat'>('overview');
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [userCredits, setUserCredits] = useState(500);
|
||||
const [userName, setUserName] = useState('Partner');
|
||||
const [userEmail, setUserEmail] = useState('');
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
|
||||
// AI Site Builder state
|
||||
const [builderPrompt, setBuilderPrompt] = useState('');
|
||||
const [builderStatus, setBuilderStatus] = useState<'idle' | 'building' | 'complete'>('idle');
|
||||
const [builderLogs, setBuilderLogs] = useState<string[]>([]);
|
||||
const [selectedProject, setSelectedProject] = useState('production-site');
|
||||
|
||||
// Chat with Team state
|
||||
const [chatMessages, setChatMessages] = useState<Array<{ sender: 'user' | 'team'; text: string; time: string }>>([
|
||||
{
|
||||
sender: 'team',
|
||||
text: 'Welcome to your Hanzo Agency dashboard. Your creative director and AI engineering team are online. How can we support your roadmap today?',
|
||||
time: 'Just now'
|
||||
}
|
||||
]);
|
||||
const [chatInput, setChatInput] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// Read potential SSO callback params from URL
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const ssoToken = params.get('token') || params.get('access_token');
|
||||
const ssoEmail = params.get('email');
|
||||
const ssoName = params.get('name');
|
||||
|
||||
if (ssoToken) {
|
||||
localStorage.setItem('authToken', ssoToken);
|
||||
}
|
||||
if (ssoEmail) {
|
||||
localStorage.setItem('userEmail', ssoEmail);
|
||||
}
|
||||
if (ssoName) {
|
||||
localStorage.setItem('userName', ssoName);
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('authToken');
|
||||
const email = localStorage.getItem('userEmail') || ssoEmail || '';
|
||||
const storedName = localStorage.getItem('userName') || ssoName || (email ? email.split('@')[0] : 'Client');
|
||||
const credits = parseInt(localStorage.getItem('userCredits') || '500');
|
||||
const adminStatus = localStorage.getItem('isAdmin') === 'true' || email.endsWith('@hanzo.ai');
|
||||
|
||||
setIsLoggedIn(!!token);
|
||||
setUserEmail(email);
|
||||
setUserName(storedName);
|
||||
setUserCredits(credits);
|
||||
setIsAdmin(adminStatus);
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('userEmail');
|
||||
localStorage.removeItem('userName');
|
||||
setIsLoggedIn(false);
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const handleRunBuilder = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!builderPrompt.trim()) return;
|
||||
|
||||
setBuilderStatus('building');
|
||||
setBuilderLogs([
|
||||
'⚡ [Hanzo AI Cloud] Connecting to dev engine & AST analyzer...',
|
||||
`⚡ [Prompt] "${builderPrompt}"`,
|
||||
'⚡ [Codebase] Inspecting design tokens, components, and routes...',
|
||||
'⚡ [Engine] Generating production-ready TypeScript + Tailwind changes...',
|
||||
'⚡ [Verification] Running automated linting & type checks...',
|
||||
'✓ [Success] 0 errors. Live site update staged for preview.',
|
||||
'🚀 Staged at https://preview.hanzo.ai/site-preview-live'
|
||||
]);
|
||||
|
||||
setTimeout(() => {
|
||||
setBuilderStatus('complete');
|
||||
setUserCredits(prev => Math.max(0, prev - 15));
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const handleSendMessage = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!chatInput.trim()) return;
|
||||
|
||||
const newMsg = {
|
||||
sender: 'user' as const,
|
||||
text: chatInput,
|
||||
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
};
|
||||
|
||||
setChatMessages(prev => [...prev, newMsg]);
|
||||
setChatInput('');
|
||||
|
||||
setTimeout(() => {
|
||||
setChatMessages(prev => [
|
||||
...prev,
|
||||
{
|
||||
sender: 'team',
|
||||
text: 'Got it! Your Project Manager has received this request. We will review assets and coordinate with our AI specialists immediately.',
|
||||
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
]);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const services = [
|
||||
{
|
||||
id: 'instant-site',
|
||||
name: '3-Page Instant Site',
|
||||
credits: 500,
|
||||
price: '$500',
|
||||
icon: Globe,
|
||||
description: 'Polished 3-page site delivered in 24 hours with domain and SEO setup.'
|
||||
},
|
||||
{
|
||||
id: 'fix-session',
|
||||
name: 'Expert Fix Session (15m)',
|
||||
credits: 35,
|
||||
price: '$35',
|
||||
icon: Zap,
|
||||
description: 'One focused problem worked through live with a senior Hanzo engineer.'
|
||||
},
|
||||
{
|
||||
id: 'build-session',
|
||||
name: 'Expert Build Session (30m)',
|
||||
credits: 65,
|
||||
price: '$65',
|
||||
icon: Code2,
|
||||
description: 'Improve and ship a real feature on your app with senior assistance.'
|
||||
},
|
||||
{
|
||||
id: 'launch-session',
|
||||
name: 'Expert Launch Session (45m)',
|
||||
credits: 95,
|
||||
price: '$95',
|
||||
icon: CheckCircle2,
|
||||
description: 'Polish, integrate, connect, and ship your project out the door.'
|
||||
},
|
||||
{
|
||||
id: 'branding-kit',
|
||||
name: 'Brand Identity & Design System',
|
||||
credits: 500,
|
||||
price: '$500',
|
||||
icon: Palette,
|
||||
description: 'Complete visual identity, logo vectors, font rules, and token guidelines.'
|
||||
},
|
||||
{
|
||||
id: 'custom-rag',
|
||||
name: 'Custom RAG / Agent Pipeline',
|
||||
credits: 1500,
|
||||
price: '$1,500',
|
||||
icon: Bot,
|
||||
description: 'Fine-tuned agents and vector knowledge bases integrated into your backend.'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="pt-28 pb-20 bg-black text-white min-h-screen">
|
||||
<div className="container-custom max-w-7xl">
|
||||
{/* Top Bar / Client Header */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center pb-8 border-b border-zinc-800/80 mb-8 gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-zinc-900 border border-white/15 flex items-center justify-center p-2.5 shadow-xl">
|
||||
<HanzoLogo size={28} className="[&>svg]:w-full [&>svg]:h-full text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">
|
||||
Welcome back, {userName}
|
||||
</h1>
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
|
||||
Active
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
Hanzo Agency Client Hub · Powered by Hanzo AI Cloud
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center flex-wrap gap-3">
|
||||
<a
|
||||
href="https://hanzo.team"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-zinc-900 hover:bg-zinc-800 border border-white/15 text-white px-4 py-2 rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-colors"
|
||||
>
|
||||
<Bot className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>Launch hanzo.team</span>
|
||||
<ExternalLink className="w-3 h-3 opacity-60" />
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://billing.hanzo.ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-zinc-900 hover:bg-zinc-800 border border-white/15 text-white px-4 py-2 rounded-lg text-xs font-medium flex items-center gap-1.5 transition-colors"
|
||||
>
|
||||
<CreditCard className="w-3.5 h-3.5 text-blue-400" />
|
||||
<span>Billing & Invoices</span>
|
||||
</a>
|
||||
|
||||
{isLoggedIn ? (
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-gray-400 hover:text-white text-xs flex items-center gap-1.5 px-3 py-2 transition-colors"
|
||||
>
|
||||
<LogOut className="w-3.5 h-3.5" />
|
||||
<span>Sign out</span>
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href={HANZO_ID_LOGIN}
|
||||
className="bg-white text-black hover:bg-white/90 px-4 py-2 rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-colors"
|
||||
>
|
||||
Sign in with Hanzo ID
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Unified Retainer & Credit Balance Card */}
|
||||
<div className="grid lg:grid-cols-3 gap-6 mb-8">
|
||||
<div className="lg:col-span-2 bg-gradient-to-br from-zinc-900/90 to-zinc-950 border border-white/10 rounded-2xl p-6 sm:p-8 flex flex-col justify-between shadow-2xl relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-80 h-80 bg-blue-500/5 rounded-full blur-3xl pointer-events-none" />
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-mono uppercase tracking-wider text-gray-400">
|
||||
Unified Retainer & Compute Balance
|
||||
</span>
|
||||
<span className="text-xs text-emerald-400 font-mono">1 credit = $1.00</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-3 my-2">
|
||||
<span className="text-4xl sm:text-5xl font-bold tracking-tight text-white">
|
||||
{userCredits.toLocaleString()}
|
||||
</span>
|
||||
<span className="text-gray-400 font-medium">credits available</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-300 max-w-xl leading-relaxed mt-2">
|
||||
Spend your retainer either way: hire senior human creatives and engineers, or run autonomous AI agents on Hanzo AI Cloud. Zero lock-in.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3 mt-6 pt-6 border-t border-white/10">
|
||||
<Link to="/purchase-credits">
|
||||
<Button className="bg-white hover:bg-gray-100 text-black text-xs font-semibold px-5 py-2.5 rounded-lg flex items-center gap-1.5 shadow-md">
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Credits
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/pricing">
|
||||
<Button className="bg-zinc-800 hover:bg-zinc-700 text-white text-xs font-medium px-4 py-2.5 rounded-lg flex items-center gap-1.5">
|
||||
<Layers className="w-4 h-4 text-purple-400" />
|
||||
Subscribe to a plan
|
||||
</Button>
|
||||
</Link>
|
||||
<a
|
||||
href="https://calendar.app.google/z1YsZQrqR4s6jQqD8"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button className="bg-zinc-800 hover:bg-zinc-700 text-white text-xs font-medium px-4 py-2.5 rounded-lg flex items-center gap-1.5">
|
||||
<Calendar className="w-4 h-4 text-emerald-400" />
|
||||
Book Sync Call
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Status / hanzo.team Launcher */}
|
||||
<div className="bg-zinc-900/60 border border-white/10 rounded-2xl p-6 flex flex-col justify-between shadow-xl">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
|
||||
<h3 className="font-semibold text-sm text-white">Hanzo Cloud & Agents</h3>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 leading-relaxed">
|
||||
Your autonomous bots execute in the cloud. Access transcripts, edit prompts, and review queued actions in the unified workspace.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://hanzo.team"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-6 w-full bg-white/10 hover:bg-white/20 border border-white/15 text-white py-2.5 px-4 rounded-lg text-xs font-semibold flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<span>Open hanzo.team Workspace</span>
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="flex border-b border-zinc-800 mb-8 overflow-x-auto no-scrollbar gap-2">
|
||||
<button
|
||||
onClick={() => setActiveTab('overview')}
|
||||
className={`pb-3 px-4 text-sm font-semibold whitespace-nowrap transition-all border-b-2 cursor-pointer ${
|
||||
activeTab === 'overview'
|
||||
? 'border-white text-white'
|
||||
: 'border-transparent text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Overview & Services
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('builder')}
|
||||
className={`pb-3 px-4 text-sm font-semibold whitespace-nowrap transition-all border-b-2 cursor-pointer flex items-center gap-1.5 ${
|
||||
activeTab === 'builder'
|
||||
? 'border-white text-white'
|
||||
: 'border-transparent text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Sparkles className="w-3.5 h-3.5 text-amber-400" />
|
||||
AI Site & App Builder
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('services')}
|
||||
className={`pb-3 px-4 text-sm font-semibold whitespace-nowrap transition-all border-b-2 cursor-pointer ${
|
||||
activeTab === 'services'
|
||||
? 'border-white text-white'
|
||||
: 'border-transparent text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Order Talent & Sprints
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('chat')}
|
||||
className={`pb-3 px-4 text-sm font-semibold whitespace-nowrap transition-all border-b-2 cursor-pointer flex items-center gap-1.5 ${
|
||||
activeTab === 'chat'
|
||||
? 'border-white text-white'
|
||||
: 'border-transparent text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<MessageSquare className="w-3.5 h-3.5 text-blue-400" />
|
||||
Chat with Hanzo Team
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* TAB CONTENT */}
|
||||
|
||||
{/* 1. OVERVIEW */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="space-y-10">
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-bold tracking-tight">Popular Services & Sprints</h2>
|
||||
<Link to="/pricing" className="text-xs text-gray-400 hover:text-white flex items-center gap-1">
|
||||
<span>View all agency plans</span>
|
||||
<ArrowRight className="w-3 h-3" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{services.map((service) => {
|
||||
const Icon = service.icon;
|
||||
const canAfford = userCredits >= service.credits;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={service.id}
|
||||
className="bg-zinc-900/40 border border-white/10 rounded-xl p-6 flex flex-col justify-between hover:border-white/30 transition-all shadow-lg"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-10 h-10 bg-zinc-800 rounded-lg flex items-center justify-center text-white">
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<span className="text-sm font-semibold font-mono text-white">
|
||||
{service.credits} credits ({service.price})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-base font-bold text-white mb-1.5">{service.name}</h3>
|
||||
<p className="text-xs text-gray-400 leading-relaxed mb-6">
|
||||
{service.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to={`/redeem/${service.id}`}
|
||||
className="w-full"
|
||||
>
|
||||
<Button
|
||||
className="w-full bg-white hover:bg-gray-100 text-black text-xs font-semibold py-2.5 rounded-lg flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<span>Order with Credits</span>
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3. AI SITE & APP BUILDER (DEV STUDIO) */}
|
||||
{activeTab === 'builder' && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-gradient-to-r from-zinc-900 to-zinc-950 border border-white/15 rounded-2xl p-6 sm:p-8 shadow-2xl">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Terminal className="w-5 h-5 text-amber-400" />
|
||||
<h2 className="text-xl font-bold">AI Site & App Builder</h2>
|
||||
<span className="text-[10px] font-mono uppercase bg-white/10 text-white/80 px-2 py-0.5 rounded border border-white/10">
|
||||
Hanzo Dev v1
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">
|
||||
Chat with AI to update your site, ship new pages, or stage full refactors. Powered by Hanzo AI Cloud.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-400">Target:</span>
|
||||
<select
|
||||
value={selectedProject}
|
||||
onChange={(e) => setSelectedProject(e.target.value)}
|
||||
className="bg-zinc-800 border border-zinc-700 text-white text-xs rounded-lg px-3 py-1.5 focus:outline-none"
|
||||
>
|
||||
<option value="production-site">Production Website (hanzo.agency)</option>
|
||||
<option value="instant-site">Client Instant Site (Preview)</option>
|
||||
<option value="new-app">New Full-Stack App</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleRunBuilder} className="space-y-4">
|
||||
<div>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={builderPrompt}
|
||||
onChange={(e) => setBuilderPrompt(e.target.value)}
|
||||
placeholder="Describe what you want to build or update (e.g. 'Add a customer testimonials carousel with 5 star rating', 'Connect Square checkout webhook', 'Redesign hero with animated 3D canvas')..."
|
||||
className="w-full bg-zinc-950 border border-white/15 rounded-xl p-4 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-white/40 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 pt-2">
|
||||
<div className="flex items-center gap-3 text-xs text-gray-400">
|
||||
<span>Estimated compute: <strong className="text-white">15 AI credits</strong></span>
|
||||
<span>•</span>
|
||||
<span>Or request human agency QA</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={builderStatus === 'building' || !builderPrompt.trim()}
|
||||
className="bg-white hover:bg-gray-100 text-black text-xs font-semibold px-6 py-2.5 rounded-lg flex items-center gap-2 shadow-md cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
<Sparkles className="w-4 h-4 text-amber-500" />
|
||||
<span>{builderStatus === 'building' ? 'Compiling Changes...' : 'Execute Live Update'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Live Build Console Output */}
|
||||
{builderLogs.length > 0 && (
|
||||
<div className="mt-6 bg-black border border-zinc-800 rounded-xl p-4 font-mono text-xs text-gray-300 space-y-1 overflow-x-auto">
|
||||
<div className="flex items-center justify-between pb-2 border-b border-zinc-800 mb-2 text-[11px] text-gray-500">
|
||||
<span>CONSOLE OUTPUT</span>
|
||||
<span>HANZO CLUSTER: spark.local</span>
|
||||
</div>
|
||||
{builderLogs.map((log, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={log.includes('Success') ? 'text-emerald-400 font-bold' : log.includes('Staged') ? 'text-cyan-300 font-semibold underline' : ''}
|
||||
>
|
||||
{log}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 4. SERVICES & RETENTION SPRINT */}
|
||||
{activeTab === 'services' && (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold tracking-tight mb-2">Plans</h2>
|
||||
<p className="text-xs text-gray-400 mb-6">
|
||||
Agency, Forward Deployed, or an Embedded Pod. Dedicated people are a conversation.
|
||||
</p>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{plans.map((plan) => (
|
||||
<div key={plan.id} className="bg-zinc-900/40 border border-white/15 rounded-2xl p-6 flex flex-col justify-between">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-white mb-1">{plan.name}</h3>
|
||||
<div className="text-3xl font-bold text-white mb-3">
|
||||
{priceLabel(plan)}
|
||||
{plan.priceMonthly !== null && <span className="text-sm font-normal text-gray-400">/month</span>}
|
||||
</div>
|
||||
<p className="text-xs text-gray-300 leading-relaxed mb-4">{plan.description}</p>
|
||||
</div>
|
||||
<Link to={planHref(plan)}>
|
||||
<Button className="w-full bg-white hover:bg-gray-100 text-black text-xs font-semibold py-3 rounded-lg">
|
||||
{plan.cta}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 5. CHAT WITH TEAM */}
|
||||
{activeTab === 'chat' && (
|
||||
<div className="max-w-4xl mx-auto bg-zinc-900/50 border border-white/10 rounded-2xl overflow-hidden shadow-2xl flex flex-col h-[580px]">
|
||||
<div className="p-4 border-b border-zinc-800 bg-zinc-900/80 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-black border border-white/20 flex items-center justify-center p-1.5">
|
||||
<HanzoLogo size={20} className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white">Hanzo Agency Project Room</h3>
|
||||
<p className="text-[11px] text-emerald-400 flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
|
||||
Creative Director & AI Leads Online
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="https://calendar.app.google/z1YsZQrqR4s6jQqD8"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-gray-400 hover:text-white flex items-center gap-1 bg-zinc-800 px-3 py-1.5 rounded-md"
|
||||
>
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
<span>Book Zoom</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow p-5 space-y-4 overflow-y-auto">
|
||||
{chatMessages.map((msg, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex flex-col ${msg.sender === 'user' ? 'items-end' : 'items-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-md p-3.5 rounded-2xl text-xs leading-relaxed ${
|
||||
msg.sender === 'user'
|
||||
? 'bg-white text-black font-medium rounded-br-none'
|
||||
: 'bg-zinc-800/90 border border-white/10 text-gray-200 rounded-bl-none'
|
||||
}`}
|
||||
>
|
||||
{msg.text}
|
||||
</div>
|
||||
<span className="text-[10px] text-gray-500 mt-1 px-1">{msg.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSendMessage} className="p-4 border-t border-zinc-800 bg-zinc-950 flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={chatInput}
|
||||
onChange={(e) => setChatInput(e.target.value)}
|
||||
placeholder="Ask a question, request a new feature, or outline your next campaign..."
|
||||
className="flex-grow bg-zinc-900 border border-zinc-800 rounded-xl px-4 py-2.5 text-xs text-white placeholder-gray-500 focus:outline-none focus:border-white/30"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!chatInput.trim()}
|
||||
className="bg-white hover:bg-gray-100 text-black px-4 py-2.5 rounded-xl text-xs font-semibold cursor-pointer disabled:opacity-40"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -1,114 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Mail, ArrowLeft } from 'lucide-react';
|
||||
|
||||
const EmailInvitation = () => {
|
||||
const [emailsSent, setEmailsSent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate sending invitation emails
|
||||
if (!emailsSent) {
|
||||
console.log('📧 Sending invitation emails to admins...');
|
||||
console.log('Email sent to: [email protected]');
|
||||
console.log('Email sent to: [email protected]');
|
||||
setEmailsSent(true);
|
||||
}
|
||||
}, [emailsSent]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white pt-32 pb-20">
|
||||
<div className="container-custom max-w-3xl">
|
||||
<Link to="/" className="inline-flex items-center gap-2 text-gray-400 hover:text-white mb-8">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Home
|
||||
</Link>
|
||||
|
||||
<div className="text-center mb-12">
|
||||
<div className="w-20 h-20 bg-white/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<Mail className="h-10 w-10 text-white" />
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4">Admin Invitations Sent</h1>
|
||||
<p className="text-xl text-gray-400">
|
||||
Email invitations have been sent to the admin accounts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-8 mb-8">
|
||||
<h2 className="text-2xl font-bold mb-6">Email Preview</h2>
|
||||
|
||||
<div className="bg-black/50 border border-gray-700 rounded-lg p-6 mb-6">
|
||||
<p className="text-sm text-gray-400 mb-2">To: a@hanzo.ai, z@hanzo.ai</p>
|
||||
<p className="text-sm text-gray-400 mb-4">Subject: Admin Access - Hanzo Agency Credit System</p>
|
||||
|
||||
<div className="border-t border-gray-700 pt-4">
|
||||
<p className="mb-4">Hello Admin,</p>
|
||||
|
||||
<p className="mb-4">
|
||||
You have been granted admin access to the Hanzo Agency Credit System.
|
||||
As an admin, you can:
|
||||
</p>
|
||||
|
||||
<ul className="list-disc list-inside mb-4 text-gray-300">
|
||||
<li>View all orders and transactions</li>
|
||||
<li>Monitor user activity and credit purchases</li>
|
||||
<li>Track service redemptions and status</li>
|
||||
<li>Export order data for reporting</li>
|
||||
</ul>
|
||||
|
||||
<p className="mb-4">
|
||||
To access the admin dashboard, please sign up or log in using this email address:
|
||||
</p>
|
||||
|
||||
<div className="bg-gray-800 rounded p-4 mb-4">
|
||||
<p className="font-mono text-sm">Email: [your-admin-email]@hanzo.ai</p>
|
||||
<p className="font-mono text-sm">Password: [Create your secure password]</p>
|
||||
</div>
|
||||
|
||||
<p className="mb-6">
|
||||
Once logged in, you'll see an "Admin Panel" button in your dashboard.
|
||||
</p>
|
||||
|
||||
<p>Best regards,<br/>Hanzo Agency Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-8">
|
||||
<h3 className="text-xl font-bold mb-4">Admin Accounts</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center p-4 bg-black/30 rounded-lg">
|
||||
<div>
|
||||
<p className="font-semibold">a@hanzo.ai</p>
|
||||
<p className="text-sm text-gray-400">Full admin access</p>
|
||||
</div>
|
||||
<Link to="/signup">
|
||||
<Button className="bg-white hover:bg-gray-100 text-black px-4 py-2 rounded-lg">
|
||||
Sign Up
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center p-4 bg-black/30 rounded-lg">
|
||||
<div>
|
||||
<p className="font-semibold">z@hanzo.ai</p>
|
||||
<p className="text-sm text-gray-400">Full admin access</p>
|
||||
</div>
|
||||
<Link to="/signup">
|
||||
<Button className="bg-white hover:bg-gray-100 text-black px-4 py-2 rounded-lg">
|
||||
Sign Up
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-400 mt-6">
|
||||
Note: Admin accounts can view all platform activity and receive email notifications for important events.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailInvitation;
|
||||
@@ -323,6 +323,20 @@ const Enterprise = () => {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="partners" className="py-20 border-t border-white/10">
|
||||
<div className="container-custom max-w-3xl text-center">
|
||||
<p className="text-sm uppercase tracking-widest text-foreground/60 mb-3">Partners</p>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4">Run your own AI agency</h2>
|
||||
<p className="text-lg text-foreground/80 mb-8">
|
||||
Sell this work to your clients under your brand, on your domain. We set it up for you.
|
||||
</p>
|
||||
<Link to="/white-label" className="lets-talk-btn">
|
||||
See white-label
|
||||
<ArrowUpRight size={16} className="ml-1" />
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Footer from '@/components/Footer';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Download,
|
||||
Upload,
|
||||
Calendar,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
ExternalLink
|
||||
} from 'lucide-react';
|
||||
|
||||
interface HistoryItem {
|
||||
id: string;
|
||||
type: 'purchase' | 'redemption';
|
||||
credits: number;
|
||||
amount?: number;
|
||||
service?: string;
|
||||
description: string;
|
||||
date: string;
|
||||
status?: 'completed' | 'pending' | 'active';
|
||||
}
|
||||
|
||||
const History = () => {
|
||||
const navigate = useNavigate();
|
||||
const [history, setHistory] = useState<HistoryItem[]>([]);
|
||||
const [filter, setFilter] = useState<'all' | 'purchases' | 'redemptions'>('all');
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user is logged in
|
||||
const authToken = localStorage.getItem('authToken');
|
||||
if (!authToken) {
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Load history
|
||||
const creditHistory = JSON.parse(localStorage.getItem('creditHistory') || '[]');
|
||||
setHistory(creditHistory.reverse()); // Show newest first
|
||||
}, [navigate]);
|
||||
|
||||
const filteredHistory = history.filter(item => {
|
||||
if (filter === 'all') return true;
|
||||
if (filter === 'purchases') return item.type === 'purchase';
|
||||
if (filter === 'redemptions') return item.type === 'redemption';
|
||||
return true;
|
||||
});
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusBadge = (status?: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return (
|
||||
<span className="flex items-center gap-1 text-green-400 text-sm">
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
Completed
|
||||
</span>
|
||||
);
|
||||
case 'active':
|
||||
return (
|
||||
<span className="flex items-center gap-1 text-blue-400 text-sm">
|
||||
<Clock className="h-4 w-4" />
|
||||
Active
|
||||
</span>
|
||||
);
|
||||
case 'pending':
|
||||
return (
|
||||
<span className="flex items-center gap-1 text-yellow-400 text-sm">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
Ready to Start
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartAssignment = () => {
|
||||
window.open('https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ23sAA3nns4Ntw1GiKXDOwDgH6uPDXV8aqFr8mKqF8m-r8uduBrTw5ZQI7afVAAJFPD_UBgYqu_', '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="pt-32 pb-20 bg-black text-white min-h-screen">
|
||||
<div className="container-custom max-w-6xl">
|
||||
{/* Header */}
|
||||
<div className="mb-12">
|
||||
<Link to="/dashboard" className="inline-flex items-center gap-2 text-gray-400 hover:text-white mb-6">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Dashboard
|
||||
</Link>
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4">Transaction History</h1>
|
||||
<p className="text-gray-400 text-lg">
|
||||
View your credit purchases and service redemptions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-wrap gap-4 mb-8">
|
||||
<button
|
||||
onClick={() => setFilter('all')}
|
||||
className={`px-4 py-2 rounded-lg transition-colors ${
|
||||
filter === 'all'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-gray-800 text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
All Transactions
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('purchases')}
|
||||
className={`px-4 py-2 rounded-lg transition-colors flex items-center gap-2 ${
|
||||
filter === 'purchases'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-gray-800 text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Purchases
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('redemptions')}
|
||||
className={`px-4 py-2 rounded-lg transition-colors flex items-center gap-2 ${
|
||||
filter === 'redemptions'
|
||||
? 'bg-white text-black'
|
||||
: 'bg-gray-800 text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Upload className="h-4 w-4" />
|
||||
Redemptions
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* History List */}
|
||||
{filteredHistory.length === 0 ? (
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-12 text-center">
|
||||
<p className="text-gray-400 mb-4">No transactions found</p>
|
||||
<Link to="/purchase-credits">
|
||||
<Button className="bg-white hover:bg-gray-100 text-black px-6 py-3 rounded-lg">
|
||||
Purchase Credits
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filteredHistory.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-gray-900/30 border border-gray-800 rounded-xl p-6 hover:border-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h3 className="text-lg font-semibold">{item.description}</h3>
|
||||
{getStatusBadge(item.status)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-400">
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{formatDate(item.date)}
|
||||
</span>
|
||||
{item.service && (
|
||||
<span className="bg-gray-800 px-3 py-1 rounded-full">
|
||||
{item.service}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="text-right">
|
||||
<p className={`text-2xl font-bold ${
|
||||
item.type === 'purchase' ? 'text-green-400' : 'text-white'
|
||||
}`}>
|
||||
{item.type === 'purchase' ? '+' : ''}{item.credits.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">credits</p>
|
||||
</div>
|
||||
|
||||
{item.type === 'redemption' && item.status === 'pending' && (
|
||||
<Button
|
||||
onClick={handleStartAssignment}
|
||||
className="bg-white hover:bg-gray-100 text-black px-4 py-2 rounded-lg flex items-center gap-2"
|
||||
>
|
||||
Start
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{item.amount && (
|
||||
<div className="text-right min-w-[100px]">
|
||||
<p className="text-lg font-semibold">${item.amount.toLocaleString()}</p>
|
||||
<p className="text-sm text-gray-400">paid</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="mt-12 grid md:grid-cols-3 gap-6">
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-6">
|
||||
<p className="text-gray-400 mb-2">Total Purchased</p>
|
||||
<p className="text-3xl font-bold">
|
||||
{history
|
||||
.filter(item => item.type === 'purchase')
|
||||
.reduce((sum, item) => sum + item.credits, 0)
|
||||
.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">credits</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-6">
|
||||
<p className="text-gray-400 mb-2">Total Redeemed</p>
|
||||
<p className="text-3xl font-bold">
|
||||
{Math.abs(history
|
||||
.filter(item => item.type === 'redemption')
|
||||
.reduce((sum, item) => sum + item.credits, 0))
|
||||
.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">credits</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-6">
|
||||
<p className="text-gray-400 mb-2">Total Spent</p>
|
||||
<p className="text-3xl font-bold">
|
||||
${history
|
||||
.filter(item => item.type === 'purchase' && item.amount)
|
||||
.reduce((sum, item) => sum + (item.amount || 0), 0)
|
||||
.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">USD</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default History;
|
||||
@@ -1,69 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Footer from '@/components/Footer';
|
||||
import { ArrowRight, ShieldCheck, ExternalLink } from 'lucide-react';
|
||||
import { HanzoLogo } from '@hanzo/logo';
|
||||
|
||||
const HANZO_ID_LOGIN = 'https://hanzo.id/login?redirect=https%3A%2F%2Fhanzo.agency%2Fdashboard';
|
||||
const HANZO_ID_SIGNUP = 'https://hanzo.id/signup?redirect=https%3A%2F%2Fhanzo.agency%2Fdashboard';
|
||||
|
||||
const Login = () => {
|
||||
return (
|
||||
<>
|
||||
<main className="pt-32 pb-24 bg-black text-white min-h-screen flex items-center justify-center">
|
||||
<div className="container-custom max-w-md w-full px-4">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-14 h-14 rounded-2xl bg-zinc-900 border border-white/20 flex items-center justify-center mx-auto mb-5 p-2.5 shadow-2xl">
|
||||
<HanzoLogo size={32} className="[&>svg]:w-full [&>svg]:h-full text-white" />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl font-bold mb-2">Sign in to Hanzo</h1>
|
||||
<p className="text-sm text-gray-400">
|
||||
One account for Hanzo Agency and hanzo.team
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-900/60 border border-white/10 rounded-2xl p-8 backdrop-blur-xl shadow-2xl space-y-6">
|
||||
<div className="space-y-3">
|
||||
<a
|
||||
href={HANZO_ID_LOGIN}
|
||||
className="w-full bg-white hover:bg-white/90 text-black py-3.5 px-6 text-sm font-semibold rounded-full transition-all flex items-center justify-center gap-2.5 shadow-lg group cursor-pointer"
|
||||
>
|
||||
<span>Continue with Hanzo ID</span>
|
||||
<ArrowRight className="w-4 h-4 transition-transform group-hover:translate-x-1" />
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://hanzo.team"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full bg-white/5 hover:bg-white/10 border border-white/10 text-white py-3 px-6 text-xs font-medium rounded-full transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<span>Launch hanzo.team workspace</span>
|
||||
<ExternalLink className="w-3.5 h-3.5 opacity-60" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-white/10 text-center space-y-3">
|
||||
<p className="text-xs text-gray-400">
|
||||
Don't have a Hanzo ID yet?{' '}
|
||||
<a
|
||||
href={HANZO_ID_SIGNUP}
|
||||
className="text-white hover:underline font-medium"
|
||||
>
|
||||
Create Hanzo ID
|
||||
</a>
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-2 text-[11px] text-gray-500">
|
||||
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>Encrypted OAuth2 / SSO via Hanzo IAM</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
+44
-14
@@ -10,6 +10,17 @@ import ProgressBar from '@/components/onboarding/ProgressBar';
|
||||
import FormNavigation from '@/components/onboarding/FormNavigation';
|
||||
import { formSections } from '@/data/onboardingData';
|
||||
import { FormDataType } from '@/types/onboarding';
|
||||
import { contact } from '@/data/contact';
|
||||
import { EMAIL, fileLead, mailto } from '@/lib/leads';
|
||||
|
||||
/** The brief as one text: each answered question, then its answer. */
|
||||
function brief(data: FormDataType): string {
|
||||
return formSections
|
||||
.flatMap((s) => s.fields)
|
||||
.filter((f) => f.id !== 'email' && f.id !== 'name' && typeof data[f.id] === 'string' && (data[f.id] as string).trim())
|
||||
.map((f) => `${f.label}\n${(data[f.id] as string).trim()}`)
|
||||
.join('\n\n');
|
||||
}
|
||||
|
||||
const OnboardingForm = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -17,6 +28,7 @@ const OnboardingForm = () => {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [formData, setFormData] = useState<FormDataType>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [missed, setMissed] = useState(false);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { id, value } = e.target;
|
||||
@@ -36,7 +48,8 @@ const OnboardingForm = () => {
|
||||
const requiredFields = currentFields.filter(field => field.required);
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (!formData[field.id]) {
|
||||
const bad = field.id === 'email' ? !EMAIL.test(String(formData.email ?? '').trim()) : !formData[field.id];
|
||||
if (bad) {
|
||||
toast({
|
||||
title: "Missing information",
|
||||
description: `Please fill in the field: ${field.label}`,
|
||||
@@ -63,17 +76,21 @@ const OnboardingForm = () => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Here you would typically send the data to your backend
|
||||
// For now, we'll just simulate a successful submission
|
||||
setTimeout(() => {
|
||||
setIsSubmitting(false);
|
||||
toast({
|
||||
title: "Onboarding complete!",
|
||||
description: "Thank you for providing your information. We'll be in touch soon!",
|
||||
});
|
||||
sessionStorage.setItem('onboardingComplete', 'true');
|
||||
navigate('/onboarding-success');
|
||||
}, 2000);
|
||||
const lead = await fileLead({
|
||||
email: String(formData.email ?? '').trim(),
|
||||
name: String(formData.name ?? ''),
|
||||
company: String(formData.companyName ?? ''),
|
||||
need: brief(formData),
|
||||
source: 'hanzo.agency/onboarding',
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
if (!lead) {
|
||||
setMissed(true);
|
||||
return;
|
||||
}
|
||||
toast({ title: 'Brief received', description: 'We start from it at your kickoff.' });
|
||||
sessionStorage.setItem('onboardingComplete', 'true');
|
||||
navigate('/onboarding-success');
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -84,9 +101,9 @@ const OnboardingForm = () => {
|
||||
<div className="container-custom">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-3xl md:text-4xl font-bold mb-2 text-black">Help Us Create Your Perfect Website</h1>
|
||||
<h1 className="text-3xl md:text-4xl font-bold mb-2 text-black">Share your brief</h1>
|
||||
<p className="text-lg text-black/70">
|
||||
Please fill out this onboarding form as thoroughly as possible so we can deliver the best results for your project.
|
||||
The more you tell us, the faster your team starts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -97,6 +114,19 @@ const OnboardingForm = () => {
|
||||
title={formSections[currentStep].title}
|
||||
/>
|
||||
|
||||
{missed ? (
|
||||
<div role="alert" className="mb-6 rounded-lg border border-gray-200 bg-gray-50 p-4 text-black">
|
||||
<p className="font-medium mb-3">Send your brief by email, or bring it to your kickoff call.</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<a href={mailto(`Brief: ${String(formData.companyName ?? '')}`, `${String(formData.name ?? '')} <${String(formData.email ?? '')}>\n\n${brief(formData)}`)} className="rounded-full bg-black px-5 py-2 text-sm font-medium text-white">
|
||||
Email the brief
|
||||
</a>
|
||||
<a href={contact.booking} className="rounded-full border border-black px-5 py-2 text-sm font-medium">
|
||||
Book the kickoff
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormSection
|
||||
section={formSections[currentStep]}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CheckCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
import Footer from '@/components/Footer';
|
||||
import { app, contact } from '@/data/contact';
|
||||
|
||||
const OnboardingSuccess = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -30,29 +31,19 @@ const OnboardingSuccess = () => {
|
||||
<CheckCircle size={80} className="text-accent" />
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl md:text-4xl font-bold mb-4">Thank You!</h1>
|
||||
<h1 className="text-3xl md:text-4xl font-bold mb-4">Thank you</h1>
|
||||
|
||||
<p className="text-lg text-primary/70 mb-6">
|
||||
Your onboarding information has been successfully submitted. Our team will review your details and begin working on your project right away.
|
||||
</p>
|
||||
|
||||
<p className="text-primary/70 mb-8">
|
||||
We'll be in touch within 1-2 business days to discuss the next steps and confirm any additional details we might need.
|
||||
<p className="text-lg text-primary/70 mb-8">
|
||||
Your brief is with the team. Book your kickoff and we start from it.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigate('/dashboard')}
|
||||
>
|
||||
Go to Dashboard
|
||||
<Button asChild className="bg-accent hover:bg-accent/90 text-white">
|
||||
<a href={contact.booking}>Book your kickoff</a>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="bg-accent hover:bg-accent/90 text-white"
|
||||
onClick={() => window.location.href = 'mailto:support@hanzo.ai'}
|
||||
>
|
||||
Contact Support
|
||||
<Button asChild variant="outline">
|
||||
<a href={app.home}>Open Hanzo</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+26
-230
@@ -1,243 +1,39 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import Footer from '@/components/Footer';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Check, Lock, Mail, Loader2, CreditCard, Coins, Landmark } from 'lucide-react';
|
||||
import { useLocation, Link } from 'react-router-dom';
|
||||
import { checkoutUrl } from '@/lib/commerce';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { planById, priceLabel } from '@/data/plans';
|
||||
|
||||
type Method = 'card' | 'crypto' | 'wire';
|
||||
|
||||
// What the pay site accepts, shown so a reader knows before they leave. The pay
|
||||
// site collects the details and settles; this page only hands off.
|
||||
const PAYMENT_METHODS: { value: Method; label: string; icon: React.ElementType; desc: string }[] = [
|
||||
{ value: 'card', label: 'Card (Square)', icon: CreditCard, desc: 'Visa, Mastercard, Amex' },
|
||||
{ value: 'crypto', label: 'Crypto', icon: Coins, desc: 'ETH, BTC, USDC' },
|
||||
{ value: 'wire', label: 'Wire Transfer', icon: Landmark, desc: 'Bank wire (ACH / SWIFT)' },
|
||||
];
|
||||
import { planById } from '@/data/plans';
|
||||
|
||||
/**
|
||||
* /payment?plan=<id>: the checkout for a plan, at hanzo.ai/pay. The plan cards
|
||||
* link to the checkout directly; this address keeps older links working.
|
||||
*/
|
||||
const Payment = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [paymentMethod, setPaymentMethod] = useState<Method>('card');
|
||||
const location = useLocation();
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
// Only a priced plan has a checkout; Enterprise is priced per engagement.
|
||||
const found = planById(searchParams.get('plan'));
|
||||
const plan = found?.priceMonthly === null ? undefined : found;
|
||||
const { toast } = useToast();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
name: '',
|
||||
});
|
||||
const [search] = useSearchParams();
|
||||
const plan = planById(search.get('plan'));
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.add('dark');
|
||||
return () => {
|
||||
document.body.classList.remove('dark');
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handlePayment = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!plan) return;
|
||||
setIsLoading(true);
|
||||
// The pay site takes the plan, collects the payment details and settles,
|
||||
// then returns to /payment-success. This page never handles them.
|
||||
window.location.href = checkoutUrl(plan.id, '/payment-success');
|
||||
};
|
||||
|
||||
// No plan, no checkout. A ?plan= that names nothing we sell has no price to
|
||||
// put in front of someone, so ask for one rather than choose one for them.
|
||||
if (!plan) {
|
||||
return (
|
||||
<>
|
||||
<main className="pt-32 pb-20 bg-black text-white min-h-screen">
|
||||
<div className="container-custom max-w-xl text-center">
|
||||
<h1 className="text-3xl font-bold mb-4">Pick a plan first</h1>
|
||||
<p className="text-gray-400 mb-8">
|
||||
That link does not name a plan we sell. Our current plans and prices are on the
|
||||
pricing page.
|
||||
</p>
|
||||
<Link
|
||||
to="/pricing"
|
||||
className="inline-flex items-center justify-center rounded-full px-8 py-3 text-base font-medium bg-white text-black hover:bg-white/90 transition-colors"
|
||||
>
|
||||
See pricing
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (plan) window.location.replace(checkoutUrl(plan));
|
||||
}, [plan]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="pt-32 pb-20 bg-black text-white min-h-screen">
|
||||
<div className="container-custom max-w-7xl">
|
||||
<div className="grid lg:grid-cols-2 gap-12">
|
||||
{/* Left Column - Form */}
|
||||
<div className="order-2 lg:order-1">
|
||||
<form onSubmit={handlePayment} className="space-y-8">
|
||||
{/* Contact Section */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-6">Get Started</h2>
|
||||
<p className="text-sm text-gray-400 mb-6">
|
||||
Enter your details below and choose your preferred payment method.
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="name" className="text-sm text-gray-300 mb-1 block">Full Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="Your full name"
|
||||
required
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="bg-gray-900/50 border-gray-800 text-white px-4 py-3 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="email" className="text-sm text-gray-300 mb-1 block">Email</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="[email protected]"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
className="bg-gray-900/50 border-gray-800 text-white pl-4 pr-10 py-3 rounded-lg"
|
||||
/>
|
||||
<Mail className="absolute right-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 text-right">
|
||||
<Link to="/login" className="text-white hover:text-gray-300 text-sm">
|
||||
Already have an account? Log in
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Method Selector */}
|
||||
<div>
|
||||
<Label className="text-sm text-gray-300 mb-3 block">Payment Method</Label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{PAYMENT_METHODS.map((method) => {
|
||||
const Icon = method.icon;
|
||||
const selected = paymentMethod === method.value;
|
||||
return (
|
||||
<button
|
||||
key={method.value}
|
||||
type="button"
|
||||
onClick={() => setPaymentMethod(method.value)}
|
||||
className={`flex flex-col items-center gap-2 p-4 rounded-lg border transition-colors ${
|
||||
selected
|
||||
? 'border-white bg-white/10 text-white'
|
||||
: 'border-gray-800 bg-gray-900/30 text-gray-400 hover:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
<span className="text-sm font-medium">{method.label}</span>
|
||||
<span className="text-xs text-gray-500">{method.desc}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit: hand off to the pay site */}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading || !formData.email || !formData.name}
|
||||
className="w-full bg-white hover:bg-gray-100 text-black py-4 text-lg font-semibold rounded-lg transition-colors"
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
{'Redirecting to checkout…'}
|
||||
</span>
|
||||
) : (
|
||||
'Continue to payment'
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center justify-center gap-2 text-sm text-gray-400">
|
||||
<Lock className="h-4 w-4" />
|
||||
<span>Secure payment via Hanzo Commerce</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Order Summary */}
|
||||
<div className="order-1 lg:order-2">
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-6 lg:sticky lg:top-32">
|
||||
<h2 className="text-2xl font-bold mb-6">Order Summary</h2>
|
||||
|
||||
{/* Product Item */}
|
||||
<div className="flex items-start space-x-4 pb-6 border-b border-gray-800">
|
||||
<div className="w-16 h-16 bg-gray-800 rounded-lg flex items-center justify-center">
|
||||
<span className="text-2xl font-bold text-white">H</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold">{plan.name}</h3>
|
||||
<p className="text-sm text-gray-400">{plan.description}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-semibold">{priceLabel(plan)}</p>
|
||||
<p className="text-sm text-gray-400">/month</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing Breakdown */}
|
||||
<div className="mt-6 space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Subtotal</span>
|
||||
<span>{priceLabel(plan)}</span>
|
||||
</div>
|
||||
<div className="pt-3 border-t border-gray-800">
|
||||
<div className="flex justify-between text-lg font-bold">
|
||||
<span>Total</span>
|
||||
<span className="flex items-center">
|
||||
<span className="text-sm text-gray-400 mr-2">USD</span>
|
||||
{priceLabel(plan)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 text-right mt-1">
|
||||
Billed monthly
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* What's Included */}
|
||||
<div className="mt-8">
|
||||
<h3 className="font-semibold mb-4">What's included:</h3>
|
||||
<ul className="space-y-3">
|
||||
{plan.features.map((feature, index) => (
|
||||
<li key={index} className="flex items-start gap-3 text-sm">
|
||||
<Check className="h-4 w-4 text-white mt-0.5 flex-shrink-0" />
|
||||
<span className="text-gray-300">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Security Note */}
|
||||
<div className="mt-8 p-4 bg-gray-800/30 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<Lock className="h-4 w-4" />
|
||||
<span>Your payment information is encrypted and secure</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="container-custom max-w-xl text-center">
|
||||
{plan ? (
|
||||
<p className="text-gray-400">Opening the checkout for {plan.name}…</p>
|
||||
) : (
|
||||
<>
|
||||
<h1 className="text-3xl font-bold mb-4">Pick a plan</h1>
|
||||
<p className="text-gray-400 mb-8">Our plans and prices are on the pricing page.</p>
|
||||
<Link
|
||||
to="/pricing"
|
||||
className="inline-flex items-center justify-center rounded-full px-8 py-3 text-base font-medium bg-white text-black hover:bg-white/90 transition-colors"
|
||||
>
|
||||
See pricing
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Footer from '@/components/Footer';
|
||||
import { XCircle, ArrowLeft } from 'lucide-react';
|
||||
|
||||
const PaymentCancel = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="pt-32 pb-20 bg-black text-white min-h-screen">
|
||||
<div className="container-custom max-w-2xl text-center">
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-8 mb-8">
|
||||
<XCircle className="h-16 w-16 text-gray-400 mx-auto mb-6" />
|
||||
<h1 className="text-4xl font-bold mb-4">Payment Cancelled</h1>
|
||||
<p className="text-gray-300 text-lg mb-8">
|
||||
Your payment was cancelled and no charges were made.
|
||||
</p>
|
||||
|
||||
<div className="bg-gray-900/50 rounded-lg p-6 mb-8">
|
||||
<h3 className="font-semibold mb-2">Changed your mind?</h3>
|
||||
<p className="text-gray-400 mb-4">
|
||||
No worries! Your selected items are still available. You can return to complete your purchase at any time.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
onClick={() => navigate('/purchase-credits')}
|
||||
className="bg-white hover:bg-gray-100 text-black"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Return to Purchase
|
||||
</Button>
|
||||
<Link to="/dashboard">
|
||||
<Button variant="outline" className="border-white text-white hover:bg-white hover:text-black">
|
||||
Go to Dashboard
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-400">
|
||||
<p>Need help? Contact our support team at support@hanzo.ai</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentCancel;
|
||||
@@ -1,187 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate, useSearchParams, Link } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Footer from '@/components/Footer';
|
||||
import { CheckCircle, ArrowRight, Loader2, AlertCircle } from 'lucide-react';
|
||||
import { trackPurchaseSuccess } from '@/lib/commerce';
|
||||
|
||||
const PaymentSuccess = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isProcessing, setIsProcessing] = useState(true);
|
||||
const [error, setError] = useState(false);
|
||||
const [purchaseDetails, setPurchaseDetails] = useState<any>(null);
|
||||
|
||||
const sessionId = searchParams.get('session_id');
|
||||
|
||||
useEffect(() => {
|
||||
const processPayment = async () => {
|
||||
if (!sessionId) {
|
||||
setError(true);
|
||||
setIsProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get stored purchase details
|
||||
const pendingPurchase = localStorage.getItem('pendingPurchase');
|
||||
if (pendingPurchase) {
|
||||
const details = JSON.parse(pendingPurchase);
|
||||
setPurchaseDetails(details);
|
||||
|
||||
// Update user credits
|
||||
const currentCredits = parseInt(localStorage.getItem('userCredits') || '0');
|
||||
const newCredits = currentCredits + details.credits;
|
||||
localStorage.setItem('userCredits', newCredits.toString());
|
||||
|
||||
// Add to purchase history
|
||||
const history = JSON.parse(localStorage.getItem('creditHistory') || '[]');
|
||||
history.push({
|
||||
id: sessionId,
|
||||
type: 'purchase',
|
||||
credits: details.credits,
|
||||
amount: details.amount,
|
||||
description: `Purchased ${details.credits} credits`,
|
||||
date: new Date().toISOString(),
|
||||
checkoutSessionId: sessionId
|
||||
});
|
||||
localStorage.setItem('creditHistory', JSON.stringify(history));
|
||||
|
||||
// ONE order_completed for this purchase. The pack, the credits and the
|
||||
// quantity are PROPERTIES of it — a second "credit_purchase_completed"
|
||||
// event for the same moment would double every conversion count.
|
||||
trackPurchaseSuccess(
|
||||
sessionId,
|
||||
[{
|
||||
id: details.packId,
|
||||
name: details.productName,
|
||||
category: 'credit_packs',
|
||||
price: details.unitPrice,
|
||||
quantity: details.quantity,
|
||||
credits: details.credits
|
||||
}],
|
||||
details.amount
|
||||
);
|
||||
|
||||
// Clear pending purchase
|
||||
localStorage.removeItem('pendingPurchase');
|
||||
}
|
||||
|
||||
setIsProcessing(false);
|
||||
} catch (error) {
|
||||
console.error('Error processing payment:', error);
|
||||
setError(true);
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
processPayment();
|
||||
}, [sessionId]);
|
||||
|
||||
if (isProcessing) {
|
||||
return (
|
||||
<>
|
||||
<main className="pt-32 pb-20 bg-black text-white min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Loader2 className="h-12 w-12 animate-spin mx-auto mb-4" />
|
||||
<h2 className="text-2xl font-bold mb-2">Processing your payment...</h2>
|
||||
<p className="text-gray-400">Please wait while we confirm your purchase.</p>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<main className="pt-32 pb-20 bg-black text-white min-h-screen">
|
||||
<div className="container-custom max-w-2xl text-center">
|
||||
<div className="bg-red-900/20 border border-red-800 rounded-xl p-8 mb-8">
|
||||
<AlertCircle className="h-16 w-16 text-red-500 mx-auto mb-6" />
|
||||
<h1 className="text-3xl font-bold mb-4">Payment Verification Error</h1>
|
||||
<p className="text-gray-300 mb-6">
|
||||
We couldn't verify your payment. This might happen if you accessed this page directly.
|
||||
If you just completed a payment, please contact support with your session ID.
|
||||
</p>
|
||||
{sessionId && (
|
||||
<p className="text-sm text-gray-400 mb-6">
|
||||
Session ID: {sessionId}
|
||||
</p>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => navigate('/purchase-credits')}
|
||||
className="bg-white hover:bg-gray-100 text-black"
|
||||
>
|
||||
Return to Purchase
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="pt-32 pb-20 bg-black text-white min-h-screen">
|
||||
<div className="container-custom max-w-2xl text-center">
|
||||
<div className="bg-green-900/20 border border-green-800 rounded-xl p-8 mb-8">
|
||||
<CheckCircle className="h-16 w-16 text-green-500 mx-auto mb-6" />
|
||||
<h1 className="text-4xl font-bold mb-4">Payment Successful!</h1>
|
||||
<p className="text-gray-300 text-lg mb-8">
|
||||
Your payment has been processed successfully.
|
||||
{purchaseDetails && ` ${purchaseDetails.credits} credits have been added to your account.`}
|
||||
</p>
|
||||
|
||||
{purchaseDetails && (
|
||||
<div className="bg-gray-900/30 rounded-lg p-6 mb-8 text-left max-w-md mx-auto">
|
||||
<h3 className="font-semibold mb-4 text-center">Purchase Summary</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Credits Purchased:</span>
|
||||
<span>{purchaseDetails.credits.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Amount Paid:</span>
|
||||
<span>${purchaseDetails.amount.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Date:</span>
|
||||
<span>{new Date().toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link to="/dashboard">
|
||||
<Button className="bg-white hover:bg-gray-100 text-black">
|
||||
Go to Dashboard
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/services">
|
||||
<Button variant="outline" className="border-white text-white hover:bg-white hover:text-black">
|
||||
Browse Services
|
||||
<ArrowRight className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-sm text-gray-400">
|
||||
<p>Transaction ID: {sessionId}</p>
|
||||
<p className="mt-2">
|
||||
A receipt has been sent to your email address.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentSuccess;
|
||||
@@ -1,268 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { ArrowRight, Github, BookOpen, Cloud, Code, Users, Zap, Server, GitBranch, Database, Shield, Globe } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const Platform = () => {
|
||||
const features = [
|
||||
{
|
||||
title: "Local Development",
|
||||
description: "Start building on your local machine with our powerful development tools and instant feedback loops.",
|
||||
icon: <Code className="h-6 w-6" />,
|
||||
link: "https://github.com/hanzoai/platform#local-development"
|
||||
},
|
||||
{
|
||||
title: "Open Source Infrastructure",
|
||||
description: "Built on battle-tested open source technologies with full transparency and community support.",
|
||||
icon: <GitBranch className="h-6 w-6" />,
|
||||
link: "https://github.com/hanzoai/platform#open-source"
|
||||
},
|
||||
{
|
||||
title: "Seamless Cloud Transition",
|
||||
description: "Deploy to any cloud provider or on-premises infrastructure with zero vendor lock-in.",
|
||||
icon: <Cloud className="h-6 w-6" />,
|
||||
link: "https://github.com/hanzoai/platform#cloud-deployment"
|
||||
},
|
||||
{
|
||||
title: "AI Development Toolkit",
|
||||
description: "Integrated AI tools and frameworks to accelerate your machine learning workflows.",
|
||||
icon: <Zap className="h-6 w-6" />,
|
||||
link: "https://github.com/hanzoai/ai-sdk"
|
||||
},
|
||||
{
|
||||
title: "Community-Driven",
|
||||
description: "Join thousands of developers building the future of deployment infrastructure together.",
|
||||
icon: <Users className="h-6 w-6" />,
|
||||
link: "https://discord.gg/CJCyAsm9Vr"
|
||||
}
|
||||
];
|
||||
|
||||
const stats = [
|
||||
{ value: "4.8M+", label: "Deploys per month", trend: "+23%" },
|
||||
{ value: "150K+", label: "Active developers", trend: "+45%" },
|
||||
{ value: "Multi-region", label: "Redundancy with automated failover", trend: "Stable" },
|
||||
{ value: "50ms", label: "Avg deploy time", trend: "-12%" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-white dark:from-gray-950 dark:to-gray-900">
|
||||
{/* Hero Section */}
|
||||
<section className="relative py-20 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
Hanzo Platform
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 max-w-3xl mx-auto">
|
||||
The open-source deployment platform: push-to-deploy the moment you commit, with the freedom to run it anywhere.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link to="https://github.com/hanzoai/platform" target="_blank">
|
||||
<Button size="lg" className="gap-2">
|
||||
<Github className="h-5 w-5" />
|
||||
Get on GitHub
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="https://docs.hanzo.ai/platform/quickstart" target="_blank">
|
||||
<Button size="lg" variant="outline" className="gap-2">
|
||||
Quick Start
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="https://github.com/hanzoai/platform/releases/latest" target="_blank">
|
||||
<Button size="lg" variant="outline">
|
||||
Download CLI
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Local Development Section */}
|
||||
<section className="py-16 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-center mb-12 text-gray-900 dark:text-white">
|
||||
Everything Starts Local
|
||||
</h2>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{features.map((feature, index) => (
|
||||
<Card key={index} className="p-6 hover:shadow-lg transition-shadow">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
||||
{feature.icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-lg mb-2">{feature.title}</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||
{feature.description}
|
||||
</p>
|
||||
<Link
|
||||
to={feature.link}
|
||||
target="_blank"
|
||||
className="inline-flex items-center text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
Learn More
|
||||
<ArrowRight className="h-4 w-4 ml-1" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Platform vs Cloud Comparison */}
|
||||
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-gray-50 dark:bg-gray-900/50">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-center mb-12 text-gray-900 dark:text-white">
|
||||
Hanzo Platform vs. Hanzo Cloud
|
||||
</h2>
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
<Card className="p-8">
|
||||
<h3 className="text-2xl font-semibold mb-4">Hanzo Platform</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||
Self-hosted, open-source deployment platform
|
||||
</p>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-center gap-2">
|
||||
<Server className="h-5 w-5 text-green-600" />
|
||||
<span>Run on your infrastructure</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5 text-green-600" />
|
||||
<span>Complete data control</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Code className="h-5 w-5 text-green-600" />
|
||||
<span>Unlimited customization</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5 text-green-600" />
|
||||
<span>No vendor lock-in</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Link to="https://iam.hanzo.ai/register" target="_blank">
|
||||
<Button className="w-full mt-6">Get Started Free</Button>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
<Card className="p-8 border-blue-500">
|
||||
<h3 className="text-2xl font-semibold mb-4">Hanzo Cloud</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
||||
Fully managed cloud deployment service
|
||||
</p>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-center gap-2">
|
||||
<Cloud className="h-5 w-5 text-blue-600" />
|
||||
<span>Zero configuration needed</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Globe className="h-5 w-5 text-blue-600" />
|
||||
<span>Global edge network</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Zap className="h-5 w-5 text-blue-600" />
|
||||
<span>Automatic scaling</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5 text-blue-600" />
|
||||
<span>Managed security updates</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Link to="/pricing">
|
||||
<Button variant="outline" className="w-full mt-6">View Pricing</Button>
|
||||
</Link>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Complete AI Engineering Stack */}
|
||||
<section className="py-16 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h2 className="text-3xl font-bold mb-6 text-gray-900 dark:text-white">
|
||||
Complete AI Engineering Stack
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8 max-w-3xl mx-auto">
|
||||
Everything you need to build, deploy, and scale AI applications in one integrated platform.
|
||||
</p>
|
||||
<Link to="https://github.com/hanzoai" target="_blank">
|
||||
<Button size="lg" variant="outline" className="gap-2">
|
||||
<Github className="h-5 w-5" />
|
||||
View all components on GitHub
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Usage Metrics */}
|
||||
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-gray-50 dark:bg-gray-900/50">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h2 className="text-3xl font-bold text-center mb-12 text-gray-900 dark:text-white">
|
||||
4.8M+ deploys per month (and counting)
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
{stats.map((stat, index) => (
|
||||
<Card key={index} className="p-6 text-center">
|
||||
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
|
||||
{stat.value}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300 mt-2">
|
||||
{stat.label}
|
||||
</div>
|
||||
<div className={`text-xs mt-2 ${
|
||||
stat.trend.includes('+') ? 'text-green-600' :
|
||||
stat.trend.includes('-') ? 'text-blue-600' :
|
||||
'text-gray-500'
|
||||
}`}>
|
||||
{stat.trend}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8 bg-white dark:bg-gray-800 rounded-lg p-8 shadow-lg">
|
||||
<div className="h-64 flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded">
|
||||
<div className="text-center">
|
||||
<Zap className="h-16 w-16 mx-auto text-blue-600 dark:text-blue-400 mb-4" />
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
Real-time metrics visualization coming soon
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Start Building CTA */}
|
||||
<section className="py-20 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-4xl mx-auto text-center">
|
||||
<h2 className="text-3xl font-bold mb-6 text-gray-900 dark:text-white">
|
||||
Start building locally today
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8">
|
||||
Join thousands of developers who are building the future with Hanzo Platform.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link to="https://github.com/hanzoai/platform" target="_blank">
|
||||
<Button size="lg" className="gap-2">
|
||||
<Github className="h-5 w-5" />
|
||||
Get on GitHub
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="https://docs.hanzo.ai" target="_blank">
|
||||
<Button size="lg" variant="outline" className="gap-2">
|
||||
<BookOpen className="h-5 w-5" />
|
||||
Read the Docs
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Platform;
|
||||
@@ -18,7 +18,7 @@ export default function Pricing() {
|
||||
<PlanCards />
|
||||
<NeedMore />
|
||||
|
||||
<p className="text-xs text-center text-foreground/50 mt-8">Pay with card, crypto, or wire transfer</p>
|
||||
<p className="text-xs text-center text-foreground/50 mt-8">Billed monthly by card.</p>
|
||||
</div>
|
||||
|
||||
<div className="container-custom mt-24">
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Footer from '@/components/Footer';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Check,
|
||||
Star,
|
||||
Zap,
|
||||
TrendingUp,
|
||||
Shield
|
||||
} from 'lucide-react';
|
||||
import { formatCurrency, checkoutUrl } from '@/lib/commerce';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { EVENTS } from '@hanzo/event';
|
||||
import { analytics } from '@/analytics';
|
||||
|
||||
const PurchaseCredits = () => {
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const [selectedPack, setSelectedPack] = useState<string | null>(null);
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user is logged in
|
||||
const authToken = localStorage.getItem('authToken');
|
||||
if (!authToken) {
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
const creditPacks = [
|
||||
{
|
||||
id: 'starter',
|
||||
credits: 100,
|
||||
price: 100,
|
||||
perCredit: 1.00,
|
||||
icon: Zap,
|
||||
features: [
|
||||
'100 credits',
|
||||
'$1 per credit',
|
||||
'Perfect for trying services',
|
||||
'No expiration'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'professional',
|
||||
credits: 500,
|
||||
price: 450,
|
||||
perCredit: 0.90,
|
||||
savings: 50,
|
||||
icon: TrendingUp,
|
||||
features: [
|
||||
'500 credits',
|
||||
'$0.90 per credit',
|
||||
'Save $50 (10% off)',
|
||||
'Most popular choice'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'enterprise',
|
||||
credits: 2000,
|
||||
price: 1500,
|
||||
perCredit: 0.75,
|
||||
savings: 500,
|
||||
icon: Star,
|
||||
features: [
|
||||
'2,000 credits',
|
||||
'$0.75 per credit',
|
||||
'Save $500 (25% off)',
|
||||
'Best value for teams'
|
||||
],
|
||||
badge: 'BEST DEAL'
|
||||
}
|
||||
];
|
||||
|
||||
const handlePackSelect = (packId: string) => {
|
||||
setSelectedPack(packId);
|
||||
const pack = creditPacks.find(p => p.id === packId);
|
||||
if (pack) {
|
||||
// ONE event for one click. "Viewed the pack" and "added it to a cart" are
|
||||
// the same user-visible moment on this page — there is no cart — so
|
||||
// emitting both would inflate intent twice over.
|
||||
analytics.capture(
|
||||
EVENTS.PLAN_CLICKED,
|
||||
{ plan: pack.id, credits: pack.credits, category: 'credit_packs' },
|
||||
{ productId: pack.id, quantity: 1, revenue: pack.price, currency: 'USD' },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePurchase = async () => {
|
||||
if (!selectedPack) return;
|
||||
|
||||
setIsLoading(true);
|
||||
const pack = creditPacks.find(p => p.id === selectedPack);
|
||||
if (!pack) return;
|
||||
|
||||
// Redirect to billing portal for credit purchases
|
||||
window.location.href = checkoutUrl(selectedPack, '/payment-success');
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const selectedPackDetails = creditPacks.find(p => p.id === selectedPack);
|
||||
const totalPrice = selectedPackDetails ? selectedPackDetails.price * quantity : 0;
|
||||
const totalCredits = selectedPackDetails ? selectedPackDetails.credits * quantity : 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="pt-32 pb-20 bg-black text-white min-h-screen">
|
||||
<div className="container-custom max-w-6xl">
|
||||
{/* Header */}
|
||||
<div className="mb-12">
|
||||
<Link to="/dashboard" className="inline-flex items-center gap-2 text-gray-400 hover:text-white mb-6">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Dashboard
|
||||
</Link>
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4">Purchase Credits</h1>
|
||||
<p className="text-gray-400 text-lg">
|
||||
Choose a credit pack that fits your needs. 1 credit = $1 value.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Credit Packs */}
|
||||
<div className="grid md:grid-cols-3 gap-6 mb-12">
|
||||
{creditPacks.map((pack) => {
|
||||
const Icon = pack.icon;
|
||||
const isSelected = selectedPack === pack.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={pack.id}
|
||||
className={`relative bg-gray-900/30 border-2 rounded-xl p-6 cursor-pointer transition-all ${
|
||||
isSelected
|
||||
? 'border-white'
|
||||
: 'border-gray-800 hover:border-gray-600'
|
||||
} ${pack.id === 'enterprise' ? 'md:scale-105' : ''}`}
|
||||
onClick={() => handlePackSelect(pack.id)}
|
||||
>
|
||||
{pack.badge && (
|
||||
<div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-white text-black px-4 py-1 rounded-full text-sm font-bold">
|
||||
{pack.badge}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-12 h-12 bg-gray-800 rounded-lg flex items-center justify-center">
|
||||
<Icon className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="w-6 h-6 bg-white rounded-full flex items-center justify-center">
|
||||
<Check className="h-4 w-4 text-black" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-bold mb-2">
|
||||
{pack.credits.toLocaleString()} Credits
|
||||
</h3>
|
||||
<p className="text-3xl font-bold mb-1">
|
||||
${pack.price.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-gray-400 text-sm mb-6">
|
||||
${pack.perCredit.toFixed(2)} per credit
|
||||
</p>
|
||||
|
||||
{pack.savings && (
|
||||
<div className="bg-green-900/30 border border-green-800 rounded-lg px-3 py-2 mb-4">
|
||||
<p className="text-green-400 text-sm font-semibold">
|
||||
Save ${pack.savings} ({Math.round((pack.savings / (pack.credits * 1)) * 100)}% off)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ul className="space-y-3">
|
||||
{pack.features.map((feature, index) => (
|
||||
<li key={index} className="flex items-start gap-3 text-sm">
|
||||
<Check className="h-4 w-4 text-white mt-0.5 flex-shrink-0" />
|
||||
<span className="text-gray-300">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Quantity and Purchase */}
|
||||
{selectedPack && (
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-8">
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-4">Select Quantity</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||
className="w-10 h-10 bg-gray-800 hover:bg-gray-700 rounded-lg flex items-center justify-center transition-colors"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
value={quantity}
|
||||
onChange={(e) => setQuantity(Math.max(1, parseInt(e.target.value) || 1))}
|
||||
className="w-20 bg-gray-900/50 border border-gray-700 text-white text-center py-2 rounded-lg"
|
||||
min="1"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setQuantity(quantity + 1)}
|
||||
className="w-10 h-10 bg-gray-800 hover:bg-gray-700 rounded-lg flex items-center justify-center transition-colors"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
Buy multiple packs at once
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-4">Order Summary</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Credits</span>
|
||||
<span className="font-semibold">{totalCredits.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Unit Price</span>
|
||||
<span>${selectedPackDetails.price.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Quantity</span>
|
||||
<span>×{quantity}</span>
|
||||
</div>
|
||||
<div className="pt-3 border-t border-gray-800">
|
||||
<div className="flex justify-between text-xl font-bold">
|
||||
<span>Total</span>
|
||||
<span>${totalPrice.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handlePurchase}
|
||||
disabled={isLoading}
|
||||
className="w-full bg-white hover:bg-gray-100 text-black py-3 text-lg font-semibold rounded-lg transition-colors mt-6"
|
||||
>
|
||||
{isLoading ? 'Redirecting to payment...' : 'Continue to Payment'}
|
||||
</Button>
|
||||
|
||||
<div className="mt-4 flex items-center justify-center gap-2 text-sm text-gray-400">
|
||||
<Shield className="h-4 w-4" />
|
||||
<span>Secure payment via Hanzo Billing</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PurchaseCredits;
|
||||
@@ -1,331 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Footer from '@/components/Footer';
|
||||
import {
|
||||
ArrowLeft,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
ExternalLink,
|
||||
Calendar,
|
||||
Clock
|
||||
} from 'lucide-react';
|
||||
import { notifyAdmins } from '@/utils/emailNotifications';
|
||||
|
||||
const RedeemService = () => {
|
||||
const { serviceId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [isRedeeming, setIsRedeeming] = useState(false);
|
||||
const [isRedeemed, setIsRedeemed] = useState(false);
|
||||
const [userCredits, setUserCredits] = useState(0);
|
||||
|
||||
const services = {
|
||||
'revision': {
|
||||
name: 'Revision',
|
||||
credits: 50,
|
||||
description: 'Get revisions on existing work',
|
||||
details: [
|
||||
'Professional review of your existing design',
|
||||
'Up to 3 rounds of revisions',
|
||||
'Feedback implementation within 48 hours',
|
||||
'Direct communication with designer'
|
||||
]
|
||||
},
|
||||
'new-page': {
|
||||
name: 'New Page',
|
||||
credits: 50,
|
||||
description: 'Add a new page to your website',
|
||||
details: [
|
||||
'Custom page design matching your site',
|
||||
'Mobile-responsive layout',
|
||||
'SEO optimization included',
|
||||
'Content integration support'
|
||||
]
|
||||
},
|
||||
'custom-artwork': {
|
||||
name: 'Custom Artwork',
|
||||
credits: 150,
|
||||
description: 'Perfect for social media content',
|
||||
details: [
|
||||
'Original artwork creation',
|
||||
'Multiple format exports',
|
||||
'Social media optimization',
|
||||
'Commercial usage rights'
|
||||
]
|
||||
},
|
||||
'logo-design': {
|
||||
name: 'Logo Design',
|
||||
credits: 200,
|
||||
description: 'Professional logo design',
|
||||
details: [
|
||||
'3 initial concepts',
|
||||
'Unlimited revisions on chosen concept',
|
||||
'Multiple file formats (SVG, PNG, etc.)',
|
||||
'Brand guidelines document'
|
||||
]
|
||||
},
|
||||
'branding-kit': {
|
||||
name: 'Branding Kit',
|
||||
credits: 500,
|
||||
description: 'Complete brand identity package',
|
||||
details: [
|
||||
'Logo design with variations',
|
||||
'Color palette and typography',
|
||||
'Business card and letterhead',
|
||||
'Brand guidelines document',
|
||||
'Social media templates'
|
||||
]
|
||||
},
|
||||
'instant-site': {
|
||||
name: '3-Page Instant Site',
|
||||
credits: 500,
|
||||
description: '24-hour turnaround website',
|
||||
details: [
|
||||
'Custom 3-page website design',
|
||||
'Mobile-responsive development',
|
||||
'24-hour delivery guarantee',
|
||||
'Basic SEO setup included',
|
||||
'Training on how to update content'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const service = services[serviceId as keyof typeof services];
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user is logged in
|
||||
const authToken = localStorage.getItem('authToken');
|
||||
if (!authToken) {
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
// Load user credits
|
||||
const credits = parseInt(localStorage.getItem('userCredits') || '0');
|
||||
setUserCredits(credits);
|
||||
|
||||
// Check if service exists
|
||||
if (!service) {
|
||||
navigate('/dashboard');
|
||||
}
|
||||
}, [navigate, service]);
|
||||
|
||||
const handleRedeem = () => {
|
||||
if (!service || userCredits < service.credits) return;
|
||||
|
||||
setIsRedeeming(true);
|
||||
|
||||
// Simulate redemption
|
||||
setTimeout(() => {
|
||||
// Deduct credits
|
||||
const newCredits = userCredits - service.credits;
|
||||
localStorage.setItem('userCredits', newCredits.toString());
|
||||
|
||||
// Add to history
|
||||
const history = JSON.parse(localStorage.getItem('creditHistory') || '[]');
|
||||
history.push({
|
||||
id: Date.now().toString(),
|
||||
type: 'redemption',
|
||||
credits: -service.credits,
|
||||
service: service.name,
|
||||
description: `Redeemed ${service.name}`,
|
||||
date: new Date().toISOString(),
|
||||
status: 'pending'
|
||||
});
|
||||
localStorage.setItem('creditHistory', JSON.stringify(history));
|
||||
|
||||
// Send email notification to admins
|
||||
const userEmail = localStorage.getItem('userEmail') || '';
|
||||
notifyAdmins.serviceRedeemed(userEmail, service.name, service.credits);
|
||||
|
||||
setIsRedeeming(false);
|
||||
setIsRedeemed(true);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const handleStartAssignment = () => {
|
||||
// Send notification that service is being started
|
||||
const userEmail = localStorage.getItem('userEmail') || '';
|
||||
if (service) {
|
||||
notifyAdmins.serviceStarted(userEmail, service.name);
|
||||
}
|
||||
|
||||
window.open('https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ23sAA3nns4Ntw1GiKXDOwDgH6uPDXV8aqFr8mKqF8m-r8uduBrTw5ZQI7afVAAJFPD_UBgYqu_', '_blank');
|
||||
};
|
||||
|
||||
if (!service) return null;
|
||||
|
||||
const canAfford = userCredits >= service.credits;
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="pt-32 pb-20 bg-black text-white min-h-screen">
|
||||
<div className="container-custom max-w-4xl">
|
||||
{/* Header */}
|
||||
<div className="mb-12">
|
||||
<Link to="/dashboard" className="inline-flex items-center gap-2 text-gray-400 hover:text-white mb-6">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back to Dashboard
|
||||
</Link>
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
{isRedeemed ? 'Service Redeemed!' : `Redeem ${service.name}`}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{isRedeemed ? (
|
||||
// Redeemed State
|
||||
<div className="space-y-8">
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-8 text-center">
|
||||
<div className="w-20 h-20 bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<CheckCircle className="h-10 w-10 text-green-400" />
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-4">
|
||||
{service.name} Successfully Redeemed
|
||||
</h2>
|
||||
<p className="text-gray-400 mb-8 max-w-2xl mx-auto">
|
||||
Your service has been redeemed and is ready to start. Click the button below to schedule your consultation and begin the project.
|
||||
</p>
|
||||
|
||||
<div className="bg-yellow-900/20 border border-yellow-800 rounded-lg p-6 mb-8 max-w-2xl mx-auto">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="h-5 w-5 text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-left">
|
||||
<p className="text-yellow-400 font-semibold mb-1">Important: Manual Start Required</p>
|
||||
<p className="text-sm text-gray-300">
|
||||
This service will not start automatically. You must click "Start Assignment" below to schedule your consultation and begin the work.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleStartAssignment}
|
||||
className="bg-white hover:bg-gray-100 text-black px-8 py-4 text-lg font-semibold rounded-lg transition-colors inline-flex items-center gap-3"
|
||||
>
|
||||
<Calendar className="h-5 w-5" />
|
||||
Start Assignment
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<p className="text-sm text-gray-400 mt-4">
|
||||
This will open your calendar to schedule a consultation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<Clock className="h-5 w-5" />
|
||||
What happens next?
|
||||
</h3>
|
||||
<ol className="space-y-3 text-gray-300">
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="bg-gray-800 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm flex-shrink-0 mt-0.5">1</span>
|
||||
<span>Click "Start Assignment" to open the scheduling page</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="bg-gray-800 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm flex-shrink-0 mt-0.5">2</span>
|
||||
<span>Choose a convenient time for your consultation</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="bg-gray-800 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm flex-shrink-0 mt-0.5">3</span>
|
||||
<span>Our team will begin working on your project</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="bg-gray-800 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm flex-shrink-0 mt-0.5">4</span>
|
||||
<span>Receive your completed work within the promised timeframe</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Pre-Redemption State
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-8">
|
||||
<h2 className="text-2xl font-bold mb-4">{service.name}</h2>
|
||||
<p className="text-gray-400 mb-6">{service.description}</p>
|
||||
|
||||
<div className="mb-8">
|
||||
<h3 className="font-semibold mb-4">What's included:</h3>
|
||||
<ul className="space-y-3">
|
||||
{service.details.map((detail, index) => (
|
||||
<li key={index} className="flex items-start gap-3 text-sm">
|
||||
<CheckCircle className="h-4 w-4 text-white mt-0.5 flex-shrink-0" />
|
||||
<span className="text-gray-300">{detail}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800/50 rounded-lg p-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-400">Credit Cost</span>
|
||||
<span className="text-2xl font-bold">{service.credits} credits</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="bg-gray-900/30 border border-gray-800 rounded-xl p-8">
|
||||
<h3 className="text-xl font-semibold mb-6">Redeem This Service</h3>
|
||||
|
||||
<div className="space-y-4 mb-8">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Your Credits</span>
|
||||
<span className="font-semibold">{userCredits.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Service Cost</span>
|
||||
<span className="font-semibold">-{service.credits}</span>
|
||||
</div>
|
||||
<div className="pt-4 border-t border-gray-800">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Credits After</span>
|
||||
<span className={`font-bold ${canAfford ? 'text-white' : 'text-red-400'}`}>
|
||||
{canAfford ? (userCredits - service.credits).toLocaleString() : 'Insufficient'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!canAfford && (
|
||||
<div className="bg-red-900/20 border border-red-800 rounded-lg p-4 mb-6">
|
||||
<p className="text-red-400 text-sm">
|
||||
You need {service.credits - userCredits} more credits to redeem this service.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleRedeem}
|
||||
disabled={!canAfford || isRedeeming}
|
||||
className={`w-full ${
|
||||
canAfford
|
||||
? 'bg-white hover:bg-gray-100 text-black'
|
||||
: 'bg-gray-800 text-gray-500 cursor-not-allowed'
|
||||
} py-3 text-lg font-semibold rounded-lg transition-colors`}
|
||||
>
|
||||
{isRedeeming ? 'Processing...' : canAfford ? 'Redeem Service' : 'Insufficient Credits'}
|
||||
</Button>
|
||||
|
||||
{!canAfford && (
|
||||
<Link to="/purchase-credits">
|
||||
<Button className="w-full bg-gray-800 hover:bg-gray-700 text-white py-3 rounded-lg mt-4">
|
||||
Purchase Credits
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RedeemService;
|
||||
@@ -7,6 +7,7 @@ import CaseStudyTrio from '@/components/CaseStudyTrio';
|
||||
import { PlanCards } from '@/components/Pricing';
|
||||
import { serviceCategories, services } from '@/data/services';
|
||||
import ServiceTemplate from './services/ServiceTemplate';
|
||||
import { contact } from '@/data/contact';
|
||||
|
||||
const CATEGORIES = [
|
||||
{ key: 'engineering' as const, title: 'Engineering' },
|
||||
@@ -132,7 +133,7 @@ const ServicesPage: React.FC = () => {
|
||||
<Link to="/pricing" className="bg-white text-black px-8 py-4 rounded-full font-medium hover:bg-white/90 inline-flex items-center text-lg">
|
||||
See pricing <ArrowRight className="ml-2 h-5 w-5" />
|
||||
</Link>
|
||||
<a href="https://calendar.app.google/z1YsZQrqR4s6jQqD8" className="border border-white px-8 py-4 rounded-full font-medium hover:bg-white/10 inline-flex items-center text-lg">
|
||||
<a href={contact.booking} className="border border-white px-8 py-4 rounded-full font-medium hover:bg-white/10 inline-flex items-center text-lg">
|
||||
Schedule a Consultation
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Footer from '@/components/Footer';
|
||||
import { ArrowRight, ShieldCheck, ExternalLink } from 'lucide-react';
|
||||
import { HanzoLogo } from '@hanzo/logo';
|
||||
|
||||
const HANZO_ID_SIGNUP = 'https://hanzo.id/signup?redirect=https%3A%2F%2Fhanzo.agency%2Fdashboard';
|
||||
const HANZO_ID_LOGIN = 'https://hanzo.id/login?redirect=https%3A%2F%2Fhanzo.agency%2Fdashboard';
|
||||
|
||||
const Signup = () => {
|
||||
return (
|
||||
<>
|
||||
<main className="pt-32 pb-24 bg-black text-white min-h-screen flex items-center justify-center">
|
||||
<div className="container-custom max-w-md w-full px-4">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-14 h-14 rounded-2xl bg-zinc-900 border border-white/20 flex items-center justify-center mx-auto mb-5 p-2.5 shadow-2xl">
|
||||
<HanzoLogo size={32} className="[&>svg]:w-full [&>svg]:h-full text-white" />
|
||||
</div>
|
||||
<h1 className="text-3xl sm:text-4xl font-bold mb-2">Create Hanzo Account</h1>
|
||||
<p className="text-sm text-gray-400">
|
||||
One identity for Hanzo Agency and hanzo.team
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-900/60 border border-white/10 rounded-2xl p-8 backdrop-blur-xl shadow-2xl space-y-6">
|
||||
<div className="space-y-3">
|
||||
<a
|
||||
href={HANZO_ID_SIGNUP}
|
||||
className="w-full bg-white hover:bg-white/90 text-black py-3.5 px-6 text-sm font-semibold rounded-full transition-all flex items-center justify-center gap-2.5 shadow-lg group cursor-pointer"
|
||||
>
|
||||
<span>Create with Hanzo ID</span>
|
||||
<ArrowRight className="w-4 h-4 transition-transform group-hover:translate-x-1" />
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://hanzo.team"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full bg-white/5 hover:bg-white/10 border border-white/10 text-white py-3 px-6 text-xs font-medium rounded-full transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<span>Launch hanzo.team workspace</span>
|
||||
<ExternalLink className="w-3.5 h-3.5 opacity-60" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-white/10 text-center space-y-3">
|
||||
<p className="text-xs text-gray-400">
|
||||
Already have a Hanzo ID?{' '}
|
||||
<a
|
||||
href={HANZO_ID_LOGIN}
|
||||
className="text-white hover:underline font-medium"
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-2 text-[11px] text-gray-500">
|
||||
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400" />
|
||||
<span>Zero vendor lock-in · Unified SSO across Hanzo estate</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Signup;
|
||||
@@ -3,9 +3,11 @@ import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { Check } from 'lucide-react';
|
||||
import Footer from '@/components/Footer';
|
||||
import { planById, priceLabel } from '@/data/plans';
|
||||
import { checkoutUrl } from '@/lib/commerce';
|
||||
import { contact } from '@/data/contact';
|
||||
|
||||
/** The receipt lives with the pay site; every charge on the account is listed there. */
|
||||
const INVOICES = 'https://pay.hanzo.ai/invoices';
|
||||
const INVOICES = 'https://hanzo.ai/pay/invoices';
|
||||
|
||||
/** Commerce statuses for a charge that did not go through. */
|
||||
const FAILED = new Set(['failed', 'declined', 'canceled', 'cancelled', 'error']);
|
||||
@@ -22,9 +24,8 @@ const secondary =
|
||||
const Subscribed = () => {
|
||||
const [search] = useSearchParams();
|
||||
const status = search.get('checkout');
|
||||
const found = planById(search.get('plan'));
|
||||
// Only a priced plan has a checkout to come back from.
|
||||
const plan = found?.priceMonthly === null ? undefined : found;
|
||||
// The checkout returns the commerce slug; planById reads either name.
|
||||
const plan = planById(search.get('plan'));
|
||||
|
||||
let body: ReactNode;
|
||||
if (!plan || !status) {
|
||||
@@ -50,7 +51,7 @@ const Subscribed = () => {
|
||||
<p className="text-sm uppercase tracking-widest text-gray-400 mb-2">Subscription active</p>
|
||||
<h1 className="text-3xl md:text-4xl font-bold mb-4">Welcome to {plan.name}</h1>
|
||||
<p className="text-gray-300 mb-10">
|
||||
{priceLabel(plan)}/month. {plan.terms}. Your receipt is in your invoices.
|
||||
{priceLabel(plan)}/month, billed monthly. Your receipt is in your invoices.
|
||||
</p>
|
||||
|
||||
<div className="text-left bg-gray-900/30 border border-gray-800 rounded-xl p-6 mb-10">
|
||||
@@ -58,7 +59,9 @@ const Subscribed = () => {
|
||||
<ol className="space-y-4 text-gray-300">
|
||||
<li className="flex gap-3">
|
||||
<span className="font-mono text-white">1</span>
|
||||
<span>Your advisor reaches out by email within 24 hours to schedule your kickoff.</span>
|
||||
<span>
|
||||
<a href={contact.booking} className="underline">Book your kickoff call</a> at a time that suits you.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="font-mono text-white">2</span>
|
||||
@@ -85,7 +88,7 @@ const Subscribed = () => {
|
||||
{plan.name} is not active yet. Try the checkout again, or talk to us and we will set it up with you.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row justify-center gap-4">
|
||||
<Link to={`/payment?plan=${plan.id}`} className={primary}>Try again</Link>
|
||||
<a href={checkoutUrl(plan)} className={primary}>Try again</a>
|
||||
<Link to="/contact" className={secondary}>Contact us</Link>
|
||||
</div>
|
||||
</>
|
||||
@@ -95,7 +98,7 @@ const Subscribed = () => {
|
||||
<>
|
||||
<h1 className="text-3xl md:text-4xl font-bold mb-4">Your {plan.name} subscription is pending</h1>
|
||||
<p className="text-gray-400 mb-8">
|
||||
It starts as soon as the payment settles. Your advisor reaches out by email once it does.
|
||||
It starts as soon as the payment settles.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row justify-center gap-4">
|
||||
<a href={INVOICES} className={primary}>View invoices</a>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ArrowUpRight, Check } from 'lucide-react';
|
||||
import Footer from '@/components/Footer';
|
||||
import { priceLabel, whiteLabel } from '@/data/plans';
|
||||
import { checkoutUrl } from '@/lib/commerce';
|
||||
|
||||
/** How a partner goes from checkout to selling. */
|
||||
const STEPS = [
|
||||
{ title: 'Subscribe', body: 'Pay the first month by card. Your plan starts right away.' },
|
||||
{ title: 'Kickoff', body: 'Tell us your name, your logo, your colors, your domain and your prices.' },
|
||||
{ title: 'Launch', body: 'We set up your agency site. You sell to your clients under your brand.' },
|
||||
];
|
||||
|
||||
/**
|
||||
* /white-label: the partner plan, sold here and nowhere else. The price and
|
||||
* features are the commerce row `white-label` (src/data/plans.ts), and the
|
||||
* button opens the checkout on it. Linked from the Enterprise page's partner
|
||||
* section and hanzo.ai/affiliate; no pricing grid and no nav menu carries it.
|
||||
*/
|
||||
const WhiteLabel = () => (
|
||||
<>
|
||||
<main className="bg-black text-white">
|
||||
<section className="pt-40 pb-20">
|
||||
<div className="container-custom max-w-4xl text-center">
|
||||
<p className="text-sm uppercase tracking-widest text-white/60 mb-4">White-label</p>
|
||||
<h1 className="text-5xl md:text-7xl font-bold tracking-tight mb-6">Your own AI agency. Your brand.</h1>
|
||||
<p className="text-xl text-white/80 max-w-2xl mx-auto">
|
||||
Sell AI, engineering, design and growth work to your clients under your name, on your domain. We set it up
|
||||
for you.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="pb-20">
|
||||
<div className="container-custom max-w-4xl grid md:grid-cols-2 gap-8">
|
||||
<div className="rounded-2xl border border-white/15 bg-white/[0.03] p-8">
|
||||
<h2 className="text-2xl font-bold mb-1">{whiteLabel.name}</h2>
|
||||
<p className="text-white/70 mb-6">{whiteLabel.description}</p>
|
||||
<p className="mb-1">
|
||||
<span className="text-5xl font-bold tracking-tight">{priceLabel(whiteLabel)}</span>
|
||||
<span className="ml-2 text-white/70">/month</span>
|
||||
</p>
|
||||
<p className="text-sm text-white/60 mb-8">{whiteLabel.terms}.</p>
|
||||
<div className="flex flex-col gap-3">
|
||||
<a href={checkoutUrl(whiteLabel)} className="lets-talk-btn w-full justify-center text-lg py-3">
|
||||
{whiteLabel.cta}
|
||||
<ArrowUpRight size={20} className="ml-2" />
|
||||
</a>
|
||||
<Link to="/contact" className="lets-talk-btn w-full justify-center py-3">
|
||||
Talk to us
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-white/15 bg-white/[0.03] p-8">
|
||||
<h2 className="text-2xl font-bold mb-6">What you get</h2>
|
||||
<ul className="space-y-4">
|
||||
{whiteLabel.features.map((f) => (
|
||||
<li key={f} className="flex items-start">
|
||||
<Check size={20} className="text-accent mr-3 mt-1 flex-shrink-0" />
|
||||
<span>{f}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="pb-24">
|
||||
<div className="container-custom max-w-4xl">
|
||||
<h2 className="text-3xl font-bold mb-8 text-center">How it works</h2>
|
||||
<ol className="grid md:grid-cols-3 gap-6">
|
||||
{STEPS.map((s, i) => (
|
||||
<li key={s.title} className="rounded-2xl border border-white/10 p-6">
|
||||
<span className="font-mono text-white/50">{i + 1}</span>
|
||||
<h3 className="text-xl font-bold mt-2 mb-2">{s.title}</h3>
|
||||
<p className="text-white/70">{s.body}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<p className="text-center text-white/70 mt-12">
|
||||
Want the Hanzo team to do the work for your company instead?{' '}
|
||||
<Link to="/pricing" className="text-white underline">
|
||||
See the plans
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
|
||||
export default WhiteLabel;
|
||||
@@ -1,99 +0,0 @@
|
||||
// Email notification utility
|
||||
// In production, this would integrate with an email service like SendGrid, AWS SES, etc.
|
||||
|
||||
interface EmailData {
|
||||
to: string[];
|
||||
subject: string;
|
||||
body: string;
|
||||
type: 'signup' | 'purchase' | 'redemption' | 'service_start';
|
||||
}
|
||||
|
||||
export const sendEmailNotification = async (data: EmailData) => {
|
||||
// Simulate email sending
|
||||
console.log('📧 Email Notification Sent:');
|
||||
console.log('To:', data.to.join(', '));
|
||||
console.log('Subject:', data.subject);
|
||||
console.log('Body:', data.body);
|
||||
console.log('Type:', data.type);
|
||||
console.log('---');
|
||||
|
||||
// In production, this would be an API call like:
|
||||
// await fetch('/api/send-email', {
|
||||
// method: 'POST',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify(data)
|
||||
// });
|
||||
|
||||
// Store notification in localStorage for demo purposes
|
||||
const notifications = JSON.parse(localStorage.getItem('emailNotifications') || '[]');
|
||||
notifications.push({
|
||||
...data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
localStorage.setItem('emailNotifications', JSON.stringify(notifications));
|
||||
};
|
||||
|
||||
export const notifyAdmins = {
|
||||
newSignup: (userEmail: string, userName: string) => {
|
||||
sendEmailNotification({
|
||||
to: ['[email protected]', '[email protected]'],
|
||||
subject: 'New User Signup - Hanzo Agency',
|
||||
body: `A new user has signed up:
|
||||
|
||||
Name: ${userName}
|
||||
Email: ${userEmail}
|
||||
Time: ${new Date().toLocaleString()}
|
||||
|
||||
Please log in to the admin dashboard to view details.`,
|
||||
type: 'signup'
|
||||
});
|
||||
},
|
||||
|
||||
creditsPurchased: (userEmail: string, credits: number, amount: number) => {
|
||||
sendEmailNotification({
|
||||
to: ['[email protected]', '[email protected]'],
|
||||
subject: 'New Credit Purchase - Hanzo Agency',
|
||||
body: `A user has purchased credits:
|
||||
|
||||
User: ${userEmail}
|
||||
Credits: ${credits}
|
||||
Amount: $${amount}
|
||||
Time: ${new Date().toLocaleString()}
|
||||
|
||||
Please log in to the admin dashboard to view details.`,
|
||||
type: 'purchase'
|
||||
});
|
||||
},
|
||||
|
||||
serviceRedeemed: (userEmail: string, service: string, credits: number) => {
|
||||
sendEmailNotification({
|
||||
to: ['[email protected]', '[email protected]'],
|
||||
subject: 'Service Redeemed - Hanzo Agency',
|
||||
body: `A user has redeemed a service:
|
||||
|
||||
User: ${userEmail}
|
||||
Service: ${service}
|
||||
Credits Used: ${credits}
|
||||
Time: ${new Date().toLocaleString()}
|
||||
|
||||
Please log in to the admin dashboard to view details.`,
|
||||
type: 'redemption'
|
||||
});
|
||||
},
|
||||
|
||||
serviceStarted: (userEmail: string, service: string) => {
|
||||
sendEmailNotification({
|
||||
to: ['[email protected]', '[email protected]'],
|
||||
subject: 'Service Started - Hanzo Agency',
|
||||
body: `A user has started their service:
|
||||
|
||||
User: ${userEmail}
|
||||
Service: ${service}
|
||||
Time: ${new Date().toLocaleString()}
|
||||
Status: Booking scheduled
|
||||
|
||||
Please log in to the admin dashboard to view details.`,
|
||||
type: 'service_start'
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user