diff --git a/src/App.tsx b/src/App.tsx index e912b37..42e03bb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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")); @@ -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"; @@ -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 = () => { > Loading…}> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> + : } /> - } /> - } /> - } /> + : } /> + : } /> + : } /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} - } /> + : } /> diff --git a/src/components/ProfilePhotoCropper.tsx b/src/components/ProfilePhotoCropper.tsx index d3cf078..4492e7c 100644 --- a/src/components/ProfilePhotoCropper.tsx +++ b/src/components/ProfilePhotoCropper.tsx @@ -3,6 +3,7 @@ import Cropper, { Area } from "react-easy-crop"; import { Dialog, DialogContent, + DialogDescription, DialogHeader, DialogTitle, DialogFooter, @@ -143,6 +144,9 @@ const ProfilePhotoCropper: React.FC = ({ Adjust Profile Photo + + Crop, zoom, and rotate your profile image before saving. + {} diff --git a/src/components/layout/MobileLayout.tsx b/src/components/layout/MobileLayout.tsx index 9380176..44af7d2 100644 --- a/src/components/layout/MobileLayout.tsx +++ b/src/components/layout/MobileLayout.tsx @@ -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'; @@ -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' }, ]; @@ -47,7 +48,7 @@ export const MobileLayout = ({ {}
- + - {} + + Navigation Menu + + Open app navigation links and user account shortcuts. + +
{user && (
@@ -69,7 +75,15 @@ export const MobileLayout = ({
)} -
+
{ + const target = event.target as HTMLElement; + if (target.closest("button, a, [data-close-drawer='true']")) { + setIsDrawerOpen(false); + } + }} + > {drawerContent}
diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index ad32c15..9ba100b 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -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, @@ -27,6 +27,12 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return ( + + Command Menu + + Search and run available commands. + + {children} diff --git a/src/components/views/ActivityLogView.tsx b/src/components/views/ActivityLogView.tsx index 712827a..c2bb021 100644 --- a/src/components/views/ActivityLogView.tsx +++ b/src/components/views/ActivityLogView.tsx @@ -170,6 +170,20 @@ function secondsForLogsInRange(logs: ActivityLog[], rangeStart: Date, rangeEnd: 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 { @@ -1233,7 +1247,7 @@ export default function ActivityLogView({

Total time worked

- {Math.floor(totalActiveSeconds / 3600)}h {Math.floor((totalActiveSeconds % 3600) / 60)}m + {formatSecondsToHoursMinutes(totalActiveSeconds)}

@@ -1276,7 +1290,7 @@ export default function ActivityLogView({

Efficiency

-

{Math.floor(taskStats.dailyActiveAvg / 60)}h {taskStats.dailyActiveAvg % 60}m

+

{formatHoursMinutes(taskStats.dailyActiveAvg)}

Daily Active (Avg)

diff --git a/src/components/views/DesignView.tsx b/src/components/views/DesignView.tsx index 7fb771a..1e18778 100644 --- a/src/components/views/DesignView.tsx +++ b/src/components/views/DesignView.tsx @@ -127,7 +127,7 @@ const DesignView = () => { }; return ( -
+
{}
diff --git a/src/components/views/DesktopView.tsx b/src/components/views/DesktopView.tsx index b1ba9ef..b630c29 100644 --- a/src/components/views/DesktopView.tsx +++ b/src/components/views/DesktopView.tsx @@ -810,7 +810,10 @@ const DesktopView = ({ isPreview = false }: { isPreview?: boolean }) => { return ( -
+
+ {/* Full-viewport canvas — main column is transparent so this is visible (not body bg-black). */} +
+ {/* Full Screen Landing Page Overlay */} {isLanding && (
{
)} - + {/* Sidebar Panel - Dark & Solid */} { {/* Main Content Panel - The "Card" Look */} - -
-
- {/* Matching left-edge mask to eliminate antialias seam */} -
- - {/* Background Gradients - Inside the Rounded Container */} -
-
- - + +
+
{/* Header - Always show for main app content */}
@@ -988,7 +983,7 @@ const DesktopView = ({ isPreview = false }: { isPreview?: boolean }) => { {/* Content Area */}
{(isExiting || !isLanding) && renderActiveView()}
diff --git a/src/components/views/MobileView.tsx b/src/components/views/MobileView.tsx index e54972c..6efa4f8 100644 --- a/src/components/views/MobileView.tsx +++ b/src/components/views/MobileView.tsx @@ -12,15 +12,12 @@ import { FileText, Video, Settings, - LogOut, Bell } from "lucide-react"; import { MobileLayout } from "@/components/layout/MobileLayout"; import Workspace from "@/components/workspace/Workspace"; -import DashboardView from "./DashboardView"; import TasksView from "./TasksView"; import PeopleView from "./PeopleView"; -import ActivityLogView from "./ActivityLogView"; import CalendarView from "./CalendarView"; import { NotesView } from "@/components/notes/NotesView"; import MeetView from "./MeetView"; @@ -32,15 +29,22 @@ import { WifiOff, RefreshCw } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Card, CardContent } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; +import MobileActivityLogView from "@/components/views/mobile/MobileActivityLogView"; +import MobileDashboardView from "@/components/views/mobile/MobileDashboardView"; const MobileView = () => { const [activeTab, setActiveTab] = useState("Home"); const [currentUser, setCurrentUser] = useState(null); const { isError: userMeError, refetch: refetchMe } = useMe(); const [usersList, setUsersList] = useState([]); + const [activityLogs, setActivityLogs] = useState([]); + const [leaderTasks, setLeaderTasks] = useState([]); + const [teamTasks, setTeamTasks] = useState([]); + const [teamSessions, setTeamSessions] = useState([]); + const [ownedTeams, setOwnedTeams] = useState([]); + const [myTeams, setMyTeams] = useState([]); + const [elapsedTime, setElapsedTime] = useState("00:00:00"); + const [sessionStartTime, setSessionStartTime] = useState(null); const navigate = useNavigate(); const { toast } = useToast(); @@ -84,12 +88,169 @@ const MobileView = () => { navigate(`/projects/${id}`, { state: { from: '/dashboard/workspace' } }); }; - const handleSignOut = async () => { + 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)); + }); + }; + + useEffect(() => { + const storedSession = localStorage.getItem("currentSession"); + if (!storedSession) { return; } try { - await signOutAndClearState(auth); - navigate("/login"); + const parsed = JSON.parse(storedSession); + if (parsed?.startTime) { + setSessionStartTime(new Date(parsed.startTime)); + } + } catch { + // Ignore invalid local session payloads. + } + }, []); + + useEffect(() => { + if (!sessionStartTime) { return; } + const timer = setInterval(() => { + const now = new Date(); + const diff = Math.max(0, Math.floor((now.getTime() - sessionStartTime.getTime()) / 1000)); + const hours = Math.floor(diff / 3600).toString().padStart(2, "0"); + const minutes = Math.floor((diff % 3600) / 60).toString().padStart(2, "0"); + const seconds = (diff % 60).toString().padStart(2, "0"); + setElapsedTime(`${hours}:${minutes}:${seconds}`); + }, 1000); + + return () => clearInterval(timer); + }, [sessionStartTime]); + + useEffect(() => { + if (activeTab !== "Activity" || !currentUser) { return; } + + let cancelled = false; + const fetchActivityData = async () => { + try { + const token = await currentUser.getIdToken(); + const [sessionsRes, projectsRes, ownedTeamsRes, myTeamsRes] = await Promise.all([ + fetch(`${API_BASE_URL}/api/sessions/${currentUser.uid}`, { headers: { Authorization: `Bearer ${token}` } }), + fetch(`${API_BASE_URL}/api/projects`, { headers: { Authorization: `Bearer ${token}` } }), + fetch(`${API_BASE_URL}/api/teams/owned`, { headers: { Authorization: `Bearer ${token}` } }), + fetch(`${API_BASE_URL}/api/teams/mine`, { headers: { Authorization: `Bearer ${token}` } }), + ]); + + if (cancelled) { return; } + + if (sessionsRes.ok) { + const logsData = await sessionsRes.json(); + setActivityLogs(Array.isArray(logsData) ? logsData : []); + } + + if (projectsRes.ok) { + const projects = await projectsRes.json(); + if (Array.isArray(projects)) { + const allTasks = buildActivityLogTasks(projects); + setTeamTasks(allTasks); + const myTasks = filterCommitCapableTasks(allTasks, currentUser.uid); + const receivedTasks = myTasks.filter( + (task: any) => task.assignedBy !== currentUser.uid && task.createdBy !== currentUser.uid + ); + setLeaderTasks(receivedTasks); + } + } + + if (ownedTeamsRes.ok) { + const teams = await ownedTeamsRes.json(); + setOwnedTeams(Array.isArray(teams) ? teams : []); + } + + if (myTeamsRes.ok) { + const teams = await myTeamsRes.json(); + setMyTeams(Array.isArray(teams) ? teams : []); + } + + if (usersList.length > 0) { + const teamSessionsRes = await fetch(`${API_BASE_URL}/api/sessions/batch`, { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, + body: JSON.stringify({ userIds: usersList.map((user) => user.uid) }), + }); + if (!cancelled && teamSessionsRes.ok) { + const sessions = await teamSessionsRes.json(); + setTeamSessions(Array.isArray(sessions) ? sessions : []); + } + } + } catch (error) { + if (!cancelled) { + console.error("Failed to load mobile activity data:", error); + } + } + }; + + fetchActivityData(); + const intervalId = setInterval(fetchActivityData, 30000); + return () => { + cancelled = true; + clearInterval(intervalId); + }; + }, [activeTab, currentUser, usersList]); + + const handleDeleteLog = async (logId: string) => { + if (!currentUser) { return; } + try { + const token = await currentUser.getIdToken(); + const response = await fetch(`${API_BASE_URL}/api/sessions/${logId}`, { + method: "DELETE", + headers: { Authorization: `Bearer ${token}` }, + }); + if (response.ok) { + setActivityLogs((prev) => prev.filter((log) => log._id !== logId)); + } else { + throw new Error("Failed to delete log"); + } + } catch (error) { + toast({ title: "Error", description: "Failed to delete log.", variant: "destructive" }); + } + }; + + const handleClearLogs = async () => { + if (!currentUser) { return; } + try { + const token = await currentUser.getIdToken(); + const response = await fetch(`${API_BASE_URL}/api/sessions/user/${currentUser.uid}`, { + method: "DELETE", + headers: { Authorization: `Bearer ${token}` }, + }); + if (response.ok) { + setActivityLogs([]); + } else { + throw new Error("Failed to clear logs"); + } } catch (error) { - console.error("Error signing out", error); + toast({ title: "Error", description: "Failed to clear logs.", variant: "destructive" }); } }; @@ -109,7 +270,7 @@ const MobileView = () => { const renderContent = () => { switch (activeTab) { case "Home": - return currentUser ? : null; + return currentUser ? : null; case "Projects": return currentUser ? (
@@ -123,10 +284,30 @@ const MobileView = () => { case "Tasks": return currentUser ? : null; case "Activity": - // ActivityLogView needs props. For now using placeholder or minimal props if possible. - - - return
Activity Log (Coming Soon on Mobile)
; + return ( + + ); case "People": return { }} />; @@ -138,23 +319,6 @@ const MobileView = () => { return ; case "Settings": return ; - case "Profile": - return ( -
-
- - - {currentUser?.displayName?.charAt(0)} - -

{currentUser?.displayName}

-

{currentUser?.email}

-
- -
- ); default: return null; } @@ -191,7 +355,6 @@ const MobileView = () => { photoURL: currentUser.photoURL ? getFullUrl(currentUser.photoURL) : undefined } : null} drawerContent={DrawerContent} - onFabClick={() => navigate("/new-project")} headerTitle={activeTab === 'Home' ? 'Dashboard' : activeTab} > {userMeError && currentUser && ( diff --git a/src/components/views/SettingsView.tsx b/src/components/views/SettingsView.tsx index 532b71e..b585dbe 100644 --- a/src/components/views/SettingsView.tsx +++ b/src/components/views/SettingsView.tsx @@ -549,17 +549,19 @@ export default function SettingsView() { }; return ( -
+
- - My Profile - Team - Preferences - Integrations - Support - Security - +
+ + My Profile + Team + Preferences + Integrations + Support + Security + +
{} diff --git a/src/components/views/mobile/MobileActivityLogView.tsx b/src/components/views/mobile/MobileActivityLogView.tsx new file mode 100644 index 0000000..65005c6 --- /dev/null +++ b/src/components/views/mobile/MobileActivityLogView.tsx @@ -0,0 +1,356 @@ +import { useMemo, useState } from "react"; +import { formatDistanceToNow } from "date-fns"; +import { Activity, Clock3, Trash2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +interface MobileActivityLogViewProps { + activityLogs: any[]; + tasks: any[]; + users?: any[]; + teamSessions?: any[]; + ownedTeams?: any[]; + myTeams?: any[]; + currentUserId?: string; + currentUserProfile?: { + displayName?: string; + email?: string; + photoURL?: string; + } | null; + teamTasks?: any[]; + elapsedTime: string; + onClearLogs: () => void; + onDeleteLog: (id: string) => void; +} + +const normalizeStatus = (value: unknown) => String(value ?? "").toLowerCase().trim(); + +const isCompleted = (task: any) => { + const status = normalizeStatus(task?.status); + return status.includes("complete") || status === "done"; +}; + +const isInProgress = (task: any) => { + if (isCompleted(task)) return false; + const status = normalizeStatus(task?.status); + return status.includes("progress") || status === "active" || status === "in review"; +}; + +const isOverdue = (task: any) => { + if (isCompleted(task)) return false; + const due = task?.dueDate || task?.deadline; + if (!due) return false; + return new Date(due).getTime() < Date.now(); +}; + +const MobileActivityLogView = ({ + activityLogs, + tasks, + users = [], + teamSessions = [], + ownedTeams = [], + myTeams = [], + currentUserId, + currentUserProfile, + teamTasks = [], + elapsedTime, + onClearLogs, + onDeleteLog, +}: MobileActivityLogViewProps) => { + const totalTasks = tasks.length; + const completed = tasks.filter(isCompleted).length; + const inProgress = tasks.filter(isInProgress).length; + const overdue = tasks.filter(isOverdue).length; + const [expandedMemberId, setExpandedMemberId] = useState(null); + const elapsedSeconds = (() => { + const [h, m, s] = elapsedTime.split(":").map((part) => Number(part) || 0); + return h * 3600 + m * 60 + s; + })(); + + const ownedTeamIds = new Set( + (Array.isArray(ownedTeams) ? ownedTeams : []) + .map((team: any) => String(team?.id || team?._id || team?.teamId || "")) + .filter(Boolean) + ); + + const fallbackOwnedFromMyTeams = (Array.isArray(myTeams) ? myTeams : []).filter((team: any) => { + const owner = String(team?.ownerId || team?.ownerUid || team?.leaderId || team?.createdBy || ""); + return Boolean(owner) && owner === currentUserId; + }); + + fallbackOwnedFromMyTeams.forEach((team: any) => { + const id = String(team?.id || team?._id || team?.teamId || ""); + if (id) { + ownedTeamIds.add(id); + } + }); + + const isLeader = ownedTeamIds.size > 0; + + const teamMemberStats = (() => { + if (!isLeader) { + return []; + } + + const members = new Map(); + const teamList = [...(ownedTeams || []), ...fallbackOwnedFromMyTeams]; + + teamList.forEach((team: any) => { + (team?.members || []).forEach((member: any) => { + const uid = String(typeof member === "string" ? member : member?.uid || member?.id || member?._id || ""); + if (!uid) { + return; + } + const profileFromUsers = users.find((user: any) => user.uid === uid); + members.set(uid, { + uid, + displayName: + profileFromUsers?.displayName || + (typeof member === "object" ? member?.displayName || member?.name : "") || + uid, + email: profileFromUsers?.email || (typeof member === "object" ? member?.email : undefined), + photoURL: profileFromUsers?.photoURL || (typeof member === "object" ? member?.photoURL : undefined), + }); + }); + }); + + users.forEach((user: any) => { + const memberships = Array.isArray(user?.teamMemberships) ? user.teamMemberships.map(String) : []; + if (memberships.some((teamId: string) => ownedTeamIds.has(teamId))) { + members.set(user.uid, { + uid: user.uid, + displayName: user.displayName || user.email?.split("@")[0] || user.uid, + email: user.email, + photoURL: user.photoURL, + }); + } + }); + + const totals = new Map(); + (teamSessions || []).forEach((session: any) => { + const uid = String(session?.userId || ""); + if (!uid || !members.has(uid)) { + return; + } + const secs = Number(session?.activeDuration || 0); + totals.set(uid, (totals.get(uid) || 0) + secs); + }); + + if (currentUserId && members.has(currentUserId)) { + totals.set(currentUserId, (totals.get(currentUserId) || 0) + elapsedSeconds); + } + + return Array.from(members.values()) + .map((member) => ({ + ...member, + activeSeconds: totals.get(member.uid) || 0, + })) + .sort((a, b) => b.activeSeconds - a.activeSeconds); + })(); + + const toTimeLabel = (totalSeconds: number) => { + const safe = Math.max(0, totalSeconds); + const hours = Math.floor(safe / 3600); + const minutes = Math.floor((safe % 3600) / 60); + return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`; + }; + + const memberTaskStats = useMemo(() => { + const statsMap = new Map(); + + (Array.isArray(teamTasks) ? teamTasks : []).forEach((task: any) => { + const assignees = new Set(); + if (task?.assignedTo) { + assignees.add(String(task.assignedTo)); + } + if (Array.isArray(task?.assignedUserIds)) { + task.assignedUserIds.forEach((uid: any) => assignees.add(String(uid))); + } + if (assignees.size === 0) { + return; + } + + assignees.forEach((uid) => { + const current = statsMap.get(uid) || { total: 0, completed: 0, inProgress: 0, overdue: 0 }; + current.total += 1; + if (isCompleted(task)) { + current.completed += 1; + } else if (isInProgress(task)) { + current.inProgress += 1; + } else if (isOverdue(task)) { + current.overdue += 1; + } + statsMap.set(uid, current); + }); + }); + + return statsMap; + }, [teamTasks]); + + return ( +
+ + + + + Activity Summary + + + +
+ + + + {currentUserProfile?.displayName?.charAt(0)?.toUpperCase() || "U"} + + +
+

+ {currentUserProfile?.displayName || "Your Profile"} +

+

+ {currentUserProfile?.email || currentUserId || ""} +

+
+
+

Current session: {elapsedTime}

+
+
+

Total Tasks

+

{totalTasks}

+
+
+

Completed

+

{completed}

+
+
+

In Progress

+

{inProgress}

+
+
+

Overdue

+

{overdue}

+
+
+
+
+ + {isLeader && ( + + + Team Member Activity + + + {teamMemberStats.length === 0 ? ( +

+ No team activity found yet. +

+ ) : ( + teamMemberStats.slice(0, 12).map((member) => { + const memberLabel = member.uid === currentUserId ? "You" : member.displayName; + const stats = memberTaskStats.get(member.uid) || { + total: 0, + completed: 0, + inProgress: 0, + overdue: 0, + }; + const isExpanded = expandedMemberId === member.uid; + + return ( +
+ + + {isExpanded && ( +
+
+

Total Tasks

+

{stats.total}

+
+
+

Completed

+

{stats.completed}

+
+
+

In Progress

+

{stats.inProgress}

+
+
+

Overdue

+

{stats.overdue}

+
+
+ )} +
+ ); + }) + )} +
+
+ )} + + + + Recent Activity + {activityLogs.length > 0 && ( + + )} + + + {activityLogs.length === 0 ? ( +

No activity yet.

+ ) : ( + 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 ( +
+ +
+

{title}

+

+ {formatDistanceToNow(start, { addSuffix: true })} +

+
+ {log._id && ( + + )} +
+ ); + }) + )} +
+
+
+ ); +}; + +export default MobileActivityLogView; diff --git a/src/components/views/mobile/MobileDashboardView.tsx b/src/components/views/mobile/MobileDashboardView.tsx new file mode 100644 index 0000000..dbbda7f --- /dev/null +++ b/src/components/views/mobile/MobileDashboardView.tsx @@ -0,0 +1,135 @@ +import { eachDayOfInterval, formatISO } from "date-fns"; +import { Github, Users, GitFork } from "lucide-react"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + ContributionGraph, + ContributionGraphBlock, + ContributionGraphCalendar, + ContributionGraphLegend, + ContributionGraphTotalCount, +} from "@/components/kibo-ui/contribution-graph"; +import { useGitHubStats, useGitHubContributions } from "@/hooks/useGitHubData"; +import { useProjects } from "@/hooks/useProjects"; + +const MobileDashboardView = ({ currentUser }: { currentUser: any }) => { + const { data: stats } = useGitHubStats(!!currentUser); + const { data: projects = [] } = useProjects(); + const currentYear = new Date().getFullYear(); + const { data: contributions = [] } = useGitHubContributions(currentYear, !!currentUser); + + const contributionMap = contributions.reduce((acc, c) => { + acc[c.date] = c.count; + return acc; + }, {} as Record); + + const maxCount = Math.max(...contributions.map((c) => c.count), 1); + const yearStart = new Date(currentYear, 0, 1); + const yearEnd = new Date(currentYear, 11, 31); + const days = eachDayOfInterval({ start: yearStart, end: yearEnd }); + const graphData = days.map((date) => { + const dateStr = formatISO(date, { representation: "date" }); + const count = contributionMap[dateStr] || 0; + const level = count === 0 ? 0 : Math.ceil((count / maxCount) * 4); + return { date: dateStr, count, level: Math.min(level, 4) }; + }); + + const totalTasks = projects.reduce((sum, project: any) => { + const steps = project.steps || []; + return sum + steps.reduce((stepSum: number, step: any) => stepSum + (step.tasks?.length || 0), 0); + }, 0); + + const completedTasks = projects.reduce((sum, project: any) => { + const steps = project.steps || []; + return ( + sum + + steps.reduce( + (stepSum: number, step: any) => + stepSum + (step.tasks?.filter((task: any) => task.status === "Completed" || task.status === "Done").length || 0), + 0 + ) + ); + }, 0); + + return ( +
+ + +
+ + + + {(stats?.login || currentUser?.displayName || "U").slice(0, 2).toUpperCase()} + + +
+

{stats?.name || currentUser?.displayName || "Dashboard"}

+

+ {stats?.bio || currentUser?.email || "Welcome to your mobile dashboard"} +

+
+ + + {stats?.followers ?? 0} + + + + {stats?.following ?? 0} + + + + {stats?.public_repos ?? projects.length} + +
+
+
+
+
+ + + + + + Contributions + + + + + + + {({ activity, dayIndex, weekIndex }) => ( + + )} + +
+ +
+
+
+
+ +
+ + +

{projects.length}

+

Projects

+
+
+ + +

{totalTasks}

+

Tasks

+
+
+ + +

{completedTasks}

+

Done

+
+
+
+
+ ); +}; + +export default MobileDashboardView; diff --git a/src/index.css b/src/index.css index f8df537..b53a14a 100644 --- a/src/index.css +++ b/src/index.css @@ -175,6 +175,25 @@ .hero-gradient { background: var(--gradient-hero); } + + /** + * Full-viewport canvas behind the resizable shell (not flat black). + * Main content panels use bg-transparent so this shows through. + */ + .dashboard-backdrop { + background: + radial-gradient(ellipse 110% 75% at 50% -18%, rgb(147 51 234 / 0.28), transparent 58%), + radial-gradient(ellipse 85% 55% at 100% 25%, rgb(236 72 153 / 0.18), transparent 52%), + radial-gradient(ellipse 70% 50% at 0% 95%, rgb(59 130 246 / 0.2), transparent 55%), + linear-gradient(165deg, hsl(222 47% 12%) 0%, hsl(262 35% 10%) 42%, hsl(222 47% 7%) 100%); + } + + .light .dashboard-backdrop { + background: + radial-gradient(ellipse 110% 75% at 50% -18%, rgb(147 51 234 / 0.12), transparent 58%), + radial-gradient(ellipse 85% 55% at 100% 25%, rgb(236 72 153 / 0.1), transparent 52%), + linear-gradient(165deg, hsl(210 50% 98%) 0%, hsl(260 40% 96%) 100%); + } } @layer utilities { diff --git a/src/lib/firebase.ts b/src/lib/firebase.ts index 2d840ce..d2112a2 100644 --- a/src/lib/firebase.ts +++ b/src/lib/firebase.ts @@ -34,10 +34,6 @@ if (typeof window !== "undefined" && isValidRecaptchaSiteKey(recaptchaSiteKey)) provider: new ReCaptchaV3Provider(recaptchaSiteKey), isTokenAutoRefreshEnabled: true, }); -} else if (import.meta.env.DEV && typeof window !== "undefined") { - console.info( - "[Firebase] App Check skipped: set VITE_RECAPTCHA_SITE_KEY to your reCAPTCHA v3 site key to enable.", - ); } export const auth = getAuth(app); diff --git a/src/mobile/pages/DashboardMobile.tsx b/src/mobile/pages/DashboardMobile.tsx new file mode 100644 index 0000000..15da5bc --- /dev/null +++ b/src/mobile/pages/DashboardMobile.tsx @@ -0,0 +1,5 @@ +import Dashboard from "@/pages/Dashboard"; + +const DashboardMobile = () => ; + +export default DashboardMobile; diff --git a/src/mobile/pages/IndexMobile.tsx b/src/mobile/pages/IndexMobile.tsx new file mode 100644 index 0000000..56db670 --- /dev/null +++ b/src/mobile/pages/IndexMobile.tsx @@ -0,0 +1,86 @@ +import { Link } from "react-router-dom"; +import { ArrowRight, CheckSquare, MessageSquare, CalendarDays, FolderKanban } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +const features = [ + { + title: "Workspace", + description: "Track projects and architecture in one place.", + icon: FolderKanban, + }, + { + title: "Tasks", + description: "Assign and monitor task progress quickly.", + icon: CheckSquare, + }, + { + title: "Chat & Meet", + description: "Collaborate with your team in real time.", + icon: MessageSquare, + }, + { + title: "Calendar", + description: "Stay aligned with deadlines and meetings.", + icon: CalendarDays, + }, +]; + +const IndexMobile = () => { + return ( +
+
+
+
+ Zync + Zync +
+ + Login + +
+ +
+

+ Build Faster With Your Team +

+

+ Planning, tasks, chat, notes, and progress in one mobile-ready workspace. +

+
+ + +
+
+ +
+ {features.map((feature) => { + const Icon = feature.icon; + return ( + + + + + {feature.title} + + + + {feature.description} + + + ); + })} +
+
+
+ ); +}; + +export default IndexMobile; diff --git a/src/mobile/pages/LoginMobile.tsx b/src/mobile/pages/LoginMobile.tsx new file mode 100644 index 0000000..4e65187 --- /dev/null +++ b/src/mobile/pages/LoginMobile.tsx @@ -0,0 +1,5 @@ +import Login from "@/pages/Login"; + +const LoginMobile = () => ; + +export default LoginMobile; diff --git a/src/mobile/pages/NewProjectMobile.tsx b/src/mobile/pages/NewProjectMobile.tsx new file mode 100644 index 0000000..ea38a6e --- /dev/null +++ b/src/mobile/pages/NewProjectMobile.tsx @@ -0,0 +1,5 @@ +import NewProject from "@/pages/NewProject"; + +const NewProjectMobile = () => ; + +export default NewProjectMobile; diff --git a/src/mobile/pages/NotFoundMobile.tsx b/src/mobile/pages/NotFoundMobile.tsx new file mode 100644 index 0000000..deab440 --- /dev/null +++ b/src/mobile/pages/NotFoundMobile.tsx @@ -0,0 +1,5 @@ +import NotFound from "@/pages/NotFound"; + +const NotFoundMobile = () => ; + +export default NotFoundMobile; diff --git a/src/mobile/pages/PrivacyMobile.tsx b/src/mobile/pages/PrivacyMobile.tsx new file mode 100644 index 0000000..9f67f60 --- /dev/null +++ b/src/mobile/pages/PrivacyMobile.tsx @@ -0,0 +1,5 @@ +import Privacy from "@/pages/Privacy"; + +const PrivacyMobile = () => ; + +export default PrivacyMobile; diff --git a/src/mobile/pages/PrivacyPolicyMobile.tsx b/src/mobile/pages/PrivacyPolicyMobile.tsx new file mode 100644 index 0000000..e65b609 --- /dev/null +++ b/src/mobile/pages/PrivacyPolicyMobile.tsx @@ -0,0 +1,5 @@ +import PrivacyPolicy from "@/pages/PrivacyPolicy"; + +const PrivacyPolicyMobile = () => ; + +export default PrivacyPolicyMobile; diff --git a/src/mobile/pages/ProjectDetailsMobile.tsx b/src/mobile/pages/ProjectDetailsMobile.tsx new file mode 100644 index 0000000..919582d --- /dev/null +++ b/src/mobile/pages/ProjectDetailsMobile.tsx @@ -0,0 +1,5 @@ +import ProjectDetails from "@/pages/ProjectDetails"; + +const ProjectDetailsMobile = () => ; + +export default ProjectDetailsMobile; diff --git a/src/mobile/pages/SignupMobile.tsx b/src/mobile/pages/SignupMobile.tsx new file mode 100644 index 0000000..ca3ad8a --- /dev/null +++ b/src/mobile/pages/SignupMobile.tsx @@ -0,0 +1,5 @@ +import Signup from "@/pages/Signup"; + +const SignupMobile = () => ; + +export default SignupMobile; diff --git a/src/mobile/pages/TermsMobile.tsx b/src/mobile/pages/TermsMobile.tsx new file mode 100644 index 0000000..7c793cb --- /dev/null +++ b/src/mobile/pages/TermsMobile.tsx @@ -0,0 +1,5 @@ +import Terms from "@/pages/Terms"; + +const TermsMobile = () => ; + +export default TermsMobile; diff --git a/src/mobile/pages/WelcomeToZyncMobile.tsx b/src/mobile/pages/WelcomeToZyncMobile.tsx new file mode 100644 index 0000000..1f439f5 --- /dev/null +++ b/src/mobile/pages/WelcomeToZyncMobile.tsx @@ -0,0 +1,5 @@ +import WelcomeToZync from "@/pages/WelcomeToZync"; + +const WelcomeToZyncMobile = () => ; + +export default WelcomeToZyncMobile;