refactor: atualiza dialog de confirmação para AlertDialog no EssayPage

- Substitui Dialog básico pelo AlertDialog especializado
- Melhora feedback visual na confirmação de deleção
- Mantém consistência com o design system
- Implementa padrões de acessibilidade do Radix UI
This commit is contained in:
Lucas Santana 2025-02-06 21:26:46 -03:00
parent d1e44f84b7
commit da62f5e722
10 changed files with 594 additions and 97 deletions

274
package-lock.json generated
View File

@ -16,8 +16,10 @@
"@opentelemetry/sdk-metrics": "^1.30.1",
"@opentelemetry/sdk-trace-web": "^1.30.1",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.4",
"@sentry/react": "^8.48.0",
@ -46,6 +48,7 @@
},
"devDependencies": {
"@eslint/js": "^9.9.1",
"@shadcn/ui": "^0.0.4",
"@testing-library/jest-dom": "^6.6.3",
"@types/react": "^18.3.17",
"@types/react-dom": "^18.3.5",
@ -2204,6 +2207,57 @@
}
}
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz",
"integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dialog": "1.1.6",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collapsible": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz",
@ -2260,6 +2314,24 @@
}
}
},
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
@ -2291,25 +2363,99 @@
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz",
"integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==",
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.3",
"@radix-ui/react-dismissable-layer": "1.1.5",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.2",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-portal": "1.1.3",
"@radix-ui/react-portal": "1.1.4",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "^2.6.1"
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
@ -2384,13 +2530,13 @@
}
},
"node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz",
"integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
"integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
@ -2408,6 +2554,29 @@
}
}
},
"node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
@ -2497,6 +2666,24 @@
}
}
},
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-progress": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.1.tgz",
@ -2553,9 +2740,9 @@
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
@ -3133,6 +3320,49 @@
"react": "^16.14.0 || 17.x || 18.x || 19.x"
}
},
"node_modules/@shadcn/ui": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@shadcn/ui/-/ui-0.0.4.tgz",
"integrity": "sha512-0dtu/5ApsOZ24qgaZwtif8jVwqol7a4m1x5AxPuM1k5wxhqU7t/qEfBGtaSki1R8VlbTQfCj5PAlO45NKCa7Gg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "5.2.0",
"commander": "^10.0.0",
"execa": "^7.0.0",
"fs-extra": "^11.1.0",
"node-fetch": "^3.3.0",
"ora": "^6.1.2",
"prompts": "^2.4.2",
"zod": "^3.20.2"
},
"bin": {
"ui": "dist/index.js"
}
},
"node_modules/@shadcn/ui/node_modules/chalk": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz",
"integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@shadcn/ui/node_modules/commander": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@ -8229,16 +8459,16 @@
}
},
"node_modules/react-remove-scroll": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz",
"integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==",
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz",
"integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.7",
"react-style-singleton": "^2.2.1",
"react-style-singleton": "^2.2.3",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.3",
"use-sidecar": "^1.1.2"
"use-sidecar": "^1.1.3"
},
"engines": {
"node": ">=10"

View File

@ -23,8 +23,10 @@
"@opentelemetry/sdk-metrics": "^1.30.1",
"@opentelemetry/sdk-trace-web": "^1.30.1",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.4",
"@sentry/react": "^8.48.0",
@ -53,6 +55,7 @@
},
"devDependencies": {
"@eslint/js": "^9.9.1",
"@shadcn/ui": "^0.0.4",
"@testing-library/jest-dom": "^6.6.3",
"@types/react": "^18.3.17",
"@types/react-dom": "^18.3.5",

View File

@ -0,0 +1,139 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -7,11 +7,41 @@ import { EVENT_CATEGORIES } from '../../constants/analytics';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
as?: 'button' | 'span';
trackingId: string;
variant?: 'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'link';
variant?: 'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'link' | 'destructive';
size?: 'sm' | 'md' | 'lg';
trackingProperties?: ButtonTrackingOptions;
}
export function buttonVariants({
variant = 'default',
size = 'md',
className = '',
}: {
variant?: ButtonProps['variant'];
size?: ButtonProps['size'];
className?: string;
} = {}) {
return cn(
'inline-flex items-center justify-center px-4 py-2',
'text-sm font-medium',
'rounded-md shadow-sm',
'transition-colors duration-200',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'text-white bg-purple-600 hover:bg-purple-700': variant === 'primary' || variant === 'default',
'text-gray-700 bg-white border border-gray-300 hover:bg-gray-50': variant === 'secondary',
'text-purple-600 bg-transparent hover:bg-purple-50': variant === 'ghost',
'text-purple-600 bg-transparent hover:underline': variant === 'link',
'text-purple-600 border border-purple-600 hover:bg-purple-50': variant === 'outline',
'text-white bg-red-600 hover:bg-red-700': variant === 'destructive',
'px-3 py-1.5 text-sm': size === 'sm',
'px-4 py-2 text-base': size === 'md',
'px-6 py-3 text-lg': size === 'lg',
},
className
);
}
export function Button({
as: Component = 'button',
children,
@ -41,29 +71,10 @@ export function Button({
onClick?.(event);
};
const baseStyles = cn(
'inline-flex items-center justify-center px-4 py-2',
'text-sm font-medium',
'rounded-md shadow-sm',
'transition-colors duration-200',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'text-white bg-purple-600 hover:bg-purple-700': variant === 'primary' || variant === 'default',
'text-gray-700 bg-white border border-gray-300 hover:bg-gray-50': variant === 'secondary',
'text-purple-600 bg-transparent hover:bg-purple-50': variant === 'ghost',
'text-purple-600 bg-transparent hover:underline': variant === 'link',
'text-purple-600 border border-purple-600 hover:bg-purple-50': variant === 'outline',
'px-3 py-1.5 text-sm': size === 'sm',
'px-4 py-2 text-base': size === 'md',
'px-6 py-3 text-lg': size === 'lg',
},
className
);
return (
<Component
type={Component === 'button' ? type : undefined}
className={baseStyles}
className={buttonVariants({ variant, size, className })}
onClick={handleClick}
disabled={disabled}
{...props}

View File

@ -8,13 +8,7 @@ if (!supabaseUrl || !supabaseAnonKey) {
throw new Error('Variáveis de ambiente do Supabase não configuradas')
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true
}
})
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
export const generateStoryFunction = async (prompt: StoryPrompt) => {
const { data: { session } } = await supabase.auth.getSession()

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { useParams, useNavigate } from 'react-router-dom';
import { supabase } from '@/lib/supabase';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -40,10 +40,9 @@ interface Essay {
};
}
export default function EssayAnalysisPage() {
const router = useRouter();
const { id } = router.query;
const { supabase } = supabase();
export function EssayAnalysis() {
const navigate = useNavigate();
const { id } = useParams();
const [analysis, setAnalysis] = useState<EssayAnalysis | null>(null);
const [essay, setEssay] = useState<Essay | null>(null);
const [loading, setLoading] = useState(true);
@ -94,7 +93,16 @@ export default function EssayAnalysisPage() {
return (
<div className="container mx-auto p-6">
<div className="flex items-center gap-4 mb-6">
<Button variant="ghost" onClick={() => router.push(`/student-dashboard/essays/${id}`)}>
<Button
variant="ghost"
onClick={() => navigate(`/aluno/redacoes/${id}`)}
trackingId="essay-analysis-back-button"
trackingProperties={{
action: 'back_to_essay',
page: 'essay_analysis',
essay_id: id
}}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Voltar para redação
</Button>

View File

@ -1,12 +1,22 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { useParams, useNavigate } from 'react-router-dom';
import { supabase } from '@/lib/supabase';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { ArrowLeft, Save, Send } from 'lucide-react';
import { ArrowLeft, Save, Send, Trash2 } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
interface Essay {
id: string;
@ -30,14 +40,14 @@ interface Essay {
};
}
export default function EssayPage() {
const router = useRouter();
const { id } = router.query;
const { supabase } = supabase();
export function EssayPage() {
const navigate = useNavigate();
const { id } = useParams();
const [essay, setEssay] = useState<Essay | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [wordCount, setWordCount] = useState(0);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
useEffect(() => {
if (id) {
@ -117,12 +127,27 @@ export default function EssayPage() {
if (analysisError) throw analysisError;
// Redireciona para a página de análise
router.push(`/student-dashboard/essays/${essay.id}/analysis`);
navigate(`/aluno/redacoes/${essay.id}/analise`);
} catch (error) {
console.error('Erro ao enviar para análise:', error);
}
}
async function deleteEssay() {
if (!essay) return;
try {
const { error } = await supabase
.from('student_essays')
.delete()
.eq('id', essay.id);
if (error) throw error;
navigate('/aluno/redacoes');
} catch (error) {
console.error('Erro ao deletar redação:', error);
}
}
if (loading) return <div>Carregando...</div>;
if (!essay) return <div>Redação não encontrada</div>;
@ -134,7 +159,15 @@ export default function EssayPage() {
<div className="container mx-auto p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<Button variant="ghost" onClick={() => router.push('/student-dashboard/essays')}>
<Button
variant="ghost"
onClick={() => navigate('/aluno/redacoes')}
trackingId="essay-back-to-list-button"
trackingProperties={{
action: 'back_to_essays_list',
page: 'essay_editor'
}}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Voltar
</Button>
@ -150,26 +183,58 @@ export default function EssayPage() {
<span></span>
<span>{essay.essay_genre.title}</span>
<span></span>
<Badge variant={essay.status === 'draft' ? 'secondary' : 'primary'}>
<Badge variant={essay.status === 'draft' ? 'secondary' : 'default'}>
{essay.status === 'draft' ? 'Rascunho' : 'Enviada'}
</Badge>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={saveEssay} disabled={saving}>
<Button
variant="outline"
onClick={saveEssay}
disabled={saving}
trackingId="essay-save-button"
trackingProperties={{
action: 'save_essay',
page: 'essay_editor',
status: essay.status
}}
>
<Save className="mr-2 h-4 w-4" />
{saving ? 'Salvando...' : 'Salvar'}
</Button>
{essay.status === 'draft' && (
<Button
onClick={submitForAnalysis}
disabled={!isWithinWordLimit}
title={!isWithinWordLimit ? 'Número de palavras fora do limite' : ''}
>
<Send className="mr-2 h-4 w-4" />
Enviar para análise
</Button>
<>
<Button
onClick={submitForAnalysis}
disabled={!isWithinWordLimit}
title={!isWithinWordLimit ? 'Número de palavras fora do limite' : ''}
trackingId="essay-submit-analysis-button"
trackingProperties={{
action: 'submit_for_analysis',
page: 'essay_editor',
word_count: wordCount,
within_limit: isWithinWordLimit
}}
>
<Send className="mr-2 h-4 w-4" />
Enviar para análise
</Button>
<Button
variant="destructive"
onClick={() => setShowDeleteDialog(true)}
trackingId="essay-delete-button"
trackingProperties={{
action: 'delete_essay',
page: 'essay_editor',
status: essay.status
}}
>
<Trash2 className="mr-2 h-4 w-4" />
Deletar
</Button>
</>
)}
</div>
</div>
@ -209,6 +274,23 @@ export default function EssayPage() {
</CardContent>
</Card>
</div>
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Você tem certeza?</AlertDialogTitle>
<AlertDialogDescription>
Esta ação não pode ser desfeita. Isso excluirá permanentemente sua redação.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={deleteEssay} className="bg-red-600 hover:bg-red-700">
Deletar
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { useState } from 'react';
import { useRouter } from 'next/router';
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { supabase } from '@/lib/supabase';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@ -24,15 +24,18 @@ interface EssayGenre {
};
}
export default function NewEssayPage() {
const router = useRouter();
const { supabase } = supabase();
export function NewEssay() {
const navigate = useNavigate();
const [step, setStep] = useState<'type' | 'genre'>('type');
const [selectedType, setSelectedType] = useState<EssayType | null>(null);
const [types, setTypes] = useState<EssayType[]>([]);
const [genres, setGenres] = useState<EssayGenre[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadTypes();
}, []);
// Carregar tipos textuais
async function loadTypes() {
try {
@ -69,9 +72,13 @@ export default function NewEssayPage() {
// Criar nova redação
async function createEssay(genreId: string) {
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Usuário não autenticado');
const { data, error } = await supabase
.from('student_essays')
.insert({
student_id: user.id,
type_id: selectedType!.id,
genre_id: genreId,
status: 'draft',
@ -82,7 +89,7 @@ export default function NewEssayPage() {
.single();
if (error) throw error;
router.push(`/student-dashboard/essays/${data.id}`);
navigate(`/aluno/redacoes/${data.id}`);
} catch (error) {
console.error('Erro ao criar redação:', error);
}
@ -156,7 +163,16 @@ export default function NewEssayPage() {
<div className="container mx-auto p-6">
<div className="flex items-center gap-4 mb-6">
{step === 'genre' && (
<Button variant="ghost" onClick={() => setStep('type')}>
<Button
variant="ghost"
onClick={() => setStep('type')}
trackingId="essay-new-back-button"
trackingProperties={{
action: 'back_to_type_selection',
current_step: 'genre',
page: 'new_essay'
}}
>
<ArrowLeft className="mr-2 h-4 w-4" />
Voltar
</Button>

View File

@ -1,9 +1,9 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { supabase } from '@/lib/supabase';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { PlusCircle } from 'lucide-react';
import { useRouter } from 'next/router';
import { Badge } from '@/components/ui/badge';
interface Essay {
@ -21,9 +21,8 @@ interface Essay {
};
}
export default function EssaysPage() {
const router = useRouter();
const { supabase } = supabase();
export function EssaysPage() {
const navigate = useNavigate();
const [essays, setEssays] = useState<Essay[]>([]);
const [loading, setLoading] = useState(true);
@ -53,20 +52,27 @@ export default function EssaysPage() {
function getStatusBadge(status: Essay['status']) {
const statusMap = {
draft: { label: 'Rascunho', variant: 'secondary' },
submitted: { label: 'Enviada', variant: 'primary' },
analyzed: { label: 'Analisada', variant: 'success' }
draft: { label: 'Rascunho', variant: 'secondary' as const },
submitted: { label: 'Enviada', variant: 'default' as const },
analyzed: { label: 'Analisada', variant: 'success' as const }
};
const { label, variant } = statusMap[status];
return <Badge variant={variant as any}>{label}</Badge>;
return <Badge variant={variant}>{label}</Badge>;
}
return (
<div className="container mx-auto p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">Minhas Redações</h1>
<Button onClick={() => router.push('/student-dashboard/essays/new')}>
<Button
onClick={() => navigate('/aluno/redacoes/nova')}
trackingId="essay-new-create-button"
trackingProperties={{
action: 'create_new_essay',
page: 'essays_list'
}}
>
<PlusCircle className="mr-2 h-4 w-4" />
Nova Redação
</Button>
@ -78,7 +84,15 @@ export default function EssaysPage() {
<Card>
<CardContent className="flex flex-col items-center justify-center p-6">
<p className="text-muted-foreground mb-4">Você ainda não tem nenhuma redação</p>
<Button onClick={() => router.push('/student-dashboard/essays/new')}>
<Button
onClick={() => navigate('/aluno/redacoes/nova')}
trackingId="essay-empty-create-button"
trackingProperties={{
action: 'create_first_essay',
page: 'essays_list',
context: 'empty_state'
}}
>
Criar Primeira Redação
</Button>
</CardContent>
@ -87,7 +101,7 @@ export default function EssaysPage() {
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{essays.map((essay) => (
<Card key={essay.id} className="cursor-pointer hover:shadow-lg transition-shadow"
onClick={() => router.push(`/student-dashboard/essays/${essay.id}`)}>
onClick={() => navigate(`/aluno/redacoes/${essay.id}`)}>
<CardHeader>
<div className="flex justify-between items-start">
<div>

View File

@ -34,9 +34,9 @@ import { TextSalesLetter } from './pages/landing/TextSalesLetter';
import { PhonicsPage } from "./pages/student-dashboard/PhonicsPage";
import { PhonicsProgressPage } from "./pages/student-dashboard/PhonicsProgressPage";
import { EssaysPage } from './pages/student-dashboard/essays';
import { NewEssayPage } from './pages/student-dashboard/essays/new';
import { EssayPage } from './pages/student-dashboard/essays/[id]';
import { EssayAnalysisPage } from './pages/student-dashboard/essays/[id]/analysis';
import { NewEssay } from './pages/student-dashboard/essays/NewEssay';
import { EssayPage } from './pages/student-dashboard/essays/EssayPage';
import { EssayAnalysis } from './pages/student-dashboard/essays/EssayAnalysis';
function RootLayout({ children }: { children: React.ReactNode }) {
return (
@ -233,7 +233,7 @@ export const router = createBrowserRouter([
},
{
path: 'nova',
element: <NewEssayPage />,
element: <NewEssay />,
},
{
path: ':id',
@ -241,7 +241,7 @@ export const router = createBrowserRouter([
},
{
path: ':id/analise',
element: <EssayAnalysisPage />,
element: <EssayAnalysis />,
}
]
}