diff --git a/src/app/motivators/[id]/EditableTitle.tsx b/src/app/motivators/[id]/EditableTitle.tsx deleted file mode 100644 index 69a43e2..0000000 --- a/src/app/motivators/[id]/EditableTitle.tsx +++ /dev/null @@ -1,115 +0,0 @@ -'use client'; - -import { useState, useTransition, useRef, useEffect } from 'react'; -import { updateMotivatorSession } from '@/actions/moving-motivators'; - -interface EditableMotivatorTitleProps { - sessionId: string; - initialTitle: string; - isOwner: boolean; -} - -export function EditableMotivatorTitle({ - sessionId, - initialTitle, - isOwner, -}: EditableMotivatorTitleProps) { - const [isEditing, setIsEditing] = useState(false); - const [title, setTitle] = useState(initialTitle); - const [isPending, startTransition] = useTransition(); - const inputRef = useRef(null); - const prevInitialTitleRef = useRef(initialTitle); - - useEffect(() => { - if (isEditing && inputRef.current) { - inputRef.current.focus(); - inputRef.current.select(); - } - }, [isEditing]); - - // Update local state when prop changes (e.g., from SSE) - only when not editing - // This is a valid pattern for syncing external state (SSE updates) with local state - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - if (!isEditing && prevInitialTitleRef.current !== initialTitle) { - prevInitialTitleRef.current = initialTitle; - // Synchronizing with external prop updates (e.g., from SSE) - // eslint-disable-next-line react-hooks/exhaustive-deps - setTitle(initialTitle); - } - }, [initialTitle, isEditing]); - - const handleSave = () => { - if (!title.trim()) { - setTitle(initialTitle); - setIsEditing(false); - return; - } - - if (title.trim() === initialTitle) { - setIsEditing(false); - return; - } - - startTransition(async () => { - const result = await updateMotivatorSession(sessionId, { title: title.trim() }); - if (!result.success) { - setTitle(initialTitle); - console.error(result.error); - } - setIsEditing(false); - }); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleSave(); - } else if (e.key === 'Escape') { - setTitle(initialTitle); - setIsEditing(false); - } - }; - - if (!isOwner) { - return

{title}

; - } - - if (isEditing) { - return ( - setTitle(e.target.value)} - onBlur={handleSave} - onKeyDown={handleKeyDown} - disabled={isPending} - className="w-full max-w-md rounded-lg border border-border bg-input px-3 py-1.5 text-3xl font-bold text-foreground outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 disabled:opacity-50" - /> - ); - } - - return ( - - ); -} diff --git a/src/app/motivators/[id]/page.tsx b/src/app/motivators/[id]/page.tsx index 8504fca..e85f8e6 100644 --- a/src/app/motivators/[id]/page.tsx +++ b/src/app/motivators/[id]/page.tsx @@ -4,7 +4,7 @@ import { auth } from '@/lib/auth'; import { getMotivatorSessionById } from '@/services/moving-motivators'; import { MotivatorBoard, MotivatorLiveWrapper } from '@/components/moving-motivators'; import { Badge, CollaboratorDisplay } from '@/components/ui'; -import { EditableMotivatorTitle } from './EditableTitle'; +import { EditableMotivatorTitle } from '@/components/ui'; interface MotivatorSessionPageProps { params: Promise<{ id: string }>; diff --git a/src/app/sessions/[id]/page.tsx b/src/app/sessions/[id]/page.tsx index 3008358..9133dbd 100644 --- a/src/app/sessions/[id]/page.tsx +++ b/src/app/sessions/[id]/page.tsx @@ -4,7 +4,7 @@ import { auth } from '@/lib/auth'; import { getSessionById } from '@/services/sessions'; import { SwotBoard } from '@/components/swot/SwotBoard'; import { SessionLiveWrapper } from '@/components/collaboration'; -import { EditableTitle } from '@/components/session'; +import { EditableSessionTitle } from '@/components/ui'; import { Badge, CollaboratorDisplay } from '@/components/ui'; interface SessionPageProps { @@ -44,7 +44,7 @@ export default async function SessionPage({ params }: SessionPageProps) {
- (null); - const prevInitialTitleRef = useRef(initialTitle); - - useEffect(() => { - if (isEditing && inputRef.current) { - inputRef.current.focus(); - inputRef.current.select(); - } - }, [isEditing]); - - // Update local state when prop changes (e.g., from SSE) - only when not editing - // This is a valid pattern for syncing external state (SSE updates) with local state - useEffect(() => { - if (!isEditing && prevInitialTitleRef.current !== initialTitle) { - prevInitialTitleRef.current = initialTitle; - // Synchronizing with external prop updates (e.g., from SSE) - // eslint-disable-next-line react-hooks/exhaustive-deps - setTitle(initialTitle); - } - }, [initialTitle, isEditing]); - - const handleSave = () => { - if (!title.trim()) { - setTitle(initialTitle); - setIsEditing(false); - return; - } - - if (title.trim() === initialTitle) { - setIsEditing(false); - return; - } - - startTransition(async () => { - const result = await updateYearReviewSession(sessionId, { title: title.trim() }); - if (!result.success) { - setTitle(initialTitle); - console.error(result.error); - } - setIsEditing(false); - }); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault(); - handleSave(); - } else if (e.key === 'Escape') { - setTitle(initialTitle); - setIsEditing(false); - } - }; - - if (!isOwner) { - return

{title}

; - } - - if (isEditing) { - return ( - setTitle(e.target.value)} - onBlur={handleSave} - onKeyDown={handleKeyDown} - disabled={isPending} - className="w-full max-w-md rounded-lg border border-border bg-input px-3 py-1.5 text-3xl font-bold text-foreground outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 disabled:opacity-50" - /> - ); - } - - return ( - - ); -} diff --git a/src/app/year-review/[id]/page.tsx b/src/app/year-review/[id]/page.tsx index eb091e3..7e9bc70 100644 --- a/src/app/year-review/[id]/page.tsx +++ b/src/app/year-review/[id]/page.tsx @@ -4,7 +4,7 @@ import { auth } from '@/lib/auth'; import { getYearReviewSessionById } from '@/services/year-review'; import { YearReviewBoard, YearReviewLiveWrapper } from '@/components/year-review'; import { Badge, CollaboratorDisplay } from '@/components/ui'; -import { EditableYearReviewTitle } from './EditableTitle'; +import { EditableYearReviewTitle } from '@/components/ui'; interface YearReviewSessionPageProps { params: Promise<{ id: string }>; diff --git a/src/components/session/index.ts b/src/components/session/index.ts deleted file mode 100644 index bb9e721..0000000 --- a/src/components/session/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { EditableTitle } from './EditableTitle'; diff --git a/src/components/ui/EditableMotivatorTitle.tsx b/src/components/ui/EditableMotivatorTitle.tsx new file mode 100644 index 0000000..8f765fe --- /dev/null +++ b/src/components/ui/EditableMotivatorTitle.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { EditableTitle } from './EditableTitle'; +import { updateMotivatorSession } from '@/actions/moving-motivators'; + +interface EditableMotivatorTitleProps { + sessionId: string; + initialTitle: string; + isOwner: boolean; +} + +export function EditableMotivatorTitle({ + sessionId, + initialTitle, + isOwner, +}: EditableMotivatorTitleProps) { + return ( + { + const result = await updateMotivatorSession(id, { title }); + return result; + }} + /> + ); +} + diff --git a/src/components/ui/EditableSessionTitle.tsx b/src/components/ui/EditableSessionTitle.tsx new file mode 100644 index 0000000..c9df8c6 --- /dev/null +++ b/src/components/ui/EditableSessionTitle.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { EditableTitle } from './EditableTitle'; +import { updateSessionTitle } from '@/actions/session'; + +interface EditableSessionTitleProps { + sessionId: string; + initialTitle: string; + isOwner: boolean; +} + +export function EditableSessionTitle({ + sessionId, + initialTitle, + isOwner, +}: EditableSessionTitleProps) { + return ( + { + const result = await updateSessionTitle(id, title); + return result; + }} + /> + ); +} + diff --git a/src/components/session/EditableTitle.tsx b/src/components/ui/EditableTitle.tsx similarity index 66% rename from src/components/session/EditableTitle.tsx rename to src/components/ui/EditableTitle.tsx index 37309b5..5799d35 100644 --- a/src/components/session/EditableTitle.tsx +++ b/src/components/ui/EditableTitle.tsx @@ -1,20 +1,28 @@ 'use client'; -import { useState, useTransition, useRef, useEffect } from 'react'; -import { updateSessionTitle } from '@/actions/session'; +import { useState, useTransition, useRef, useEffect, useMemo } from 'react'; interface EditableTitleProps { sessionId: string; initialTitle: string; isOwner: boolean; + onUpdate: (sessionId: string, title: string) => Promise<{ success: boolean; error?: string }>; } -export function EditableTitle({ sessionId, initialTitle, isOwner }: EditableTitleProps) { +export function EditableTitle({ + sessionId, + initialTitle, + isOwner, + onUpdate, +}: EditableTitleProps) { const [isEditing, setIsEditing] = useState(false); - const [title, setTitle] = useState(initialTitle); + const [editingTitle, setEditingTitle] = useState(''); const [isPending, startTransition] = useTransition(); const inputRef = useRef(null); + // Use editingTitle when editing, otherwise use initialTitle (synced from SSE) + const title = useMemo(() => (isEditing ? editingTitle : initialTitle), [isEditing, editingTitle, initialTitle]); + useEffect(() => { if (isEditing && inputRef.current) { inputRef.current.focus(); @@ -22,35 +30,28 @@ export function EditableTitle({ sessionId, initialTitle, isOwner }: EditableTitl } }, [isEditing]); - // Update local state when prop changes (e.g., from SSE) - only when not editing - // This is a valid pattern for syncing external state (SSE updates) with local state - useEffect(() => { - if (!isEditing) { - // Synchronizing with external prop updates (e.g., from SSE) - // eslint-disable-next-line react-hooks/exhaustive-deps - setTitle(initialTitle); - } - }, [initialTitle, isEditing]); - const handleSave = () => { - if (!title.trim()) { - setTitle(initialTitle); + const trimmedTitle = editingTitle.trim(); + if (!trimmedTitle) { + setEditingTitle(''); setIsEditing(false); return; } - if (title.trim() === initialTitle) { + if (trimmedTitle === initialTitle) { + setEditingTitle(''); setIsEditing(false); return; } startTransition(async () => { - const result = await updateSessionTitle(sessionId, title.trim()); + const result = await onUpdate(sessionId, trimmedTitle); if (!result.success) { - setTitle(initialTitle); + setEditingTitle(''); console.error(result.error); } setIsEditing(false); + setEditingTitle(''); }); }; @@ -59,7 +60,7 @@ export function EditableTitle({ sessionId, initialTitle, isOwner }: EditableTitl e.preventDefault(); handleSave(); } else if (e.key === 'Escape') { - setTitle(initialTitle); + setEditingTitle(''); setIsEditing(false); } }; @@ -73,8 +74,8 @@ export function EditableTitle({ sessionId, initialTitle, isOwner }: EditableTitl setTitle(e.target.value)} + value={editingTitle} + onChange={(e) => setEditingTitle(e.target.value)} onBlur={handleSave} onKeyDown={handleKeyDown} disabled={isPending} @@ -85,7 +86,10 @@ export function EditableTitle({ sessionId, initialTitle, isOwner }: EditableTitl return ( ); } + diff --git a/src/components/ui/EditableYearReviewTitle.tsx b/src/components/ui/EditableYearReviewTitle.tsx new file mode 100644 index 0000000..3dd887b --- /dev/null +++ b/src/components/ui/EditableYearReviewTitle.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { EditableTitle } from './EditableTitle'; +import { updateYearReviewSession } from '@/actions/year-review'; + +interface EditableYearReviewTitleProps { + sessionId: string; + initialTitle: string; + isOwner: boolean; +} + +export function EditableYearReviewTitle({ + sessionId, + initialTitle, + isOwner, +}: EditableYearReviewTitleProps) { + return ( + { + const result = await updateYearReviewSession(id, { title }); + return result; + }} + /> + ); +} + diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index bb723d5..3ccb7cc 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -3,6 +3,10 @@ export { Badge } from './Badge'; export { Button } from './Button'; export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card'; export { CollaboratorDisplay } from './CollaboratorDisplay'; +export { EditableTitle } from './EditableTitle'; +export { EditableSessionTitle } from './EditableSessionTitle'; +export { EditableMotivatorTitle } from './EditableMotivatorTitle'; +export { EditableYearReviewTitle } from './EditableYearReviewTitle'; export { Input } from './Input'; export { Modal, ModalFooter } from './Modal'; export { Textarea } from './Textarea';