modulo de agente pessoal de IA
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { NavLink as RouterNavLink, NavLinkProps } from "react-router-dom";
|
||||
import { forwardRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface NavLinkCompatProps extends Omit<NavLinkProps, "className"> {
|
||||
className?: string;
|
||||
activeClassName?: string;
|
||||
pendingClassName?: string;
|
||||
}
|
||||
|
||||
const NavLink = forwardRef<HTMLAnchorElement, NavLinkCompatProps>(
|
||||
({ className, activeClassName, pendingClassName, to, ...props }, ref) => {
|
||||
return (
|
||||
<RouterNavLink
|
||||
ref={ref}
|
||||
to={to}
|
||||
className={({ isActive, isPending }) =>
|
||||
cn(className, isActive && activeClassName, isPending && pendingClassName)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
NavLink.displayName = "NavLink";
|
||||
|
||||
export { NavLink };
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface MetricCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
subtitle?: string;
|
||||
icon: ReactNode;
|
||||
status?: "success" | "warning" | "error";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function MetricCard({
|
||||
title,
|
||||
value,
|
||||
subtitle,
|
||||
icon,
|
||||
status,
|
||||
className,
|
||||
}: MetricCardProps) {
|
||||
return (
|
||||
<div className={cn("metric-card animate-fade-in", className)}>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="p-2.5 rounded-lg bg-primary/10 text-primary">
|
||||
{icon}
|
||||
</div>
|
||||
{status && (
|
||||
<span
|
||||
className={cn("status-dot", {
|
||||
"status-connected": status === "success",
|
||||
"status-warning": status === "warning",
|
||||
"status-error": status === "error",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm mb-1">{title}</p>
|
||||
<p className="text-2xl font-semibold text-foreground">{value}</p>
|
||||
{subtitle && (
|
||||
<p className="text-muted-foreground text-xs mt-1">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: "connected" | "error" | "disconnected" | "pending";
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const statusConfig = {
|
||||
connected: {
|
||||
label: "Conectado",
|
||||
className: "bg-success/10 text-success border-success/20",
|
||||
},
|
||||
error: {
|
||||
label: "Erro",
|
||||
className: "bg-destructive/10 text-destructive border-destructive/20",
|
||||
},
|
||||
disconnected: {
|
||||
label: "Desconectado",
|
||||
className: "bg-muted text-muted-foreground border-border",
|
||||
},
|
||||
pending: {
|
||||
label: "Pendente",
|
||||
className: "bg-warning/10 text-warning border-warning/20",
|
||||
},
|
||||
};
|
||||
|
||||
export function StatusBadge({ status, label }: StatusBadgeProps) {
|
||||
const config = statusConfig[status];
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border",
|
||||
config.className
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn("w-1.5 h-1.5 rounded-full", {
|
||||
"bg-success": status === "connected",
|
||||
"bg-destructive": status === "error",
|
||||
"bg-muted-foreground": status === "disconnected",
|
||||
"bg-warning": status === "pending",
|
||||
})}
|
||||
/>
|
||||
{label || config.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Eye, EyeOff, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import asanaLogo from "@/modules/intelligence-ia/assets/asana-logo.png";
|
||||
|
||||
interface Workspace {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface AsanaCardProps {
|
||||
apiKey?: string;
|
||||
workspaces?: Workspace[];
|
||||
users?: User[];
|
||||
selectedWorkspaceId?: string;
|
||||
selectedUserId?: string;
|
||||
loadingWorkspaces?: boolean;
|
||||
loadingUsers?: boolean;
|
||||
loadingIntegration?: boolean;
|
||||
hasIntegration?: boolean;
|
||||
saving?: boolean;
|
||||
onSave?: (apiKey: string, workspaceId: string, userId: string) => void | Promise<void>;
|
||||
onConfirmApiKey?: (apiKey: string) => void | Promise<void>;
|
||||
onWorkspaceChange?: (workspaceId: string) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function AsanaCard({
|
||||
apiKey = "",
|
||||
workspaces = [],
|
||||
users = [],
|
||||
selectedWorkspaceId = "",
|
||||
selectedUserId = "",
|
||||
loadingWorkspaces = false,
|
||||
loadingUsers = false,
|
||||
loadingIntegration = false,
|
||||
hasIntegration = false,
|
||||
saving = false,
|
||||
onSave,
|
||||
onConfirmApiKey,
|
||||
onWorkspaceChange,
|
||||
}: AsanaCardProps) {
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [key, setKey] = useState(apiKey);
|
||||
const [selectedWorkspace, setSelectedWorkspace] = useState(selectedWorkspaceId);
|
||||
const [selectedUser, setSelectedUser] = useState(selectedUserId);
|
||||
const [isApiKeyConfirmed, setIsApiKeyConfirmed] = useState(!!apiKey);
|
||||
|
||||
// Atualiza os valores quando as props mudam (carregamento inicial)
|
||||
useEffect(() => {
|
||||
if (apiKey) {
|
||||
setKey(apiKey);
|
||||
setIsApiKeyConfirmed(true);
|
||||
}
|
||||
}, [apiKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedWorkspaceId) {
|
||||
setSelectedWorkspace(selectedWorkspaceId);
|
||||
}
|
||||
}, [selectedWorkspaceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedUserId) {
|
||||
setSelectedUser(selectedUserId);
|
||||
}
|
||||
}, [selectedUserId]);
|
||||
|
||||
const handleKeyChange = (value: string) => {
|
||||
setKey(value);
|
||||
setSelectedWorkspace("");
|
||||
setSelectedUser("");
|
||||
setIsApiKeyConfirmed(false);
|
||||
};
|
||||
|
||||
const handleConfirmApiKey = async () => {
|
||||
if (key.length >= 5) {
|
||||
setIsApiKeyConfirmed(true);
|
||||
// Chama a função assíncrona para buscar workspaces
|
||||
await onConfirmApiKey?.(key);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWorkspaceChange = async (value: string) => {
|
||||
setSelectedWorkspace(value);
|
||||
setSelectedUser("");
|
||||
// Chama a função assíncrona para buscar usuários
|
||||
await onWorkspaceChange?.(value);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
await onSave?.(key, selectedWorkspace, selectedUser);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="metric-card space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-muted flex items-center justify-center">
|
||||
<img src={asanaLogo} alt="Asana" className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-foreground font-medium">Asana</h3>
|
||||
<p className="text-muted-foreground text-sm">Gerencie tarefas e projetos</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||
API Key / Token
|
||||
</label>
|
||||
<div className="relative">
|
||||
{loadingIntegration ? (
|
||||
<div className="relative">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
type={showKey ? "text" : "password"}
|
||||
value={key}
|
||||
onChange={(e) => handleKeyChange(e.target.value)}
|
||||
placeholder="Insira sua chave de API"
|
||||
className="pr-10 bg-card border-border"
|
||||
disabled={loadingWorkspaces}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowKey(!showKey)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
disabled={loadingWorkspaces}
|
||||
>
|
||||
{showKey ? (
|
||||
<EyeOff className="w-4 h-4" />
|
||||
) : (
|
||||
<Eye className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleConfirmApiKey}
|
||||
disabled={key.length < 5 || isApiKeyConfirmed || loadingWorkspaces}
|
||||
className="shrink-0"
|
||||
>
|
||||
{loadingWorkspaces ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Carregando...
|
||||
</>
|
||||
) : isApiKeyConfirmed ? (
|
||||
"Confirmado"
|
||||
) : (
|
||||
"Confirmar"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||
Workspace
|
||||
</label>
|
||||
{loadingIntegration ? (
|
||||
<div className="relative">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<div className="absolute inset-0 flex items-center justify-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">Carregando integração...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Select
|
||||
value={selectedWorkspace}
|
||||
onValueChange={handleWorkspaceChange}
|
||||
disabled={loadingWorkspaces || loadingIntegration}
|
||||
>
|
||||
<SelectTrigger className="bg-card border-border">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
loadingWorkspaces
|
||||
? "Carregando workspaces..."
|
||||
: workspaces.length === 0
|
||||
? "Insira o token e clique em Confirmar"
|
||||
: "Selecione um workspace"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-card border-border">
|
||||
{workspaces.map((ws) => (
|
||||
<SelectItem key={ws.id} value={ws.id}>
|
||||
{ws.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{loadingWorkspaces && (
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||
Usuário
|
||||
</label>
|
||||
{loadingUsers ? (
|
||||
<div className="relative">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<div className="absolute inset-0 flex items-center justify-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">Carregando usuários...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Select
|
||||
value={selectedUser}
|
||||
onValueChange={setSelectedUser}
|
||||
disabled={loadingUsers || !selectedWorkspace}
|
||||
>
|
||||
<SelectTrigger className="bg-card border-border">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
!selectedWorkspace
|
||||
? "Selecione um workspace primeiro"
|
||||
: users.length === 0
|
||||
? "Nenhum usuário encontrado"
|
||||
: "Selecione um usuário"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-card border-border">
|
||||
{users.map((user) => (
|
||||
<SelectItem key={user.id} value={user.id}>
|
||||
{user.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={!selectedWorkspace || !selectedUser || saving}
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
{hasIntegration ? "Salvando..." : "Criando..."}
|
||||
</>
|
||||
) : hasIntegration ? (
|
||||
"Salvar"
|
||||
) : (
|
||||
"Criar Integração"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import googleCalendarLogo from "@/modules/intelligence-ia/assets/google-calendar-logo.png";
|
||||
|
||||
export function GoogleCalendarCard() {
|
||||
return (
|
||||
<div className="metric-card space-y-4 opacity-60">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-muted flex items-center justify-center">
|
||||
<img src={googleCalendarLogo} alt="Google Calendar" className="w-6 h-6 grayscale" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-foreground font-medium">Google Calendar</h3>
|
||||
<span className="text-xs bg-muted text-muted-foreground px-2 py-0.5 rounded-full">
|
||||
Em breve
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">Sincronize reuniões e eventos</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 pointer-events-none">
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||
Client ID
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
disabled
|
||||
placeholder="Insira o Client ID"
|
||||
className="bg-card border-border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||
Client Secret
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
disabled
|
||||
placeholder="Insira o Client Secret"
|
||||
className="bg-card border-border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||
Nome da Agenda
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
disabled
|
||||
placeholder="Ex: Reuniões de Trabalho"
|
||||
className="bg-card border-border"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Salvar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { FileSpreadsheet } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export function GoogleSheetsCard() {
|
||||
return (
|
||||
<div className="metric-card space-y-4 opacity-60">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-xl bg-muted flex items-center justify-center">
|
||||
<FileSpreadsheet className="w-6 h-6 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-foreground font-medium">Google Sheets</h3>
|
||||
<span className="text-xs bg-muted text-muted-foreground px-2 py-0.5 rounded-full">
|
||||
Em breve
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">Gerencie planilhas e dados</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 pointer-events-none">
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||
Client ID
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
disabled
|
||||
placeholder="Insira o Client ID"
|
||||
className="bg-card border-border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||
Client Secret
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
disabled
|
||||
placeholder="Insira o Client Secret"
|
||||
className="bg-card border-border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground mb-1.5 block">
|
||||
Link da Planilha
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
disabled
|
||||
placeholder="Cole o link da planilha aqui"
|
||||
className="bg-card border-border"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Salvar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { useState } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Plug,
|
||||
Bot,
|
||||
ChevronLeft,
|
||||
Menu,
|
||||
X,
|
||||
DollarSign,
|
||||
UserCircle,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const navItems = [
|
||||
{ title: "Home", path: "", icon: LayoutDashboard },
|
||||
{ title: "Finanças", path: "financas", icon: DollarSign },
|
||||
{ title: "Integrações", path: "integracoes", icon: Plug },
|
||||
{ title: "Meu Perfil", path: "meu-perfil", icon: UserCircle },
|
||||
];
|
||||
|
||||
export function AppSidebar() {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
const SidebarContent = () => (
|
||||
<>
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3 px-4 py-6 border-b border-sidebar-border">
|
||||
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center cyber-glow">
|
||||
<Bot className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className="animate-fade-in">
|
||||
<h1 className="text-sidebar-foreground font-semibold text-lg">AI Agent</h1>
|
||||
<p className="text-muted-foreground text-xs">Painel Admin</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-3 py-4 space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
end={item.path === ""} // Apenas a Home precisa de match exato
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"nav-item flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 w-full",
|
||||
isActive
|
||||
? "text-primary bg-sidebar-accent font-medium"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-sidebar-accent"
|
||||
)
|
||||
}
|
||||
>
|
||||
<item.icon className="w-5 h-5 flex-shrink-0" />
|
||||
{!collapsed && <span className="animate-fade-in">{item.title}</span>}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Collapse button - desktop only */}
|
||||
<div className="hidden lg:block px-3 py-4 border-t border-sidebar-border">
|
||||
<button
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
className="nav-item flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 w-full justify-center lg:justify-start text-muted-foreground hover:text-foreground hover:bg-sidebar-accent"
|
||||
>
|
||||
<ChevronLeft
|
||||
className={cn(
|
||||
"w-5 h-5 transition-transform duration-300 flex-shrink-0",
|
||||
collapsed && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
{!collapsed && <span>Recolher</span>}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
onClick={() => setMobileOpen(true)}
|
||||
className="lg:hidden fixed top-4 left-4 z-50 p-2 rounded-lg bg-card border border-border shadow-sm"
|
||||
>
|
||||
<Menu className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Mobile overlay */}
|
||||
{mobileOpen && (
|
||||
<div
|
||||
className="lg:hidden fixed inset-0 bg-background/80 backdrop-blur-sm z-40"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mobile sidebar */}
|
||||
<aside
|
||||
className={cn(
|
||||
"lg:hidden fixed left-0 top-0 h-full w-64 bg-sidebar z-50 flex flex-col border-r border-sidebar-border transition-transform duration-300",
|
||||
mobileOpen ? "translate-x-0" : "-translate-x-full"
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="absolute top-4 right-4 p-2 rounded-lg hover:bg-sidebar-accent"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
<SidebarContent />
|
||||
</aside>
|
||||
|
||||
{/* Desktop sidebar */}
|
||||
<aside
|
||||
className={cn(
|
||||
"hidden lg:flex flex-col h-screen bg-sidebar border-r border-sidebar-border transition-all duration-300 sticky top-0",
|
||||
collapsed ? "w-[72px]" : "w-64"
|
||||
)}
|
||||
>
|
||||
<SidebarContent />
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ReactNode } from "react";
|
||||
import { AppSidebar } from "./AppSidebar";
|
||||
|
||||
interface MainLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function MainLayout({ children }: MainLayoutProps) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full bg-background">
|
||||
<AppSidebar />
|
||||
<main className="flex-1 overflow-auto">
|
||||
<div className="p-4 lg:p-8 pt-16 lg:pt-8">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user