feat: add organization selection and creation screens

This commit is contained in:
Ricel Leite 2026-02-19 00:19:35 -03:00
parent b9aa833bd5
commit bd8ba302a8
7 changed files with 346 additions and 128 deletions

View File

@ -3,6 +3,8 @@ import { useAuth } from './context/AuthContext';
import Layout from './components/Layout'; import Layout from './components/Layout';
import Login from './pages/Login'; import Login from './pages/Login';
import Register from './pages/Register'; import Register from './pages/Register';
import SelectOrganization from './pages/SelectOrganization';
import CreateOrganization from './pages/CreateOrganization';
import Dashboard from './pages/Dashboard'; import Dashboard from './pages/Dashboard';
import Issues from './pages/Issues'; import Issues from './pages/Issues';
import IssueDetail from './pages/IssueDetail'; import IssueDetail from './pages/IssueDetail';
@ -17,15 +19,33 @@ function PrivateRoute({ children }) {
return user ? children : <Navigate to="/login" />; return user ? children : <Navigate to="/login" />;
} }
function OrgRoute({ children }) {
const { user, currentOrg, loading } = useAuth();
if (loading) return <div className="flex items-center justify-center h-screen">Loading...</div>;
if (!user) return <Navigate to="/login" />;
if (!currentOrg) return <Navigate to="/select-organization" />;
return children;
}
export default function App() { export default function App() {
return ( return (
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} /> <Route path="/register" element={<Register />} />
<Route path="/" element={ <Route path="/select-organization" element={
<PrivateRoute> <PrivateRoute>
<Layout /> <SelectOrganization />
</PrivateRoute> </PrivateRoute>
} />
<Route path="/create-organization" element={
<PrivateRoute>
<CreateOrganization />
</PrivateRoute>
} />
<Route path="/" element={
<OrgRoute>
<Layout />
</OrgRoute>
}> }>
<Route index element={<Dashboard />} /> <Route index element={<Dashboard />} />
<Route path="issues" element={<Issues />} /> <Route path="issues" element={<Issues />} />

View File

@ -0,0 +1,92 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { organizations } from '../services/api';
import { Building2, ArrowRight, Loader2 } from 'lucide-react';
export default function CreateOrganization() {
const [name, setName] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { selectOrg } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const res = await organizations.create({ name });
selectOrg(res.data);
navigate('/');
} catch (err) {
setError(err.response?.data?.detail || 'Failed to create organization');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-950 px-6">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-indigo-600/10 border border-indigo-500/20 mb-4">
<Building2 size={32} className="text-indigo-400" />
</div>
<h1 className="text-2xl font-bold text-white mb-2">Create Your Organization</h1>
<p className="text-gray-400">
Get started by creating your first organization
</p>
</div>
<div className="card">
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-sm text-red-400 animate-fade-in">
{error}
</div>
)}
<div>
<label className="block text-xs font-medium text-gray-400 mb-2 uppercase tracking-wide">
Organization Name
</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
className="input"
placeholder="Acme Inc."
required
autoFocus
/>
<p className="mt-2 text-xs text-gray-500">
You can change this later in settings
</p>
</div>
<button
type="submit"
disabled={loading}
className="btn btn-primary w-full h-11 justify-center"
>
{loading ? (
<Loader2 size={16} className="animate-spin" />
) : (
<>
Create Organization
<ArrowRight size={16} />
</>
)}
</button>
</form>
</div>
<p className="text-center mt-6 text-xs text-gray-500">
You can invite team members after creating your organization
</p>
</div>
</div>
);
}

View File

@ -0,0 +1,106 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { organizations } from '../services/api';
import { Building2, Plus, ArrowRight, Loader2 } from 'lucide-react';
export default function SelectOrganization() {
const [orgs, setOrgs] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const { selectOrg } = useAuth();
const navigate = useNavigate();
useEffect(() => {
loadOrganizations();
}, []);
const loadOrganizations = async () => {
try {
const res = await organizations.list();
setOrgs(res.data);
// Auto-select if only one
if (res.data.length === 1) {
handleSelect(res.data[0]);
}
} catch (err) {
setError(err.response?.data?.detail || 'Failed to load organizations');
} finally {
setLoading(false);
}
};
const handleSelect = (org) => {
selectOrg(org);
navigate('/');
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-950">
<Loader2 size={32} className="text-indigo-400 animate-spin" />
</div>
);
}
if (orgs.length === 0) {
navigate('/create-organization');
return null;
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-950 px-6">
<div className="w-full max-w-2xl">
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-indigo-600/10 border border-indigo-500/20 mb-4">
<Building2 size={32} className="text-indigo-400" />
</div>
<h1 className="text-2xl font-bold text-white mb-2">Select Organization</h1>
<p className="text-gray-400">
Choose which organization you want to work with
</p>
</div>
{error && (
<div className="p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-sm text-red-400 mb-6 animate-fade-in">
{error}
</div>
)}
<div className="grid gap-3 mb-6">
{orgs.map(org => (
<button
key={org.id}
onClick={() => handleSelect(org)}
className="card hover:bg-gray-800/50 transition-colors p-6 text-left group"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-indigo-600/10 border border-indigo-500/20 flex items-center justify-center">
<Building2 size={24} className="text-indigo-400" />
</div>
<div>
<h3 className="font-semibold text-white mb-1">{org.name}</h3>
<p className="text-sm text-gray-500">
{org.member_count || 0} {org.member_count === 1 ? 'member' : 'members'}
</p>
</div>
</div>
<ArrowRight size={20} className="text-gray-600 group-hover:text-indigo-400 transition-colors" />
</div>
</button>
))}
</div>
<button
onClick={() => navigate('/create-organization')}
className="btn btn-secondary w-full h-11 justify-center"
>
<Plus size={16} />
Create New Organization
</button>
</div>
</div>
);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JIRA AI Fixer</title> <title>JIRA AI Fixer</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg"> <link rel="icon" type="image/svg+xml" href="/favicon.svg">
<script type="module" crossorigin src="/assets/index-8UkLrHEv.js"></script> <script type="module" crossorigin src="/assets/index-Be0hyHsH.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BXtUC8s7.css"> <link rel="stylesheet" crossorigin href="/assets/index-gQZrNcqD.css">
</head> </head>
<body class="bg-gray-900 text-white"> <body class="bg-gray-900 text-white">
<div id="root"></div> <div id="root"></div>