feat(web): add workspace management UI (M2 #12)
- Create workspace listing page at /settings/workspaces - List all user workspaces with role badges - Create new workspace functionality - Display member count per workspace - Create workspace detail page at /settings/workspaces/[id] - Workspace settings (name, ID, created date) - Member management with role editing - Invite member functionality - Delete workspace (owner only) - Add workspace components: - WorkspaceCard: Display workspace info with role badge - WorkspaceSettings: Edit workspace settings and delete - MemberList: Display and manage workspace members - InviteMember: Send invitations with role selection - Add WorkspaceMemberWithUser type to shared package - Follow existing app patterns for styling and structure - Use mock data (ready for API integration)
This commit is contained in:
32
apps/web/src/components/team/TeamCard.tsx
Normal file
32
apps/web/src/components/team/TeamCard.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Team } from "@mosaic/shared";
|
||||
import { Card, CardHeader, CardContent } from "@mosaic/ui";
|
||||
import Link from "next/link";
|
||||
|
||||
interface TeamCardProps {
|
||||
team: Team;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export function TeamCard({ team, workspaceId }: TeamCardProps) {
|
||||
return (
|
||||
<Link href={`/settings/workspaces/${workspaceId}/teams/${team.id}`}>
|
||||
<Card className="hover:shadow-lg transition-shadow cursor-pointer">
|
||||
<CardHeader>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{team.name}</h3>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{team.description ? (
|
||||
<p className="text-sm text-gray-600">{team.description}</p>
|
||||
) : (
|
||||
<p className="text-sm text-gray-400 italic">No description</p>
|
||||
)}
|
||||
<div className="mt-3 flex items-center gap-2 text-xs text-gray-500">
|
||||
<span>
|
||||
Created {new Date(team.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
139
apps/web/src/components/team/TeamSettings.tsx
Normal file
139
apps/web/src/components/team/TeamSettings.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { Team } from "@mosaic/shared";
|
||||
import { Card, CardHeader, CardContent, CardFooter, Button, Input, Textarea } from "@mosaic/ui";
|
||||
|
||||
interface TeamSettingsProps {
|
||||
team: Team;
|
||||
workspaceId: string;
|
||||
onUpdate: (data: { name?: string; description?: string }) => Promise<void>;
|
||||
onDelete: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function TeamSettings({ team, workspaceId, onUpdate, onDelete }: TeamSettingsProps) {
|
||||
const [name, setName] = useState(team.name);
|
||||
const [description, setDescription] = useState(team.description || "");
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
|
||||
const hasChanges = name !== team.name || description !== (team.description || "");
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!hasChanges) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await onUpdate({
|
||||
name: name !== team.name ? name : undefined,
|
||||
description: description !== (team.description || "") ? description : undefined,
|
||||
});
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to update team:", error);
|
||||
alert("Failed to update team. Please try again.");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setName(team.name);
|
||||
setDescription(team.description || "");
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await onDelete();
|
||||
} catch (error) {
|
||||
console.error("Failed to delete team:", error);
|
||||
alert("Failed to delete team. Please try again.");
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Team Settings</h2>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
label="Team Name"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
setIsEditing(true);
|
||||
}}
|
||||
placeholder="Enter team name"
|
||||
fullWidth
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<Textarea
|
||||
label="Description"
|
||||
value={description}
|
||||
onChange={(e) => {
|
||||
setDescription(e.target.value);
|
||||
setIsEditing(true);
|
||||
}}
|
||||
placeholder="Enter team description (optional)"
|
||||
fullWidth
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex gap-2">
|
||||
{isEditing && (
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || isSaving || !name.trim()}
|
||||
>
|
||||
{isSaving ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={handleCancel} disabled={isSaving}>
|
||||
Cancel
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{!showDeleteConfirm ? (
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Delete Team
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-sm text-gray-600 self-center">
|
||||
Are you sure?
|
||||
</span>
|
||||
<Button variant="danger" onClick={handleDelete} disabled={isDeleting}>
|
||||
{isDeleting ? "Deleting..." : "Confirm Delete"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setShowDeleteConfirm(false)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user