Novo modal de criacao de organizzacao

This commit is contained in:
Gabriel Santana de Menezes 2026-05-03 22:02:43 -04:00
parent da18ab77ad
commit 14b9b6985a

View file

@ -1,4 +1,5 @@
import { motion } from 'framer-motion' import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { import {
Building2, Building2,
Plus, Plus,
@ -8,20 +9,58 @@ import {
ArrowRight, ArrowRight,
ExternalLink, ExternalLink,
ShieldCheck, ShieldCheck,
UserPlus UserPlus,
X,
Loader2,
Layout as LayoutIcon,
Globe,
Zap
} from 'lucide-react' } from 'lucide-react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext' import { useAuth } from '../contexts/AuthContext'
import { cn } from '../lib/utils' import { cn } from '../lib/utils'
import { organizationService } from '../services/organizationService'
export function OrganizationsPage() { export function OrganizationsPage() {
const { user, activeOrg, setActiveOrg } = useAuth() const { user, activeOrg, setActiveOrg, refreshUser } = useAuth()
const [showCreateModal, setShowCreateModal] = useState(false)
const [newOrgName, setNewOrgName] = useState('')
const [isCreating, setIsCreating] = useState(false)
const [error, setError] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const handleCreateOrganization = async (e: React.FormEvent) => {
e.preventDefault()
if (!newOrgName.trim()) return
setIsCreating(true)
setError(null)
try {
await organizationService.create(newOrgName)
await refreshUser()
setNewOrgName('')
setShowCreateModal(false)
} catch (err) {
console.error('Failed to create organization', err)
setError('Erro ao criar organização. Tente outro nome.')
} finally {
setIsCreating(false)
}
}
const filteredOrgs = user?.organizations.filter(org =>
org.name.toLowerCase().includes(searchQuery.toLowerCase())
) || []
return ( return (
<div className="space-y-12 pb-20"> <div className="space-y-12 pb-20">
{/* Header Section */} {/* Header Section */}
<header className="flex flex-col gap-6 md:flex-row md:items-center md:justify-between"> <header className="flex flex-col gap-6 md:flex-row md:items-center md:justify-between">
<div> <div>
<div className="flex items-center gap-2 text-brand font-black uppercase tracking-widest text-[10px] mb-2">
<Globe size={14} />
Hub Global
</div>
<h1 className="text-4xl font-black tracking-tight text-text-main"> <h1 className="text-4xl font-black tracking-tight text-text-main">
Organizações Organizações
</h1> </h1>
@ -30,20 +69,30 @@ export function OrganizationsPage() {
</p> </p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="relative hidden md:block">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" size={16} />
<input
type="text"
placeholder="Buscar..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 pr-4 py-2.5 bg-surface border border-border-soft rounded-xl text-sm outline-none focus:border-brand focus:ring-4 focus:ring-brand/5 transition-all w-48 font-medium"
/>
</div>
<Link <Link
to="/organizations/join" to="/organizations/join"
className="inline-flex items-center justify-center gap-2 rounded-xl bg-surface border border-border-soft px-6 py-3 text-sm font-bold text-text-main shadow-sm transition-all hover:bg-surface-muted active:scale-95" className="inline-flex items-center justify-center gap-2 rounded-xl bg-surface border border-border-soft px-6 py-3 text-sm font-bold text-text-main shadow-sm transition-all hover:bg-surface-muted active:scale-95"
> >
<Search size={18} /> <Search size={18} />
Encontrar Organização Encontrar
</Link> </Link>
<Link <button
to="/organizations/new" onClick={() => setShowCreateModal(true)}
className="inline-flex items-center justify-center gap-2 rounded-xl bg-brand px-6 py-3 text-sm font-bold text-white shadow-lg shadow-brand/20 transition-all hover:scale-[1.02] hover:bg-brand-strong active:scale-95" className="inline-flex items-center justify-center gap-2 rounded-xl bg-brand px-6 py-3 text-sm font-bold text-white shadow-lg shadow-brand/20 transition-all hover:scale-[1.02] hover:bg-brand-dark active:scale-95"
> >
<Plus size={18} /> <Plus size={18} />
Nova Organização Nova Org
</Link> </button>
</div> </div>
</header> </header>
@ -55,7 +104,8 @@ export function OrganizationsPage() {
</div> </div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{user?.organizations.map((org) => { <AnimatePresence mode="popLayout">
{filteredOrgs.map((org, index) => {
const isActive = org.id === activeOrg?.id const isActive = org.id === activeOrg?.id
const isOwner = org.roles.includes('OWNER') const isOwner = org.roles.includes('OWNER')
const isAdmin = org.roles.includes('ADMIN') const isAdmin = org.roles.includes('ADMIN')
@ -63,11 +113,13 @@ export function OrganizationsPage() {
return ( return (
<motion.div <motion.div
key={org.id} key={org.id}
layout
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className={cn( className={cn(
"group relative overflow-hidden rounded-[2.5rem] border bg-surface p-8 transition-all hover:shadow-2xl hover:shadow-brand/5", "group relative overflow-hidden rounded-[2.5rem] border bg-surface p-8 transition-all hover:shadow-2xl hover:shadow-brand/5",
isActive ? "border-brand ring-1 ring-brand/10" : "border-border-soft hover:border-brand/30" isActive ? "border-brand ring-1 ring-brand/10 shadow-lg shadow-brand/5" : "border-border-soft hover:border-brand/30"
)} )}
> >
{/* Active Indicator */} {/* Active Indicator */}
@ -80,13 +132,13 @@ export function OrganizationsPage() {
<div className="relative z-10"> <div className="relative z-10">
<div className={cn( <div className={cn(
"flex h-16 w-16 items-center justify-center rounded-2xl text-2xl font-black shadow-inner transition-transform group-hover:scale-110", "flex h-16 w-16 items-center justify-center rounded-2xl text-2xl font-black shadow-inner transition-all group-hover:scale-110",
isActive ? "bg-brand text-white" : "bg-surface-muted text-text-muted group-hover:bg-brand group-hover:text-white" isActive ? "bg-brand text-white shadow-lg shadow-brand/20" : "bg-surface-muted text-text-muted group-hover:bg-brand group-hover:text-white"
)}> )}>
{org.name.substring(0, 2).toUpperCase()} {org.name.substring(0, 2).toUpperCase()}
</div> </div>
<h3 className="mt-6 text-xl font-black text-text-main leading-tight"> <h3 className="mt-6 text-xl font-black text-text-main leading-tight group-hover:text-brand transition-colors">
{org.name} {org.name}
</h3> </h3>
@ -104,13 +156,13 @@ export function OrganizationsPage() {
{!isActive ? ( {!isActive ? (
<button <button
onClick={() => setActiveOrg(org.id)} onClick={() => setActiveOrg(org.id)}
className="flex-1 rounded-xl bg-app-bg py-3 text-xs font-black uppercase tracking-widest text-text-main transition-all hover:bg-brand hover:text-white" className="flex-1 rounded-xl bg-app-bg py-3 text-xs font-black uppercase tracking-widest text-text-main transition-all hover:bg-brand hover:text-white shadow-sm"
> >
Selecionar Selecionar
</button> </button>
) : ( ) : (
<div className="flex-1 py-3 text-xs font-black uppercase tracking-widest text-brand text-center"> <div className="flex-1 py-3 text-xs font-black uppercase tracking-widest text-brand text-center font-black">
Selecionada Organização Atual
</div> </div>
)} )}
@ -133,18 +185,19 @@ export function OrganizationsPage() {
})} })}
{/* Quick Create Card */} {/* Quick Create Card */}
<Link <button
to="/organizations/new" onClick={() => setShowCreateModal(true)}
className="flex flex-col items-center justify-center gap-4 rounded-[2.5rem] border-2 border-dashed border-border-soft bg-surface/50 p-8 text-center transition-all hover:border-brand/30 hover:bg-brand/5 active:scale-[0.98]" className="flex flex-col items-center justify-center gap-4 rounded-[2.5rem] border-2 border-dashed border-border-soft bg-surface/50 p-8 text-center transition-all hover:border-brand/30 hover:bg-brand/5 active:scale-[0.98] group"
> >
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-app-bg text-text-muted transition-colors group-hover:bg-brand group-hover:text-white"> <div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-app-bg text-text-muted transition-colors group-hover:bg-brand group-hover:text-white">
<Plus size={32} /> <Plus size={32} />
</div> </div>
<div> <div>
<h3 className="font-bold text-text-main">Criar nova organização</h3> <h3 className="font-bold text-text-main group-hover:text-brand transition-colors">Nova Organização</h3>
<p className="text-sm text-text-muted">Inicie um novo projeto com sua equipe.</p> <p className="text-sm text-text-muted">Inicie um novo workspace.</p>
</div> </div>
</Link> </button>
</AnimatePresence>
</div> </div>
</section> </section>
@ -154,7 +207,7 @@ export function OrganizationsPage() {
<h2 className="text-3xl font-black tracking-tight leading-none"> <h2 className="text-3xl font-black tracking-tight leading-none">
Procurando uma equipe? Procurando uma equipe?
</h2> </h2>
<p className="mt-4 text-white/80 font-medium"> <p className="mt-4 text-white/80 font-medium text-lg">
Explore organizações públicas ou use um código de convite para se juntar aos seus colegas de trabalho. Explore organizações públicas ou use um código de convite para se juntar aos seus colegas de trabalho.
</p> </p>
<div className="mt-10 flex flex-wrap gap-4"> <div className="mt-10 flex flex-wrap gap-4">
@ -181,30 +234,101 @@ export function OrganizationsPage() {
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-accent-soft text-accent mb-6"> <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-accent-soft text-accent mb-6">
<UserPlus size={24} /> <UserPlus size={24} />
</div> </div>
<h3 className="text-xl font-bold text-text-main">Como funciona?</h3> <h3 className="text-xl font-bold text-text-main">Gestão de Equipe</h3>
<p className="mt-3 text-text-muted leading-relaxed"> <p className="mt-3 text-text-muted leading-relaxed font-medium">
Organizações são espaços isolados para seus projetos e tarefas. Você pode ser dono de várias ou participar como colaborador em outras. Toda a sua atividade é filtrada pela organização ativa. Organizações são espaços isolados para seus projetos e tarefas. Você pode alternar facilmente entre elas e gerenciar diferentes equipes.
</p> </p>
<a href="#" className="mt-6 inline-flex items-center gap-1.5 text-sm font-bold text-accent hover:underline"> <a href="#" className="mt-6 inline-flex items-center gap-1.5 text-sm font-bold text-accent hover:underline">
Saiba mais sobre permissões Entender hierarquia
<ExternalLink size={14} /> <ExternalLink size={14} />
</a> </a>
</div> </div>
<div className="rounded-[2rem] border border-border-soft bg-surface p-8"> <div className="rounded-[2rem] border border-border-soft bg-surface p-8">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-brand-soft text-brand mb-6"> <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-brand-soft text-brand mb-6">
<ShieldCheck size={24} /> <Zap size={24} />
</div> </div>
<h3 className="text-xl font-bold text-text-main">Segurança & Funções</h3> <h3 className="text-xl font-bold text-text-main">Início Imediato</h3>
<p className="mt-3 text-text-muted leading-relaxed"> <p className="mt-3 text-text-muted leading-relaxed font-medium">
Cada organização define seus próprios papéis (OWNER, ADMIN, MEMBER). Apenas administradores e donos podem gerenciar membros e configurações do espaço. Crie uma organização em segundos e comece a convidar seus membros. O BeeTask facilita a colaboração desde o primeiro dia.
</p> </p>
<a href="#" className="mt-6 inline-flex items-center gap-1.5 text-sm font-bold text-brand hover:underline"> <a href="#" className="mt-6 inline-flex items-center gap-1.5 text-sm font-bold text-brand hover:underline">
Ver detalhes de segurança Dicas de onboarding
<ExternalLink size={14} /> <ExternalLink size={14} />
</a> </a>
</div> </div>
</div> </div>
{/* Create Organization Modal */}
<AnimatePresence>
{showCreateModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-app-bg/80 backdrop-blur-sm">
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="w-full max-w-lg bg-surface border border-border-soft rounded-[2.5rem] shadow-panel p-10 relative"
>
<button
onClick={() => setShowCreateModal(false)}
className="absolute top-8 right-8 text-text-muted hover:text-brand transition-colors"
>
<X size={24} />
</button>
<div className="text-center mb-10">
<div className="w-16 h-16 bg-brand text-white rounded-2xl flex items-center justify-center mb-6 shadow-lg shadow-brand/20 mx-auto">
<Building2 size={32} />
</div>
<h2 className="text-3xl font-black text-text-main tracking-tight mb-3">Nova Organização</h2>
<p className="text-text-muted font-medium">Inicie um novo workspace colaborativo agora mesmo.</p>
</div>
<form onSubmit={handleCreateOrganization} className="space-y-6">
<div className="space-y-2">
<label className="text-sm font-bold text-text-main ml-1">Nome da Organização</label>
<div className="relative group">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<LayoutIcon className="h-5 w-5 text-text-muted group-focus-within:text-brand transition-colors" />
</div>
<input
type="text"
autoFocus
required
value={newOrgName}
onChange={(e) => setNewOrgName(e.target.value)}
className="block w-full pl-12 pr-4 py-4 bg-surface-muted/50 border border-border-soft rounded-2xl text-text-main placeholder-text-muted/60 focus:outline-none focus:ring-4 focus:ring-brand/10 focus:border-brand transition-all font-bold"
placeholder="Ex: Bee Tech, Creative Agency..."
/>
</div>
</div>
{error && (
<div className="p-4 bg-rose-500/10 border border-rose-500/20 rounded-2xl text-rose-500 text-xs font-bold flex items-center gap-3">
<X size={16} />
{error}
</div>
)}
<button
type="submit"
disabled={isCreating || !newOrgName.trim()}
className="w-full flex items-center justify-center gap-3 bg-brand hover:bg-brand-dark disabled:bg-brand/50 disabled:cursor-not-allowed text-white font-black py-4 px-6 rounded-2xl transition-all shadow-lg shadow-brand/20 active:scale-[0.98]"
>
{isCreating ? (
<Loader2 className="animate-spin" size={20} />
) : (
<>
Criar Agora
<ArrowRight size={20} />
</>
)}
</button>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
</div> </div>
) )
} }