feat: melhora página de perfil e integra preferências de notificação
- Atualiza cores das badges para padrão cyan do projeto - Adiciona degradê no header do card de perfil - Implementa upload de foto de perfil via API Convex - Integra notificações do Convex com preferências do usuário - Cria API /api/notifications/send para verificar preferências - Melhora layout das páginas de login/recuperação com degradê - Adiciona badge "Helpdesk" e título "Raven" consistente 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1bc08d3a5f
commit
ab7dfa81ca
8 changed files with 543 additions and 59 deletions
|
|
@ -1,11 +1,10 @@
|
|||
"use client"
|
||||
|
||||
import { FormEvent, useMemo, useState } from "react"
|
||||
import { FormEvent, useMemo, useRef, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
Settings2,
|
||||
Share2,
|
||||
ShieldCheck,
|
||||
UserPlus,
|
||||
|
|
@ -18,10 +17,10 @@ import {
|
|||
Mail,
|
||||
Key,
|
||||
User,
|
||||
Building2,
|
||||
Shield,
|
||||
Clock,
|
||||
Camera,
|
||||
Loader2,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
|
|
@ -55,11 +54,11 @@ const ROLE_LABELS: Record<string, string> = {
|
|||
}
|
||||
|
||||
const ROLE_COLORS: Record<string, string> = {
|
||||
admin: "bg-violet-100 text-violet-700 border-violet-200",
|
||||
manager: "bg-blue-100 text-blue-700 border-blue-200",
|
||||
agent: "bg-cyan-100 text-cyan-700 border-cyan-200",
|
||||
collaborator: "bg-slate-100 text-slate-700 border-slate-200",
|
||||
customer: "bg-slate-100 text-slate-700 border-slate-200",
|
||||
admin: "bg-cyan-100 text-cyan-800 border-cyan-300",
|
||||
manager: "bg-cyan-50 text-cyan-700 border-cyan-200",
|
||||
agent: "bg-cyan-50 text-cyan-600 border-cyan-200",
|
||||
collaborator: "bg-neutral-100 text-neutral-600 border-neutral-200",
|
||||
customer: "bg-neutral-100 text-neutral-600 border-neutral-200",
|
||||
}
|
||||
|
||||
const SETTINGS_ACTIONS: SettingsAction[] = [
|
||||
|
|
@ -326,6 +325,55 @@ function ProfileEditCard({
|
|||
const [newPassword, setNewPassword] = useState("")
|
||||
const [confirmPassword, setConfirmPassword] = useState("")
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [localAvatarUrl, setLocalAvatarUrl] = useState(avatarUrl)
|
||||
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
async function handleAvatarUpload(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
// Valida tamanho (5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
toast.error("Arquivo muito grande. Máximo 5MB.")
|
||||
return
|
||||
}
|
||||
|
||||
// Valida tipo
|
||||
if (!["image/jpeg", "image/png", "image/webp", "image/gif"].includes(file.type)) {
|
||||
toast.error("Tipo de arquivo não permitido. Use JPG, PNG, WebP ou GIF.")
|
||||
return
|
||||
}
|
||||
|
||||
setIsUploadingAvatar(true)
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append("file", file)
|
||||
|
||||
const res = await fetch("/api/profile/avatar", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({ error: "Erro ao fazer upload" }))
|
||||
throw new Error(data.error || "Erro ao fazer upload")
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
setLocalAvatarUrl(data.avatarUrl)
|
||||
toast.success("Foto atualizada com sucesso!")
|
||||
} catch (error) {
|
||||
console.error("Erro ao fazer upload:", error)
|
||||
toast.error(error instanceof Error ? error.message : "Erro ao fazer upload da foto")
|
||||
} finally {
|
||||
setIsUploadingAvatar(false)
|
||||
// Limpa o input para permitir reselecionar o mesmo arquivo
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasChanges = useMemo(() => {
|
||||
const nameChanged = editName.trim() !== name
|
||||
|
|
@ -387,25 +435,39 @@ function ProfileEditCard({
|
|||
}
|
||||
|
||||
return (
|
||||
<Card className="rounded-2xl border border-border/60 bg-white shadow-sm">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<Card className="rounded-2xl border border-border/60 bg-white shadow-sm overflow-hidden">
|
||||
{/* Header com degradê */}
|
||||
<div className="h-20 bg-gradient-to-r from-neutral-800 via-neutral-700 to-neutral-600" />
|
||||
<CardHeader className="pb-4 -mt-10">
|
||||
<div className="flex items-end gap-4">
|
||||
<div className="relative group">
|
||||
<Avatar className="size-16 border-2 border-white shadow-md">
|
||||
<AvatarImage src={avatarUrl ?? undefined} alt={name} />
|
||||
<AvatarFallback className="bg-gradient-to-br from-cyan-500 to-blue-600 text-lg font-semibold text-white">
|
||||
<Avatar className="size-20 border-4 border-white shadow-lg ring-2 ring-neutral-200">
|
||||
<AvatarImage src={localAvatarUrl ?? undefined} alt={name} />
|
||||
<AvatarFallback className="bg-gradient-to-br from-cyan-500 to-cyan-600 text-xl font-semibold text-white">
|
||||
{initials}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp,image/gif"
|
||||
onChange={handleAvatarUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-0 flex items-center justify-center rounded-full bg-black/50 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
onClick={() => toast.info("Upload de foto em breve!")}
|
||||
className="absolute inset-0 flex items-center justify-center rounded-full bg-black/50 opacity-0 transition-opacity group-hover:opacity-100 disabled:cursor-not-allowed"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isUploadingAvatar}
|
||||
>
|
||||
<Camera className="size-5 text-white" />
|
||||
{isUploadingAvatar ? (
|
||||
<Loader2 className="size-5 text-white animate-spin" />
|
||||
) : (
|
||||
<Camera className="size-5 text-white" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex-1 pb-1">
|
||||
<CardTitle className="text-lg font-semibold text-neutral-900">{name || "Usuário"}</CardTitle>
|
||||
<CardDescription className="text-sm text-neutral-500">{email}</CardDescription>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue