feat: add organization selection and creation screens
This commit is contained in:
parent
b9aa833bd5
commit
bd8ba302a8
|
|
@ -3,6 +3,8 @@ import { useAuth } from './context/AuthContext';
|
|||
import Layout from './components/Layout';
|
||||
import Login from './pages/Login';
|
||||
import Register from './pages/Register';
|
||||
import SelectOrganization from './pages/SelectOrganization';
|
||||
import CreateOrganization from './pages/CreateOrganization';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Issues from './pages/Issues';
|
||||
import IssueDetail from './pages/IssueDetail';
|
||||
|
|
@ -17,15 +19,33 @@ function PrivateRoute({ children }) {
|
|||
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() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/" element={
|
||||
<Route path="/select-organization" element={
|
||||
<PrivateRoute>
|
||||
<Layout />
|
||||
<SelectOrganization />
|
||||
</PrivateRoute>
|
||||
} />
|
||||
<Route path="/create-organization" element={
|
||||
<PrivateRoute>
|
||||
<CreateOrganization />
|
||||
</PrivateRoute>
|
||||
} />
|
||||
<Route path="/" element={
|
||||
<OrgRoute>
|
||||
<Layout />
|
||||
</OrgRoute>
|
||||
}>
|
||||
<Route index element={<Dashboard />} />
|
||||
<Route path="issues" element={<Issues />} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
|
@ -5,8 +5,8 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>JIRA AI Fixer</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<script type="module" crossorigin src="/assets/index-8UkLrHEv.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BXtUC8s7.css">
|
||||
<script type="module" crossorigin src="/assets/index-Be0hyHsH.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-gQZrNcqD.css">
|
||||
</head>
|
||||
<body class="bg-gray-900 text-white">
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue