Design#114
Conversation
Route mobile users through a new src/mobile/pages structure and add a mobile-first home page while preserving existing desktop pages. Made-with: Cursor
Render a real mobile activity log experience with live session/task data and replace the bottom Profile tab with Settings for better navigation consistency. Made-with: Cursor
Improve mobile UX by adding a dedicated dashboard layout, closing drawer on navigation, refining activity summaries/member drilldown, and adding missing dialog metadata to remove accessibility warnings. Made-with: Cursor
fix(mobile): polish nav, dashboard, and accessibility
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds mobile-specific routing and UI updates to support a refreshed “design” experience across desktop and mobile, including new mobile dashboard/activity views and a new app-wide dashboard backdrop.
Changes:
- Introduces mobile route variants (incl. a mobile landing page) and switches routes based on
useIsMobile. - Adds new mobile dashboard + activity log views and wires activity/session fetching into
MobileView. - Updates desktop layout styling (transparent main panel + full-viewport gradient backdrop) and improves accessibility in several dialogs.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/App.tsx | Routes now choose mobile vs desktop components using useIsMobile; adds lazy imports for mobile pages. |
| src/mobile/pages/IndexMobile.tsx | New mobile landing page with feature cards and primary CTAs. |
| src/mobile/pages/LoginMobile.tsx | Mobile wrapper component for Login page. |
| src/mobile/pages/SignupMobile.tsx | Mobile wrapper component for Signup page. |
| src/mobile/pages/DashboardMobile.tsx | Mobile wrapper component for Dashboard page. |
| src/mobile/pages/NewProjectMobile.tsx | Mobile wrapper component for NewProject page. |
| src/mobile/pages/ProjectDetailsMobile.tsx | Mobile wrapper component for ProjectDetails page. |
| src/mobile/pages/PrivacyPolicyMobile.tsx | Mobile wrapper component for PrivacyPolicy page. |
| src/mobile/pages/PrivacyMobile.tsx | Mobile wrapper component for Privacy page. |
| src/mobile/pages/TermsMobile.tsx | Mobile wrapper component for Terms page. |
| src/mobile/pages/WelcomeToZyncMobile.tsx | Mobile wrapper component for WelcomeToZync page. |
| src/mobile/pages/NotFoundMobile.tsx | Mobile wrapper component for NotFound page. |
| src/components/views/MobileView.tsx | Uses new mobile dashboard/activity views; adds activity/session/team data fetching and log actions. |
| src/components/views/mobile/MobileDashboardView.tsx | New compact dashboard view for mobile with GitHub stats + contribution graph + task counts. |
| src/components/views/mobile/MobileActivityLogView.tsx | New mobile activity summary + team member activity + recent activity list with delete/clear. |
| src/components/layout/MobileLayout.tsx | Updates nav items and improves drawer accessibility/close behavior. |
| src/components/views/DesktopView.tsx | Adds fixed “dashboard-backdrop” layer and makes main panel transparent to show it. |
| src/index.css | Defines the new .dashboard-backdrop gradient (dark + light variants). |
| src/components/views/DesignView.tsx | Makes Design view background transparent to align with new backdrop. |
| src/components/views/SettingsView.tsx | Makes the settings tabs horizontally scrollable on smaller screens. |
| src/components/views/ActivityLogView.tsx | Extracts time formatting helpers and reuses them in the UI. |
| src/components/ui/command.tsx | Adds a screen-reader-only dialog header/description for the command menu. |
| src/components/ProfilePhotoCropper.tsx | Adds a screen-reader-only dialog description for the cropper modal. |
| src/lib/firebase.ts | Removes a DEV-only console info message for App Check being skipped. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| activityLogs.slice(0, 20).map((log, index) => { | ||
| const start = new Date(log.startTime); | ||
| const title = log.title || log.eventType || "Activity session"; | ||
| const logKey = | ||
| log._id || | ||
| `${log.startTime || "no-start"}-${log.eventType || title}-${log.userId || "no-user"}-${index}`; | ||
| return ( | ||
| <div key={logKey} className="rounded-lg border border-border/60 p-3 flex items-start gap-2"> | ||
| <Clock3 className="h-4 w-4 text-primary mt-0.5" /> | ||
| <div className="min-w-0 flex-1"> | ||
| <p className="text-sm font-medium truncate">{title}</p> | ||
| <p className="text-[11px] text-muted-foreground"> | ||
| {formatDistanceToNow(start, { addSuffix: true })} | ||
| </p> |
There was a problem hiding this comment.
formatDistanceToNow(new Date(log.startTime)) will throw a RangeError if log.startTime is missing or not a valid date string/number. Add a guard (e.g., validate start.getTime()), and render a safe fallback label when the timestamp is invalid.
| <Button | ||
| size="icon" | ||
| variant="ghost" | ||
| className="h-7 w-7 text-destructive" | ||
| onClick={() => onDeleteLog(log._id)} | ||
| > | ||
| <Trash2 className="h-3.5 w-3.5" /> | ||
| </Button> | ||
| )} |
There was a problem hiding this comment.
The delete action is an icon-only button with no accessible name. Add an aria-label (and/or title) so screen readers can announce the control (e.g., “Delete activity log entry”).
| @@ -32,6 +44,7 @@ const AppContent = () => { | |||
| useUserSync(); | |||
| useSyncData(); // Trigger local-first data fetch and Dexie sync on login/app load | |||
| const location = useLocation(); | |||
| const isMobile = useIsMobile(); | |||
|
|
|||
|
|
|||
| const getPageKey = (pathname: string) => { | |||
| @@ -55,33 +68,33 @@ const AppContent = () => { | |||
| > | |||
| <Suspense fallback={<div className="h-full w-full flex items-center justify-center text-sm text-muted-foreground">Loading…</div>}> | |||
| <Routes location={location}> | |||
| <Route path="/" element={<Index />} /> | |||
| <Route path="/login" element={<Login />} /> | |||
| <Route path="/signup" element={<Signup />} /> | |||
| <Route path="/welcome" element={<WelcomeToZync />} /> | |||
| <Route path="/dashboard" element={<Dashboard />} /> | |||
| <Route path="/dashboard/workspace" element={<Dashboard />} /> | |||
| <Route path="/dashboard/workspace/project/:id" element={<Dashboard />} /> | |||
| <Route path="/dashboard/projects" element={<Dashboard />} /> | |||
| <Route path="/dashboard/calendar" element={<Dashboard />} /> | |||
| <Route path="/dashboard/design" element={<Dashboard />} /> | |||
| <Route path="/dashboard/tasks" element={<Dashboard />} /> | |||
| <Route path="/dashboard/notes" element={<Dashboard />} /> | |||
| <Route path="/dashboard/files" element={<Dashboard />} /> | |||
| <Route path="/dashboard/activity" element={<Dashboard />} /> | |||
| <Route path="/dashboard/people" element={<Dashboard />} /> | |||
| <Route path="/dashboard/meet" element={<Dashboard />} /> | |||
| <Route path="/dashboard/settings" element={<Dashboard />} /> | |||
| <Route path="/dashboard/chat" element={<Dashboard />} /> | |||
| <Route path="/dashboard/new-project" element={<Dashboard />} /> | |||
| <Route path="/new-project" element={<NewProject />} /> | |||
| <Route path="/projects/:id" element={<ProjectDetails />} /> | |||
| <Route path="/" element={isMobile ? <IndexMobile /> : <Index />} /> | |||
| <Route path="/login" element={isMobile ? <LoginMobile /> : <Login />} /> | |||
| <Route path="/signup" element={isMobile ? <SignupMobile /> : <Signup />} /> | |||
| <Route path="/welcome" element={isMobile ? <WelcomeToZyncMobile /> : <WelcomeToZync />} /> | |||
| <Route path="/dashboard" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
| <Route path="/dashboard/workspace" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
| <Route path="/dashboard/workspace/project/:id" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
| <Route path="/dashboard/projects" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
| <Route path="/dashboard/calendar" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
| <Route path="/dashboard/design" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
| <Route path="/dashboard/tasks" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
| <Route path="/dashboard/notes" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
| <Route path="/dashboard/files" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
| <Route path="/dashboard/activity" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
| <Route path="/dashboard/people" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
| <Route path="/dashboard/meet" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
| <Route path="/dashboard/settings" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
| <Route path="/dashboard/chat" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
| <Route path="/dashboard/new-project" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
| <Route path="/new-project" element={isMobile ? <NewProjectMobile /> : <NewProject />} /> | |||
There was a problem hiding this comment.
Several of the new *Mobile route components are simple passthrough wrappers around the existing pages (e.g. DashboardMobile just renders pages/Dashboard, and pages/Dashboard already branches on useIsMobile). This adds an extra lazy-loaded chunk and a second mobile check without changing behavior. Consider routing directly to the page and letting it handle responsive rendering, or make the mobile route components contain actual mobile-specific logic to justify the extra indirection.
| @@ -55,33 +68,33 @@ const AppContent = () => { | |||
| > | |||
| <Suspense fallback={<div className="h-full w-full flex items-center justify-center text-sm text-muted-foreground">Loading…</div>}> | |||
| <Routes location={location}> | |||
| <Route path="/" element={<Index />} /> | |||
| <Route path="/login" element={<Login />} /> | |||
| <Route path="/signup" element={<Signup />} /> | |||
| <Route path="/welcome" element={<WelcomeToZync />} /> | |||
| <Route path="/dashboard" element={<Dashboard />} /> | |||
| <Route path="/dashboard/workspace" element={<Dashboard />} /> | |||
| <Route path="/dashboard/workspace/project/:id" element={<Dashboard />} /> | |||
| <Route path="/dashboard/projects" element={<Dashboard />} /> | |||
| <Route path="/dashboard/calendar" element={<Dashboard />} /> | |||
| <Route path="/dashboard/design" element={<Dashboard />} /> | |||
| <Route path="/dashboard/tasks" element={<Dashboard />} /> | |||
| <Route path="/dashboard/notes" element={<Dashboard />} /> | |||
| <Route path="/dashboard/files" element={<Dashboard />} /> | |||
| <Route path="/dashboard/activity" element={<Dashboard />} /> | |||
| <Route path="/dashboard/people" element={<Dashboard />} /> | |||
| <Route path="/dashboard/meet" element={<Dashboard />} /> | |||
| <Route path="/dashboard/settings" element={<Dashboard />} /> | |||
| <Route path="/dashboard/chat" element={<Dashboard />} /> | |||
| <Route path="/dashboard/new-project" element={<Dashboard />} /> | |||
| <Route path="/new-project" element={<NewProject />} /> | |||
| <Route path="/projects/:id" element={<ProjectDetails />} /> | |||
| <Route path="/" element={isMobile ? <IndexMobile /> : <Index />} /> | |||
| <Route path="/login" element={isMobile ? <LoginMobile /> : <Login />} /> | |||
| <Route path="/signup" element={isMobile ? <SignupMobile /> : <Signup />} /> | |||
| <Route path="/welcome" element={isMobile ? <WelcomeToZyncMobile /> : <WelcomeToZync />} /> | |||
| <Route path="/dashboard" element={isMobile ? <DashboardMobile /> : <Dashboard />} /> | |||
There was a problem hiding this comment.
useIsMobile() currently returns false on the initial render (the hook initializes state to undefined and returns !!isMobile), so mobile users will briefly render the desktop routes before the effect runs. Consider delaying route rendering until the mobile breakpoint is resolved (e.g., return undefined from the hook and show a loading shell), or initialize the hook state from window.innerWidth to avoid a desktop->mobile flicker and unnecessary chunk loads.
| navigate("/login"); | ||
| const parsed = JSON.parse(storedSession); | ||
| if (parsed?.startTime) { | ||
| setSessionStartTime(new Date(parsed.startTime)); |
There was a problem hiding this comment.
storedSession.startTime is parsed into a Date without validating it. If it’s missing/invalid, sessionStartTime.getTime() becomes NaN and the timer will render NaN:NaN:NaN. Validate the parsed date (e.g., check !Number.isNaN(date.getTime())) before calling setSessionStartTime.
| setSessionStartTime(new Date(parsed.startTime)); | |
| const parsedStartTime = new Date(parsed.startTime); | |
| if (!Number.isNaN(parsedStartTime.getTime())) { | |
| setSessionStartTime(parsedStartTime); | |
| } |
| const buildActivityLogTasks = (projects: any[]) => { | ||
| return projects.flatMap((project: any) => | ||
| (project.steps || []).flatMap((step: any) => | ||
| (step.tasks || []).map((task: any) => ({ | ||
| ...task, | ||
| projectId: project._id || project.id, | ||
| projectName: project.name, | ||
| githubRepoName: project.githubRepoName, | ||
| githubRepoOwner: project.githubRepoOwner, | ||
| githubRepo: project.githubRepo, | ||
| repoIds: project.githubRepoIds, | ||
| projectOwnerId: project.ownerUid || project.ownerId, | ||
| })) | ||
| ) | ||
| ); | ||
| }; | ||
|
|
||
| const filterCommitCapableTasks = (tasks: any[], userId: string) => { | ||
| return tasks.filter((task: any) => { | ||
| const assignedTo = task?.assignedTo; | ||
| const assignedUserIds = Array.isArray(task?.assignedUserIds) ? task.assignedUserIds : []; | ||
| const hasRepoLink = Boolean( | ||
| task?.githubRepoOwner || | ||
| task?.githubRepoName || | ||
| task?.githubRepo || | ||
| (Array.isArray(task?.repoIds) && task.repoIds.length > 0) | ||
| ); | ||
| const hasCommitCode = Boolean(task?.commitCode); | ||
|
|
||
| return hasRepoLink && hasCommitCode && (assignedTo === userId || assignedUserIds.includes(userId)); | ||
| }); | ||
| }; | ||
|
|
There was a problem hiding this comment.
buildActivityLogTasks / filterCommitCapableTasks appear to duplicate the same logic in src/components/views/DesktopView.tsx (e.g. around DesktopView.tsx:295-326). To avoid divergence/bugs over time, consider extracting these helpers into a shared utility module and reusing it from both desktop and mobile views.
| const buildActivityLogTasks = (projects: any[]) => { | |
| return projects.flatMap((project: any) => | |
| (project.steps || []).flatMap((step: any) => | |
| (step.tasks || []).map((task: any) => ({ | |
| ...task, | |
| projectId: project._id || project.id, | |
| projectName: project.name, | |
| githubRepoName: project.githubRepoName, | |
| githubRepoOwner: project.githubRepoOwner, | |
| githubRepo: project.githubRepo, | |
| repoIds: project.githubRepoIds, | |
| projectOwnerId: project.ownerUid || project.ownerId, | |
| })) | |
| ) | |
| ); | |
| }; | |
| const filterCommitCapableTasks = (tasks: any[], userId: string) => { | |
| return tasks.filter((task: any) => { | |
| const assignedTo = task?.assignedTo; | |
| const assignedUserIds = Array.isArray(task?.assignedUserIds) ? task.assignedUserIds : []; | |
| const hasRepoLink = Boolean( | |
| task?.githubRepoOwner || | |
| task?.githubRepoName || | |
| task?.githubRepo || | |
| (Array.isArray(task?.repoIds) && task.repoIds.length > 0) | |
| ); | |
| const hasCommitCode = Boolean(task?.commitCode); | |
| return hasRepoLink && hasCommitCode && (assignedTo === userId || assignedUserIds.includes(userId)); | |
| }); | |
| }; | |
| const getProjectTaskMetadata = (project: any) => ({ | |
| projectId: project._id || project.id, | |
| projectName: project.name, | |
| githubRepoName: project.githubRepoName, | |
| githubRepoOwner: project.githubRepoOwner, | |
| githubRepo: project.githubRepo, | |
| repoIds: project.githubRepoIds, | |
| projectOwnerId: project.ownerUid || project.ownerId, | |
| }); | |
| const flattenProjectTasks = (projects: any[]) => { | |
| return projects.flatMap((project: any) => | |
| (project.steps || []).flatMap((step: any) => | |
| (step.tasks || []).map((task: any) => ({ | |
| ...task, | |
| ...getProjectTaskMetadata(project), | |
| })) | |
| ) | |
| ); | |
| }; | |
| const hasTaskRepoLink = (task: any) => { | |
| return Boolean( | |
| task?.githubRepoOwner || | |
| task?.githubRepoName || | |
| task?.githubRepo || | |
| (Array.isArray(task?.repoIds) && task.repoIds.length > 0) | |
| ); | |
| }; | |
| const isCommitCapableTaskForUser = (task: any, userId: string) => { | |
| const assignedTo = task?.assignedTo; | |
| const assignedUserIds = Array.isArray(task?.assignedUserIds) ? task.assignedUserIds : []; | |
| const hasCommitCode = Boolean(task?.commitCode); | |
| return hasTaskRepoLink(task) && hasCommitCode && (assignedTo === userId || assignedUserIds.includes(userId)); | |
| }; | |
| const buildActivityLogTasks = (projects: any[]) => { | |
| return flattenProjectTasks(projects); | |
| }; | |
| const filterCommitCapableTasks = (tasks: any[], userId: string) => { | |
| return tasks.filter((task: any) => isCommitCapableTaskForUser(task, userId)); | |
| }; |
Description
Type of Change
Related Issues
Screenshots (if applicable)
Testing
Checklist