1486 lines
53 KiB
TypeScript

"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<Event, "id">) => void
onEventUpdate?: (id: string, event: Partial<Event>) => 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<Event[]>(initialEvents)
const [currentDate, setCurrentDate] = useState(new Date())
const [view, setView] = useState<"month" | "week" | "day" | "list">(defaultView)
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null)
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [isCreating, setIsCreating] = useState(false)
const [draggedEvent, setDraggedEvent] = useState<Event | null>(null)
const [newEvent, setNewEvent] = useState<Partial<Event>>({
title: "",
description: "",
color: colors[0].value,
category: categories[0],
tags: [],
})
const [searchQuery, setSearchQuery] = useState("")
const [selectedColors, setSelectedColors] = useState<string[]>([])
const [selectedTags, setSelectedTags] = useState<string[]>([])
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
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 (
<div className={cn("flex flex-col gap-4", className)}>
{/* Header */}
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
<h2 className="text-xl font-semibold sm:text-2xl">
{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"}
</h2>
<div className="flex items-center gap-2">
<Button variant="outline" size="icon" onClick={() => navigateDate("prev")} className="h-8 w-8">
<ChevronLeft className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm" onClick={() => setCurrentDate(new Date())}>
Hoje
</Button>
<Button variant="outline" size="icon" onClick={() => navigateDate("next")} className="h-8 w-8">
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
{/* Mobile: Select dropdown */}
<div className="sm:hidden">
<Select value={view} onValueChange={(value: any) => setView(value)}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="month">
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4" />
Mês
</div>
</SelectItem>
<SelectItem value="week">
<div className="flex items-center gap-2">
<Grid3x3 className="h-4 w-4" />
Semana
</div>
</SelectItem>
<SelectItem value="day">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
Dia
</div>
</SelectItem>
<SelectItem value="list">
<div className="flex items-center gap-2">
<List className="h-4 w-4" />
Lista
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{/* Desktop: Button group */}
<div className="hidden sm:flex items-center gap-1 rounded-lg border bg-background p-1">
<Button
variant={view === "month" ? "secondary" : "ghost"}
size="sm"
onClick={() => setView("month")}
className="h-8"
>
<Calendar className="h-4 w-4" />
<span className="ml-1">Mês</span>
</Button>
<Button
variant={view === "week" ? "secondary" : "ghost"}
size="sm"
onClick={() => setView("week")}
className="h-8"
>
<Grid3x3 className="h-4 w-4" />
<span className="ml-1">Semana</span>
</Button>
<Button
variant={view === "day" ? "secondary" : "ghost"}
size="sm"
onClick={() => setView("day")}
className="h-8"
>
<Clock className="h-4 w-4" />
<span className="ml-1">Dia</span>
</Button>
<Button
variant={view === "list" ? "secondary" : "ghost"}
size="sm"
onClick={() => setView("list")}
className="h-8"
>
<List className="h-4 w-4" />
<span className="ml-1">Lista</span>
</Button>
</div>
<Button
onClick={() => {
setIsCreating(true)
setIsDialogOpen(true)
}}
className="w-full sm:w-auto"
>
<Plus className="mr-2 h-4 w-4" />
Novo Evento
</Button>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Buscar eventos..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
{searchQuery && (
<Button
variant="ghost"
size="icon"
className="absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2"
onClick={() => setSearchQuery("")}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
{/* Mobile: Horizontal scroll with full-length buttons */}
<div className="sm:hidden -mx-4 px-4">
<div className="flex gap-2 overflow-x-auto pb-2 scrollbar-hide">
{/* Color Filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 whitespace-nowrap flex-shrink-0 bg-transparent">
<Filter className="h-4 w-4" />
Cores
{selectedColors.length > 0 && (
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
{selectedColors.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel>Filtrar por Cor</DropdownMenuLabel>
<DropdownMenuSeparator />
{colors.map((color) => (
<DropdownMenuCheckboxItem
key={color.value}
checked={selectedColors.includes(color.value)}
onCheckedChange={(checked) => {
setSelectedColors((prev) =>
checked ? [...prev, color.value] : prev.filter((c) => c !== color.value),
)
}}
>
<div className="flex items-center gap-2">
<div className={cn("h-3 w-3 rounded", color.bg)} />
{color.name}
</div>
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{/* Tag Filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 whitespace-nowrap flex-shrink-0 bg-transparent">
<Filter className="h-4 w-4" />
Tags
{selectedTags.length > 0 && (
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
{selectedTags.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel>Filtrar por Tag</DropdownMenuLabel>
<DropdownMenuSeparator />
{availableTags.map((tag) => (
<DropdownMenuCheckboxItem
key={tag}
checked={selectedTags.includes(tag)}
onCheckedChange={(checked) => {
setSelectedTags((prev) => (checked ? [...prev, tag] : prev.filter((t) => t !== tag)))
}}
>
{tag}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{/* Category Filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 whitespace-nowrap flex-shrink-0 bg-transparent">
<Filter className="h-4 w-4" />
Categorias
{selectedCategories.length > 0 && (
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
{selectedCategories.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel>Filtrar por Categoria</DropdownMenuLabel>
<DropdownMenuSeparator />
{categories.map((category) => (
<DropdownMenuCheckboxItem
key={category}
checked={selectedCategories.includes(category)}
onCheckedChange={(checked) => {
setSelectedCategories((prev) =>
checked ? [...prev, category] : prev.filter((c) => c !== category),
)
}}
>
{category}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="gap-2 whitespace-nowrap flex-shrink-0"
>
<X className="h-4 w-4" />
Limpar Filtros
</Button>
)}
</div>
</div>
{/* Desktop: Original layout */}
<div className="hidden sm:flex items-center gap-2">
{/* Color Filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 bg-transparent">
<Filter className="h-4 w-4" />
Cores
{selectedColors.length > 0 && (
<Badge variant="secondary" className="ml-1 h-5 px-1">
{selectedColors.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuLabel>Filtrar por Cor</DropdownMenuLabel>
<DropdownMenuSeparator />
{colors.map((color) => (
<DropdownMenuCheckboxItem
key={color.value}
checked={selectedColors.includes(color.value)}
onCheckedChange={(checked) => {
setSelectedColors((prev) =>
checked ? [...prev, color.value] : prev.filter((c) => c !== color.value),
)
}}
>
<div className="flex items-center gap-2">
<div className={cn("h-3 w-3 rounded", color.bg)} />
{color.name}
</div>
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{/* Tag Filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 bg-transparent">
<Filter className="h-4 w-4" />
Tags
{selectedTags.length > 0 && (
<Badge variant="secondary" className="ml-1 h-5 px-1">
{selectedTags.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuLabel>Filtrar por Tag</DropdownMenuLabel>
<DropdownMenuSeparator />
{availableTags.map((tag) => (
<DropdownMenuCheckboxItem
key={tag}
checked={selectedTags.includes(tag)}
onCheckedChange={(checked) => {
setSelectedTags((prev) => (checked ? [...prev, tag] : prev.filter((t) => t !== tag)))
}}
>
{tag}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{/* Category Filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 bg-transparent">
<Filter className="h-4 w-4" />
Categorias
{selectedCategories.length > 0 && (
<Badge variant="secondary" className="ml-1 h-5 px-1">
{selectedCategories.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuLabel>Filtrar por Categoria</DropdownMenuLabel>
<DropdownMenuSeparator />
{categories.map((category) => (
<DropdownMenuCheckboxItem
key={category}
checked={selectedCategories.includes(category)}
onCheckedChange={(checked) => {
setSelectedCategories((prev) =>
checked ? [...prev, category] : prev.filter((c) => c !== category),
)
}}
>
{category}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{hasActiveFilters && (
<Button variant="ghost" size="sm" onClick={clearFilters} className="gap-2">
<X className="h-4 w-4" />
Limpar
</Button>
)}
</div>
</div>
{hasActiveFilters && (
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-muted-foreground">Filtros ativos:</span>
{selectedColors.map((colorValue) => {
const color = getColorClasses(colorValue)
return (
<Badge key={colorValue} variant="secondary" className="gap-1">
<div className={cn("h-2 w-2 rounded-full", color.bg)} />
{color.name}
<button
onClick={() => setSelectedColors((prev) => prev.filter((c) => c !== colorValue))}
className="ml-1 hover:text-foreground"
>
<X className="h-3 w-3" />
</button>
</Badge>
)
})}
{selectedTags.map((tag) => (
<Badge key={tag} variant="secondary" className="gap-1">
{tag}
<button
onClick={() => setSelectedTags((prev) => prev.filter((t) => t !== tag))}
className="ml-1 hover:text-foreground"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
{selectedCategories.map((category) => (
<Badge key={category} variant="secondary" className="gap-1">
{category}
<button
onClick={() => setSelectedCategories((prev) => prev.filter((c) => c !== category))}
className="ml-1 hover:text-foreground"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
)}
{/* Calendar Views - Pass filteredEvents instead of events */}
{view === "month" && (
<MonthView
currentDate={currentDate}
events={filteredEvents}
onEventClick={(event) => {
setSelectedEvent(event)
setIsDialogOpen(true)
}}
onDragStart={(event) => handleDragStart(event)}
onDragEnd={() => handleDragEnd()}
onDrop={handleDrop}
getColorClasses={getColorClasses}
/>
)}
{view === "week" && (
<WeekView
currentDate={currentDate}
events={filteredEvents}
onEventClick={(event) => {
setSelectedEvent(event)
setIsDialogOpen(true)
}}
onDragStart={(event) => handleDragStart(event)}
onDragEnd={() => handleDragEnd()}
onDrop={handleDrop}
getColorClasses={getColorClasses}
/>
)}
{view === "day" && (
<DayView
currentDate={currentDate}
events={filteredEvents}
onEventClick={(event) => {
setSelectedEvent(event)
setIsDialogOpen(true)
}}
onDragStart={(event) => handleDragStart(event)}
onDragEnd={() => handleDragEnd()}
onDrop={handleDrop}
getColorClasses={getColorClasses}
/>
)}
{view === "list" && (
<ListView
events={filteredEvents}
onEventClick={(event) => {
setSelectedEvent(event)
setIsDialogOpen(true)
}}
getColorClasses={getColorClasses}
/>
)}
{/* Event Dialog */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{isCreating ? "Criar Evento" : "Detalhes do Evento"}</DialogTitle>
<DialogDescription>
{isCreating ? "Adicione um novo evento ao seu calendário" : "Visualizar e editar detalhes do evento"}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title">Título</Label>
<Input
id="title"
value={isCreating ? newEvent.title : selectedEvent?.title}
onChange={(e) =>
isCreating
? setNewEvent((prev) => ({ ...prev, title: e.target.value }))
: setSelectedEvent((prev) => (prev ? { ...prev, title: e.target.value } : null))
}
placeholder="Título do evento"
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Descrição</Label>
<Textarea
id="description"
value={isCreating ? newEvent.description : selectedEvent?.description}
onChange={(e) =>
isCreating
? setNewEvent((prev) => ({
...prev,
description: e.target.value,
}))
: setSelectedEvent((prev) => (prev ? { ...prev, description: e.target.value } : null))
}
placeholder="Descrição do evento"
rows={3}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="startTime">Início</Label>
<Input
id="startTime"
type="datetime-local"
value={
isCreating
? newEvent.startTime
? new Date(newEvent.startTime.getTime() - newEvent.startTime.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16)
: ""
: selectedEvent
? new Date(
selectedEvent.startTime.getTime() - selectedEvent.startTime.getTimezoneOffset() * 60000,
)
.toISOString()
.slice(0, 16)
: ""
}
onChange={(e) => {
const date = new Date(e.target.value)
isCreating
? setNewEvent((prev) => ({ ...prev, startTime: date }))
: setSelectedEvent((prev) => (prev ? { ...prev, startTime: date } : null))
}}
/>
</div>
<div className="space-y-2">
<Label htmlFor="endTime">Fim</Label>
<Input
id="endTime"
type="datetime-local"
value={
isCreating
? newEvent.endTime
? new Date(newEvent.endTime.getTime() - newEvent.endTime.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16)
: ""
: selectedEvent
? new Date(selectedEvent.endTime.getTime() - selectedEvent.endTime.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, 16)
: ""
}
onChange={(e) => {
const date = new Date(e.target.value)
isCreating
? setNewEvent((prev) => ({ ...prev, endTime: date }))
: setSelectedEvent((prev) => (prev ? { ...prev, endTime: date } : null))
}}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="category">Categoria</Label>
<Select
value={isCreating ? newEvent.category : selectedEvent?.category}
onValueChange={(value) =>
isCreating
? setNewEvent((prev) => ({ ...prev, category: value }))
: setSelectedEvent((prev) => (prev ? { ...prev, category: value } : null))
}
>
<SelectTrigger id="category">
<SelectValue placeholder="Selecione a categoria" />
</SelectTrigger>
<SelectContent>
{categories.map((cat) => (
<SelectItem key={cat} value={cat}>
{cat}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="color">Cor</Label>
<Select
value={isCreating ? newEvent.color : selectedEvent?.color}
onValueChange={(value) =>
isCreating
? setNewEvent((prev) => ({ ...prev, color: value }))
: setSelectedEvent((prev) => (prev ? { ...prev, color: value } : null))
}
>
<SelectTrigger id="color">
<SelectValue placeholder="Selecione a cor" />
</SelectTrigger>
<SelectContent>
{colors.map((color) => (
<SelectItem key={color.value} value={color.value}>
<div className="flex items-center gap-2">
<div className={cn("h-4 w-4 rounded", color.bg)} />
{color.name}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label>Tags</Label>
<div className="flex flex-wrap gap-2">
{availableTags.map((tag) => {
const isSelected = isCreating ? newEvent.tags?.includes(tag) : selectedEvent?.tags?.includes(tag)
return (
<Badge
key={tag}
variant={isSelected ? "default" : "outline"}
className="cursor-pointer transition-all hover:scale-105"
onClick={() => toggleTag(tag, isCreating)}
>
{tag}
</Badge>
)
})}
</div>
</div>
</div>
<DialogFooter>
{!isCreating && (
<Button variant="destructive" onClick={() => selectedEvent && handleDeleteEvent(selectedEvent.id)}>
Deletar
</Button>
)}
<Button
variant="outline"
onClick={() => {
setIsDialogOpen(false)
setIsCreating(false)
setSelectedEvent(null)
}}
>
Cancelar
</Button>
<Button onClick={isCreating ? handleCreateEvent : handleUpdateEvent}>
{isCreating ? "Criar" : "Salvar"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}
// EventCard component with hover effect
function EventCard({
event,
onEventClick,
onDragStart,
onDragEnd,
getColorClasses,
variant = "default",
}: {
event: Event
onEventClick: (event: Event) => void
onDragStart: (event: Event) => void
onDragEnd: () => void
getColorClasses: (color: string) => { bg: string; text: string }
variant?: "default" | "compact" | "detailed"
}) {
const [isHovered, setIsHovered] = useState(false)
const colorClasses = getColorClasses(event.color)
const formatTime = (date: Date) => {
return date.toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
})
}
const getDuration = () => {
const diff = event.endTime.getTime() - event.startTime.getTime()
const hours = Math.floor(diff / (1000 * 60 * 60))
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
if (hours > 0) {
return `${hours}h ${minutes}m`
}
return `${minutes}m`
}
if (variant === "compact") {
return (
<div
draggable
onDragStart={() => onDragStart(event)}
onDragEnd={onDragEnd}
onClick={() => onEventClick(event)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className="relative cursor-pointer"
>
<div
className={cn(
"rounded px-1.5 py-0.5 text-xs font-medium transition-all duration-300",
colorClasses.bg,
"text-white truncate animate-in fade-in slide-in-from-top-1",
isHovered && "scale-105 shadow-lg z-10",
)}
>
{event.title}
</div>
{isHovered && (
<div className="absolute left-0 top-full z-50 mt-1 w-64 animate-in fade-in slide-in-from-top-2 duration-200">
<Card className="border-2 p-3 shadow-xl">
<div className="space-y-2">
<div className="flex items-start justify-between gap-2">
<h4 className="font-semibold text-sm leading-tight">{event.title}</h4>
<div className={cn("h-3 w-3 rounded-full flex-shrink-0", colorClasses.bg)} />
</div>
{event.description && <p className="text-xs text-muted-foreground line-clamp-2">{event.description}</p>}
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
<span>
{formatTime(event.startTime)} - {formatTime(event.endTime)}
</span>
<span className="text-[10px]">({getDuration()})</span>
</div>
<div className="flex flex-wrap gap-1">
{event.category && (
<Badge variant="secondary" className="text-[10px] h-5">
{event.category}
</Badge>
)}
{event.tags?.map((tag) => (
<Badge key={tag} variant="outline" className="text-[10px] h-5">
{tag}
</Badge>
))}
</div>
</div>
</Card>
</div>
)}
</div>
)
}
if (variant === "detailed") {
return (
<div
draggable
onDragStart={() => onDragStart(event)}
onDragEnd={onDragEnd}
onClick={() => onEventClick(event)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={cn(
"cursor-pointer rounded-lg p-3 transition-all duration-300",
colorClasses.bg,
"text-white animate-in fade-in slide-in-from-left-2",
isHovered && "scale-[1.03] shadow-2xl ring-2 ring-white/50",
)}
>
<div className="font-semibold">{event.title}</div>
{event.description && <div className="mt-1 text-sm opacity-90 line-clamp-2">{event.description}</div>}
<div className="mt-2 flex items-center gap-2 text-xs opacity-80">
<Clock className="h-3 w-3" />
{formatTime(event.startTime)} - {formatTime(event.endTime)}
</div>
{isHovered && (
<div className="mt-2 flex flex-wrap gap-1 animate-in fade-in slide-in-from-bottom-1 duration-200">
{event.category && (
<Badge variant="secondary" className="text-xs">
{event.category}
</Badge>
)}
{event.tags?.map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">
{tag}
</Badge>
))}
</div>
)}
</div>
)
}
return (
<div
draggable
onDragStart={() => onDragStart(event)}
onDragEnd={onDragEnd}
onClick={() => onEventClick(event)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className="relative"
>
<div
className={cn(
"cursor-pointer rounded px-2 py-1 text-xs font-medium transition-all duration-300",
colorClasses.bg,
"text-white animate-in fade-in slide-in-from-left-1",
isHovered && "scale-105 shadow-lg z-10",
)}
>
<div className="truncate">{event.title}</div>
</div>
{isHovered && (
<div className="absolute left-0 top-full z-50 mt-1 w-72 animate-in fade-in slide-in-from-top-2 duration-200">
<Card className="border-2 p-4 shadow-xl">
<div className="space-y-3">
<div className="flex items-start justify-between gap-2">
<h4 className="font-semibold leading-tight">{event.title}</h4>
<div className={cn("h-4 w-4 rounded-full flex-shrink-0", colorClasses.bg)} />
</div>
{event.description && <p className="text-sm text-muted-foreground">{event.description}</p>}
<div className="space-y-1.5">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Clock className="h-3.5 w-3.5" />
<span>
{formatTime(event.startTime)} - {formatTime(event.endTime)}
</span>
<span className="text-[10px]">({getDuration()})</span>
</div>
<div className="flex flex-wrap gap-1">
{event.category && (
<Badge variant="secondary" className="text-xs">
{event.category}
</Badge>
)}
{event.tags?.map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">
{tag}
</Badge>
))}
</div>
</div>
</div>
</Card>
</div>
)}
</div>
)
}
// Month View Component
function MonthView({
currentDate,
events,
onEventClick,
onDragStart,
onDragEnd,
onDrop,
getColorClasses,
}: {
currentDate: Date
events: Event[]
onEventClick: (event: Event) => void
onDragStart: (event: Event) => void
onDragEnd: () => void
onDrop: (date: Date) => void
getColorClasses: (color: string) => { bg: string; text: string }
}) {
const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1)
const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0)
const startDate = new Date(firstDayOfMonth)
startDate.setDate(startDate.getDate() - startDate.getDay())
const days = []
const currentDay = new Date(startDate)
for (let i = 0; i < 42; i++) {
days.push(new Date(currentDay))
currentDay.setDate(currentDay.getDate() + 1)
}
const getEventsForDay = (date: Date) => {
return events.filter((event) => {
const eventDate = new Date(event.startTime)
return (
eventDate.getDate() === date.getDate() &&
eventDate.getMonth() === date.getMonth() &&
eventDate.getFullYear() === date.getFullYear()
)
})
}
return (
<Card className="overflow-hidden">
<div className="grid grid-cols-7 border-b">
{["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => (
<div key={day} className="border-r p-2 text-center text-xs font-medium last:border-r-0 sm:text-sm">
<span className="hidden sm:inline">{day}</span>
<span className="sm:hidden">{day.charAt(0)}</span>
</div>
))}
</div>
<div className="grid grid-cols-7">
{days.map((day, index) => {
const dayEvents = getEventsForDay(day)
const isCurrentMonth = day.getMonth() === currentDate.getMonth()
const isToday = day.toDateString() === new Date().toDateString()
return (
<div
key={index}
className={cn(
"min-h-20 border-b border-r p-1 transition-colors last:border-r-0 sm:min-h-24 sm:p-2",
!isCurrentMonth && "bg-muted/30",
"hover:bg-accent/50",
)}
onDragOver={(e) => e.preventDefault()}
onDrop={() => onDrop(day)}
>
<div
className={cn(
"mb-1 flex h-5 w-5 items-center justify-center rounded-full text-xs sm:h-6 sm:w-6 sm:text-sm",
isToday && "bg-primary text-primary-foreground font-semibold",
)}
>
{day.getDate()}
</div>
<div className="space-y-1">
{dayEvents.slice(0, 3).map((event) => (
<EventCard
key={event.id}
event={event}
onEventClick={onEventClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
getColorClasses={getColorClasses}
variant="compact"
/>
))}
{dayEvents.length > 3 && (
<div className="text-[10px] text-muted-foreground sm:text-xs">+{dayEvents.length - 3} mais</div>
)}
</div>
</div>
)
})}
</div>
</Card>
)
}
// Week View Component
function WeekView({
currentDate,
events,
onEventClick,
onDragStart,
onDragEnd,
onDrop,
getColorClasses,
}: {
currentDate: Date
events: Event[]
onEventClick: (event: Event) => void
onDragStart: (event: Event) => void
onDragEnd: () => void
onDrop: (date: Date, hour: number) => void
getColorClasses: (color: string) => { bg: string; text: string }
}) {
const startOfWeek = new Date(currentDate)
startOfWeek.setDate(currentDate.getDay())
const weekDays = Array.from({ length: 7 }, (_, i) => {
const day = new Date(startOfWeek)
day.setDate(startOfWeek.getDate() + i)
return day
})
const hours = Array.from({ length: 24 }, (_, i) => i)
const getEventsForDayAndHour = (date: Date, hour: number) => {
return events.filter((event) => {
const eventDate = new Date(event.startTime)
const eventHour = eventDate.getHours()
return (
eventDate.getDate() === date.getDate() &&
eventDate.getMonth() === date.getMonth() &&
eventDate.getFullYear() === date.getFullYear() &&
eventHour === hour
)
})
}
return (
<Card className="overflow-auto">
<div className="grid grid-cols-8 border-b">
<div className="border-r p-2 text-center text-xs font-medium sm:text-sm">Hora</div>
{weekDays.map((day) => (
<div
key={day.toISOString()}
className="border-r p-2 text-center text-xs font-medium last:border-r-0 sm:text-sm"
>
<div className="hidden sm:block">{day.toLocaleDateString("pt-BR", { weekday: "short" })}</div>
<div className="sm:hidden">{day.toLocaleDateString("pt-BR", { weekday: "narrow" })}</div>
<div className="text-[10px] text-muted-foreground sm:text-xs">
{day.toLocaleDateString("pt-BR", { month: "short", day: "numeric" })}
</div>
</div>
))}
</div>
<div className="grid grid-cols-8">
{hours.map((hour) => (
<>
<div
key={`time-${hour}`}
className="border-b border-r p-1 text-[10px] text-muted-foreground sm:p-2 sm:text-xs"
>
{hour.toString().padStart(2, "0")}:00
</div>
{weekDays.map((day) => {
const dayEvents = getEventsForDayAndHour(day, hour)
return (
<div
key={`${day.toISOString()}-${hour}`}
className="min-h-12 border-b border-r p-0.5 transition-colors hover:bg-accent/50 last:border-r-0 sm:min-h-16 sm:p-1"
onDragOver={(e) => e.preventDefault()}
onDrop={() => onDrop(day, hour)}
>
<div className="space-y-1">
{dayEvents.map((event) => (
<EventCard
key={event.id}
event={event}
onEventClick={onEventClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
getColorClasses={getColorClasses}
variant="default"
/>
))}
</div>
</div>
)
})}
</>
))}
</div>
</Card>
)
}
// Day View Component
function DayView({
currentDate,
events,
onEventClick,
onDragStart,
onDragEnd,
onDrop,
getColorClasses,
}: {
currentDate: Date
events: Event[]
onEventClick: (event: Event) => void
onDragStart: (event: Event) => void
onDragEnd: () => void
onDrop: (date: Date, hour: number) => void
getColorClasses: (color: string) => { bg: string; text: string }
}) {
const hours = Array.from({ length: 24 }, (_, i) => i)
const getEventsForHour = (hour: number) => {
return events.filter((event) => {
const eventDate = new Date(event.startTime)
const eventHour = eventDate.getHours()
return (
eventDate.getDate() === currentDate.getDate() &&
eventDate.getMonth() === currentDate.getMonth() &&
eventDate.getFullYear() === currentDate.getFullYear() &&
eventHour === hour
)
})
}
return (
<Card className="overflow-auto">
<div className="space-y-0">
{hours.map((hour) => {
const hourEvents = getEventsForHour(hour)
return (
<div
key={hour}
className="flex border-b last:border-b-0"
onDragOver={(e) => e.preventDefault()}
onDrop={() => onDrop(currentDate, hour)}
>
<div className="w-14 flex-shrink-0 border-r p-2 text-xs text-muted-foreground sm:w-20 sm:p-3 sm:text-sm">
{hour.toString().padStart(2, "0")}:00
</div>
<div className="min-h-16 flex-1 p-1 transition-colors hover:bg-accent/50 sm:min-h-20 sm:p-2">
<div className="space-y-2">
{hourEvents.map((event) => (
<EventCard
key={event.id}
event={event}
onEventClick={onEventClick}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
getColorClasses={getColorClasses}
variant="detailed"
/>
))}
</div>
</div>
</div>
)
})}
</div>
</Card>
)
}
// List View Component
function ListView({
events,
onEventClick,
getColorClasses,
}: {
events: Event[]
onEventClick: (event: Event) => void
getColorClasses: (color: string) => { bg: string; text: string }
}) {
const sortedEvents = [...events].sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
const groupedEvents = sortedEvents.reduce(
(acc, event) => {
const dateKey = event.startTime.toLocaleDateString("pt-BR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})
if (!acc[dateKey]) {
acc[dateKey] = []
}
acc[dateKey].push(event)
return acc
},
{} as Record<string, Event[]>,
)
return (
<Card className="p-3 sm:p-4">
<div className="space-y-6">
{Object.entries(groupedEvents).map(([date, dateEvents]) => (
<div key={date} className="space-y-3">
<h3 className="text-xs font-semibold text-muted-foreground sm:text-sm">{date}</h3>
<div className="space-y-2">
{dateEvents.map((event) => {
const colorClasses = getColorClasses(event.color)
return (
<div
key={event.id}
onClick={() => onEventClick(event)}
className="group cursor-pointer rounded-lg border bg-card p-3 transition-all hover:shadow-md hover:scale-[1.01] animate-in fade-in slide-in-from-bottom-2 duration-300 sm:p-4"
>
<div className="flex items-start gap-2 sm:gap-3">
<div className={cn("mt-1 h-2.5 w-2.5 rounded-full sm:h-3 sm:w-3", colorClasses.bg)} />
<div className="flex-1 min-w-0">
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<h4 className="font-semibold text-sm group-hover:text-primary transition-colors sm:text-base truncate">
{event.title}
</h4>
{event.description && (
<p className="mt-1 text-xs text-muted-foreground sm:text-sm line-clamp-2">
{event.description}
</p>
)}
</div>
<div className="flex flex-wrap gap-1">
{event.category && (
<Badge variant="secondary" className="text-xs">
{event.category}
</Badge>
)}
</div>
</div>
<div className="mt-2 flex flex-wrap items-center gap-2 text-[10px] text-muted-foreground sm:gap-4 sm:text-xs">
<div className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{event.startTime.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })} - {event.endTime.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })}
</div>
{event.tags && event.tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{event.tags.map((tag) => (
<Badge key={tag} variant="outline" className="text-[10px] h-4 sm:text-xs sm:h-5">
{tag}
</Badge>
))}
</div>
)}
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
))}
{sortedEvents.length === 0 && (
<div className="py-12 text-center text-sm text-muted-foreground sm:text-base">Nenhum evento encontrado</div>
)}
</div>
</Card>
)
}