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:
117
apps/web/src/components/knowledge/EntryList.tsx
Normal file
117
apps/web/src/components/knowledge/EntryList.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { KnowledgeEntryWithTags } from "@mosaic/shared";
|
||||
import { EntryCard } from "./EntryCard";
|
||||
import { BookOpen } from "lucide-react";
|
||||
|
||||
interface EntryListProps {
|
||||
entries: KnowledgeEntryWithTags[];
|
||||
isLoading: boolean;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
}
|
||||
|
||||
export function EntryList({
|
||||
entries,
|
||||
isLoading,
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
}: EntryListProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center p-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<span className="ml-3 text-gray-600">Loading entries...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!entries || entries.length === 0) {
|
||||
return (
|
||||
<div className="text-center p-12 bg-white rounded-lg shadow-sm border border-gray-200">
|
||||
<BookOpen className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||
<p className="text-lg text-gray-700 font-medium">No entries found</p>
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
Try adjusting your filters or create a new entry
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Entry cards */}
|
||||
<div className="space-y-4">
|
||||
{entries.map((entry) => (
|
||||
<EntryCard key={entry.id} entry={entry} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-center gap-2 pt-6">
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => {
|
||||
// Show first, last, current, and pages around current
|
||||
const shouldShow =
|
||||
page === 1 ||
|
||||
page === totalPages ||
|
||||
Math.abs(page - currentPage) <= 1;
|
||||
|
||||
// Show ellipsis
|
||||
const showEllipsisBefore = page === currentPage - 2 && currentPage > 3;
|
||||
const showEllipsisAfter = page === currentPage + 2 && currentPage < totalPages - 2;
|
||||
|
||||
if (!shouldShow && !showEllipsisBefore && !showEllipsisAfter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (showEllipsisBefore || showEllipsisAfter) {
|
||||
return (
|
||||
<span key={`ellipsis-${page}`} className="px-2 text-gray-500">
|
||||
...
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={page}
|
||||
onClick={() => onPageChange(page)}
|
||||
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
page === currentPage
|
||||
? "bg-blue-600 text-white"
|
||||
: "text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Results info */}
|
||||
<div className="text-center text-sm text-gray-500">
|
||||
Page {currentPage} of {totalPages} ({entries.length} entries)
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user