refactor: improve team management, OKRs, and session components
This commit is contained in:
@@ -21,70 +21,71 @@ const categoryStyles: Record<SwotCategory, { ring: string; text: string }> = {
|
||||
THREAT: { ring: 'ring-threat', text: 'text-threat' },
|
||||
};
|
||||
|
||||
export const SwotCard = memo(forwardRef<HTMLDivElement, SwotCardProps>(
|
||||
(
|
||||
{ item, sessionId, isSelected, isHighlighted, isDragging, linkMode, onSelect, ...props },
|
||||
ref
|
||||
) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [content, setContent] = useState(item.content);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
export const SwotCard = memo(
|
||||
forwardRef<HTMLDivElement, SwotCardProps>(
|
||||
(
|
||||
{ item, sessionId, isSelected, isHighlighted, isDragging, linkMode, onSelect, ...props },
|
||||
ref
|
||||
) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [content, setContent] = useState(item.content);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const styles = categoryStyles[item.category];
|
||||
const styles = categoryStyles[item.category];
|
||||
|
||||
async function handleSave() {
|
||||
if (content.trim() === item.content) {
|
||||
setIsEditing(false);
|
||||
return;
|
||||
async function handleSave() {
|
||||
if (content.trim() === item.content) {
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.trim()) {
|
||||
// If empty, delete
|
||||
startTransition(async () => {
|
||||
await deleteSwotItem(item.id, sessionId);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
await updateSwotItem(item.id, sessionId, { content: content.trim() });
|
||||
setIsEditing(false);
|
||||
});
|
||||
}
|
||||
|
||||
if (!content.trim()) {
|
||||
// If empty, delete
|
||||
async function handleDelete() {
|
||||
startTransition(async () => {
|
||||
await deleteSwotItem(item.id, sessionId);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
await updateSwotItem(item.id, sessionId, { content: content.trim() });
|
||||
setIsEditing(false);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
startTransition(async () => {
|
||||
await deleteSwotItem(item.id, sessionId);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDuplicate() {
|
||||
startTransition(async () => {
|
||||
await duplicateSwotItem(item.id, sessionId);
|
||||
});
|
||||
}
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
setContent(item.content);
|
||||
setIsEditing(false);
|
||||
async function handleDuplicate() {
|
||||
startTransition(async () => {
|
||||
await duplicateSwotItem(item.id, sessionId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
if (linkMode) {
|
||||
onSelect();
|
||||
function handleKeyDown(e: React.KeyboardEvent) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
setContent(item.content);
|
||||
setIsEditing(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
onClick={handleClick}
|
||||
className={`
|
||||
function handleClick() {
|
||||
if (linkMode) {
|
||||
onSelect();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
onClick={handleClick}
|
||||
className={`
|
||||
group relative rounded-lg border bg-card p-3 shadow-sm transition-all
|
||||
${isDragging ? 'shadow-lg ring-2 ring-primary' : 'border-border'}
|
||||
${isSelected ? `ring-2 ${styles.ring}` : ''}
|
||||
@@ -92,109 +93,110 @@ export const SwotCard = memo(forwardRef<HTMLDivElement, SwotCardProps>(
|
||||
${linkMode ? 'cursor-pointer hover:ring-2 hover:ring-primary/50' : ''}
|
||||
${isPending ? 'opacity-50' : ''}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{isEditing ? (
|
||||
<textarea
|
||||
autoFocus
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleSave}
|
||||
className="w-full resize-none rounded border-0 bg-transparent p-0 text-sm text-foreground focus:outline-none focus:ring-0"
|
||||
rows={2}
|
||||
disabled={isPending}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-foreground whitespace-pre-wrap">{item.content}</p>
|
||||
{...props}
|
||||
>
|
||||
{isEditing ? (
|
||||
<textarea
|
||||
autoFocus
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleSave}
|
||||
className="w-full resize-none rounded border-0 bg-transparent p-0 text-sm text-foreground focus:outline-none focus:ring-0"
|
||||
rows={2}
|
||||
disabled={isPending}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-foreground whitespace-pre-wrap">{item.content}</p>
|
||||
|
||||
{/* Actions (visible on hover) */}
|
||||
{!linkMode && (
|
||||
<div className="absolute right-1 top-1 flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsEditing(true);
|
||||
}}
|
||||
className="rounded p-1 text-muted hover:bg-card-hover hover:text-foreground"
|
||||
aria-label="Modifier"
|
||||
>
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
{/* Actions (visible on hover) */}
|
||||
{!linkMode && (
|
||||
<div className="absolute right-1 top-1 flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsEditing(true);
|
||||
}}
|
||||
className="rounded p-1 text-muted hover:bg-card-hover hover:text-foreground"
|
||||
aria-label="Modifier"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDuplicate();
|
||||
}}
|
||||
className="rounded p-1 text-muted hover:bg-primary/10 hover:text-primary"
|
||||
aria-label="Dupliquer"
|
||||
>
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDuplicate();
|
||||
}}
|
||||
className="rounded p-1 text-muted hover:bg-primary/10 hover:text-primary"
|
||||
aria-label="Dupliquer"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete();
|
||||
}}
|
||||
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
|
||||
aria-label="Supprimer"
|
||||
>
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete();
|
||||
}}
|
||||
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
|
||||
aria-label="Supprimer"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<svg
|
||||
className="h-3.5 w-3.5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selection indicator in link mode */}
|
||||
{linkMode && isSelected && (
|
||||
<div
|
||||
className={`absolute -right-1 -top-1 rounded-full bg-card p-0.5 shadow ${styles.text}`}
|
||||
>
|
||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
));
|
||||
{/* Selection indicator in link mode */}
|
||||
{linkMode && isSelected && (
|
||||
<div
|
||||
className={`absolute -right-1 -top-1 rounded-full bg-card p-0.5 shadow ${styles.text}`}
|
||||
>
|
||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
SwotCard.displayName = 'SwotCard';
|
||||
|
||||
Reference in New Issue
Block a user