From c9ed48a29faca5b191238562b8641d4b27f63f3d Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Thu, 27 Nov 2025 14:41:59 +0100 Subject: [PATCH] feat: editable title for owner --- dev.db | Bin 147456 -> 147456 bytes src/actions/session.ts | 72 +++++++++++++++ src/app/sessions/[id]/page.tsx | 7 +- src/components/session/EditableTitle.tsx | 106 +++++++++++++++++++++++ src/components/session/index.ts | 2 + 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 src/actions/session.ts create mode 100644 src/components/session/EditableTitle.tsx create mode 100644 src/components/session/index.ts diff --git a/dev.db b/dev.db index e22196a4850a77d09e3f9ca3a22a5f2583dfb557..6ee2b29e6a8c085f1144a3342f2f494456df1b4f 100644 GIT binary patch delta 332 zcmZo@;B08%oFL73Z=#Gdu#AtVu~CVM!(LR~-QL}Oa^QbS zmXOrqlF7H@@;HqxtxS#e3=K@1gW|Ub#WQX)VD@GEJ)M05<4h*D-$31wY;v;9_KeB7 znHd==$w?Ll1_oJ`Mg{rBsl{f6#TkXZ+a;$nzOIntWvgT0kKxK0>e7@}V_V(Jk?l_GDxo$e243nfyuxoIzr@vo(Xn>1jh^tGr zQb}e>PO6fX63FdJwMGU;rn-iPx<=+9hQ?MfZ&+v>7+4t?Z08qe`YOS!&H83Ky8%-u o6YCqG1NvB}YnUsnZ0o<`*rcVa401sFWva=BorVa~%BLWCN7bizQM@N%U z{}}~IZ*^~VliOZylPq5kgIHg;SYH9GFat~n8J7o;0hzNA5P1fd`<4L-w<4DT;&>bk z2M+)bTn@Mn6b*0;qzlmt2MR6;EC^c%w+9ci5fJYNw+|Nr^%w&x1}K*YFalMx5fID; Tm+UnHEtjbm0uHxhHUgaq5pOaS diff --git a/src/actions/session.ts b/src/actions/session.ts new file mode 100644 index 0000000..d0a44d4 --- /dev/null +++ b/src/actions/session.ts @@ -0,0 +1,72 @@ +'use server'; + +import { revalidatePath } from 'next/cache'; +import { auth } from '@/lib/auth'; +import * as sessionsService from '@/services/sessions'; + +export async function updateSessionTitle(sessionId: string, title: string) { + const session = await auth(); + if (!session?.user?.id) { + return { success: false, error: 'Non autorisé' }; + } + + if (!title.trim()) { + return { success: false, error: 'Le titre ne peut pas être vide' }; + } + + try { + const result = await sessionsService.updateSession(sessionId, session.user.id, { + title: title.trim(), + }); + + if (result.count === 0) { + return { success: false, error: 'Session non trouvée ou non autorisé' }; + } + + // Emit event for real-time sync + await sessionsService.createSessionEvent(sessionId, session.user.id, 'SESSION_UPDATED', { + title: title.trim(), + }); + + revalidatePath(`/sessions/${sessionId}`); + revalidatePath('/sessions'); + return { success: true }; + } catch (error) { + console.error('Error updating session title:', error); + return { success: false, error: 'Erreur lors de la mise à jour' }; + } +} + +export async function updateSessionCollaborator(sessionId: string, collaborator: string) { + const session = await auth(); + if (!session?.user?.id) { + return { success: false, error: 'Non autorisé' }; + } + + if (!collaborator.trim()) { + return { success: false, error: 'Le nom du collaborateur ne peut pas être vide' }; + } + + try { + const result = await sessionsService.updateSession(sessionId, session.user.id, { + collaborator: collaborator.trim(), + }); + + if (result.count === 0) { + return { success: false, error: 'Session non trouvée ou non autorisé' }; + } + + // Emit event for real-time sync + await sessionsService.createSessionEvent(sessionId, session.user.id, 'SESSION_UPDATED', { + collaborator: collaborator.trim(), + }); + + revalidatePath(`/sessions/${sessionId}`); + revalidatePath('/sessions'); + return { success: true }; + } catch (error) { + console.error('Error updating session collaborator:', error); + return { success: false, error: 'Erreur lors de la mise à jour' }; + } +} + diff --git a/src/app/sessions/[id]/page.tsx b/src/app/sessions/[id]/page.tsx index 9b231b6..98ac952 100644 --- a/src/app/sessions/[id]/page.tsx +++ b/src/app/sessions/[id]/page.tsx @@ -4,6 +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 { Badge } from '@/components/ui'; interface SessionPageProps { @@ -43,7 +44,11 @@ export default async function SessionPage({ params }: SessionPageProps) {
-

{session.title}

+

👤 {session.collaborator}

diff --git a/src/components/session/EditableTitle.tsx b/src/components/session/EditableTitle.tsx new file mode 100644 index 0000000..3b580d8 --- /dev/null +++ b/src/components/session/EditableTitle.tsx @@ -0,0 +1,106 @@ +'use client'; + +import { useState, useTransition, useRef, useEffect } from 'react'; +import { updateSessionTitle } from '@/actions/session'; + +interface EditableTitleProps { + sessionId: string; + initialTitle: string; + isOwner: boolean; +} + +export function EditableTitle({ sessionId, initialTitle, isOwner }: EditableTitleProps) { + const [isEditing, setIsEditing] = useState(false); + const [title, setTitle] = useState(initialTitle); + const [isPending, startTransition] = useTransition(); + const inputRef = useRef(null); + + useEffect(() => { + if (isEditing && inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, [isEditing]); + + // Update local state when prop changes (e.g., from SSE) + useEffect(() => { + if (!isEditing) { + 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 updateSessionTitle(sessionId, 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/components/session/index.ts b/src/components/session/index.ts new file mode 100644 index 0000000..0086b2f --- /dev/null +++ b/src/components/session/index.ts @@ -0,0 +1,2 @@ +export { EditableTitle } from './EditableTitle'; +