Files
workshop-manager/src/components/collaboration/BaseSessionLiveWrapper.tsx
Froidefond Julien a8c05aa841 perf(quick-wins): batch collaborator resolution, debounce SSE refresh, loading states
- Eliminate N+1 on resolveCollaborator: add batchResolveCollaborators() in
  auth.ts (2 DB queries max regardless of session count), update all 4
  workshop services to use post-batch mapping
- Debounce router.refresh() in useLive.ts (300ms) to group simultaneous
  SSE events and avoid cascade re-renders
- Call cleanupOldEvents fire-and-forget in createEvent to purge old SSE
  events inline without blocking the response
- Add loading.tsx skeletons on /sessions and /users matching actual page
  layout (PageHeader + content structure)
- Lazy-load ShareModal via next/dynamic in BaseSessionLiveWrapper to reduce
  initial JS bundle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 08:07:22 +01:00

109 lines
3.0 KiB
TypeScript

'use client';
import { useState, useCallback } from 'react';
import dynamic from 'next/dynamic';
import { useLive, type LiveEvent } from '@/hooks/useLive';
import { CollaborationToolbar } from './CollaborationToolbar';
import type { ShareRole } from '@prisma/client';
const ShareModal = dynamic(() => import('./ShareModal').then((m) => m.ShareModal), { ssr: false });
import type { TeamWithMembers, Share } from '@/lib/share-utils';
export type LiveApiPath = 'sessions' | 'motivators' | 'weather' | 'year-review' | 'weekly-checkin' | 'gif-mood';
interface ShareModalConfig {
title: string;
sessionSubtitle: string;
helpText: React.ReactNode;
}
interface BaseSessionLiveWrapperConfig {
apiPath: LiveApiPath;
shareModal: ShareModalConfig;
onShareWithEmail: (
email: string,
role: ShareRole
) => Promise<{ success: boolean; error?: string }>;
onRemoveShare: (userId: string) => Promise<unknown>;
onShareWithTeam?: (
teamId: string,
role: ShareRole
) => Promise<{ success: boolean; error?: string }>;
}
interface BaseSessionLiveWrapperProps {
sessionId: string;
sessionTitle: string;
currentUserId: string;
shares: Share[];
isOwner: boolean;
canEdit: boolean;
userTeams?: TeamWithMembers[];
children: React.ReactNode;
config: BaseSessionLiveWrapperConfig;
}
export function BaseSessionLiveWrapper({
sessionId,
sessionTitle,
currentUserId,
shares,
isOwner,
canEdit,
userTeams = [],
children,
config,
}: BaseSessionLiveWrapperProps) {
const [shareModalOpen, setShareModalOpen] = useState(false);
const [lastEventUser, setLastEventUser] = useState<string | null>(null);
const handleEvent = useCallback((event: LiveEvent) => {
// Show who made the last change
if (event.user?.name || event.user?.email) {
setLastEventUser(event.user.name || event.user.email);
// Clear after 3 seconds
setTimeout(() => setLastEventUser(null), 3000);
}
}, []);
const { isConnected, error } = useLive({
sessionId,
apiPath: config.apiPath,
currentUserId,
onEvent: handleEvent,
});
return (
<>
<CollaborationToolbar
isConnected={isConnected}
error={error}
lastEventUser={lastEventUser}
canEdit={canEdit}
shares={shares}
onShareClick={() => setShareModalOpen(true)}
/>
{/* Content */}
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
{/* Share Modal */}
<ShareModal
isOpen={shareModalOpen}
onClose={() => setShareModalOpen(false)}
title={config.shareModal.title}
sessionSubtitle={config.shareModal.sessionSubtitle}
sessionTitle={sessionTitle}
shares={shares}
isOwner={isOwner}
userTeams={userTeams}
currentUserId={currentUserId}
onShareWithEmail={config.onShareWithEmail}
onShareWithTeam={config.onShareWithTeam}
onRemoveShare={config.onRemoveShare}
helpText={config.shareModal.helpText}
/>
</>
);
}