From 3022cbfc4b1d4b0109bf47fe14dcb33f5bec2b97 Mon Sep 17 00:00:00 2001 From: Jonas Francisco Date: Wed, 5 Nov 2025 22:23:41 -0300 Subject: [PATCH 1/6] =?UTF-8?q?fix(search):=20mover=20=C3=ADcone=20de=20bu?= =?UTF-8?q?sca=20para=20fora=20do=20input=20e=20estilizar=20limpeza?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- susconecta/components/event-manager.tsx | 1485 +++++++++++++++++ .../features/general/event-manager.tsx | 52 +- 2 files changed, 1521 insertions(+), 16 deletions(-) create mode 100644 susconecta/components/event-manager.tsx diff --git a/susconecta/components/event-manager.tsx b/susconecta/components/event-manager.tsx new file mode 100644 index 0000000..1a19417 --- /dev/null +++ b/susconecta/components/event-manager.tsx @@ -0,0 +1,1485 @@ +"use client" + +import React, { useState, useCallback, useMemo } from "react" +import { Button } from "@/components/ui/button" +import { Card } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Badge } from "@/components/ui/badge" +import { ChevronLeft, ChevronRight, Plus, Calendar, Clock, Grid3x3, List, Search, Filter, X } from "lucide-react" +import { cn } from "@/lib/utils" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuCheckboxItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +export interface Event { + id: string + title: string + description?: string + startTime: Date + endTime: Date + color: string + category?: string + attendees?: string[] + tags?: string[] +} + +export interface EventManagerProps { + events?: Event[] + onEventCreate?: (event: Omit) => void + onEventUpdate?: (id: string, event: Partial) => void + onEventDelete?: (id: string) => void + categories?: string[] + colors?: { name: string; value: string; bg: string; text: string }[] + defaultView?: "month" | "week" | "day" | "list" + className?: string + availableTags?: string[] +} + +const defaultColors = [ + { name: "Blue", value: "blue", bg: "bg-blue-500", text: "text-blue-700" }, + { name: "Green", value: "green", bg: "bg-green-500", text: "text-green-700" }, + { name: "Purple", value: "purple", bg: "bg-purple-500", text: "text-purple-700" }, + { name: "Orange", value: "orange", bg: "bg-orange-500", text: "text-orange-700" }, + { name: "Pink", value: "pink", bg: "bg-pink-500", text: "text-pink-700" }, + { name: "Red", value: "red", bg: "bg-red-500", text: "text-red-700" }, +] + +export function EventManager({ + events: initialEvents = [], + onEventCreate, + onEventUpdate, + onEventDelete, + categories = ["Meeting", "Task", "Reminder", "Personal"], + colors = defaultColors, + defaultView = "month", + className, + availableTags = ["Important", "Urgent", "Work", "Personal", "Team", "Client"], +}: EventManagerProps) { + const [events, setEvents] = useState(initialEvents) + const [currentDate, setCurrentDate] = useState(new Date()) + const [view, setView] = useState<"month" | "week" | "day" | "list">(defaultView) + const [selectedEvent, setSelectedEvent] = useState(null) + const [isDialogOpen, setIsDialogOpen] = useState(false) + const [isCreating, setIsCreating] = useState(false) + const [draggedEvent, setDraggedEvent] = useState(null) + const [newEvent, setNewEvent] = useState>({ + title: "", + description: "", + color: colors[0].value, + category: categories[0], + tags: [], + }) + + const [searchQuery, setSearchQuery] = useState("") + const [selectedColors, setSelectedColors] = useState([]) + const [selectedTags, setSelectedTags] = useState([]) + const [selectedCategories, setSelectedCategories] = useState([]) + + const filteredEvents = useMemo(() => { + return events.filter((event) => { + // Search filter + if (searchQuery) { + const query = searchQuery.toLowerCase() + const matchesSearch = + event.title.toLowerCase().includes(query) || + event.description?.toLowerCase().includes(query) || + event.category?.toLowerCase().includes(query) || + event.tags?.some((tag) => tag.toLowerCase().includes(query)) + + if (!matchesSearch) return false + } + + // Color filter + if (selectedColors.length > 0 && !selectedColors.includes(event.color)) { + return false + } + + // Tag filter + if (selectedTags.length > 0) { + const hasMatchingTag = event.tags?.some((tag) => selectedTags.includes(tag)) + if (!hasMatchingTag) return false + } + + // Category filter + if (selectedCategories.length > 0 && event.category && !selectedCategories.includes(event.category)) { + return false + } + + return true + }) + }, [events, searchQuery, selectedColors, selectedTags, selectedCategories]) + + const hasActiveFilters = selectedColors.length > 0 || selectedTags.length > 0 || selectedCategories.length > 0 + + const clearFilters = () => { + setSelectedColors([]) + setSelectedTags([]) + setSelectedCategories([]) + setSearchQuery("") + } + + const handleCreateEvent = useCallback(() => { + if (!newEvent.title || !newEvent.startTime || !newEvent.endTime) return + + const event: Event = { + id: Math.random().toString(36).substr(2, 9), + title: newEvent.title, + description: newEvent.description, + startTime: newEvent.startTime, + endTime: newEvent.endTime, + color: newEvent.color || colors[0].value, + category: newEvent.category, + attendees: newEvent.attendees, + tags: newEvent.tags || [], + } + + setEvents((prev) => [...prev, event]) + onEventCreate?.(event) + setIsDialogOpen(false) + setIsCreating(false) + setNewEvent({ + title: "", + description: "", + color: colors[0].value, + category: categories[0], + tags: [], + }) + }, [newEvent, colors, categories, onEventCreate]) + + const handleUpdateEvent = useCallback(() => { + if (!selectedEvent) return + + setEvents((prev) => prev.map((e) => (e.id === selectedEvent.id ? selectedEvent : e))) + onEventUpdate?.(selectedEvent.id, selectedEvent) + setIsDialogOpen(false) + setSelectedEvent(null) + }, [selectedEvent, onEventUpdate]) + + const handleDeleteEvent = useCallback( + (id: string) => { + setEvents((prev) => prev.filter((e) => e.id !== id)) + onEventDelete?.(id) + setIsDialogOpen(false) + setSelectedEvent(null) + }, + [onEventDelete], + ) + + const handleDragStart = useCallback((event: Event) => { + setDraggedEvent(event) + }, []) + + const handleDragEnd = useCallback(() => { + setDraggedEvent(null) + }, []) + + const handleDrop = useCallback( + (date: Date, hour?: number) => { + if (!draggedEvent) return + + const duration = draggedEvent.endTime.getTime() - draggedEvent.startTime.getTime() + const newStartTime = new Date(date) + if (hour !== undefined) { + newStartTime.setHours(hour, 0, 0, 0) + } + const newEndTime = new Date(newStartTime.getTime() + duration) + + const updatedEvent = { + ...draggedEvent, + startTime: newStartTime, + endTime: newEndTime, + } + + setEvents((prev) => prev.map((e) => (e.id === draggedEvent.id ? updatedEvent : e))) + onEventUpdate?.(draggedEvent.id, updatedEvent) + setDraggedEvent(null) + }, + [draggedEvent, onEventUpdate], + ) + + const navigateDate = useCallback( + (direction: "prev" | "next") => { + setCurrentDate((prev) => { + const newDate = new Date(prev) + if (view === "month") { + newDate.setMonth(prev.getMonth() + (direction === "next" ? 1 : -1)) + } else if (view === "week") { + newDate.setDate(prev.getDate() + (direction === "next" ? 7 : -7)) + } else if (view === "day") { + newDate.setDate(prev.getDate() + (direction === "next" ? 1 : -1)) + } + return newDate + }) + }, + [view], + ) + + const getColorClasses = useCallback( + (colorValue: string) => { + const color = colors.find((c) => c.value === colorValue) + return color || colors[0] + }, + [colors], + ) + + const toggleTag = (tag: string, isCreating: boolean) => { + if (isCreating) { + setNewEvent((prev) => ({ + ...prev, + tags: prev.tags?.includes(tag) ? prev.tags.filter((t) => t !== tag) : [...(prev.tags || []), tag], + })) + } else { + setSelectedEvent((prev) => + prev + ? { + ...prev, + tags: prev.tags?.includes(tag) ? prev.tags.filter((t) => t !== tag) : [...(prev.tags || []), tag], + } + : null, + ) + } + } + + return ( +
+ {/* Header */} +
+
+

+ {view === "month" && + currentDate.toLocaleDateString("pt-BR", { + month: "long", + year: "numeric", + })} + {view === "week" && + `Semana de ${currentDate.toLocaleDateString("pt-BR", { + month: "short", + day: "numeric", + })}`} + {view === "day" && + currentDate.toLocaleDateString("pt-BR", { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + })} + {view === "list" && "Todos os eventos"} +

+
+ + + +
+
+ +
+ {/* Mobile: Select dropdown */} +
+ +
+ + {/* Desktop: Button group */} +
+ + + + +
+ + +
+
+ +
+
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> + {searchQuery && ( + + )} +
+ + {/* Mobile: Horizontal scroll with full-length buttons */} +
+
+ {/* Color Filter */} + + + + + + Filtrar por Cor + + {colors.map((color) => ( + { + setSelectedColors((prev) => + checked ? [...prev, color.value] : prev.filter((c) => c !== color.value), + ) + }} + > +
+
+ {color.name} +
+ + ))} + + + + {/* Tag Filter */} + + + + + + Filtrar por Tag + + {availableTags.map((tag) => ( + { + setSelectedTags((prev) => (checked ? [...prev, tag] : prev.filter((t) => t !== tag))) + }} + > + {tag} + + ))} + + + + {/* Category Filter */} + + + + + + Filtrar por Categoria + + {categories.map((category) => ( + { + setSelectedCategories((prev) => + checked ? [...prev, category] : prev.filter((c) => c !== category), + ) + }} + > + {category} + + ))} + + + + {hasActiveFilters && ( + + )} +
+
+ + {/* Desktop: Original layout */} +
+ {/* Color Filter */} + + + + + + Filtrar por Cor + + {colors.map((color) => ( + { + setSelectedColors((prev) => + checked ? [...prev, color.value] : prev.filter((c) => c !== color.value), + ) + }} + > +
+
+ {color.name} +
+ + ))} + + + + {/* Tag Filter */} + + + + + + Filtrar por Tag + + {availableTags.map((tag) => ( + { + setSelectedTags((prev) => (checked ? [...prev, tag] : prev.filter((t) => t !== tag))) + }} + > + {tag} + + ))} + + + + {/* Category Filter */} + + + + + + Filtrar por Categoria + + {categories.map((category) => ( + { + setSelectedCategories((prev) => + checked ? [...prev, category] : prev.filter((c) => c !== category), + ) + }} + > + {category} + + ))} + + + + {hasActiveFilters && ( + + )} +
+
+ + {hasActiveFilters && ( +
+ Filtros ativos: + {selectedColors.map((colorValue) => { + const color = getColorClasses(colorValue) + return ( + +
+ {color.name} + + + ) + })} + {selectedTags.map((tag) => ( + + {tag} + + + ))} + {selectedCategories.map((category) => ( + + {category} + + + ))} +
+ )} + + {/* Calendar Views - Pass filteredEvents instead of events */} + {view === "month" && ( + { + setSelectedEvent(event) + setIsDialogOpen(true) + }} + onDragStart={(event) => handleDragStart(event)} + onDragEnd={() => handleDragEnd()} + onDrop={handleDrop} + getColorClasses={getColorClasses} + /> + )} + + {view === "week" && ( + { + setSelectedEvent(event) + setIsDialogOpen(true) + }} + onDragStart={(event) => handleDragStart(event)} + onDragEnd={() => handleDragEnd()} + onDrop={handleDrop} + getColorClasses={getColorClasses} + /> + )} + + {view === "day" && ( + { + setSelectedEvent(event) + setIsDialogOpen(true) + }} + onDragStart={(event) => handleDragStart(event)} + onDragEnd={() => handleDragEnd()} + onDrop={handleDrop} + getColorClasses={getColorClasses} + /> + )} + + {view === "list" && ( + { + setSelectedEvent(event) + setIsDialogOpen(true) + }} + getColorClasses={getColorClasses} + /> + )} + + {/* Event Dialog */} + + + + {isCreating ? "Criar Evento" : "Detalhes do Evento"} + + {isCreating ? "Adicione um novo evento ao seu calendário" : "Visualizar e editar detalhes do evento"} + + + +
+
+ + + isCreating + ? setNewEvent((prev) => ({ ...prev, title: e.target.value })) + : setSelectedEvent((prev) => (prev ? { ...prev, title: e.target.value } : null)) + } + placeholder="Título do evento" + /> +
+ +
+ +