Files
stack/apps/web/src/app/(authenticated)/settings/workspaces/page.tsx
Jason Woltje 20c9e68e1b
All checks were successful
ci/woodpecker/push/web Pipeline was successful
feat(web): wire workspaces settings page to real API (MS21-UI-003) (#574)
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-02-28 20:48:24 +00:00

165 lines
5.7 KiB
TypeScript

"use client";
import type { ReactElement, SyntheticEvent } from "react";
import { useCallback, useEffect, useState } from "react";
import { WorkspaceCard } from "@/components/workspace/WorkspaceCard";
import { createWorkspace, fetchUserWorkspaces, type UserWorkspace } from "@/lib/api/workspaces";
import Link from "next/link";
function getErrorMessage(error: unknown, fallback: string): string {
if (error instanceof Error) {
return error.message;
}
return fallback;
}
/**
* Workspaces Page
* Fetches and creates workspaces through the real API.
*/
export default function WorkspacesPage(): ReactElement {
const [workspaces, setWorkspaces] = useState<UserWorkspace[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [loadError, setLoadError] = useState<string | null>(null);
const [isCreating, setIsCreating] = useState(false);
const [newWorkspaceName, setNewWorkspaceName] = useState("");
const [createError, setCreateError] = useState<string | null>(null);
const loadWorkspaces = useCallback(async (): Promise<void> => {
setIsLoading(true);
try {
const data = await fetchUserWorkspaces();
setWorkspaces(data);
setLoadError(null);
} catch (error) {
setLoadError(getErrorMessage(error, "Failed to load workspaces"));
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
void loadWorkspaces();
}, [loadWorkspaces]);
const workspacesWithRoles = workspaces.map((workspace) => ({
...workspace,
settings: {},
createdAt: new Date(workspace.createdAt),
updatedAt: new Date(workspace.createdAt),
userRole: workspace.role,
memberCount: 1,
}));
const handleCreateWorkspace = async (e: SyntheticEvent<HTMLFormElement>): Promise<void> => {
e.preventDefault();
const workspaceName = newWorkspaceName.trim();
if (!workspaceName) return;
setIsCreating(true);
setCreateError(null);
try {
await createWorkspace({ name: workspaceName });
setNewWorkspaceName("");
await loadWorkspaces();
} catch (error) {
setCreateError(getErrorMessage(error, "Failed to create workspace"));
} finally {
setIsCreating(false);
}
};
return (
<main className="container mx-auto px-4 py-8 max-w-5xl">
<div className="mb-8">
<div className="flex items-center justify-between mb-2">
<h1 className="text-3xl font-bold text-gray-900">Workspaces</h1>
<Link href="/settings" className="text-sm text-blue-600 hover:text-blue-700">
Back to Settings
</Link>
</div>
<p className="text-gray-600">Manage your workspaces and collaborate with your team</p>
</div>
{/* Create New Workspace */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Create New Workspace</h2>
<form onSubmit={handleCreateWorkspace} className="flex gap-3">
<input
type="text"
value={newWorkspaceName}
onChange={(e) => {
setNewWorkspaceName(e.target.value);
}}
placeholder="Enter workspace name..."
disabled={isCreating}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100"
/>
<button
type="submit"
disabled={isCreating || !newWorkspaceName.trim()}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
>
{isCreating ? "Creating..." : "Create Workspace"}
</button>
</form>
{createError !== null && (
<div className="mt-3 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
{createError}
</div>
)}
</div>
{/* Workspace List */}
<div className="space-y-4">
<h2 className="text-xl font-semibold text-gray-900">
Your Workspaces ({isLoading ? "..." : workspacesWithRoles.length})
</h2>
{loadError !== null ? (
<div className="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-red-700">
{loadError}
</div>
) : isLoading ? (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-12 text-center text-gray-600">
Loading workspaces...
</div>
) : workspacesWithRoles.length === 0 ? (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-12 text-center">
<svg
className="mx-auto h-12 w-12 text-gray-400 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
/>
</svg>
<h3 className="text-lg font-medium text-gray-900 mb-2">No workspaces yet</h3>
<p className="text-gray-600">Create your first workspace to get started</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{workspacesWithRoles.map((workspace) => (
<WorkspaceCard
key={workspace.id}
workspace={workspace}
userRole={workspace.userRole}
memberCount={workspace.memberCount}
/>
))}
</div>
)}
</div>
</main>
);
}