Add project creation from team dashboard and update seed data
- Add project creation modal to TeamDashboardPage with team_id assignment - Update createProject API function to accept optional team_id - Update seed data to create a "Demo Team" and assign all projects to it - Admin user is added as team owner when present
This commit is contained in:
@@ -166,7 +166,7 @@ export async function listProjectsSimple(params: ListParams = {}): Promise<Proje
|
||||
return data.items;
|
||||
}
|
||||
|
||||
export async function createProject(data: { name: string; description?: string; is_public?: boolean }): Promise<Project> {
|
||||
export async function createProject(data: { name: string; description?: string; is_public?: boolean; team_id?: string }): Promise<Project> {
|
||||
const response = await fetch(`${API_BASE}/projects`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
||||
@@ -221,3 +221,100 @@
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-bg-tertiary);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--color-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.5rem;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
margin: 0 0 1.5rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px var(--color-primary-alpha);
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
display: block;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.empty-state .btn {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Link, useParams, useNavigate } from 'react-router-dom';
|
||||
import { TeamDetail, Project, PaginatedResponse } from '../types';
|
||||
import { getTeam, listTeamProjects } from '../api';
|
||||
import { getTeam, listTeamProjects, createProject } from '../api';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { Badge } from '../components/Badge';
|
||||
import { Breadcrumb } from '../components/Breadcrumb';
|
||||
@@ -15,6 +15,9 @@ function TeamDashboardPage() {
|
||||
const [projects, setProjects] = useState<PaginatedResponse<Project> | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showProjectForm, setShowProjectForm] = useState(false);
|
||||
const [newProject, setNewProject] = useState({ name: '', description: '', is_public: true });
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
const loadTeamData = useCallback(async () => {
|
||||
if (!slug) return;
|
||||
@@ -38,6 +41,22 @@ function TeamDashboardPage() {
|
||||
loadTeamData();
|
||||
}, [loadTeamData]);
|
||||
|
||||
async function handleCreateProject(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!team) return;
|
||||
try {
|
||||
setCreating(true);
|
||||
const project = await createProject({ ...newProject, team_id: team.id });
|
||||
setNewProject({ name: '', description: '', is_public: true });
|
||||
setShowProjectForm(false);
|
||||
navigate(`/project/${project.name}`);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create project');
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="team-dashboard">
|
||||
@@ -124,13 +143,64 @@ function TeamDashboardPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showProjectForm && (
|
||||
<div className="modal-overlay" onClick={() => setShowProjectForm(false)}>
|
||||
<div className="modal-content" onClick={e => e.stopPropagation()}>
|
||||
<h2>Create New Project</h2>
|
||||
<form onSubmit={handleCreateProject}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="project-name">Project Name</label>
|
||||
<input
|
||||
id="project-name"
|
||||
type="text"
|
||||
value={newProject.name}
|
||||
onChange={e => setNewProject({ ...newProject, name: e.target.value })}
|
||||
placeholder="my-project"
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="project-description">Description (optional)</label>
|
||||
<textarea
|
||||
id="project-description"
|
||||
value={newProject.description}
|
||||
onChange={e => setNewProject({ ...newProject, description: e.target.value })}
|
||||
placeholder="What is this project for?"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newProject.is_public}
|
||||
onChange={e => setNewProject({ ...newProject, is_public: e.target.checked })}
|
||||
/>
|
||||
Public project
|
||||
</label>
|
||||
<span className="form-hint">Public projects are visible to everyone</span>
|
||||
</div>
|
||||
<div className="form-actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={() => setShowProjectForm(false)}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={creating}>
|
||||
{creating ? 'Creating...' : 'Create Project'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="team-section">
|
||||
<div className="section-header">
|
||||
<h2>Projects</h2>
|
||||
{isAdminOrOwner && (
|
||||
<Link to="/" className="btn btn-primary btn-sm">
|
||||
<button className="btn btn-primary btn-sm" onClick={() => setShowProjectForm(true)}>
|
||||
+ New Project
|
||||
</Link>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -138,7 +208,9 @@ function TeamDashboardPage() {
|
||||
<div className="empty-state">
|
||||
<p>No projects in this team yet.</p>
|
||||
{isAdminOrOwner && (
|
||||
<p className="empty-hint">Create a project and assign it to this team to get started.</p>
|
||||
<button className="btn btn-primary" onClick={() => setShowProjectForm(true)}>
|
||||
Create Project
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user