Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 38 additions & 25 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom";
import { ThemeProvider } from "@/components/ThemeProvider";
import { motion, AnimatePresence } from "framer-motion";
import { lazy, Suspense } from "react";
import { useIsMobile } from "@/hooks/use-mobile";

const Index = lazy(() => import("./pages/Index"));
const Login = lazy(() => import("./pages/Login"));
Expand All @@ -20,6 +21,17 @@ const PrivacyPolicy = lazy(() => import("./pages/PrivacyPolicy"));
const Privacy = lazy(() => import("./pages/Privacy"));
const Terms = lazy(() => import("./pages/Terms"));
const WelcomeToZync = lazy(() => import("./pages/WelcomeToZync"));
const IndexMobile = lazy(() => import("./mobile/pages/IndexMobile"));
const LoginMobile = lazy(() => import("./mobile/pages/LoginMobile"));
const SignupMobile = lazy(() => import("./mobile/pages/SignupMobile"));
const NotFoundMobile = lazy(() => import("./mobile/pages/NotFoundMobile"));
const DashboardMobile = lazy(() => import("./mobile/pages/DashboardMobile"));
const NewProjectMobile = lazy(() => import("./mobile/pages/NewProjectMobile"));
const ProjectDetailsMobile = lazy(() => import("./mobile/pages/ProjectDetailsMobile"));
const PrivacyPolicyMobile = lazy(() => import("./mobile/pages/PrivacyPolicyMobile"));
const PrivacyMobile = lazy(() => import("./mobile/pages/PrivacyMobile"));
const TermsMobile = lazy(() => import("./mobile/pages/TermsMobile"));
const WelcomeToZyncMobile = lazy(() => import("./mobile/pages/WelcomeToZyncMobile"));
import { useActivityTracker } from "./hooks/use-activity-tracker";
import { useChatNotifications } from "./hooks/use-chat-notifications";
import { useUserSync } from "./hooks/use-user-sync";
Expand All @@ -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) => {
Expand All @@ -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 />} />
Comment on lines 46 to +75
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
<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 />} />
Comment on lines 24 to +90
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
<Route path="/projects/:id" element={isMobile ? <ProjectDetailsMobile /> : <ProjectDetails />} />

