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:
Jason Woltje
2026-01-29 16:59:26 -06:00
parent 287a0e2556
commit 5291fece26
43 changed files with 4152 additions and 99 deletions

View File

@@ -0,0 +1,66 @@
import type { Workspace } from "@mosaic/shared";
import { WorkspaceMemberRole } from "@mosaic/shared";
import Link from "next/link";
interface WorkspaceCardProps {
workspace: Workspace;
userRole: WorkspaceMemberRole;
memberCount: number;
}
const roleColors: Record<WorkspaceMemberRole, string> = {
[WorkspaceMemberRole.OWNER]: "bg-purple-100 text-purple-700",
[WorkspaceMemberRole.ADMIN]: "bg-blue-100 text-blue-700",
[WorkspaceMemberRole.MEMBER]: "bg-green-100 text-green-700",
[WorkspaceMemberRole.GUEST]: "bg-gray-100 text-gray-700",
};
const roleLabels: Record<WorkspaceMemberRole, string> = {
[WorkspaceMemberRole.OWNER]: "Owner",
[WorkspaceMemberRole.ADMIN]: "Admin",
[WorkspaceMemberRole.MEMBER]: "Member",
[WorkspaceMemberRole.GUEST]: "Guest",
};
export function WorkspaceCard({ workspace, userRole, memberCount }: WorkspaceCardProps) {
return (
<Link
href={`/settings/workspaces/${workspace.id}`}
className="block bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{workspace.name}
</h3>
<div className="flex items-center gap-3 text-sm text-gray-600">
<span className="flex items-center gap-1">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
{memberCount} {memberCount === 1 ? "member" : "members"}
</span>
</div>
</div>
<span
className={`px-3 py-1 rounded-full text-xs font-medium ${roleColors[userRole]}`}
>
{roleLabels[userRole]}
</span>
</div>
<div className="mt-4 text-sm text-gray-500">
Created {new Date(workspace.createdAt).toLocaleDateString()}
</div>
</Link>
);
}