dashboard#108
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR updates the dashboard experience by restoring/improving the welcome flow, adding team logo + team management enhancements, and introducing Firestore-backed persistence for team/task analytics, alongside backend notification/security tweaks and dependency/config updates.
Changes:
- Add team logo system and refactor team settings UI (including ownership transfer).
- Add Firestore persistence hooks for team membership and task analytics, and extend Activity Log analytics/filtering UI.
- Backend: tighten
/api/users/syncsecurity/notifications and add/api/supportroute; bump tooling/deps and Boneyard registry artifacts.
Reviewed changes
Copilot reviewed 36 out of 40 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.ts | Removes optimizeDeps entry config; sets CSS minifier. |
| tsconfig.json | Suppresses TypeScript deprecation warnings via ignoreDeprecations. |
| tsconfig.app.json | Same ignoreDeprecations setting for app TS config. |
| src/pages/WelcomeToZync.tsx | Guards /welcome by redirecting existing users post-auth. |
| src/pages/ProjectDetails.tsx | Updates ActivityGraph import path (currently appears incorrect). |
| src/lib/team-logos.ts | Adds team logo registry + helpers for deterministic/random selection. |
| src/lib/postLoginRedirect.ts | Refines “fresh account” detection + fallback behavior for welcome routing. |
| src/lib/firebase.ts | Exposes Firestore instance (db). |
| src/hooks/useTeamPersistence.ts | New Firestore team membership sync hook. |
| src/hooks/useTaskPersistence.ts | New Firestore task analytics persistence hook. |
| src/hooks/useSyncData.ts | Removes verbose sync console logging. |
| src/hooks/useNotePresence.ts | Gates debug logging behind import.meta.env.DEV. |
| src/hooks/use-user-sync.ts | Adds sync-in-progress guard around user sync flow (but drops error catch). |
| src/hooks/use-activity-tracker.ts | Prevents duplicate session starts by checking sessionIdRef before starting. |
| src/components/views/TasksView.tsx | Adjusts SelectItem keys with fallback indices. |
| src/components/views/TaskDetailDrawer.tsx | Tracks “task opened” via Firestore persistence hook. |
| src/components/views/SettingsView.tsx | Refactors Team tab UI into selectable grid + adds ownership transfer UI/actions. |
| src/components/views/MobileView.tsx | Switches several imports to relative paths (local refactor). |
| src/components/views/JoinTeamDialog.tsx | Syncs join-team action to Firestore for analytics. |
| src/components/views/DesktopView.tsx | Refactors Activity Log fetching (parallel + slower polling) and passes team data to ActivityLogView. |
| src/components/views/CreateTeamDialog.tsx | Adds team logo picker + Firestore sync on team creation. |
| src/components/views/ActivityLogView.tsx | Major redesign: team/member filters, merged team sources, persisted task stats, enhanced feed rendering. |
| src/components/ui/skeletons/index.ts | Removes legacy skeleton re-exports (left intentionally empty). |
| src/bones/workspace-project-card.bones.json | Adds Boneyard capture artifact for skeletons. |
| src/bones/task-list-item.bones.json | Adds Boneyard capture artifact for skeletons. |
| src/bones/registry.ts | Adds generated Boneyard registry and registers bones JSON. |
| src/bones/project-card-grid.bones.json | Adds Boneyard capture artifact for skeletons. |
| src/bones/calendar-events-grid.bones.json | Adds Boneyard capture artifact for skeletons. |
| package.json | Forces Vite dev startup + bumps Vite version. |
| package-lock.json | Lockfile updates for Vite and transitive deps. |
| backend/tests/user_sync_security.test.js | Extends tests for new user notification + UID-mismatch behavior. |
| backend/services/googleMeet.js | Trims GOOGLE_REFRESH_TOKEN before setting credentials. |
| backend/routes/userRoutes.js | Adds UID mismatch protection and new-user notification dispatch logic using env recipients. |
| backend/routes/teamRoutes.js | Adds PATCH /:teamId/transfer-ownership endpoint. |
| backend/routes/supportRoutes.js | Switches support email sender to mailer service + updates default recipient. |
| backend/package.json | Adds Jest dev dependency. |
| backend/index.js | Registers /api/support route. |
| backend/.env.example | Deleted. |
| .gitignore | Adds docs/ to ignore list (and retains backend/). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { useTaskUpdates } from "@/hooks/use-task-updates"; | ||
| import KanbanBoard from "@/components/workspace/KanbanBoard"; | ||
| import { ActivityGraph } from "@/components/views/ActivityGraph"; | ||
| import { ActivityGraph } from "@/components/views/activity/ActivityGraph"; |
There was a problem hiding this comment.
Import path for ActivityGraph points to "@/components/views/activity/ActivityGraph", but the component file currently lives at "src/components/views/ActivityGraph.tsx". This will cause a module resolution/build failure. Update the import to the actual path (or add/move the file so the import matches the new structure).
| markTaskOpened(task.id); | ||
| } | ||
| }, [open, task?.id]); |
There was a problem hiding this comment.
This effect calls markTaskOpened but it is missing from the dependency array, which will trip react-hooks/exhaustive-deps and can capture a stale function reference. Include markTaskOpened in deps (or memoize it in the hook) and consider calling it with void to make the fire-and-forget intent explicit.
| markTaskOpened(task.id); | |
| } | |
| }, [open, task?.id]); | |
| void markTaskOpened(task.id); | |
| } | |
| }, [markTaskOpened, open, task?.id]); |
| const total = taskList.length; | ||
| const inProgress = taskList.filter(isInProgressTask).length; | ||
| const completed = taskList.filter(isCompletedTask).length; | ||
| const overdue = taskList.filter(isOverdueTask).length; | ||
| return { total, inProgress, completed, overdue }; | ||
| }, [taskList]); | ||
| const completedCount = taskList.filter(isCompletedTask).length; | ||
|
|
||
| const hasAnyCommit = taskList.some(t => Boolean( | ||
| (t as any).commitUrl || | ||
| (t as any).commitMessage || | ||
| (t as any).commitInfo?.message | ||
| )); | ||
| const inProgress = hasAnyCommit ? 1 : 0; | ||
| const efficiency = total ? Math.round((completedCount / total) * 100) : 0; | ||
|
|
||
| return { | ||
| total, | ||
| inProgress, | ||
| completed: completedCount, | ||
| overdue: persistedStats?.overdue || 0, | ||
| efficiency, | ||
| dailyActiveAvg: dailyStats.avgMins | ||
| }; | ||
| }, [taskList, persistedStats, selectedUserId, currentUserId, dailyStats.avgMins]); |
There was a problem hiding this comment.
taskStats sets inProgress to 1 if any task has commit evidence, which makes the count incorrect (and ignores the existing isInProgressTask helper). Compute inProgress as the number of tasks matching the in-progress criteria so analytics are accurate.
| <div className="flex items-center gap-3"> | ||
| <div className="h-12 w-12 flex items-center justify-center rounded-xl bg-blue-500/10 border border-blue-500/20"> | ||
| {(() => { | ||
| const lid = selectedTeam.logoId || getDeterministicLogoId(selectedTeam.id); |
There was a problem hiding this comment.
selectedTeam.id is used here, but elsewhere in this component teams may come back as { _id, ... } (you already normalize with id || _id earlier). If selectedTeam.id is undefined, deterministic logo selection will be inconsistent and can throw if downstream expects a string. Use selectedTeam.id || selectedTeam._id consistently.
| const lid = selectedTeam.logoId || getDeterministicLogoId(selectedTeam.id); | |
| const teamId = selectedTeam.id || selectedTeam._id; | |
| const lid = selectedTeam.logoId || getDeterministicLogoId(teamId); |
| setTeamsData((prev: any[]) => prev.filter(t => t.id !== teamId)); | ||
| if (selectedTeamId === teamId) { | ||
| setSelectedTeamId(null); | ||
| } | ||
| setUserData((prev: any) => ({ | ||
| ...prev, | ||
| teamMemberships: prev.teamMemberships?.filter((id: string) => id !== teamId) || [] |
There was a problem hiding this comment.
Local state updates after leaving/deleting a team filter by t.id, but teams can also be keyed by _id in this component. This can leave deleted/left teams in UI state. Filter by (t.id || t._id) (and likewise compare teamId against both forms).
| setTeamsData((prev: any[]) => prev.filter(t => t.id !== teamId)); | |
| if (selectedTeamId === teamId) { | |
| setSelectedTeamId(null); | |
| } | |
| setUserData((prev: any) => ({ | |
| ...prev, | |
| teamMemberships: prev.teamMemberships?.filter((id: string) => id !== teamId) || [] | |
| setTeamsData((prev: any[]) => prev.filter(t => (t.id || t._id) !== teamId)); | |
| if (selectedTeamId === teamId || teamsData?.some((t: any) => (t.id === selectedTeamId || t._id === selectedTeamId) && (t.id === teamId || t._id === teamId))) { | |
| setSelectedTeamId(null); | |
| } | |
| setUserData((prev: any) => ({ | |
| ...prev, | |
| teamMemberships: prev.teamMemberships?.filter((membership: any) => { | |
| const membershipTeamId = typeof membership === "string" ? membership : (membership?.id || membership?._id); | |
| return membershipTeamId !== teamId; | |
| }) || [] |
| const joinTeamSync = async (inviteCode: string, userId: string) => { | ||
| try { | ||
| // Find team by invite code | ||
| const q = query(collection(db, "teams"), where("inviteCode", "==", inviteCode)); | ||
| const unsubscribe = onSnapshot(q, (snapshot) => { | ||
| snapshot.forEach(async (teamDoc) => { | ||
| await updateDoc(doc(db, "teams", teamDoc.id), { | ||
| members: arrayUnion(userId) | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
joinTeamSync uses onSnapshot for what appears to be a one-time lookup, but never unsubscribes. This leaves a live listener running and can repeatedly re-apply updates. Use a one-time read (getDocs) or unsubscribe immediately after the first snapshot/update completes.
| const markTaskOpened = async (taskId: string) => { | ||
| if (!userId) return; | ||
| // In Firestore, we should ideally track individual task statuses in a subcollection | ||
| // but to satisfy "Overdue = user just opened the task" simply, we can increment a counter or track in a map | ||
| try { | ||
| const docRef = doc(db, "tasks", userId); | ||
| const snap = await getDoc(docRef); | ||
| let currentStats = snap.exists() ? snap.data() as TaskStats : { total: 0, inProgress: 0, completed: 0, overdue: 0 }; | ||
|
|
||
| // For now, let's just increment overdue if this is a "new" opening (simulated) | ||
| // A better way would be tracking specific task IDs in a sub-collection | ||
| await setDoc(docRef, { ...currentStats, overdue: (currentStats.overdue || 0) + 1 }, { merge: true }); | ||
| } catch (error) { |
There was a problem hiding this comment.
markTaskOpened does a read-then-write (getDoc + setDoc) to increment overdue, which is non-atomic and can lose increments under concurrent calls. Prefer updateDoc with FieldValue.increment (or a transaction) and keep the default TaskStats shape consistent (include efficiency/dailyActiveAvg) to avoid partial documents.
| if (currentTeamId) { | ||
| setSelectedTeamId(currentTeamId); | ||
| } else if (selectedTeamId === 'all' && allTeams.length > 0) { | ||
| setSelectedTeamId(allTeams[0].id); | ||
| } |
There was a problem hiding this comment.
This effect reads selectedTeamId but it isn't listed in the dependency array, which will fail react-hooks/exhaustive-deps. Add selectedTeamId to deps or restructure the logic so it doesn't rely on a closed-over state value.
| if (currentTeamId) { | |
| setSelectedTeamId(currentTeamId); | |
| } else if (selectedTeamId === 'all' && allTeams.length > 0) { | |
| setSelectedTeamId(allTeams[0].id); | |
| } | |
| setSelectedTeamId((prevSelectedTeamId) => { | |
| if (currentTeamId) { | |
| return currentTeamId; | |
| } | |
| if (prevSelectedTeamId === 'all' && allTeams.length > 0) { | |
| return allTeams[0].id; | |
| } | |
| return prevSelectedTeamId; | |
| }); |
| if (!currentUser) return; | ||
| const team = teamsData.find((t: any) => t.id === teamId); | ||
| const member = team?.memberDetails?.find((m: any) => m.uid === newOwnerId); | ||
|
|
There was a problem hiding this comment.
handleTransferOwnership looks up the team with t.id === teamId, but the rest of the component treats IDs as t.id || t._id. If the API returns _id, member lookup (for the confirm text) will fail. Use the same (t.id || t._id) normalization here.
| export interface TeamLogo { | ||
| id: TeamLogoId; | ||
| icon: any; | ||
| label: string; | ||
| } |
There was a problem hiding this comment.
TeamLogo.icon is typed as any, which defeats type safety and can hide invalid icon components. Prefer using the lucide-react icon type (e.g., LucideIcon/ComponentType) and tighten getLogoById to accept TeamLogoId to prevent accidental invalid IDs.
Description
Type of Change
Related Issues
Screenshots (if applicable)
Testing
Checklist