<Route path="/privacy-policy" element={<PrivacyPolicy />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/terms" element={<Terms />} />
<Route path="/privacy-policy" element={isMobile ? <PrivacyPolicyMobile /> : <PrivacyPolicy />} />
<Route path="/privacy" element={isMobile ? <PrivacyMobile /> : <Privacy />} />
<Route path="/terms" element={isMobile ? <TermsMobile /> : <Terms />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
<Route path="*" element={isMobile ? <NotFoundMobile /> : <NotFound />} />
</Routes>
</Suspense>
</motion.div>
Expand Down
4 changes: 4 additions & 0 deletions src/components/ProfilePhotoCropper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Cropper, { Area } from "react-easy-crop";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
Expand Down Expand Up @@ -143,6 +144,9 @@ const ProfilePhotoCropper: React.FC<ProfilePhotoCropperProps> = ({
<DialogContent className="sm:max-w-[500px] p-0 gap-0 overflow-hidden">
<DialogHeader className="px-6 pt-6 pb-2">
<DialogTitle>Adjust Profile Photo</DialogTitle>
<DialogDescription className="sr-only">
Crop, zoom, and rotate your profile image before saving.
</DialogDescription>
</DialogHeader>

{}
Expand Down
28 changes: 21 additions & 7 deletions src/components/layout/MobileLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { Menu, Search, Plus, Home, Folder, CheckSquare, Bell, User } from 'lucide-react';
import { Menu, Search, Plus, Home, Video, CheckSquare, Bell, Settings } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetTrigger, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Sheet, SheetContent, SheetTrigger, SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet';
import { cn } from '@/lib/utils';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';

Expand Down Expand Up @@ -30,13 +30,14 @@ export const MobileLayout = ({
onFabClick,
rightHeaderAction
}: MobileLayoutProps) => {
const [isDrawerOpen, setIsDrawerOpen] = React.useState(false);

const navItems = [
{ id: 'Home', icon: Home, label: 'Home' },
{ id: 'Projects', icon: Folder, label: 'Projects' },
{ id: 'Meet', icon: Video, label: 'Meet' },
{ id: 'Tasks', icon: CheckSquare, label: 'Tasks' },
{ id: 'Activity', icon: Bell, label: 'Activity' },
{ id: 'Profile', icon: User, label: 'Profile' },
{ id: 'Settings', icon: Settings, label: 'Settings' },
];


Expand All @@ -47,15 +48,20 @@ export const MobileLayout = ({
{}
<header className="h-14 border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 flex items-center justify-between px-4 shrink-0 z-40">
<div className="flex items-center gap-3">
<Sheet>
<Sheet open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="-ml-2 h-10 w-10">
<Menu className="h-6 w-6" />
<span className="sr-only">Open Menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-[85%] sm:w-[350px] p-0">
{}
<SheetHeader className="sr-only">
<SheetTitle>Navigation Menu</SheetTitle>
<SheetDescription>
Open app navigation links and user account shortcuts.
</SheetDescription>
</SheetHeader>
<div className="flex flex-col h-full bg-background">
{user && (
<div className="p-6 border-b flex items-center gap-4 bg-muted/20">
Expand All @@ -69,7 +75,15 @@ export const MobileLayout = ({
</div>
</div>
)}
<div className="flex-1 overflow-y-auto">
<div
className="flex-1 overflow-y-auto"
onClick={(event) => {
const target = event.target as HTMLElement;
if (target.closest("button, a, [data-close-drawer='true']")) {
setIsDrawerOpen(false);
}
}}
>
{drawerContent}
</div>
</div>
Expand Down
8 changes: 7 additions & 1 deletion src/components/ui/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";

import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";

const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
Expand All @@ -27,6 +27,12 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<DialogHeader className="sr-only">
<DialogTitle>Command Menu</DialogTitle>
<DialogDescription>
Search and run available commands.
</DialogDescription>
</DialogHeader>
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
Expand Down
18 changes: 16 additions & 2 deletions src/components/views/ActivityLogView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@
}

function normalizeUid(value: any): string {
if (!value) return '';

Check failure on line 99 in src/components/views/ActivityLogView.tsx

View workflow job for this annotation

GitHub Actions / Lint & Type Check

Expected { after 'if' condition
if (typeof value === 'string') return value;

Check failure on line 100 in src/components/views/ActivityLogView.tsx

View workflow job for this annotation

GitHub Actions / Lint & Type Check

Expected { after 'if' condition
if (typeof value === 'object') {
return String(value.uid || value.id || value._id || '');
}
Expand All @@ -124,7 +124,7 @@
}

function isInProgressTask(t: any): boolean {
if (isCompletedTask(t)) return false;

Check failure on line 127 in src/components/views/ActivityLogView.tsx

View workflow job for this annotation

GitHub Actions / Lint & Type Check

Expected { after 'if' condition
const s = normStatus(t?.status);
const hasCommitEvidence = Boolean(
t?.commitUrl ||
Expand All @@ -134,7 +134,7 @@
);

// Only count tasks that have a real commit attached.
if (!hasCommitEvidence) return false;

Check failure on line 137 in src/components/views/ActivityLogView.tsx

View workflow job for this annotation

GitHub Actions / Lint & Type Check

Expected { after 'if' condition

return s.includes('progress') || s === 'active' || s === 'in review';
}
Expand Down Expand Up @@ -170,6 +170,20 @@
return total;
}

function formatHoursMinutes(totalMinutes: number): string {
const safeMinutes = Math.max(0, Math.floor(totalMinutes));
const hours = Math.floor(safeMinutes / 60);
const minutes = safeMinutes % 60;
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
}

function formatSecondsToHoursMinutes(totalSeconds: number): string {
const safeSeconds = Math.max(0, Math.floor(totalSeconds));
const hours = Math.floor(safeSeconds / 3600);
const minutes = Math.floor((safeSeconds % 3600) / 60);
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
}

type FeedTag = 'Commit' | 'Completed' | 'Invite' | 'Deadline' | 'Comment' | 'Session';

interface FeedItem {
Expand Down Expand Up @@ -217,7 +231,7 @@
const normalizedCurrentUserId = useMemo(() => normalizeUid(currentUserId), [currentUserId]);

useEffect(() => {
if (!normalizedCurrentUserId) return;

Check failure on line 234 in src/components/views/ActivityLogView.tsx

View workflow job for this annotation

GitHub Actions / Lint & Type Check

Expected { after 'if' condition
// Keep Firestore teams aligned with backend source of truth to avoid owner/member mismatch.
syncTeamsFromApi([...(Array.isArray(ownedTeams) ? ownedTeams : []), ...(Array.isArray(myTeamsFromApi) ? myTeamsFromApi : [])], normalizedCurrentUserId);
}, [syncTeamsFromApi, normalizedCurrentUserId, ownedTeams, myTeamsFromApi]);
Expand All @@ -226,7 +240,7 @@
const map = new Map<string, any>();
(Array.isArray(ownedTeams) ? ownedTeams : []).forEach((t: any) => {
const id = t?.id || t?._id || t?.teamId;
if (!id) return;

Check failure on line 243 in src/components/views/ActivityLogView.tsx

View workflow job for this annotation

GitHub Actions / Lint & Type Check

Expected { after 'if' condition
const prev = map.get(id) || {};
map.set(id, {
...prev,
Expand All @@ -242,7 +256,7 @@
const map = new Map<string, any>();
[...(Array.isArray(myTeamsFromApi) ? myTeamsFromApi : []), ...(Array.isArray(myTeamsFromHook) ? myTeamsFromHook : [])].forEach((t: any) => {
const id = t?.id || t?._id;
if (!id) return;

Check failure on line 259 in src/components/views/ActivityLogView.tsx

View workflow job for this annotation

GitHub Actions / Lint & Type Check

Expected { after 'if' condition
const prev = map.get(id) || {};
map.set(id, { ...prev, ...t, id });
});
Expand All @@ -255,7 +269,7 @@

mergedOwnedTeams.forEach((t: any) => {
const id = t?.id || t?._id;
if (!id) return;

Check failure on line 272 in src/components/views/ActivityLogView.tsx

View workflow job for this annotation

GitHub Actions / Lint & Type Check

Expected { after 'if' condition
const prev = map.get(id) || {};
map.set(id, {
...prev,
Expand All @@ -270,7 +284,7 @@

mergedMyTeams.forEach((t: any) => {
const id = t?.id || t?._id;
if (!id) return;

Check failure on line 287 in src/components/views/ActivityLogView.tsx

View workflow job for this annotation

GitHub Actions / Lint & Type Check

Expected { after 'if' condition
const prev = map.get(id) || {};
map.set(id, {
...prev,
Expand Down Expand Up @@ -310,7 +324,7 @@
() =>
allTeams.filter((t: any) => {
const id = t?.id || t?._id;
if (id && ownedTeamIdSet.has(id)) return true;

Check failure on line 327 in src/components/views/ActivityLogView.tsx

View workflow job for this annotation

GitHub Actions / Lint & Type Check

Expected { after 'if' condition
const owner = extractOwnerUid(t);
return Boolean(normalizedCurrentUserId) && owner === normalizedCurrentUserId;
}),
Expand Down Expand Up @@ -1233,7 +1247,7 @@
<div className="text-right">
<p className="text-[10px] text-text3 uppercase tracking-[0.15em] font-bold mb-1">Total time worked</p>
<h3 className="text-4xl font-bold tracking-tight text-text1">
{Math.floor(totalActiveSeconds / 3600)}h {Math.floor((totalActiveSeconds % 3600) / 60)}m
{formatSecondsToHoursMinutes(totalActiveSeconds)}
</h3>
</div>
</div>
Expand Down Expand Up @@ -1276,7 +1290,7 @@
<p>Efficiency</p>
</div>
<div>
<p className="text-text1 font-bold">{Math.floor(taskStats.dailyActiveAvg / 60)}h {taskStats.dailyActiveAvg % 60}m</p>
<p className="text-text1 font-bold">{formatHoursMinutes(taskStats.dailyActiveAvg)}</p>
<p>Daily Active (Avg)</p>
</div>
<div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/DesignView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const DesignView = () => {
};

return (
<div ref={scrollRef} className="h-full bg-background overflow-y-auto w-full">
<div ref={scrollRef} className="h-full bg-transparent overflow-y-auto w-full">
{}
<div className="w-full max-w-[1800px] mx-auto pt-16 pb-12 px-6 md:px-10 flex flex-col items-start gap-8">
<div className="w-full flex flex-col md:flex-row justify-between items-end gap-6 border-b border-border/40 pb-6">
Expand Down
23 changes: 9 additions & 14 deletions src/components/views/DesktopView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,10 @@ const DesktopView = ({ isPreview = false }: { isPreview?: boolean }) => {

return (

<div className="h-screen w-full bg-black text-foreground overflow-hidden relative font-sans">
<div className="h-screen w-full relative text-foreground overflow-hidden font-sans">
{/* Full-viewport canvas — main column is transparent so this is visible (not body bg-black). */}
<div className="pointer-events-none fixed inset-0 z-0 dashboard-backdrop" aria-hidden />

{/* Full Screen Landing Page Overlay */}
{isLanding && (
<div className={cn(
Expand All @@ -829,7 +832,7 @@ const DesktopView = ({ isPreview = false }: { isPreview?: boolean }) => {
</div>
)}

<PanelGroup direction="horizontal" autoSaveId="persistence" className="w-full h-full">
<PanelGroup direction="horizontal" autoSaveId="persistence" className="relative z-[1] h-full w-full bg-transparent">
{/* Sidebar Panel - Dark & Solid */}
<Panel
ref={sidebarRef}
Expand Down Expand Up @@ -937,17 +940,9 @@ const DesktopView = ({ isPreview = false }: { isPreview?: boolean }) => {
<PanelResizeHandle className="w-px bg-transparent opacity-0" />

{/* Main Content Panel - The "Card" Look */}
<Panel defaultSize={84}>
<div className="h-full w-full p-0 bg-black -ml-2">
<div className="h-full w-full bg-black rounded-r-[32px] rounded-l-none overflow-hidden relative border-none shadow-none flex flex-col">
{/* Matching left-edge mask to eliminate antialias seam */}
<div className="absolute left-0 top-0 h-full w-3 bg-black pointer-events-none z-[95]" />

{/* Background Gradients - Inside the Rounded Container */}
<div className="absolute top-[-10%] right-[20%] w-[500px] h-[500px] bg-rose-600/20 rounded-full blur-[120px] pointer-events-none mix-blend-screen" />
<div className="absolute top-[10%] right-[-10%] w-[600px] h-[600px] bg-indigo-600/20 rounded-full blur-[120px] pointer-events-none mix-blend-screen" />


<Panel defaultSize={84} className="min-h-0 bg-transparent">
<div className="h-full w-full p-0 bg-transparent -ml-2">
<div className="h-full w-full bg-transparent rounded-r-[32px] rounded-l-none overflow-hidden relative border-none shadow-none flex flex-col">
{/* Header - Always show for main app content */}
<div className="flex items-center justify-between px-8 py-5 bg-transparent backdrop-blur-none sticky top-0 z-20">
<div className="flex items-center gap-4">
Expand Down Expand Up @@ -988,7 +983,7 @@ const DesktopView = ({ isPreview = false }: { isPreview?: boolean }) => {

{/* Content Area */}
<div
className="flex-1 overflow-y-auto relative z-10 w-full hover:overflow-y-overlay custom-scrollbar"
className="flex-1 overflow-y-auto relative z-10 w-full bg-transparent hover:overflow-y-overlay custom-scrollbar"
>
{(isExiting || !isLanding) && renderActiveView()}
</div>
Expand Down
Loading
Loading