Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "elo",
"version": "1.51.0",
"version": "1.51.1",
"private": true,
"type": "module",
"scripts": {
Expand Down
10 changes: 8 additions & 2 deletions src/app/(authenticated)/admin/suggestions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1938,6 +1938,7 @@ export default function AdminSuggestionsPage() {
// Menu de ações (clique direito no card): editar ou excluir
const [actionsSuggestion, setActionsSuggestion] = useState<SuggestionLocal | null>(null)
const [isActionsDialogOpen, setIsActionsDialogOpen] = useState(false)
const [actionsPosition, setActionsPosition] = useState<{ x: number; y: number } | null>(null)

const deleteSuggestionMutation = api.suggestion.deleteAdmin.useMutation({
onSuccess: () => {
Expand All @@ -1955,8 +1956,9 @@ export default function AdminSuggestionsPage() {
},
})

const openActionsDialog = (suggestion: SuggestionLocal) => {
const openActionsDialog = (suggestion: SuggestionLocal, position: { x: number; y: number }) => {
setActionsSuggestion(suggestion)
setActionsPosition(position)
setIsActionsDialogOpen(true)
}

Expand Down Expand Up @@ -2618,8 +2620,12 @@ export default function AdminSuggestionsPage() {
open={isActionsDialogOpen}
onOpenChange={(open) => {
setIsActionsDialogOpen(open)
if (!open) setActionsSuggestion(null)
if (!open) {
setActionsSuggestion(null)
setActionsPosition(null)
}
}}
position={actionsPosition}
ideaNumber={actionsSuggestion?.ideaNumber}
isDeleting={deleteSuggestionMutation.isPending}
onEdit={() => {
Expand Down
95 changes: 76 additions & 19 deletions src/components/admin/suggestion/idea-actions-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client"

import { useEffect, useState } from "react"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { useEffect, useLayoutEffect, useRef, useState } from "react"
import { createPortal } from "react-dom"
import { Button } from "@/components/ui/button"
import { Edit, Trash2, Loader2, AlertTriangle } from "lucide-react"

Expand All @@ -12,6 +12,8 @@ function formatIdeaNumber(ideaNumber: number): string {
export interface IdeaActionsDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
/** Posição do cursor onde o menu deve surgir. */
position?: { x: number; y: number } | null
/** Número da ideia exibido no cabeçalho (ex.: #042). */
ideaNumber?: number
/** Acionado ao escolher "Editar". */
Expand All @@ -22,38 +24,90 @@ export interface IdeaActionsDialogProps {
isDeleting?: boolean
}

const MENU_WIDTH = 224 // w-56
const VIEWPORT_MARGIN = 8

/**
* Menu de ações de uma ideia (acionado por clique direito no card do kanban).
* Dialog enxuto, com sombra suave ao redor, oferecendo Editar ou Excluir
* (a exclusão exige confirmação dentro do próprio dialog).
* Surge discretamente ao lado do cursor — sem overlay escurecendo a tela —
* oferecendo Editar ou Excluir (a exclusão exige confirmação no próprio menu).
*/
export function IdeaActionsDialog({
open,
onOpenChange,
position,
ideaNumber,
onEdit,
onDelete,
isDeleting = false,
}: IdeaActionsDialogProps) {
const [confirmingDelete, setConfirmingDelete] = useState(false)
const [mounted, setMounted] = useState(false)
const panelRef = useRef<HTMLDivElement>(null)
const [coords, setCoords] = useState<{ top: number; left: number }>({ top: 0, left: 0 })

useEffect(() => setMounted(true), [])

// Reinicia para a tela inicial sempre que o dialog reabrir.
// Reinicia para a tela inicial sempre que o menu fechar.
useEffect(() => {
if (!open) setConfirmingDelete(false)
}, [open])

// Fechar com ESC.
useEffect(() => {
if (!open) return
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onOpenChange(false)
}
window.addEventListener("keydown", onKey)
return () => window.removeEventListener("keydown", onKey)
}, [open, onOpenChange])

// Posiciona próximo ao cursor, mantendo o menu dentro da viewport.
useLayoutEffect(() => {
if (!open || !position) return
const panel = panelRef.current
const width = panel?.offsetWidth ?? MENU_WIDTH
const height = panel?.offsetHeight ?? 0
let left = position.x
let top = position.y
if (left + width + VIEWPORT_MARGIN > window.innerWidth) {
left = window.innerWidth - width - VIEWPORT_MARGIN
}
if (top + height + VIEWPORT_MARGIN > window.innerHeight) {
top = window.innerHeight - height - VIEWPORT_MARGIN
}
setCoords({
top: Math.max(VIEWPORT_MARGIN, top),
left: Math.max(VIEWPORT_MARGIN, left),
})
}, [open, position, confirmingDelete])

if (!mounted || !open || !position) return null

const title = ideaNumber !== undefined ? `Ideia #${formatIdeaNumber(ideaNumber)}` : "Ideia"

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
hideClose
className="max-w-[20rem] gap-0 overflow-hidden rounded-xl border-border/60 p-0 shadow-2xl"
return createPortal(
// Camada transparente apenas para capturar cliques fora e fechar o menu.
<div
className="fixed inset-0 z-50"
onMouseDown={() => onOpenChange(false)}
onContextMenu={(e) => {
e.preventDefault()
onOpenChange(false)
}}
>
<div
ref={panelRef}
role="menu"
style={{ top: coords.top, left: coords.left }}
onMouseDown={(e) => e.stopPropagation()}
className="fixed w-56 overflow-hidden rounded-xl border border-border/60 bg-popover text-popover-foreground shadow-[0_12px_28px_-10px_rgba(0,0,0,0.4)] duration-150 animate-in fade-in-0 zoom-in-95"
>
{confirmingDelete ? (
<div className="p-5">
<div className="p-4">
<div className="flex items-start gap-3">
<div className="mt-0.5 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-destructive/10 text-destructive">
<div className="mt-0.5 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-destructive/10 text-destructive">
<AlertTriangle className="h-4 w-4" aria-hidden />
</div>
<div className="space-y-1">
Expand All @@ -63,7 +117,7 @@ export function IdeaActionsDialog({
</p>
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<div className="mt-4 flex justify-end gap-2">
<Button
variant="outline"
size="sm"
Expand Down Expand Up @@ -91,29 +145,32 @@ export function IdeaActionsDialog({
</div>
</div>
) : (
<div className="p-2">
<div className="px-3 pb-2 pt-2">
<div className="p-1.5">
<div className="px-2.5 pb-1.5 pt-1">
<p className="text-xs font-medium text-muted-foreground">{title}</p>
</div>
<button
type="button"
role="menuitem"
onClick={onEdit}
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm font-medium transition-colors hover:bg-muted focus:bg-muted focus:outline-none"
className="flex w-full items-center gap-2.5 rounded-md px-2.5 py-2 text-left text-sm font-medium transition-colors hover:bg-muted focus:bg-muted focus:outline-none"
>
<Edit className="h-4 w-4 text-muted-foreground" aria-hidden />
Editar
</button>
<button
type="button"
role="menuitem"
onClick={() => setConfirmingDelete(true)}
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left text-sm font-medium text-destructive transition-colors hover:bg-destructive/10 focus:bg-destructive/10 focus:outline-none"
className="flex w-full items-center gap-2.5 rounded-md px-2.5 py-2 text-left text-sm font-medium text-destructive transition-colors hover:bg-destructive/10 focus:bg-destructive/10 focus:outline-none"
>
<Trash2 className="h-4 w-4" aria-hidden />
Excluir
</button>
</div>
)}
</DialogContent>
</Dialog>
</div>
</div>,
document.body,
)
}
4 changes: 2 additions & 2 deletions src/components/admin/suggestion/suggestions-kanban-board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export interface SuggestionsKanbanBoardProps<
onDragEnd: OnDragEndResponder
onOpenSuggestion: (suggestion: T) => void
/** Clique direito sobre um card — recebe a ideia e a posição do cursor. */
onContextMenuSuggestion?: (suggestion: T) => void
onContextMenuSuggestion?: (suggestion: T, position: { x: number; y: number }) => void
}

const SKELETON_CARDS_PER_COLUMN = 3
Expand Down Expand Up @@ -155,7 +155,7 @@ export function SuggestionsKanbanBoard<T extends SuggestionsKanbanCard>({
onContextMenu={(e) => {
if (!onContextMenuSuggestion) return
e.preventDefault()
onContextMenuSuggestion(s)
onContextMenuSuggestion(s, { x: e.clientX, y: e.clientY })
}}
>
<CardContent className="p-2 md:p-3">
Expand Down
Loading