- Add findagram.co React frontend with product search, brands, categories - Add findadispo.com React frontend with dispensary locator - Wire findagram to backend /api/az/* endpoints - Update category/brand links to route to /products with filters - Add k8s manifests for both frontends - Add multi-domain user support migrations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
287 lines
7.9 KiB
TypeScript
Executable File
287 lines
7.9 KiB
TypeScript
Executable File
import { ReactNode, useEffect, useState } from 'react';
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
import { useAuthStore } from '../store/authStore';
|
|
import { api } from '../lib/api';
|
|
import {
|
|
LayoutDashboard,
|
|
Store,
|
|
Building2,
|
|
FolderOpen,
|
|
Package,
|
|
Target,
|
|
TrendingUp,
|
|
Wrench,
|
|
Activity,
|
|
Clock,
|
|
Calendar,
|
|
Shield,
|
|
FileText,
|
|
Settings,
|
|
LogOut,
|
|
CheckCircle,
|
|
Key,
|
|
Users
|
|
} from 'lucide-react';
|
|
|
|
interface LayoutProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
interface VersionInfo {
|
|
build_version: string;
|
|
git_sha: string;
|
|
build_time: string;
|
|
image_tag: string;
|
|
}
|
|
|
|
interface NavLinkProps {
|
|
to: string;
|
|
icon: ReactNode;
|
|
label: string;
|
|
isActive: boolean;
|
|
}
|
|
|
|
function NavLink({ to, icon, label, isActive }: NavLinkProps) {
|
|
return (
|
|
<a
|
|
href={to}
|
|
className={`flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
isActive
|
|
? 'bg-blue-50 text-blue-600'
|
|
: 'text-gray-700 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
<span className="flex-shrink-0">{icon}</span>
|
|
<span>{label}</span>
|
|
</a>
|
|
);
|
|
}
|
|
|
|
interface NavSectionProps {
|
|
title: string;
|
|
children: ReactNode;
|
|
}
|
|
|
|
function NavSection({ title, children }: NavSectionProps) {
|
|
return (
|
|
<div className="space-y-1">
|
|
<div className="px-3 mb-2">
|
|
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
|
{title}
|
|
</h3>
|
|
</div>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function Layout({ children }: LayoutProps) {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const { user, logout } = useAuthStore();
|
|
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null);
|
|
|
|
useEffect(() => {
|
|
const fetchVersion = async () => {
|
|
try {
|
|
const data = await api.getVersion();
|
|
setVersionInfo(data);
|
|
} catch (error) {
|
|
console.error('Failed to fetch version info:', error);
|
|
}
|
|
};
|
|
fetchVersion();
|
|
}, []);
|
|
|
|
const handleLogout = () => {
|
|
logout();
|
|
navigate('/login');
|
|
};
|
|
|
|
const isActive = (path: string, exact = true) => {
|
|
if (exact) {
|
|
return location.pathname === path;
|
|
}
|
|
return location.pathname.startsWith(path);
|
|
};
|
|
|
|
return (
|
|
<div className="flex min-h-screen bg-gray-50">
|
|
{/* Sidebar */}
|
|
<div
|
|
className="w-64 bg-white border-r border-gray-200 flex flex-col"
|
|
style={{
|
|
position: 'sticky',
|
|
top: 0,
|
|
height: '100vh',
|
|
overflowY: 'auto'
|
|
}}
|
|
>
|
|
{/* Logo/Brand */}
|
|
<div className="px-6 py-5 border-b border-gray-200">
|
|
<h1 className="text-lg font-semibold text-gray-900">CannaIQ</h1>
|
|
<p className="text-xs text-gray-500 mt-0.5">{user?.email}</p>
|
|
{versionInfo && (
|
|
<p className="text-xs text-gray-400 mt-1">
|
|
{versionInfo.build_version} ({versionInfo.git_sha.slice(0, 7)})
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 px-3 py-4 space-y-6">
|
|
<NavSection title="Main">
|
|
<NavLink
|
|
to="/dashboard"
|
|
icon={<LayoutDashboard className="w-4 h-4" />}
|
|
label="Dashboard"
|
|
isActive={isActive('/dashboard', true)}
|
|
/>
|
|
<NavLink
|
|
to="/dispensaries"
|
|
icon={<Building2 className="w-4 h-4" />}
|
|
label="Dispensaries"
|
|
isActive={isActive('/dispensaries')}
|
|
/>
|
|
<NavLink
|
|
to="/categories"
|
|
icon={<FolderOpen className="w-4 h-4" />}
|
|
label="Categories"
|
|
isActive={isActive('/categories')}
|
|
/>
|
|
<NavLink
|
|
to="/products"
|
|
icon={<Package className="w-4 h-4" />}
|
|
label="Products"
|
|
isActive={isActive('/products')}
|
|
/>
|
|
<NavLink
|
|
to="/campaigns"
|
|
icon={<Target className="w-4 h-4" />}
|
|
label="Campaigns"
|
|
isActive={isActive('/campaigns')}
|
|
/>
|
|
<NavLink
|
|
to="/analytics"
|
|
icon={<TrendingUp className="w-4 h-4" />}
|
|
label="Analytics"
|
|
isActive={isActive('/analytics')}
|
|
/>
|
|
</NavSection>
|
|
|
|
<NavSection title="AZ Data">
|
|
<NavLink
|
|
to="/wholesale-analytics"
|
|
icon={<TrendingUp className="w-4 h-4" />}
|
|
label="Wholesale Analytics"
|
|
isActive={isActive('/wholesale-analytics')}
|
|
/>
|
|
<NavLink
|
|
to="/az"
|
|
icon={<Store className="w-4 h-4" />}
|
|
label="AZ Stores"
|
|
isActive={isActive('/az', false)}
|
|
/>
|
|
<NavLink
|
|
to="/az-schedule"
|
|
icon={<Calendar className="w-4 h-4" />}
|
|
label="AZ Schedule"
|
|
isActive={isActive('/az-schedule')}
|
|
/>
|
|
</NavSection>
|
|
|
|
<NavSection title="Scraper">
|
|
<NavLink
|
|
to="/scraper-tools"
|
|
icon={<Wrench className="w-4 h-4" />}
|
|
label="Tools"
|
|
isActive={isActive('/scraper-tools')}
|
|
/>
|
|
<NavLink
|
|
to="/scraper-schedule"
|
|
icon={<Clock className="w-4 h-4" />}
|
|
label="Schedule"
|
|
isActive={isActive('/scraper-schedule')}
|
|
/>
|
|
<NavLink
|
|
to="/scraper-monitor"
|
|
icon={<Activity className="w-4 h-4" />}
|
|
label="Monitor"
|
|
isActive={isActive('/scraper-monitor')}
|
|
/>
|
|
</NavSection>
|
|
|
|
<NavSection title="System">
|
|
<NavLink
|
|
to="/changes"
|
|
icon={<CheckCircle className="w-4 h-4" />}
|
|
label="Change Approval"
|
|
isActive={isActive('/changes')}
|
|
/>
|
|
<NavLink
|
|
to="/api-permissions"
|
|
icon={<Key className="w-4 h-4" />}
|
|
label="API Permissions"
|
|
isActive={isActive('/api-permissions')}
|
|
/>
|
|
<NavLink
|
|
to="/proxies"
|
|
icon={<Shield className="w-4 h-4" />}
|
|
label="Proxies"
|
|
isActive={isActive('/proxies')}
|
|
/>
|
|
<NavLink
|
|
to="/logs"
|
|
icon={<FileText className="w-4 h-4" />}
|
|
label="Logs"
|
|
isActive={isActive('/logs')}
|
|
/>
|
|
<NavLink
|
|
to="/settings"
|
|
icon={<Settings className="w-4 h-4" />}
|
|
label="Settings"
|
|
isActive={isActive('/settings')}
|
|
/>
|
|
<NavLink
|
|
to="/users"
|
|
icon={<Users className="w-4 h-4" />}
|
|
label="Users"
|
|
isActive={isActive('/users')}
|
|
/>
|
|
</NavSection>
|
|
</nav>
|
|
|
|
{/* Logout */}
|
|
<div className="px-3 py-4 border-t border-gray-200">
|
|
<button
|
|
onClick={handleLogout}
|
|
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium text-red-600 hover:bg-red-50 transition-colors"
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
<span>Logout</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Version Footer */}
|
|
{versionInfo && (
|
|
<div className="px-3 py-2 border-t border-gray-200 bg-gray-50">
|
|
<p className="text-xs text-gray-500 text-center">
|
|
{versionInfo.build_version} ({versionInfo.git_sha.slice(0, 7)})
|
|
</p>
|
|
<p className="text-xs text-gray-400 text-center mt-0.5">
|
|
{versionInfo.image_tag}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<div className="flex-1 overflow-y-auto">
|
|
<div className="max-w-7xl mx-auto px-8 py-8">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|