- 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>
109 lines
3.0 KiB
TypeScript
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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|