feat: refactor session components to utilize BaseSessionLiveWrapper, streamlining sharing functionality and reducing code duplication across various session types
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m14s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m14s
This commit is contained in:
@@ -27,24 +27,14 @@ import {
|
|||||||
} from '@/lib/workshops';
|
} from '@/lib/workshops';
|
||||||
import { useClickOutside } from '@/hooks/useClickOutside';
|
import { useClickOutside } from '@/hooks/useClickOutside';
|
||||||
|
|
||||||
|
import type { Share } from '@/lib/share-utils';
|
||||||
|
|
||||||
const TYPE_TABS = [
|
const TYPE_TABS = [
|
||||||
{ value: 'all' as const, icon: '📋', label: 'Tous' },
|
{ value: 'all' as const, icon: '📋', label: 'Tous' },
|
||||||
{ value: 'team' as const, icon: '🏢', label: 'Équipe' },
|
{ value: 'team' as const, icon: '🏢', label: 'Équipe' },
|
||||||
...WORKSHOPS.map((w) => ({ value: w.id, icon: w.icon, label: w.labelShort })),
|
...WORKSHOPS.map((w) => ({ value: w.id, icon: w.icon, label: w.labelShort })),
|
||||||
];
|
];
|
||||||
|
|
||||||
interface ShareUser {
|
|
||||||
id: string;
|
|
||||||
name: string | null;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Share {
|
|
||||||
id: string;
|
|
||||||
role: 'VIEWER' | 'EDITOR';
|
|
||||||
user: ShareUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ResolvedCollaborator {
|
interface ResolvedCollaborator {
|
||||||
raw: string;
|
raw: string;
|
||||||
matchedUser: {
|
matchedUser: {
|
||||||
|
|||||||
100
src/components/collaboration/BaseSessionLiveWrapper.tsx
Normal file
100
src/components/collaboration/BaseSessionLiveWrapper.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { useLive, type LiveEvent } from '@/hooks/useLive';
|
||||||
|
import { CollaborationToolbar } from './CollaborationToolbar';
|
||||||
|
import { ShareModal } from './ShareModal';
|
||||||
|
import type { ShareRole } from '@prisma/client';
|
||||||
|
import type { TeamWithMembers, Share } from '@/lib/share-utils';
|
||||||
|
|
||||||
|
export type LiveApiPath = 'sessions' | 'motivators' | 'weather' | 'year-review' | 'weekly-checkin';
|
||||||
|
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
src/components/collaboration/CollaborationToolbar.tsx
Normal file
51
src/components/collaboration/CollaborationToolbar.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { LiveIndicator } from './LiveIndicator';
|
||||||
|
import { ShareButton } from './ShareButton';
|
||||||
|
import { CollaboratorAvatars } from './CollaboratorAvatars';
|
||||||
|
import type { Share } from '@/lib/share-utils';
|
||||||
|
|
||||||
|
interface CollaborationToolbarProps {
|
||||||
|
isConnected: boolean;
|
||||||
|
error: string | null;
|
||||||
|
lastEventUser: string | null;
|
||||||
|
canEdit: boolean;
|
||||||
|
shares: Share[];
|
||||||
|
onShareClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CollaborationToolbar({
|
||||||
|
isConnected,
|
||||||
|
error,
|
||||||
|
lastEventUser,
|
||||||
|
canEdit,
|
||||||
|
shares,
|
||||||
|
onShareClick,
|
||||||
|
}: CollaborationToolbarProps) {
|
||||||
|
return (
|
||||||
|
<div className="mb-4 flex items-center justify-between rounded-lg border border-border bg-card p-3">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<LiveIndicator isConnected={isConnected} error={error} />
|
||||||
|
|
||||||
|
{lastEventUser && (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted animate-pulse">
|
||||||
|
<span>✏️</span>
|
||||||
|
<span>{lastEventUser} édite...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!canEdit && (
|
||||||
|
<div className="flex items-center gap-2 rounded-full bg-yellow/10 px-3 py-1.5 text-sm text-yellow">
|
||||||
|
<span>👁️</span>
|
||||||
|
<span>Mode lecture</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CollaboratorAvatars shares={shares} />
|
||||||
|
<ShareButton onClick={onShareClick} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
src/components/collaboration/CollaboratorAvatars.tsx
Normal file
37
src/components/collaboration/CollaboratorAvatars.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Avatar } from '@/components/ui/Avatar';
|
||||||
|
import type { Share } from '@/lib/share-utils';
|
||||||
|
|
||||||
|
interface CollaboratorAvatarsProps {
|
||||||
|
shares: Share[];
|
||||||
|
maxVisible?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CollaboratorAvatars({ shares, maxVisible = 3 }: CollaboratorAvatarsProps) {
|
||||||
|
if (shares.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleShares = shares.slice(0, maxVisible);
|
||||||
|
const remainingCount = shares.length - maxVisible;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex -space-x-2">
|
||||||
|
{visibleShares.map((share) => (
|
||||||
|
<Avatar
|
||||||
|
key={share.id}
|
||||||
|
email={share.user.email}
|
||||||
|
name={share.user.name}
|
||||||
|
size={32}
|
||||||
|
className="border-2 border-card"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{remainingCount > 0 && (
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-card bg-muted/20 text-xs font-medium text-muted">
|
||||||
|
+{remainingCount}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,27 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { BaseSessionLiveWrapper } from './BaseSessionLiveWrapper';
|
||||||
import { useSessionLive, type LiveEvent } from '@/hooks/useSessionLive';
|
|
||||||
import { LiveIndicator } from './LiveIndicator';
|
|
||||||
import { ShareModal } from './ShareModal';
|
|
||||||
import { shareSessionAction, removeShareAction } from '@/actions/share';
|
import { shareSessionAction, removeShareAction } from '@/actions/share';
|
||||||
import { Button } from '@/components/ui/Button';
|
import type { TeamWithMembers, Share } from '@/lib/share-utils';
|
||||||
import { Avatar } from '@/components/ui/Avatar';
|
|
||||||
import type { ShareRole } from '@prisma/client';
|
|
||||||
import type { TeamWithMembers } from '@/lib/share-utils';
|
|
||||||
|
|
||||||
interface ShareUser {
|
|
||||||
id: string;
|
|
||||||
name: string | null;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Share {
|
|
||||||
id: string;
|
|
||||||
role: ShareRole;
|
|
||||||
user: ShareUser;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SessionLiveWrapperProps {
|
interface SessionLiveWrapperProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -44,105 +25,33 @@ export function SessionLiveWrapper({
|
|||||||
userTeams = [],
|
userTeams = [],
|
||||||
children,
|
children,
|
||||||
}: SessionLiveWrapperProps) {
|
}: SessionLiveWrapperProps) {
|
||||||
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 } = useSessionLive({
|
|
||||||
sessionId,
|
|
||||||
currentUserId,
|
|
||||||
onEvent: handleEvent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<BaseSessionLiveWrapper
|
||||||
{/* Header toolbar */}
|
sessionId={sessionId}
|
||||||
<div className="mb-4 flex items-center justify-between rounded-lg border border-border bg-card p-3">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<LiveIndicator isConnected={isConnected} error={error} />
|
|
||||||
|
|
||||||
{lastEventUser && (
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted animate-pulse">
|
|
||||||
<span>✏️</span>
|
|
||||||
<span>{lastEventUser} édite...</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!canEdit && (
|
|
||||||
<div className="flex items-center gap-2 rounded-full bg-yellow/10 px-3 py-1.5 text-sm text-yellow">
|
|
||||||
<span>👁️</span>
|
|
||||||
<span>Mode lecture</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{/* Collaborators avatars */}
|
|
||||||
{shares.length > 0 && (
|
|
||||||
<div className="flex -space-x-2">
|
|
||||||
{shares.slice(0, 3).map((share) => (
|
|
||||||
<Avatar
|
|
||||||
key={share.id}
|
|
||||||
email={share.user.email}
|
|
||||||
name={share.user.name}
|
|
||||||
size={32}
|
|
||||||
className="border-2 border-card"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{shares.length > 3 && (
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-card bg-muted/20 text-xs font-medium text-muted">
|
|
||||||
+{shares.length - 3}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setShareModalOpen(true)}>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
className="mr-2 h-4 w-4"
|
|
||||||
>
|
|
||||||
<path d="M13 4.5a2.5 2.5 0 11.702 1.737L6.97 9.604a2.518 2.518 0 010 .792l6.733 3.367a2.5 2.5 0 11-.671 1.341l-6.733-3.367a2.5 2.5 0 110-3.475l6.733-3.366A2.52 2.52 0 0113 4.5z" />
|
|
||||||
</svg>
|
|
||||||
Partager
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
|
|
||||||
|
|
||||||
{/* Share Modal */}
|
|
||||||
<ShareModal
|
|
||||||
isOpen={shareModalOpen}
|
|
||||||
onClose={() => setShareModalOpen(false)}
|
|
||||||
title="Partager la session"
|
|
||||||
sessionSubtitle="Session"
|
|
||||||
sessionTitle={sessionTitle}
|
sessionTitle={sessionTitle}
|
||||||
|
currentUserId={currentUserId}
|
||||||
shares={shares}
|
shares={shares}
|
||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
|
canEdit={canEdit}
|
||||||
userTeams={userTeams}
|
userTeams={userTeams}
|
||||||
currentUserId={currentUserId}
|
config={{
|
||||||
onShareWithEmail={(email, role) => shareSessionAction(sessionId, email, role)}
|
apiPath: 'sessions',
|
||||||
onRemoveShare={(userId) => removeShareAction(sessionId, userId)}
|
shareModal: {
|
||||||
helpText={
|
title: 'Partager la session',
|
||||||
|
sessionSubtitle: 'Session',
|
||||||
|
helpText: (
|
||||||
<>
|
<>
|
||||||
<strong>Éditeur</strong> : peut modifier les items et actions
|
<strong>Éditeur</strong> : peut modifier les items et actions
|
||||||
<br />
|
<br />
|
||||||
<strong>Lecteur</strong> : peut uniquement consulter
|
<strong>Lecteur</strong> : peut uniquement consulter
|
||||||
</>
|
</>
|
||||||
}
|
),
|
||||||
/>
|
},
|
||||||
</>
|
onShareWithEmail: (email, role) => shareSessionAction(sessionId, email, role),
|
||||||
|
onRemoveShare: (userId) => removeShareAction(sessionId, userId),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BaseSessionLiveWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/components/collaboration/ShareButton.tsx
Normal file
23
src/components/collaboration/ShareButton.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
|
||||||
|
interface ShareButtonProps {
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShareButton({ onClick }: ShareButtonProps) {
|
||||||
|
return (
|
||||||
|
<Button variant="outline" size="sm" onClick={onClick}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
className="mr-2 h-4 w-4"
|
||||||
|
>
|
||||||
|
<path d="M13 4.5a2.5 2.5 0 11.702 1.737L6.97 9.604a2.518 2.518 0 010 .792l6.733 3.367a2.5 2.5 0 11-.671 1.341l-6.733-3.367a2.5 2.5 0 110-3.475l6.733-3.366A2.52 2.52 0 0113 4.5z" />
|
||||||
|
</svg>
|
||||||
|
Partager
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,22 +8,9 @@ import { Button } from '@/components/ui/Button';
|
|||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { Avatar } from '@/components/ui/Avatar';
|
import { Avatar } from '@/components/ui/Avatar';
|
||||||
import { Select } from '@/components/ui/Select';
|
import { Select } from '@/components/ui/Select';
|
||||||
import { getTeamMembersForShare, type TeamWithMembers } from '@/lib/share-utils';
|
import { getTeamMembersForShare, type TeamWithMembers, type Share } from '@/lib/share-utils';
|
||||||
import type { ShareRole } from '@prisma/client';
|
import type { ShareRole } from '@prisma/client';
|
||||||
|
|
||||||
interface ShareUser {
|
|
||||||
id: string;
|
|
||||||
name: string | null;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Share {
|
|
||||||
id: string;
|
|
||||||
role: ShareRole;
|
|
||||||
user: ShareUser;
|
|
||||||
createdAt?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ShareTab = 'teamMember' | 'team' | 'email';
|
type ShareTab = 'teamMember' | 'team' | 'email';
|
||||||
|
|
||||||
interface ShareModalProps {
|
interface ShareModalProps {
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
export { LiveIndicator } from './LiveIndicator';
|
export { LiveIndicator } from './LiveIndicator';
|
||||||
export { ShareModal } from './ShareModal';
|
export { ShareModal } from './ShareModal';
|
||||||
export { SessionLiveWrapper } from './SessionLiveWrapper';
|
export { SessionLiveWrapper } from './SessionLiveWrapper';
|
||||||
|
export { BaseSessionLiveWrapper } from './BaseSessionLiveWrapper';
|
||||||
|
export { ShareButton } from './ShareButton';
|
||||||
|
export { CollaboratorAvatars } from './CollaboratorAvatars';
|
||||||
|
export { CollaborationToolbar } from './CollaborationToolbar';
|
||||||
|
export type { LiveApiPath } from './BaseSessionLiveWrapper';
|
||||||
|
|||||||
@@ -1,27 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { BaseSessionLiveWrapper } from '@/components/collaboration/BaseSessionLiveWrapper';
|
||||||
import { useMotivatorLive, type MotivatorLiveEvent } from '@/hooks/useMotivatorLive';
|
|
||||||
import { LiveIndicator } from '@/components/collaboration/LiveIndicator';
|
|
||||||
import { ShareModal } from '@/components/collaboration/ShareModal';
|
|
||||||
import { shareMotivatorSession, removeMotivatorShare } from '@/actions/moving-motivators';
|
import { shareMotivatorSession, removeMotivatorShare } from '@/actions/moving-motivators';
|
||||||
import type { TeamWithMembers } from '@/lib/share-utils';
|
import type { TeamWithMembers, Share } from '@/lib/share-utils';
|
||||||
import { Button } from '@/components/ui/Button';
|
|
||||||
import { Avatar } from '@/components/ui/Avatar';
|
|
||||||
import type { ShareRole } from '@prisma/client';
|
|
||||||
|
|
||||||
interface ShareUser {
|
|
||||||
id: string;
|
|
||||||
name: string | null;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Share {
|
|
||||||
id: string;
|
|
||||||
role: ShareRole;
|
|
||||||
user: ShareUser;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MotivatorLiveWrapperProps {
|
interface MotivatorLiveWrapperProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -44,105 +25,33 @@ export function MotivatorLiveWrapper({
|
|||||||
userTeams = [],
|
userTeams = [],
|
||||||
children,
|
children,
|
||||||
}: MotivatorLiveWrapperProps) {
|
}: MotivatorLiveWrapperProps) {
|
||||||
const [shareModalOpen, setShareModalOpen] = useState(false);
|
|
||||||
const [lastEventUser, setLastEventUser] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleEvent = useCallback((event: MotivatorLiveEvent) => {
|
|
||||||
// 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 } = useMotivatorLive({
|
|
||||||
sessionId,
|
|
||||||
currentUserId,
|
|
||||||
onEvent: handleEvent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<BaseSessionLiveWrapper
|
||||||
{/* Header toolbar */}
|
sessionId={sessionId}
|
||||||
<div className="mb-4 flex items-center justify-between rounded-lg border border-border bg-card p-3">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<LiveIndicator isConnected={isConnected} error={error} />
|
|
||||||
|
|
||||||
{lastEventUser && (
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted animate-pulse">
|
|
||||||
<span>✏️</span>
|
|
||||||
<span>{lastEventUser} édite...</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!canEdit && (
|
|
||||||
<div className="flex items-center gap-2 rounded-full bg-yellow/10 px-3 py-1.5 text-sm text-yellow">
|
|
||||||
<span>👁️</span>
|
|
||||||
<span>Mode lecture</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{/* Collaborators avatars */}
|
|
||||||
{shares.length > 0 && (
|
|
||||||
<div className="flex -space-x-2">
|
|
||||||
{shares.slice(0, 3).map((share) => (
|
|
||||||
<Avatar
|
|
||||||
key={share.id}
|
|
||||||
email={share.user.email}
|
|
||||||
name={share.user.name}
|
|
||||||
size={32}
|
|
||||||
className="border-2 border-card"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{shares.length > 3 && (
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-card bg-muted/20 text-xs font-medium text-muted">
|
|
||||||
+{shares.length - 3}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setShareModalOpen(true)}>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
className="mr-2 h-4 w-4"
|
|
||||||
>
|
|
||||||
<path d="M13 4.5a2.5 2.5 0 11.702 1.737L6.97 9.604a2.518 2.518 0 010 .792l6.733 3.367a2.5 2.5 0 11-.671 1.341l-6.733-3.367a2.5 2.5 0 110-3.475l6.733-3.366A2.52 2.52 0 0113 4.5z" />
|
|
||||||
</svg>
|
|
||||||
Partager
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
|
|
||||||
|
|
||||||
{/* Share Modal */}
|
|
||||||
<ShareModal
|
|
||||||
isOpen={shareModalOpen}
|
|
||||||
onClose={() => setShareModalOpen(false)}
|
|
||||||
title="Partager la session"
|
|
||||||
sessionSubtitle="Session Moving Motivators"
|
|
||||||
sessionTitle={sessionTitle}
|
sessionTitle={sessionTitle}
|
||||||
|
currentUserId={currentUserId}
|
||||||
shares={shares}
|
shares={shares}
|
||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
|
canEdit={canEdit}
|
||||||
userTeams={userTeams}
|
userTeams={userTeams}
|
||||||
currentUserId={currentUserId}
|
config={{
|
||||||
onShareWithEmail={(email, role) => shareMotivatorSession(sessionId, email, role)}
|
apiPath: 'motivators',
|
||||||
onRemoveShare={(userId) => removeMotivatorShare(sessionId, userId)}
|
shareModal: {
|
||||||
helpText={
|
title: 'Partager la session',
|
||||||
|
sessionSubtitle: 'Session Moving Motivators',
|
||||||
|
helpText: (
|
||||||
<>
|
<>
|
||||||
<strong>Éditeur</strong> : peut modifier les cartes et leurs positions
|
<strong>Éditeur</strong> : peut modifier les cartes et leurs positions
|
||||||
<br />
|
<br />
|
||||||
<strong>Lecteur</strong> : peut uniquement consulter
|
<strong>Lecteur</strong> : peut uniquement consulter
|
||||||
</>
|
</>
|
||||||
}
|
),
|
||||||
/>
|
},
|
||||||
</>
|
onShareWithEmail: (email, role) => shareMotivatorSession(sessionId, email, role),
|
||||||
|
onRemoveShare: (userId) => removeMotivatorShare(sessionId, userId),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BaseSessionLiveWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { BaseSessionLiveWrapper } from '@/components/collaboration/BaseSessionLiveWrapper';
|
||||||
import { useWeatherLive, type WeatherLiveEvent } from '@/hooks/useWeatherLive';
|
|
||||||
import { LiveIndicator } from '@/components/collaboration/LiveIndicator';
|
|
||||||
import { ShareModal } from '@/components/collaboration/ShareModal';
|
|
||||||
import { shareWeatherSession, shareWeatherSessionToTeam, removeWeatherShare } from '@/actions/weather';
|
import { shareWeatherSession, shareWeatherSessionToTeam, removeWeatherShare } from '@/actions/weather';
|
||||||
import { Button } from '@/components/ui/Button';
|
import type { TeamWithMembers, Share } from '@/lib/share-utils';
|
||||||
import { Avatar } from '@/components/ui/Avatar';
|
|
||||||
import type { ShareRole } from '@prisma/client';
|
|
||||||
|
|
||||||
interface ShareUser {
|
|
||||||
id: string;
|
|
||||||
name: string | null;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Share {
|
|
||||||
id: string;
|
|
||||||
role: ShareRole;
|
|
||||||
user: ShareUser;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Team {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string | null;
|
|
||||||
userRole: 'ADMIN' | 'MEMBER';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WeatherLiveWrapperProps {
|
interface WeatherLiveWrapperProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -36,7 +11,7 @@ interface WeatherLiveWrapperProps {
|
|||||||
shares: Share[];
|
shares: Share[];
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
userTeams?: Team[];
|
userTeams?: TeamWithMembers[];
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,106 +25,34 @@ export function WeatherLiveWrapper({
|
|||||||
userTeams = [],
|
userTeams = [],
|
||||||
children,
|
children,
|
||||||
}: WeatherLiveWrapperProps) {
|
}: WeatherLiveWrapperProps) {
|
||||||
const [shareModalOpen, setShareModalOpen] = useState(false);
|
|
||||||
const [lastEventUser, setLastEventUser] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleEvent = useCallback((event: WeatherLiveEvent) => {
|
|
||||||
// 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 } = useWeatherLive({
|
|
||||||
sessionId,
|
|
||||||
currentUserId,
|
|
||||||
onEvent: handleEvent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<BaseSessionLiveWrapper
|
||||||
{/* Header toolbar */}
|
sessionId={sessionId}
|
||||||
<div className="mb-4 flex items-center justify-between rounded-lg border border-border bg-card p-3">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<LiveIndicator isConnected={isConnected} error={error} />
|
|
||||||
|
|
||||||
{lastEventUser && (
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted animate-pulse">
|
|
||||||
<span>✏️</span>
|
|
||||||
<span>{lastEventUser} édite...</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!canEdit && (
|
|
||||||
<div className="flex items-center gap-2 rounded-full bg-yellow/10 px-3 py-1.5 text-sm text-yellow">
|
|
||||||
<span>👁️</span>
|
|
||||||
<span>Mode lecture</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{/* Collaborators avatars */}
|
|
||||||
{shares.length > 0 && (
|
|
||||||
<div className="flex -space-x-2">
|
|
||||||
{shares.slice(0, 3).map((share) => (
|
|
||||||
<Avatar
|
|
||||||
key={share.id}
|
|
||||||
email={share.user.email}
|
|
||||||
name={share.user.name}
|
|
||||||
size={32}
|
|
||||||
className="border-2 border-card"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{shares.length > 3 && (
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-card bg-muted/20 text-xs font-medium text-muted">
|
|
||||||
+{shares.length - 3}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setShareModalOpen(true)}>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
className="mr-2 h-4 w-4"
|
|
||||||
>
|
|
||||||
<path d="M13 4.5a2.5 2.5 0 11.702 1.737L6.97 9.604a2.518 2.518 0 010 .792l6.733 3.367a2.5 2.5 0 11-.671 1.341l-6.733-3.367a2.5 2.5 0 110-3.475l6.733-3.366A2.52 2.52 0 0113 4.5z" />
|
|
||||||
</svg>
|
|
||||||
Partager
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
|
|
||||||
|
|
||||||
{/* Share Modal */}
|
|
||||||
<ShareModal
|
|
||||||
isOpen={shareModalOpen}
|
|
||||||
onClose={() => setShareModalOpen(false)}
|
|
||||||
title="Partager la météo"
|
|
||||||
sessionSubtitle="Météo personnelle"
|
|
||||||
sessionTitle={sessionTitle}
|
sessionTitle={sessionTitle}
|
||||||
|
currentUserId={currentUserId}
|
||||||
shares={shares}
|
shares={shares}
|
||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
|
canEdit={canEdit}
|
||||||
userTeams={userTeams}
|
userTeams={userTeams}
|
||||||
currentUserId={currentUserId}
|
config={{
|
||||||
onShareWithEmail={(email, role) => shareWeatherSession(sessionId, email, role)}
|
apiPath: 'weather',
|
||||||
onShareWithTeam={(teamId, role) => shareWeatherSessionToTeam(sessionId, teamId, role)}
|
shareModal: {
|
||||||
onRemoveShare={(userId) => removeWeatherShare(sessionId, userId)}
|
title: 'Partager la météo',
|
||||||
helpText={
|
sessionSubtitle: 'Météo personnelle',
|
||||||
|
helpText: (
|
||||||
<>
|
<>
|
||||||
<strong>Éditeur</strong> : peut modifier sa météo et voir celle des autres
|
<strong>Éditeur</strong> : peut modifier sa météo et voir celle des autres
|
||||||
<br />
|
<br />
|
||||||
<strong>Lecteur</strong> : peut uniquement consulter
|
<strong>Lecteur</strong> : peut uniquement consulter
|
||||||
</>
|
</>
|
||||||
}
|
),
|
||||||
/>
|
},
|
||||||
</>
|
onShareWithEmail: (email, role) => shareWeatherSession(sessionId, email, role),
|
||||||
|
onShareWithTeam: (teamId, role) => shareWeatherSessionToTeam(sessionId, teamId, role),
|
||||||
|
onRemoveShare: (userId) => removeWeatherShare(sessionId, userId),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BaseSessionLiveWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { BaseSessionLiveWrapper } from '@/components/collaboration/BaseSessionLiveWrapper';
|
||||||
import { useWeeklyCheckInLive, type WeeklyCheckInLiveEvent } from '@/hooks/useWeeklyCheckInLive';
|
|
||||||
import { LiveIndicator } from '@/components/collaboration/LiveIndicator';
|
|
||||||
import { ShareModal } from '@/components/collaboration/ShareModal';
|
|
||||||
import { shareWeeklyCheckInSession, removeWeeklyCheckInShare } from '@/actions/weekly-checkin';
|
import { shareWeeklyCheckInSession, removeWeeklyCheckInShare } from '@/actions/weekly-checkin';
|
||||||
import type { TeamWithMembers } from '@/lib/share-utils';
|
import type { TeamWithMembers, Share } from '@/lib/share-utils';
|
||||||
import { Button } from '@/components/ui/Button';
|
|
||||||
import { Avatar } from '@/components/ui/Avatar';
|
|
||||||
import type { ShareRole } from '@prisma/client';
|
|
||||||
|
|
||||||
interface ShareUser {
|
|
||||||
id: string;
|
|
||||||
name: string | null;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Share {
|
|
||||||
id: string;
|
|
||||||
role: ShareRole;
|
|
||||||
user: ShareUser;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WeeklyCheckInLiveWrapperProps {
|
interface WeeklyCheckInLiveWrapperProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -44,105 +25,33 @@ export function WeeklyCheckInLiveWrapper({
|
|||||||
userTeams = [],
|
userTeams = [],
|
||||||
children,
|
children,
|
||||||
}: WeeklyCheckInLiveWrapperProps) {
|
}: WeeklyCheckInLiveWrapperProps) {
|
||||||
const [shareModalOpen, setShareModalOpen] = useState(false);
|
|
||||||
const [lastEventUser, setLastEventUser] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleEvent = useCallback((event: WeeklyCheckInLiveEvent) => {
|
|
||||||
// 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 } = useWeeklyCheckInLive({
|
|
||||||
sessionId,
|
|
||||||
currentUserId,
|
|
||||||
onEvent: handleEvent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<BaseSessionLiveWrapper
|
||||||
{/* Header toolbar */}
|
sessionId={sessionId}
|
||||||
<div className="mb-4 flex items-center justify-between rounded-lg border border-border bg-card p-3">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<LiveIndicator isConnected={isConnected} error={error} />
|
|
||||||
|
|
||||||
{lastEventUser && (
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted animate-pulse">
|
|
||||||
<span>✏️</span>
|
|
||||||
<span>{lastEventUser} édite...</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!canEdit && (
|
|
||||||
<div className="flex items-center gap-2 rounded-full bg-yellow/10 px-3 py-1.5 text-sm text-yellow">
|
|
||||||
<span>👁️</span>
|
|
||||||
<span>Mode lecture</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{/* Collaborators avatars */}
|
|
||||||
{shares.length > 0 && (
|
|
||||||
<div className="flex -space-x-2">
|
|
||||||
{shares.slice(0, 3).map((share) => (
|
|
||||||
<Avatar
|
|
||||||
key={share.id}
|
|
||||||
email={share.user.email}
|
|
||||||
name={share.user.name}
|
|
||||||
size={32}
|
|
||||||
className="border-2 border-card"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{shares.length > 3 && (
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-card bg-muted/20 text-xs font-medium text-muted">
|
|
||||||
+{shares.length - 3}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setShareModalOpen(true)}>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
className="mr-2 h-4 w-4"
|
|
||||||
>
|
|
||||||
<path d="M13 4.5a2.5 2.5 0 11.702 1.737L6.97 9.604a2.518 2.518 0 010 .792l6.733 3.367a2.5 2.5 0 11-.671 1.341l-6.733-3.367a2.5 2.5 0 110-3.475l6.733-3.366A2.52 2.52 0 0113 4.5z" />
|
|
||||||
</svg>
|
|
||||||
Partager
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
|
|
||||||
|
|
||||||
{/* Share Modal */}
|
|
||||||
<ShareModal
|
|
||||||
isOpen={shareModalOpen}
|
|
||||||
onClose={() => setShareModalOpen(false)}
|
|
||||||
title="Options de partage"
|
|
||||||
sessionSubtitle="Check-in hebdomadaire"
|
|
||||||
sessionTitle={sessionTitle}
|
sessionTitle={sessionTitle}
|
||||||
|
currentUserId={currentUserId}
|
||||||
shares={shares}
|
shares={shares}
|
||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
|
canEdit={canEdit}
|
||||||
userTeams={userTeams}
|
userTeams={userTeams}
|
||||||
currentUserId={currentUserId}
|
config={{
|
||||||
onShareWithEmail={(email, role) => shareWeeklyCheckInSession(sessionId, email, role)}
|
apiPath: 'weekly-checkin',
|
||||||
onRemoveShare={(userId) => removeWeeklyCheckInShare(sessionId, userId)}
|
shareModal: {
|
||||||
helpText={
|
title: 'Options de partage',
|
||||||
|
sessionSubtitle: 'Check-in hebdomadaire',
|
||||||
|
helpText: (
|
||||||
<>
|
<>
|
||||||
<strong>Éditeur</strong> : peut modifier les items et leurs catégories
|
<strong>Éditeur</strong> : peut modifier les items et leurs catégories
|
||||||
<br />
|
<br />
|
||||||
<strong>Lecteur</strong> : peut uniquement consulter
|
<strong>Lecteur</strong> : peut uniquement consulter
|
||||||
</>
|
</>
|
||||||
}
|
),
|
||||||
/>
|
},
|
||||||
</>
|
onShareWithEmail: (email, role) => shareWeeklyCheckInSession(sessionId, email, role),
|
||||||
|
onRemoveShare: (userId) => removeWeeklyCheckInShare(sessionId, userId),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BaseSessionLiveWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { BaseSessionLiveWrapper } from '@/components/collaboration/BaseSessionLiveWrapper';
|
||||||
import { useYearReviewLive, type YearReviewLiveEvent } from '@/hooks/useYearReviewLive';
|
|
||||||
import { LiveIndicator } from '@/components/collaboration/LiveIndicator';
|
|
||||||
import { ShareModal } from '@/components/collaboration/ShareModal';
|
|
||||||
import { shareYearReviewSession, removeYearReviewShare } from '@/actions/year-review';
|
import { shareYearReviewSession, removeYearReviewShare } from '@/actions/year-review';
|
||||||
import type { TeamWithMembers } from '@/lib/share-utils';
|
import type { TeamWithMembers, Share } from '@/lib/share-utils';
|
||||||
import { Button } from '@/components/ui/Button';
|
|
||||||
import { Avatar } from '@/components/ui/Avatar';
|
|
||||||
import type { ShareRole } from '@prisma/client';
|
|
||||||
|
|
||||||
interface ShareUser {
|
|
||||||
id: string;
|
|
||||||
name: string | null;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Share {
|
|
||||||
id: string;
|
|
||||||
role: ShareRole;
|
|
||||||
user: ShareUser;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface YearReviewLiveWrapperProps {
|
interface YearReviewLiveWrapperProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -44,106 +25,34 @@ export function YearReviewLiveWrapper({
|
|||||||
userTeams = [],
|
userTeams = [],
|
||||||
children,
|
children,
|
||||||
}: YearReviewLiveWrapperProps) {
|
}: YearReviewLiveWrapperProps) {
|
||||||
const [shareModalOpen, setShareModalOpen] = useState(false);
|
|
||||||
const [lastEventUser, setLastEventUser] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleEvent = useCallback((event: YearReviewLiveEvent) => {
|
|
||||||
// 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 } = useYearReviewLive({
|
|
||||||
sessionId,
|
|
||||||
currentUserId,
|
|
||||||
onEvent: handleEvent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<BaseSessionLiveWrapper
|
||||||
{/* Header toolbar */}
|
sessionId={sessionId}
|
||||||
<div className="mb-4 flex items-center justify-between rounded-lg border border-border bg-card p-3">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<LiveIndicator isConnected={isConnected} error={error} />
|
|
||||||
|
|
||||||
{lastEventUser && (
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted animate-pulse">
|
|
||||||
<span>✏️</span>
|
|
||||||
<span>{lastEventUser} édite...</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!canEdit && (
|
|
||||||
<div className="flex items-center gap-2 rounded-full bg-yellow/10 px-3 py-1.5 text-sm text-yellow">
|
|
||||||
<span>👁️</span>
|
|
||||||
<span>Mode lecture</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{/* Collaborators avatars */}
|
|
||||||
{shares.length > 0 && (
|
|
||||||
<div className="flex -space-x-2">
|
|
||||||
{shares.slice(0, 3).map((share) => (
|
|
||||||
<Avatar
|
|
||||||
key={share.id}
|
|
||||||
email={share.user.email}
|
|
||||||
name={share.user.name}
|
|
||||||
size={32}
|
|
||||||
className="border-2 border-card"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{shares.length > 3 && (
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full border-2 border-card bg-muted/20 text-xs font-medium text-muted">
|
|
||||||
+{shares.length - 3}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setShareModalOpen(true)}>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
className="mr-2 h-4 w-4"
|
|
||||||
>
|
|
||||||
<path d="M13 4.5a2.5 2.5 0 11.702 1.737L6.97 9.604a2.518 2.518 0 010 .792l6.733 3.367a2.5 2.5 0 11-.671 1.341l-6.733-3.367a2.5 2.5 0 110-3.475l6.733-3.366A2.52 2.52 0 0113 4.5z" />
|
|
||||||
</svg>
|
|
||||||
Partager
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className={!canEdit ? 'pointer-events-none opacity-90' : ''}>{children}</div>
|
|
||||||
|
|
||||||
{/* Share Modal */}
|
|
||||||
<ShareModal
|
|
||||||
isOpen={shareModalOpen}
|
|
||||||
onClose={() => setShareModalOpen(false)}
|
|
||||||
title="Partager le bilan"
|
|
||||||
sessionSubtitle="Bilan annuel"
|
|
||||||
sessionTitle={sessionTitle}
|
sessionTitle={sessionTitle}
|
||||||
|
currentUserId={currentUserId}
|
||||||
shares={shares}
|
shares={shares}
|
||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
|
canEdit={canEdit}
|
||||||
userTeams={userTeams}
|
userTeams={userTeams}
|
||||||
currentUserId={currentUserId}
|
config={{
|
||||||
onShareWithEmail={(email, role) => shareYearReviewSession(sessionId, email, role)}
|
apiPath: 'year-review',
|
||||||
onRemoveShare={(userId) => removeYearReviewShare(sessionId, userId)}
|
shareModal: {
|
||||||
helpText={
|
title: 'Partager le bilan',
|
||||||
|
sessionSubtitle: 'Bilan annuel',
|
||||||
|
helpText: (
|
||||||
<>
|
<>
|
||||||
<strong>Éditeur</strong> : peut modifier les items et leurs catégories
|
<strong>Éditeur</strong> : peut modifier les items et leurs catégories
|
||||||
<br />
|
<br />
|
||||||
<strong>Lecteur</strong> : peut uniquement consulter
|
<strong>Lecteur</strong> : peut uniquement consulter
|
||||||
</>
|
</>
|
||||||
}
|
),
|
||||||
/>
|
},
|
||||||
</>
|
onShareWithEmail: (email, role) => shareYearReviewSession(sessionId, email, role),
|
||||||
|
onRemoveShare: (userId) => removeYearReviewShare(sessionId, userId),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BaseSessionLiveWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useLive, type LiveEvent } from './useLive';
|
|
||||||
|
|
||||||
export type MotivatorLiveEvent = LiveEvent;
|
|
||||||
|
|
||||||
interface UseMotivatorLiveOptions {
|
|
||||||
sessionId: string;
|
|
||||||
currentUserId?: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
onEvent?: (event: MotivatorLiveEvent) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseMotivatorLiveReturn {
|
|
||||||
isConnected: boolean;
|
|
||||||
lastEvent: MotivatorLiveEvent | null;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMotivatorLive({
|
|
||||||
sessionId,
|
|
||||||
currentUserId,
|
|
||||||
enabled = true,
|
|
||||||
onEvent,
|
|
||||||
}: UseMotivatorLiveOptions): UseMotivatorLiveReturn {
|
|
||||||
return useLive({
|
|
||||||
sessionId,
|
|
||||||
apiPath: 'motivators',
|
|
||||||
currentUserId,
|
|
||||||
enabled,
|
|
||||||
onEvent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useLive, type LiveEvent } from './useLive';
|
|
||||||
|
|
||||||
interface UseSessionLiveOptions {
|
|
||||||
sessionId: string;
|
|
||||||
currentUserId?: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
onEvent?: (event: LiveEvent) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseSessionLiveReturn {
|
|
||||||
isConnected: boolean;
|
|
||||||
lastEvent: LiveEvent | null;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSessionLive({
|
|
||||||
sessionId,
|
|
||||||
currentUserId,
|
|
||||||
enabled = true,
|
|
||||||
onEvent,
|
|
||||||
}: UseSessionLiveOptions): UseSessionLiveReturn {
|
|
||||||
return useLive({
|
|
||||||
sessionId,
|
|
||||||
apiPath: 'sessions',
|
|
||||||
currentUserId,
|
|
||||||
enabled,
|
|
||||||
onEvent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { LiveEvent };
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { useLive, type LiveEvent } from './useLive';
|
|
||||||
|
|
||||||
interface UseWeatherLiveOptions {
|
|
||||||
sessionId: string;
|
|
||||||
currentUserId?: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
onEvent?: (event: WeatherLiveEvent) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseWeatherLiveReturn {
|
|
||||||
isConnected: boolean;
|
|
||||||
lastEvent: WeatherLiveEvent | null;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WeatherLiveEvent = LiveEvent;
|
|
||||||
|
|
||||||
export function useWeatherLive({
|
|
||||||
sessionId,
|
|
||||||
currentUserId,
|
|
||||||
enabled = true,
|
|
||||||
onEvent,
|
|
||||||
}: UseWeatherLiveOptions): UseWeatherLiveReturn {
|
|
||||||
return useLive({
|
|
||||||
sessionId,
|
|
||||||
apiPath: 'weather',
|
|
||||||
currentUserId,
|
|
||||||
enabled,
|
|
||||||
onEvent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { useLive, type LiveEvent } from './useLive';
|
|
||||||
|
|
||||||
interface UseWeeklyCheckInLiveOptions {
|
|
||||||
sessionId: string;
|
|
||||||
currentUserId?: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
onEvent?: (event: WeeklyCheckInLiveEvent) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseWeeklyCheckInLiveReturn {
|
|
||||||
isConnected: boolean;
|
|
||||||
lastEvent: WeeklyCheckInLiveEvent | null;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WeeklyCheckInLiveEvent = LiveEvent;
|
|
||||||
|
|
||||||
export function useWeeklyCheckInLive({
|
|
||||||
sessionId,
|
|
||||||
currentUserId,
|
|
||||||
enabled = true,
|
|
||||||
onEvent,
|
|
||||||
}: UseWeeklyCheckInLiveOptions): UseWeeklyCheckInLiveReturn {
|
|
||||||
return useLive({
|
|
||||||
sessionId,
|
|
||||||
apiPath: 'weekly-checkin',
|
|
||||||
currentUserId,
|
|
||||||
enabled,
|
|
||||||
onEvent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useLive, type LiveEvent } from './useLive';
|
|
||||||
|
|
||||||
interface UseYearReviewLiveOptions {
|
|
||||||
sessionId: string;
|
|
||||||
currentUserId?: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
onEvent?: (event: YearReviewLiveEvent) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseYearReviewLiveReturn {
|
|
||||||
isConnected: boolean;
|
|
||||||
lastEvent: YearReviewLiveEvent | null;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type YearReviewLiveEvent = LiveEvent;
|
|
||||||
|
|
||||||
export function useYearReviewLive({
|
|
||||||
sessionId,
|
|
||||||
currentUserId,
|
|
||||||
enabled = true,
|
|
||||||
onEvent,
|
|
||||||
}: UseYearReviewLiveOptions): UseYearReviewLiveReturn {
|
|
||||||
return useLive({
|
|
||||||
sessionId,
|
|
||||||
apiPath: 'year-review',
|
|
||||||
currentUserId,
|
|
||||||
enabled,
|
|
||||||
onEvent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,21 @@
|
|||||||
* Shared utilities for share modals across workshop types.
|
* Shared utilities for share modals across workshop types.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { ShareRole } from '@prisma/client';
|
||||||
|
|
||||||
|
export interface ShareUser {
|
||||||
|
id: string;
|
||||||
|
name: string | null;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Share {
|
||||||
|
id: string;
|
||||||
|
role: ShareRole;
|
||||||
|
user: ShareUser;
|
||||||
|
createdAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TeamMemberUser {
|
export interface TeamMemberUser {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user