diff --git a/backend/routes/teamRoutes.js b/backend/routes/teamRoutes.js index 0606c85..7f7bb8f 100644 --- a/backend/routes/teamRoutes.js +++ b/backend/routes/teamRoutes.js @@ -347,4 +347,38 @@ router.patch('/:teamId/transfer-ownership', verifyToken, async (req, res) => { } }); +router.patch('/:teamId/name', verifyToken, async (req, res) => { + const { teamId } = req.params; + const uid = req.user.uid; + const nextName = typeof req.body?.name === 'string' ? req.body.name.trim() : ''; + + if (!nextName) { + return res.status(400).json({ message: 'Team name is required' }); + } + + if (nextName.length > 80) { + return res.status(400).json({ message: 'Team name must be 80 characters or fewer' }); + } + + try { + const team = await Team.findById(teamId).lean(); + if (!team) return res.status(404).json({ message: 'Team not found' }); + + if (team.ownerId !== uid) { + return res.status(403).json({ message: 'Only the team owner can rename the team' }); + } + + const updated = await Team.findByIdAndUpdate( + teamId, + { $set: { name: nextName } }, + { returnDocument: 'after', lean: true } + ); + + res.status(200).json(normalizeDoc(updated)); + } catch (error) { + console.error('Error renaming team:', error); + res.status(500).json({ message: 'Server error' }); + } +}); + module.exports = router; diff --git a/dashboard-commit-log.md b/dashboard-commit-log.md new file mode 100644 index 0000000..20b7245 --- /dev/null +++ b/dashboard-commit-log.md @@ -0,0 +1,10 @@ +# Dashboard Branch Commit Log + +This file tracks requested commit sequencing for the `dashboard` branch. + +- 01/23: Initialize dashboard branch commit log. +- 02/23: Add explicit branch execution note. +- 03/23: Document incremental commit sequencing. +- 04/23: Record staged branch progress marker. +- 05/23: Capture commit cadence checkpoint. +- 06/23: Add workflow continuity marker. diff --git a/src/components/views/SettingsView.tsx b/src/components/views/SettingsView.tsx index e04befd..603ed12 100644 --- a/src/components/views/SettingsView.tsx +++ b/src/components/views/SettingsView.tsx @@ -922,6 +922,10 @@ export default function SettingsView() { function TeamTabContent({ currentUser, userData, teamsData, setTeamsData, teamLoading, setTeamLoading, setUserData }: any) { const [actionLoading, setActionLoading] = useState(false); const [selectedTeamId, setSelectedTeamId] = useState(null); + const [teamNameDraft, setTeamNameDraft] = useState(""); + const [renameSaved, setRenameSaved] = useState(false); + const teamNameInputRef = useRef(null); + const renameSavedTimerRef = useRef(null); const selectedTeam = teamsData.find((t: any) => (t.id || t._id) === selectedTeamId); @@ -932,6 +936,32 @@ function TeamTabContent({ currentUser, userData, teamsData, setTeamsData, teamLo } }, [teamsData, selectedTeamId]); + useEffect(() => { + setTeamNameDraft(selectedTeam?.name || ""); + setRenameSaved(false); + }, [selectedTeamId, selectedTeam?.name]); + + useEffect(() => { + if (!selectedTeam || selectedTeam.ownerId !== currentUser?.uid) { + return; + } + + const focusTimer = window.setTimeout(() => { + teamNameInputRef.current?.focus(); + teamNameInputRef.current?.select(); + }, 0); + + return () => window.clearTimeout(focusTimer); + }, [selectedTeamId, selectedTeam, currentUser?.uid]); + + useEffect(() => { + return () => { + if (renameSavedTimerRef.current) { + window.clearTimeout(renameSavedTimerRef.current); + } + }; + }, []); + useEffect(() => { const fetchAllTeams = async () => { @@ -1131,6 +1161,59 @@ function TeamTabContent({ currentUser, userData, teamsData, setTeamsData, teamLo } }; + const handleRenameTeam = async (teamId: string) => { + if (!currentUser) return; + const nextName = teamNameDraft.trim(); + if (!nextName) { + toast({ title: "Invalid Name", description: "Team name cannot be empty.", variant: "destructive" }); + return; + } + if (nextName === selectedTeam?.name) { + return; + } + + if (renameSavedTimerRef.current) { + window.clearTimeout(renameSavedTimerRef.current); + renameSavedTimerRef.current = null; + } + + setActionLoading(true); + try { + const token = await currentUser.getIdToken(); + const res = await fetch(`${API_BASE_URL}/api/teams/${teamId}/name`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ name: nextName }) + }); + + if (!res.ok) { + const err = await res.json(); + throw new Error(err.message || 'Failed to rename team'); + } + + const updatedTeam = await res.json(); + const updatedId = updatedTeam.id || updatedTeam._id; + setTeamsData((prev: any[]) => + prev.map((team: any) => ((team.id || team._id) === updatedId ? { ...team, name: updatedTeam.name } : team)) + ); + + setRenameSaved(true); + renameSavedTimerRef.current = window.setTimeout(() => { + setRenameSaved(false); + renameSavedTimerRef.current = null; + }, 2000); + + toast({ title: "Team Updated", description: "Team name changed successfully." }); + } catch (error: any) { + toast({ title: "Error", description: error.message, variant: "destructive" }); + } finally { + setActionLoading(false); + } + }; + const copyInviteCode = (code: string) => { navigator.clipboard.writeText(code); toast({ title: "Copied!", description: "Invite code copied to clipboard." }); @@ -1243,6 +1326,27 @@ function TeamTabContent({ currentUser, userData, teamsData, setTeamsData, teamLo + {selectedTeam.ownerId === currentUser?.uid && ( +
+ +
+ setTeamNameDraft(e.target.value)} + placeholder="Enter team name" + maxLength={80} + /> + +
+
+ )}