Novo modal de criacao de organizzacao
This commit is contained in:
parent
da18ab77ad
commit
14b9b6985a
1 changed files with 228 additions and 104 deletions
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue