Gerenciamento de estado global com Zustand e organização de componentes usando a metodologia Atomic Design.
Vamos combinar o Zustand para estado global com a metodologia Atomic Design para criar uma arquitetura de componentes escalável.
src/components/ ├── atoms/ # Componentes básicos │ ├── Button/ │ │ ├── Button.tsx │ │ ├── Button.stories.tsx │ │ └── index.ts │ ├── Input/ │ ├── Label/ │ ├── Badge/ │ ├── Avatar/ │ └── Icon/ ├── molecules/ # Combinações de atoms │ ├── SearchBox/ │ │ ├── SearchBox.tsx │ │ └── index.ts │ ├── FormField/ │ ├── MetricCard/ │ ├── OrderStatus/ │ └── UserMenu/ ├── organisms/ # Seções complexas │ ├── Header/ │ │ ├── Header.tsx │ │ └── index.ts │ ├── Sidebar/ │ ├── OrderTable/ │ ├── DashboardMetrics/ │ └── LoginForm/ ├── templates/ # Layouts de página │ ├── DashboardLayout/ │ ├── AuthLayout/ │ └── PublicLayout/ └── pages/ # Páginas específicas ├── DashboardPage/ ├── OrdersPage/ └── LoginPage/
import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' import { devtools } from 'zustand/middleware' import { api } from '@/lib/api' import type { User, Restaurant } from '@/types/auth' interface AuthState { // State user: User | null restaurant: Restaurant | null isLoading: boolean isAuthenticated: boolean // Actions sendMagicLink: (email: string) => Promise<void> authenticateFromLink: (code: string) => Promise<void> logout: () => Promise<void> loadUser: () => Promise<void> setLoading: (loading: boolean) => void // Computed isManager: () => boolean hasRestaurant: () => boolean } export const useAuthStore = create<AuthState>()( devtools( persist( (set, get) => ({ // Initial State user: null, restaurant: null, isLoading: false, isAuthenticated: false, // Actions setLoading: (loading: boolean) => { set({ isLoading: loading }, false, 'auth/setLoading') }, sendMagicLink: async (email: string) => { try { set({ isLoading: true }, false, 'auth/sendMagicLink/start') await api.sendAuthLink(email) } catch (error) { console.error('Erro ao enviar magic link:', error) throw error } finally { set({ isLoading: false }, false, 'auth/sendMagicLink/end') } }, authenticateFromLink: async (code: string) => { try { set({ isLoading: true }, false, 'auth/authenticate/start') const response = await api.authenticateFromLink(code) api.setToken(response.token) const profile = await api.getProfile() set({ user: profile.user, restaurant: profile.restaurant, isAuthenticated: true, isLoading: false, }, false, 'auth/authenticate/success') } catch (error) { console.error('Erro na autenticação:', error) set({ isLoading: false }, false, 'auth/authenticate/error') throw error } }, loadUser: async () => { try { set({ isLoading: true }, false, 'auth/loadUser/start') const profile = await api.getProfile() set({ user: profile.user, restaurant: profile.restaurant, isAuthenticated: true, isLoading: false, }, false, 'auth/loadUser/success') } catch (error) { api.clearToken() set({ user: null, restaurant: null, isAuthenticated: false, isLoading: false, }, false, 'auth/loadUser/error') } }, logout: async () => { try { await api.signOut() } catch (error) { console.error('Erro ao fazer logout:', error) } finally { api.clearToken() set({ user: null, restaurant: null, isAuthenticated: false, isLoading: false, }, false, 'auth/logout') } }, // Computed values isManager: () => { const { user } = get() return user?.role === 'manager' }, hasRestaurant: () => { const { restaurant } = get() return !!restaurant }, }), { name: 'auth-storage', storage: createJSONStorage(() => localStorage), partialize: (state) => ({ user: state.user, restaurant: state.restaurant, isAuthenticated: state.isAuthenticated, }), } ), { name: 'auth-store', } ) )
import { create } from 'zustand' import { devtools } from 'zustand/middleware' interface Toast { id: string title: string description?: string type: 'success' | 'error' | 'warning' | 'info' duration?: number } interface Modal { id: string component: React.ComponentType<any> props?: Record<string, any> } interface UIState { // Sidebar sidebarOpen: boolean sidebarCollapsed: boolean // Theme theme: 'light' | 'dark' | 'system' // Toasts toasts: Toast[] // Modals modals: Modal[] // Loading states globalLoading: boolean // Actions toggleSidebar: () => void collapseSidebar: (collapsed: boolean) => void setTheme: (theme: 'light' | 'dark' | 'system') => void addToast: (toast: Omit<Toast, 'id'>) => void removeToast: (id: string) => void openModal: (modal: Omit<Modal, 'id'>) => void closeModal: (id: string) => void setGlobalLoading: (loading: boolean) => void } export const useUIStore = create<UIState>()( devtools( (set, get) => ({ // Initial State sidebarOpen: true, sidebarCollapsed: false, theme: 'system', toasts: [], modals: [], globalLoading: false, // Sidebar Actions toggleSidebar: () => { set( (state) => ({ sidebarOpen: !state.sidebarOpen }), false, 'ui/toggleSidebar' ) }, collapseSidebar: (collapsed: boolean) => { set( { sidebarCollapsed: collapsed }, false, 'ui/collapseSidebar' ) }, // Theme Actions setTheme: (theme: 'light' | 'dark' | 'system') => { set({ theme }, false, 'ui/setTheme') // Apply theme to document if (typeof window !== 'undefined') { const root = window.document.documentElement if (theme === 'dark') { root.classList.add('dark') } else if (theme === 'light') { root.classList.remove('dark') } else { // System theme const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches if (systemTheme) { root.classList.add('dark') } else { root.classList.remove('dark') } } } }, // Toast Actions addToast: (toast: Omit<Toast, 'id'>) => { const id = Math.random().toString(36).substr(2, 9) const newToast = { ...toast, id } set( (state) => ({ toasts: [...state.toasts, newToast] }), false, 'ui/addToast' ) // Auto remove toast const duration = toast.duration || 5000 setTimeout(() => { get().removeToast(id) }, duration) }, removeToast: (id: string) => { set( (state) => ({ toasts: state.toasts.filter(t => t.id !== id) }), false, 'ui/removeToast' ) }, // Modal Actions openModal: (modal: Omit<Modal, 'id'>) => { const id = Math.random().toString(36).substr(2, 9) const newModal = { ...modal, id } set( (state) => ({ modals: [...state.modals, newModal] }), false, 'ui/openModal' ) }, closeModal: (id: string) => { set( (state) => ({ modals: state.modals.filter(m => m.id !== id) }), false, 'ui/closeModal' ) }, // Loading Actions setGlobalLoading: (loading: boolean) => { set({ globalLoading: loading }, false, 'ui/setGlobalLoading') }, }), { name: 'ui-store', } ) )
'use client' import { Moon, Sun, Monitor } from 'lucide-react' import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { useUIStore } from '@/store/ui-store' export function ThemeToggle() { const { theme, setTheme } = useUIStore() return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="icon"> <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <span className="sr-only">Toggle theme</span> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuItem onClick={() => setTheme('light')}> <Sun className="mr-2 h-4 w-4" /> <span>Light</span> </DropdownMenuItem> <DropdownMenuItem onClick={() => setTheme('dark')}> <Moon className="mr-2 h-4 w-4" /> <span>Dark</span> </DropdownMenuItem> <DropdownMenuItem onClick={() => setTheme('system')}> <Monitor className="mr-2 h-4 w-4" /> <span>System</span> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ) }
'use client' import { useEffect } from 'react' import { X, CheckCircle, AlertCircle, AlertTriangle, Info } from 'lucide-react' import { cn } from '@/lib/utils' import { useUIStore } from '@/store/ui-store' interface ToastProps { id: string title: string description?: string type: 'success' | 'error' | 'warning' | 'info' } const icons = { success: CheckCircle, error: AlertCircle, warning: AlertTriangle, info: Info, } const styles = { success: 'bg-green-50 border-green-200 text-green-800', error: 'bg-red-50 border-red-200 text-red-800', warning: 'bg-yellow-50 border-yellow-200 text-yellow-800', info: 'bg-blue-50 border-blue-200 text-blue-800', } export function Toast({ id, title, description, type }: ToastProps) { const { removeToast } = useUIStore() const Icon = icons[type] return ( <div className={cn( 'flex items-start space-x-3 p-4 border rounded-lg shadow-lg', styles[type] )} > <Icon className="h-5 w-5 mt-0.5 flex-shrink-0" /> <div className="flex-1 min-w-0"> <p className="font-medium">{title}</p> {description && ( <p className="text-sm opacity-90 mt-1">{description}</p> )} </div> <button onClick={() => removeToast(id)} className="flex-shrink-0 opacity-70 hover:opacity-100" > <X className="h-4 w-4" /> </button> </div> ) } export function ToastContainer() { const { toasts } = useUIStore() return ( <div className="fixed top-4 right-4 z-50 space-y-2 max-w-sm"> {toasts.map((toast) => ( <Toast key={toast.id} {...toast} /> ))} </div> ) }
'use client' import { LogOut, Settings, User } from 'lucide-react' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { useAuthStore } from '@/store/auth-store' import { useUIStore } from '@/store/ui-store' export function UserMenu() { const { user, logout } = useAuthStore() const { addToast } = useUIStore() if (!user) return null const handleLogout = async () => { try { await logout() addToast({ title: 'Logout realizado', description: 'Você foi desconectado com sucesso.', type: 'success', }) } catch (error) { addToast({ title: 'Erro no logout', description: 'Ocorreu um erro ao desconectar.', type: 'error', }) } } const getInitials = (name: string) => { return name .split(' ') .map(n => n[0]) .join('') .toUpperCase() .slice(0, 2) } return ( <DropdownMenu> <DropdownMenuTrigger className="flex items-center space-x-2 hover:bg-gray-100 rounded-lg p-2 transition-colors"> <Avatar className="h-8 w-8"> <AvatarImage src={user.avatar} alt={user.name} /> <AvatarFallback>{getInitials(user.name)}</AvatarFallback> </Avatar> <div className="hidden md:block text-left"> <p className="text-sm font-medium text-gray-900">{user.name}</p> <p className="text-xs text-gray-500">{user.email}</p> </div> </DropdownMenuTrigger> <DropdownMenuContent className="w-56" align="end"> <DropdownMenuLabel> <div className="flex flex-col space-y-1"> <p className="text-sm font-medium">{user.name}</p> <p className="text-xs text-muted-foreground">{user.email}</p> </div> </DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem> <User className="mr-2 h-4 w-4" /> <span>Perfil</span> </DropdownMenuItem> <DropdownMenuItem> <Settings className="mr-2 h-4 w-4" /> <span>Configurações</span> </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem onClick={handleLogout}> <LogOut className="mr-2 h-4 w-4" /> <span>Sair</span> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ) }
import { LucideIcon } from 'lucide-react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { cn } from '@/lib/utils' interface MetricCardProps { title: string value: string | number description?: string icon: LucideIcon trend?: { value: number label: string type: 'positive' | 'negative' | 'neutral' } className?: string } export function MetricCard({ title, value, description, icon: Icon, trend, className, }: MetricCardProps) { const trendColors = { positive: 'bg-green-100 text-green-800 border-green-200', negative: 'bg-red-100 text-red-800 border-red-200', neutral: 'bg-gray-100 text-gray-800 border-gray-200', } return ( <Card className={cn('', className)}> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-sm font-medium text-muted-foreground"> {title} </CardTitle> <Icon className="h-4 w-4 text-muted-foreground" /> </CardHeader> <CardContent> <div className="text-2xl font-bold">{value}</div> {description && ( <p className="text-xs text-muted-foreground mt-1"> {description} </p> )} {trend && ( <div className="flex items-center mt-2"> <Badge variant="outline" className={cn('text-xs', trendColors[trend.type])} > {trend.type === 'positive' && '+'} {trend.value}% {trend.label} </Badge> </div> )} </CardContent> </Card> ) }
import { useUIStore } from '@/store/ui-store' export function useToast() { const { addToast } = useUIStore() const toast = { success: (title: string, description?: string) => { addToast({ title, description, type: 'success' }) }, error: (title: string, description?: string) => { addToast({ title, description, type: 'error' }) }, warning: (title: string, description?: string) => { addToast({ title, description, type: 'warning' }) }, info: (title: string, description?: string) => { addToast({ title, description, type: 'info' }) }, } return { toast } } // Hook para operações assíncronas com toast export function useAsyncToast() { const { toast } = useToast() const executeWithToast = async <T>( operation: () => Promise<T>, messages: { loading?: string success?: string error?: string } = {} ): Promise<T> => { try { if (messages.loading) { toast.info('Carregando...', messages.loading) } const result = await operation() if (messages.success) { toast.success('Sucesso!', messages.success) } return result } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Erro desconhecido' toast.error('Erro!', messages.error || errorMessage) throw error } } return { executeWithToast, toast } }
Implemente o Zustand com Atomic Design:
Reorganize os componentes seguindo Atomic Design:
# Criar estrutura Atomic Design mkdir -p src/components/{atoms,molecules,organisms,templates,pages} mkdir -p src/components/atoms/{Button,Input,Badge,Avatar,Icon} mkdir -p src/components/molecules/{SearchBox,MetricCard,UserMenu} mkdir -p src/components/organisms/{Header,Sidebar,OrderTable}
Crie os stores do Zustand:
Implemente e teste o sistema de notificações: