diff --git a/.firebase/hosting.YnVpbGQ.cache b/.firebase/hosting.YnVpbGQ.cache index def137c..7c40981 100644 --- a/.firebase/hosting.YnVpbGQ.cache +++ b/.firebase/hosting.YnVpbGQ.cache @@ -1,14 +1,14 @@ -favicorn.svg,1755761307597,e80dddc27b301efdf9e44c2df1952423ea4b443d9ef1fe22f4bbc3797a0f7a73 -asset-manifest.json,1769145784811,7003c3de58911ea083c6b4c996323b85197ee04ff75892bc1d4c0f641015a522 robots.txt,1755761307597,b2090cf9761ef60aa06e4fab97679bd43dfa5e5df073701ead5879d7c68f1ec5 +prodhub.svg,1755761307597,c9ccf834f48ca21eaeda0f5566073e9f6757b1e14aaf82591d5afd1486ac39ba manifest.json,1755761307597,4368aeaf848ae2e048765562c289452f33ad2a175c4b1951ea8bdf2ada0d5b10 -static/js/main.ed1316f0.js.LICENSE.txt,1769145784829,016d7efb946774a48216c296cff20c2b87a782bac74ad8d5a1808a48879486df -index.html,1769145784811,8bc49c33b64813d87cf40385c7a8716995325053ca57701670095b2d5687e12b -static/css/main.4409151e.css.map,1769145784827,7dd7f69674e3cf47346aebcac1862d1851d6f1e9fb45159a938e2918f8ee8cfc +logo512.png,1755761307597,212b102aa09e51b3b3e06647e81f7801a61333e171f6582e8124379aabccb41d logo192.png,1755761307597,79e2b749561016bc8af300ea19f48347ceed3cb1a54f48ae456172eca45e08f0 -static/css/main.4409151e.css,1769145784827,5d4ab4f8c7df3fafa8c4bcbf5364d12f6628b371e2cccd12c5d5e03252317ccc +favicorn.svg,1755761307597,e80dddc27b301efdf9e44c2df1952423ea4b443d9ef1fe22f4bbc3797a0f7a73 favicon.ico,1755761307597,27edce7be5922cf0bef7d4136f69b5bfbdd5bf8c13c7b026f71187d41a00aa7d -logo512.png,1755761307597,212b102aa09e51b3b3e06647e81f7801a61333e171f6582e8124379aabccb41d -prodhub.svg,1755761307597,c9ccf834f48ca21eaeda0f5566073e9f6757b1e14aaf82591d5afd1486ac39ba -static/js/main.ed1316f0.js,1769145784829,9b44dbd94b84b0732dbff34210e2d2e9a9ab671ba9f29182eed7949187977881 -static/js/main.ed1316f0.js.map,1769145784830,438f6987590f4ffea01fb79415dd8fe340f043f1ae9aa7cbc0c8ac4975d8779c +index.html,1769584165848,7586066520dd701ac52e9e90bddbe8ca563f3019a8e1e4c9b6eff2907821ad1d +static/js/main.1a6ef1f3.js.LICENSE.txt,1769584165861,016d7efb946774a48216c296cff20c2b87a782bac74ad8d5a1808a48879486df +asset-manifest.json,1769584165848,c9c029a4709b7926190cebbb694f56283bad2f51b8a734a11de36bf9ab4b1563 +static/css/main.4409151e.css.map,1769584165863,7dd7f69674e3cf47346aebcac1862d1851d6f1e9fb45159a938e2918f8ee8cfc +static/css/main.4409151e.css,1769584165861,5d4ab4f8c7df3fafa8c4bcbf5364d12f6628b371e2cccd12c5d5e03252317ccc +static/js/main.1a6ef1f3.js,1769584165863,ac21c3362d716d7f51e1d0dd96d61a5f29e7f8f8de21bbd8de6593c502891438 +static/js/main.1a6ef1f3.js.map,1769584165870,2f58d5fb3ea4a1e82669952fbae4763de61875ee5f32a2e7f394538648e6eee0 diff --git a/src/App.js b/src/App.js index 164e859..98949f9 100644 --- a/src/App.js +++ b/src/App.js @@ -1,140 +1,19 @@ -import React, { useState, useEffect, useMemo } from 'react'; -import { initializeApp } from 'firebase/app'; -import { getAuth, onAuthStateChanged, GoogleAuthProvider, signInWithPopup, signOut, createUserWithEmailAndPassword, signInWithEmailAndPassword } from 'firebase/auth'; -import { getFirestore, collection, doc, addDoc, updateDoc, deleteDoc, onSnapshot, query, where, getDocs, writeBatch } from 'firebase/firestore'; -import { Book, Calendar, CheckSquare, ChevronDown, ChevronRight, Clock, Edit2, Flame, Info, LogOut, Plus, Repeat, Save, Sparkles, Tag, Trash2, TrendingUp, X } from 'lucide-react'; -import { callGeminiWithRetry } from './services/geminiService'; +import React, { useEffect, useMemo, useState } from 'react'; +import { collection, onSnapshot, query } from 'firebase/firestore'; +import { useAuth } from './contexts/AuthContext'; +import { appId, GOOGLE_CLIENT_ID, isFirebaseConfigured } from './config/env'; +import { db } from './config/firebase'; +import { createCalendarTokenClient, loadGoogleIdentityScript } from './services/googleCalendar'; +import LoginScreen from './components/auth/LoginScreen'; +import ConfigurationNeeded from './components/layout/ConfigurationNeeded'; +import Sidebar from './features/sidebar/Sidebar'; +import Dashboard from './features/dashboard/Dashboard'; +import ProjectDetail from './features/project/ProjectDetail'; +import AllTasksView from './features/tasks/AllTasksView'; +import ScheduleView from './features/schedule/ScheduleView'; +import HabitTrackerView from './features/habits/HabitTrackerView'; +import WeeklyReviewView from './features/review/WeeklyReviewView'; -// --- Firebase Configuration --- -const firebaseConfig = { - apiKey: process.env.REACT_APP_FIREBASE_API_KEY, - authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, - projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, - storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, - messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID, - appId: process.env.REACT_APP_FIREBASE_APP_ID -}; - -const appId = 'my-prod-hub'; -const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID || ""; - -// --- Firebase Initialization --- -let app; -let auth; -let db; - -const isFirebaseConfigured = firebaseConfig.apiKey && firebaseConfig.apiKey !== "YOUR_API_KEY"; - -if (isFirebaseConfigured) { - try { - app = initializeApp(firebaseConfig); - auth = getAuth(app); - db = getFirestore(app); - } catch (error) { - console.error("Firebase initialization error:", error); - } -} - -// --- Helper Functions --- -const formatDate = (date) => { - if (!date) return 'N/A'; - // Handles both Firestore Timestamps and JS Date objects - const d = date instanceof Date ? date : date.toDate(); - return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric' }).format(d); -}; - -const formatTime = (date) => { - if (!date) return ''; - const d = new Date(date); - // Check if it's an all-day event (time is midnight UTC) - if (d.getUTCHours() === 0 && d.getUTCMinutes() === 0 && d.getUTCSeconds() === 0) { - return 'All-day'; - } - return new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).format(d); -}; - -const getLocalDateKey = (date) => { - const d = new Date(date); - // Adjust for timezone offset to get the correct local date - d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); - return d.toISOString().split('T')[0]; // YYYY-MM-DD -}; - -// --- Configuration Error Component --- -function ConfigurationNeeded({ missingFirebase, missingGoogle }) { - return ( -
-
- -

Configuration Required

- {missingFirebase &&

Firebase Config Missing: Please ensure your Firebase environment variables (REACT_APP_FIREBASE_*) are set correctly in your .env.local file.

} - {missingGoogle &&

Google Client ID Missing: Please ensure REACT_APP_GOOGLE_CLIENT_ID is set in your .env.local file to enable Google Calendar sync.

} -
-
- ); -} - -// --- Login Screen Component --- -function LoginScreen() { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); - const [isSigningUp, setIsSigningUp] = useState(false); - - const handleGoogleSignIn = async () => { - const provider = new GoogleAuthProvider(); - try { - await signInWithPopup(auth, provider); - } catch (error) { - setError(error.message); - console.error("Google Sign-In Error:", error); - } - }; - - const handleEmailAuth = async (e) => { - e.preventDefault(); - setError(''); - try { - if (isSigningUp) { - await createUserWithEmailAndPassword(auth, email, password); - } else { - await signInWithEmailAndPassword(auth, email, password); - } - } catch (error) { - setError(error.message); - } - }; - - return ( -
-
-
-

Welcome to ProdHub

-

Sign in to continue

-
- -
Or continue with
-
- setEmail(e.target.value)} className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required /> - setPassword(e.target.value)} className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required /> - {error &&

{error}

} - -
-

- {isSigningUp ? 'Already have an account?' : "Don't have an account?"} - -

-
-
- ); -} - -// This is the main content of your application function HubApp({ user, handleSignOut }) { const [projects, setProjects] = useState([]); const [tasks, setTasks] = useState([]); @@ -143,58 +22,68 @@ function HubApp({ user, handleSignOut }) { const [habitEntries, setHabitEntries] = useState([]); const [activeView, setActiveView] = useState('dashboard'); const [selectedProjectId, setSelectedProjectId] = useState(null); - + const [syncedEvents, setSyncedEvents] = useState([]); const [tokenClient, setTokenClient] = useState(null); const [isGsiScriptLoaded, setIsGsiScriptLoaded] = useState(false); - // Effect to load the Google Sign-In script useEffect(() => { - const script = document.createElement('script'); - script.src = 'https://accounts.google.com/gsi/client'; - script.async = true; - script.defer = true; - script.onload = () => setIsGsiScriptLoaded(true); - document.body.appendChild(script); - return () => document.body.removeChild(script); + let cancelled = false; + loadGoogleIdentityScript() + .then(() => { + if (!cancelled) setIsGsiScriptLoaded(true); + }) + .catch((err) => console.error('Google Identity script failed to load', err)); + return () => { + cancelled = true; + }; }, []); - // Effect to initialize the Google token client once the script is loaded useEffect(() => { if (isGsiScriptLoaded && window.google && GOOGLE_CLIENT_ID) { - const client = window.google.accounts.oauth2.initTokenClient({ client_id: GOOGLE_CLIENT_ID, scope: 'https://www.googleapis.com/auth/calendar.readonly', callback: '' }); + const client = createCalendarTokenClient({ + clientId: GOOGLE_CLIENT_ID, + onToken: () => {}, + onError: (err) => console.error(err), + }); setTokenClient(client); } }, [isGsiScriptLoaded]); - // Effect to subscribe to Firestore data useEffect(() => { - if (!user || !db) return; + if (!user || !db) return undefined; const basePath = `artifacts/${appId}/users/${user.uid}`; const projectsQuery = query(collection(db, `${basePath}/projects`)); const unsubscribeProjects = onSnapshot(projectsQuery, (snapshot) => { - const projectsData = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); + const projectsData = snapshot.docs.map((d) => ({ id: d.id, ...d.data() })); setProjects(projectsData); - if (selectedProjectId && !snapshot.docs.some(doc => doc.id === selectedProjectId)) { + if (selectedProjectId && !snapshot.docs.some((docSnap) => docSnap.id === selectedProjectId)) { setActiveView('dashboard'); setSelectedProjectId(null); } }); const tasksQuery = query(collection(db, `${basePath}/tasks`)); - const unsubscribeTasks = onSnapshot(tasksQuery, (snapshot) => setTasks(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })))); - + const unsubscribeTasks = onSnapshot(tasksQuery, (snapshot) => + setTasks(snapshot.docs.map((d) => ({ id: d.id, ...d.data() }))) + ); + const habitsQuery = query(collection(db, `${basePath}/habits`)); - const unsubscribeHabits = onSnapshot(habitsQuery, (snapshot) => setHabits(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })))); + const unsubscribeHabits = onSnapshot(habitsQuery, (snapshot) => + setHabits(snapshot.docs.map((d) => ({ id: d.id, ...d.data() }))) + ); const goalsQuery = query(collection(db, `${basePath}/goals`)); - const unsubscribeGoals = onSnapshot(goalsQuery, (snapshot) => setGoals(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })))); + const unsubscribeGoals = onSnapshot(goalsQuery, (snapshot) => + setGoals(snapshot.docs.map((d) => ({ id: d.id, ...d.data() }))) + ); const habitEntriesQuery = query(collection(db, `${basePath}/habit_entries`)); - const unsubscribeHabitEntries = onSnapshot(habitEntriesQuery, (snapshot) => setHabitEntries(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })))); + const unsubscribeHabitEntries = onSnapshot(habitEntriesQuery, (snapshot) => + setHabitEntries(snapshot.docs.map((d) => ({ id: d.id, ...d.data() }))) + ); - // Cleanup subscriptions on component unmount return () => { unsubscribeProjects(); unsubscribeTasks(); @@ -202,10 +91,13 @@ function HubApp({ user, handleSignOut }) { unsubscribeGoals(); unsubscribeHabitEntries(); }; - }, [user, selectedProjectId]); // Re-run if user changes + }, [user, selectedProjectId]); + + const selectedProject = useMemo( + () => projects.find((p) => p.id === selectedProjectId), + [selectedProjectId, projects] + ); - const selectedProject = useMemo(() => projects.find(p => p.id === selectedProjectId), [selectedProjectId, projects]); - const handleSetView = (view, projectId = null) => { setActiveView(view); setSelectedProjectId(projectId); @@ -213,1296 +105,67 @@ function HubApp({ user, handleSignOut }) { return (
- +
- {activeView === 'dashboard' && } - {activeView === 'project' && selectedProject && } + {activeView === 'dashboard' && ( + + )} + {activeView === 'project' && selectedProject && ( + + )} {activeView === 'all_tasks' && } - {activeView === 'schedule' && } + {activeView === 'schedule' && ( + + )} {activeView === 'habit_tracker' && } - {activeView === 'weekly_review' && } + {activeView === 'weekly_review' && ( + + )}
); } -// --- Top-Level App Component (Handles Conditional Logic) --- export default function App() { - const [user, setUser] = useState(null); - const [isAuthReady, setIsAuthReady] = useState(false); - - useEffect(() => { - if (!auth) { - setIsAuthReady(true); // If firebase isn't configured, we can't check auth - return; - } - const unsubscribe = onAuthStateChanged(auth, (currentUser) => { - setUser(currentUser); - setIsAuthReady(true); - }); - return () => unsubscribe(); - }, []); - - const handleSignOut = async () => { - if(auth) { - await signOut(auth); - } - }; + const { user, isAuthReady, signInWithGoogle, emailSignIn, emailSignUp, signOutUser } = useAuth(); if (!isFirebaseConfigured || !GOOGLE_CLIENT_ID) { return ; } if (!isAuthReady) { - return
; - } - - return user ? : ; -} - -// --- Components --- -function Sidebar({ onViewChange, projects, goals, userId, handleSignOut }) { - const [isAddingProject, setIsAddingProject] = useState(false); - const [newProjectName, setNewProjectName] = useState(''); - const [newProjectType, setNewProjectType] = useState('Course'); - const [showGoalModal, setShowGoalModal] = useState(false); - - const handleAddProject = async (e) => { - e.preventDefault(); - if (!newProjectName.trim() || !userId || !db) return; - const project = { name: newProjectName, type: newProjectType, createdAt: new Date(), status: 'In Progress', progress: 0, goalId: null }; - try { - await addDoc(collection(db, `artifacts/${appId}/users/${userId}/projects`), project); - setNewProjectName(''); - setIsAddingProject(false); - } catch (error) { - console.error("Error adding project:", error); - } - }; - - const { standaloneProjects, goalProjects } = useMemo(() => { - const standalone = projects.filter(p => !p.goalId); - const grouped = projects.reduce((acc, project) => { - if (project.goalId) { - (acc[project.goalId] = acc[project.goalId] || []).push(project); - } - return acc; - }, {}); - return { standaloneProjects: standalone, goalProjects: grouped }; - }, [projects]); - - - return ( - <> - setShowGoalModal(false)} userId={userId} /> - - - ); -} - -function GoalDropdown({ goal, projects, onViewChange, userId }) { - const [isOpen, setIsOpen] = useState(true); - const [isEditing, setIsEditing] = useState(false); - const [editedName, setEditedName] = useState(goal.name); - const [showDeleteModal, setShowDeleteModal] = useState(false); - - const handleUpdate = async (e) => { - e.preventDefault(); - if (!editedName.trim()) return; - await updateDoc(doc(db, `artifacts/${appId}/users/${userId}/goals`, goal.id), { name: editedName }); - setIsEditing(false); - }; - - const handleDelete = async () => { - if (!userId || !db) return; - setShowDeleteModal(false); - const batch = writeBatch(db); - - // Delete the goal - batch.delete(doc(db, `artifacts/${appId}/users/${userId}/goals`, goal.id)); - - // Delete all projects and tasks associated with the goal - for (const project of projects) { - batch.delete(doc(db, `artifacts/${appId}/users/${userId}/projects`, project.id)); - const tasksQuery = query(collection(db, `artifacts/${appId}/users/${userId}/tasks`), where("projectId", "==", project.id)); - const tasksSnapshot = await getDocs(tasksQuery); - tasksSnapshot.forEach(taskDoc => batch.delete(taskDoc.ref)); - } - await batch.commit(); - }; - - return ( -
- setShowDeleteModal(false)} onConfirm={handleDelete} title="Delete Goal" message={`Are you sure you want to delete the goal "${goal.name}" and all its projects and tasks?`} /> -
- {isEditing ? ( -
- setEditedName(e.target.value)} className="w-full bg-gray-700 border border-gray-600 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500" autoFocus /> - -
- ) : ( - - )} -
- - -
-
- {isOpen && ( -
- {projects.map(p => ( - - ))} - {projects.length === 0 &&

No projects yet.

} -
- )} -
- ); -} - -function Dashboard({ projects, tasks, goals, onViewChange, syncedEvents }) { - const [showPlanModal, setShowPlanModal] = useState(false); - const [dailyPlan, setDailyPlan] = useState(''); - const [isPlanning, setIsPlanning] = useState(false); - const [planningError, setPlanningError] = useState(''); - - const today = new Date(); - today.setHours(0,0,0,0); - const todayKey = getLocalDateKey(new Date()); - - const upcomingTasks = tasks.filter(t => !t.completed && t.dueDate).map(t => ({...t, dueDateObj: new Date(t.dueDate)})).filter(t => t.dueDateObj >= today).sort((a, b) => a.dueDateObj - b.dueDateObj).slice(0, 5); - const overdueTasks = tasks.filter(t => !t.completed && t.dueDate && new Date(t.dueDate) < today); - - const { standaloneProjects, goalProjects } = useMemo(() => { - const standalone = projects.filter(p => !p.goalId); - const grouped = projects.reduce((acc, project) => { - if (project.goalId) { - (acc[project.goalId] = acc[project.goalId] || []).push(project); - } - return acc; - }, {}); - return { standaloneProjects: standalone, goalProjects: grouped }; - }, [projects]); - - const handlePlanMyDay = async () => { - setShowPlanModal(true); - setIsPlanning(true); - setDailyPlan(''); - setPlanningError(''); - - const apiKey = process.env.REACT_APP_GEMINI_API_KEY; - if (!apiKey) { - setPlanningError("Gemini API key is not configured."); - setIsPlanning(false); - return; - } - - const todaysTasks = tasks.filter(t => !t.completed && t.dueDate === todayKey); - const todaysEvents = syncedEvents.filter(e => getLocalDateKey(e.date) === todayKey); - - let context = `Today is ${new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}.\n\n`; - - if(overdueTasks.length > 0) { - context += "Here are the overdue tasks that need urgent attention:\n" + overdueTasks.map(t => `- ${t.title} (Priority: ${t.priority})`).join('\n') + '\n\n'; - } - - if(todaysTasks.length > 0) { - context += "Here are the tasks scheduled for today:\n" + todaysTasks.map(t => `- ${t.title} (Priority: ${t.priority})`).join('\n') + '\n\n'; - } - - if(todaysEvents.length > 0) { - context += "Here are the fixed appointments from the calendar:\n" + todaysEvents.map(e => `- ${e.title} at ${formatTime(e.date)}`).join('\n') + '\n\n'; - } else { - context += "There are no events synced from the calendar for today. You can add them from the Schedule page for a better plan.\n\n" - } - - if (overdueTasks.length === 0 && todaysTasks.length === 0 && todaysEvents.length === 0) { - setDailyPlan("You have a clear schedule today! No overdue tasks, today's tasks, or calendar events. It's a great day to get ahead on a project or start planning your next steps."); - setIsPlanning(false); - return; - } - - const prompt = `You are a productivity coach. Based on the following information, create a prioritized, motivational, and realistic schedule for the day. Group tasks logically, suggest breaks, and slot tasks around fixed appointments. Start with the most critical items. - - ${context} - - Generate a step-by-step plan.`; - - const payload = { contents: [{ role: "user", parts: [{ text: prompt }] }] }; - - try { - const result = await callGeminiWithRetry(payload, apiKey); - - if (result.candidates && result.candidates[0].content.parts[0].text) { - setDailyPlan(result.candidates[0].content.parts[0].text); - } else { - throw new Error("Received an invalid response from the AI."); - } - } catch (e) { - console.error("AI Daily Planner Error:", e); - setPlanningError(e.message || "An unexpected error occurred."); - } finally { - setIsPlanning(false); - } - }; - - return ( - <> - setShowPlanModal(false)} - plan={dailyPlan} - isLoading={isPlanning} - error={planningError} - retry={handlePlanMyDay} - /> -
-
-

Dashboard

- -
- {overdueTasks.length > 0 && ( -
-

Overdue Tasks ({overdueTasks.length})

-
{overdueTasks.map(task => )}
-
- )} -
-
-

Upcoming Deadlines

- {upcomingTasks.length > 0 ?
{upcomingTasks.map(task => )}
:

No upcoming deadlines. Great job!

} -
-
-

Projects Overview

-
- {goals.map(goal => ( - - ))} - {standaloneProjects.map(p => ( -
onViewChange('project', p.id)}> -
{p.name}{Math.round(p.progress || 0)}%
-
-
- ))} - {projects.length === 0 &&

No projects yet. Add one from the sidebar!

} -
-
-
- -
- - ); -} - -function GoalProgress({ goal, projects, onViewChange }) { - const [isOpen, setIsOpen] = useState(true); - - const goalProgress = useMemo(() => { - if (!projects || projects.length === 0) return 0; - const totalProgress = projects.reduce((sum, p) => sum + (p.progress || 0), 0); - return totalProgress / projects.length; - }, [projects]); - - return ( -
- - {isOpen && ( -
- {projects.map(p => ( -
onViewChange('project', p.id)}> -
- {p.name} - {Math.round(p.progress || 0)}% -
-
-
- ))} -
- )} -
- ); -} - - -function ProjectDetail({ project, allTasks, syncedEvents }) { - const [isAddingTask, setIsAddingTask] = useState(false); - const [newTask, setNewTask] = useState({ title: '', dueDate: '', priority: 'Medium' }); - const [editingProject, setEditingProject] = useState(null); - const [showDeleteModal, setShowDeleteModal] = useState(null); - const [isGenerating, setIsGenerating] = useState(false); - const [geminiError, setGeminiError] = useState(''); - const [showAiContextModal, setShowAiContextModal] = useState(false); - - const userId = auth.currentUser?.uid; - const tasks = useMemo(() => allTasks.filter(t => t.projectId === project.id), [allTasks, project.id]); - - // Effect to update project progress when tasks change - useEffect(() => { - if (!userId || !project || !db) return; - const completedTasks = tasks.filter(t => t.completed).length; - const totalTasks = tasks.length; - const progress = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0; - if (project.progress !== progress) { - const projectRef = doc(db, `artifacts/${appId}/users/${userId}/projects`, project.id); - updateDoc(projectRef, { progress }); - } - }, [tasks, project, userId]); - - const handleAddTask = async (e) => { - e.preventDefault(); - if (!newTask.title.trim() || !userId || !db) return; - const task = { ...newTask, projectId: project.id, completed: false, createdAt: new Date() }; - await addDoc(collection(db, `artifacts/${appId}/users/${userId}/tasks`), task); - setNewTask({ title: '', dueDate: '', priority: 'Medium' }); - setIsAddingTask(false); - }; - - const handleUpdateProject = async (e) => { - e.preventDefault(); - if (!editingProject.name.trim() || !userId || !db) return; - await updateDoc(doc(db, `artifacts/${appId}/users/${userId}/projects`, project.id), { name: editingProject.name, type: editingProject.type }); - setEditingProject(null); - }; - - const handleDeleteProject = async () => { - if (!userId || !db) return; - setShowDeleteModal(null); - // Batch delete tasks for efficiency (optional but good practice) - for (const task of tasks) { - await deleteDoc(doc(db, `artifacts/${appId}/users/${userId}/tasks`, task.id)); - } - await deleteDoc(doc(db, `artifacts/${appId}/users/${userId}/projects`, project.id)); - }; - - const handleGenerateTasks = async (customContext) => { - setShowAiContextModal(false); - const apiKey = process.env.REACT_APP_GEMINI_API_KEY; - if (!apiKey) { - setGeminiError("Gemini API key is not configured. Please add REACT_APP_GEMINI_API_KEY to your .env.local file."); - return; - } - if (!userId || !db) return; - setIsGenerating(true); - setGeminiError(''); - - const projectKeywords = project.name.toLowerCase().split(' ').filter(k => k.length > 2); - const relevantEvents = syncedEvents.filter(event => { - const eventTitle = event.title.toLowerCase(); - return projectKeywords.some(keyword => eventTitle.includes(keyword)); - }); - - let calendarContext = ""; - if (relevantEvents.length > 0) { - const eventList = relevantEvents.map(e => `- "${e.title}" on ${formatDate(e.date)}`).join('\n'); - calendarContext = `For background context, here are some of my upcoming, related events from my Google Calendar:\n${eventList}\n\n`; - } - - const userContext = customContext ? `Your main instruction is: "${customContext}"\n\n` : ""; - - const prompt = `You are a project planning assistant. Your primary goal is to generate a list of actionable to-do items based on the user's main instruction. Use the other information as background context. - - **Main Instruction from User:** - ${userContext || "Break down the project into actionable steps."} - - **Background Context:** - - Project Name: "${project.name}" - - Project Type: "${project.type}" - ${calendarContext} - - Based on the user's main instruction and the background context, generate a list of 5 to 7 to-do items. For each item, provide a title and estimate its difficulty as 'High', 'Medium', or 'Low'. Do not create tasks that are identical to the calendar events, but rather tasks that lead up to them.`; - - const payload = { contents: [{ role: "user", parts: [{ text: prompt }] }], generationConfig: { responseMimeType: "application/json", responseSchema: { type: "OBJECT", properties: { tasks: { type: "ARRAY", items: { type: "OBJECT", properties: { title: { type: "STRING" }, priority: { type: "STRING", enum: ["High", "Medium", "Low"] } }, required: ["title", "priority"] } } }, required: ["tasks"] } } }; - try { - const result = await callGeminiWithRetry(payload, apiKey); - if (result.candidates && result.candidates[0].content.parts[0].text) { - const generated = JSON.parse(result.candidates[0].content.parts[0].text); - if (generated.tasks && generated.tasks.length > 0) { - for (const task of generated.tasks) { - await addDoc(collection(db, `artifacts/${appId}/users/${userId}/tasks`), { title: task.title, priority: task.priority, projectId: project.id, completed: false, createdAt: new Date(), dueDate: '' }); - } - } - } else { throw new Error("No tasks were generated."); } - } catch (error) { - console.error("Gemini API error:", error); - setGeminiError(error.message || "An error occurred."); - } finally { - setIsGenerating(false); - } - }; - - return ( -
- setShowAiContextModal(false)} onConfirm={handleGenerateTasks} /> - setShowDeleteModal(null)} onConfirm={handleDeleteProject} title="Delete Project" message={`Are you sure you want to delete "${project.name}" and all its tasks?`} /> - {editingProject ? ( -
- setEditingProject({...editingProject, name: e.target.value})} className="flex-grow bg-gray-700 border border-gray-600 rounded-md px-3 py-2 text-2xl font-bold focus:outline-none focus:ring-2 focus:ring-blue-500" /> - - - -
- ) : ( -
-

{project.name}

{project.type}
-
- - -
-
- )} -
-
Progress{Math.round(project.progress || 0)}%
-
-
-
-
-

To-Do List

-
- - -
-
- {geminiError &&
{geminiError}
} - {isAddingTask && ( -
- setNewTask({...newTask, title: e.target.value})} placeholder="Task title..." className="md:col-span-2 bg-gray-700 border border-gray-600 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required /> -
- setNewTask({...newTask, dueDate: e.target.value})} className="bg-gray-700 border border-gray-600 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" /> - -
- -
- )} -
- {tasks.length > 0 ? tasks.sort((a,b) => a.completed - b.completed).map(task => ) :

No tasks for this project yet. Try generating some with AI!

} -
-
-
- ); -} - -function AllTasksView({ tasks, projects }) { - const [filter, setFilter] = useState('All'); - const [sortBy, setSortBy] = useState('dueDate'); - const filteredAndSortedTasks = useMemo(() => { - let filtered = tasks; - if (filter === 'Active') filtered = tasks.filter(t => !t.completed); - if (filter === 'Completed') filtered = tasks.filter(t => t.completed); - return [...filtered].sort((a, b) => { - if (sortBy === 'dueDate') return (a.dueDate ? new Date(a.dueDate) : new Date('2999-12-31')) - (b.dueDate ? new Date(b.dueDate) : new Date('2999-12-31')); - if (sortBy === 'priority') { const priorityOrder = { 'High': 1, 'Medium': 2, 'Low': 3 }; return priorityOrder[a.priority] - priorityOrder[b.priority]; } - if (sortBy === 'project') { const projectA = projects.find(p => p.id === a.projectId)?.name || ''; const projectB = projects.find(p => p.id === b.projectId)?.name || ''; return projectA.localeCompare(projectB); } - return 0; - }); - }, [tasks, projects, filter, sortBy]); - return ( -
-

All Tasks

-
-
Filter by:
-
Sort by:
-
-
{filteredAndSortedTasks.length > 0 ? filteredAndSortedTasks.map(task => ) :

No tasks found.

}
-
- ); -} - -function ScheduleView({ projects, tasks, syncedEvents, setSyncedEvents, tokenClient }) { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const handleSync = () => { - if (tokenClient) { - tokenClient.callback = async (tokenResponse) => { - if (tokenResponse && tokenResponse.access_token) { - setIsLoading(true); - setError(null); - try { - const startTime = new Date().toISOString(); - const response = await fetch(`https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=${startTime}&singleEvents=true&orderBy=startTime`, { - headers: { 'Authorization': `Bearer ${tokenResponse.access_token}` }, - }); - if (!response.ok) throw new Error('Failed to fetch calendar events.'); - const data = await response.json(); - const formattedEvents = data.items - .filter(item => !(item.summary && item.summary.toLowerCase().includes('birthday'))) - .map(item => ({ - id: `gcal-${item.id}`, - title: item.summary, - date: item.start.dateTime ? new Date(item.start.dateTime) : new Date(item.start.date), - type: 'Google Calendar', - color: 'bg-red-500' - })); - setSyncedEvents(formattedEvents); - } catch (e) { - setError('Could not fetch events. Please try again.'); - console.error(e); - } finally { - setIsLoading(false); - } - } - }; - tokenClient.requestAccessToken(); - } else { - setError("Google Auth is not ready. Please wait a moment and try again."); - } - }; - - const allEvents = useMemo(() => { - const today = new Date(); - today.setHours(0, 0, 0, 0); // Set to start of today for accurate overdue comparison - - const projectEvents = projects.map(p => ({ - id: `proj-${p.id}`, - title: p.name, - date: p.deadline ? new Date(p.deadline) : null, - type: 'Project Deadline', - color: 'bg-purple-500' - })); - - const taskEvents = tasks - .filter(t => !t.completed) // 1. Filter out completed tasks - .map(t => { - const dueDate = t.dueDate ? new Date(t.dueDate) : null; - const isOverdue = dueDate && dueDate < today; // 2. Check if task is overdue - return { - id: `task-${t.id}`, - title: t.title, - date: dueDate, - type: 'Task Deadline', - color: isOverdue ? 'bg-red-700' : 'bg-green-500', // 3. Assign color based on overdue status - isOverdue: isOverdue - }; - }); - - return [...projectEvents, ...taskEvents, ...syncedEvents] - .filter(e => e.date && !isNaN(e.date)) - .sort((a, b) => a.date - b.date); - }, [projects, tasks, syncedEvents]); - - return ( -
-
-

Schedule

- -
- {error &&
{error}
} -
-

Upcoming Deadlines & Events

-
- {allEvents.length > 0 ? allEvents.map(event => ( -
-
- {event.date.toLocaleString('default', { month: 'short' })} - {event.date.getDate()} -
-
-
-

{event.title}

-

- {event.type} - {event.isOverdue && (Overdue)} - {event.type === 'Google Calendar' && ` at ${formatTime(event.date)}`} -

-
-
- )) :

No scheduled events with deadlines.

} -
-
-
- ); -} - -function TaskItem({ task, projects = [], isCompact = false }) { - const [isEditing, setIsEditing] = useState(false); - const [editTitle, setEditTitle] = useState(task.title); - const [editPriority, setEditPriority] = useState(task.priority || 'Medium'); - const [showDeleteModal, setShowDeleteModal] = useState(false); - const userId = auth.currentUser?.uid; - - const handleToggleComplete = async () => { - if (!userId || !db) return; - await updateDoc(doc(db, `artifacts/${appId}/users/${userId}/tasks`, task.id), { completed: !task.completed, completedAt: !task.completed ? new Date() : null }); - }; - - const handleUpdate = async (e) => { - e.preventDefault(); - if (!userId || !editTitle.trim() || !db) return; - await updateDoc(doc(db, `artifacts/${appId}/users/${userId}/tasks`, task.id), { - title: editTitle, - priority: editPriority - }); - setIsEditing(false); - }; - - const handleDelete = async () => { - if (!userId || !db) return; - await deleteDoc(doc(db, `artifacts/${appId}/users/${userId}/tasks`, task.id)); - setShowDeleteModal(false); - }; - - const projectName = projects.find(p => p.id === task.projectId)?.name; - const priorityColor = { High: 'text-red-400', Medium: 'text-yellow-400', Low: 'text-green-400' }; - - if (isCompact) { return ( -
- {task.title} -
- {task.dueDate && {formatDate(new Date(task.dueDate))}} - {projectName && {projectName}} -
+
+
); } - return ( - <> - setShowDeleteModal(false)} onConfirm={handleDelete} title="Delete Task" message={`Are you sure you want to delete this task: "${task.title}"?`} /> -
- -
- {isEditing ? ( -
- setEditTitle(e.target.value)} className="flex-grow bg-gray-700 border border-gray-600 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500" autoFocus /> - - -
- ) : ( -

{task.title}

- )} -
- {task.dueDate &&
{formatDate(new Date(task.dueDate))}
} - {task.priority &&
{task.priority}
} - {projectName &&
{projectName}
} -
-
-
- - -
-
- - ); -} - -function HabitTrackerView({ habits, entries }) { - const [newHabitName, setNewHabitName] = useState(''); - const [isAdding, setIsAdding] = useState(false); - const [showAnalytics, setShowAnalytics] = useState(false); - const userId = auth.currentUser?.uid; - - const handleAddHabit = async (e) => { - e.preventDefault(); - if (!newHabitName.trim() || !userId || !db) return; - await addDoc(collection(db, `artifacts/${appId}/users/${userId}/habits`), { name: newHabitName, createdAt: new Date() }); - setNewHabitName(''); - setIsAdding(false); - }; - - const habitStreaks = useMemo(() => { - const streaks = {}; - habits.forEach(habit => { - const habitEntries = entries - .filter(e => e.habitId === habit.id && e.completed) - .map(e => e.date) - .sort((a, b) => new Date(b) - new Date(a)); - - let currentStreak = 0; - if (habitEntries.length > 0) { - const today = new Date(); - const todayKey = getLocalDateKey(today); - const yesterday = new Date(today); - yesterday.setDate(yesterday.getDate() - 1); - const yesterdayKey = getLocalDateKey(yesterday); - - if (habitEntries[0] === todayKey || habitEntries[0] === yesterdayKey) { - currentStreak = 1; - for (let i = 0; i < habitEntries.length - 1; i++) { - const current = new Date(habitEntries[i]); - const next = new Date(habitEntries[i+1]); - const diffDays = Math.round((current - next) / (1000 * 60 * 60 * 24)); - if (diffDays === 1) { - currentStreak++; - } else { - break; - } - } - } - } - streaks[habit.id] = currentStreak; - }); - return streaks; - }, [habits, entries]); - - return ( -
-
-

Habit Tracker

-
- - -
-
- - {isAdding && ( -
- setNewHabitName(e.target.value)} placeholder="e.g., Read for 30 minutes" className="flex-grow bg-gray-700 border border-gray-600 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" required /> - -
- )} - - {showAnalytics && ( -
-

Habit Analytics

-
- {habits.map(habit => e.habitId === habit.id)} />)} - {habits.length === 0 &&

No habits to analyze yet.

} -
-
- )} - -
- {habits.length === 0 && !isAdding && (

No habits defined yet. Add your first one!

)} - {habits.map(habit => ( e.habitId === habit.id)} streak={habitStreaks[habit.id] || 0} />))} -
-
- ); -} - -function HabitDayCard({ habit, entries, streak }) { - const [isEditing, setIsEditing] = useState(false); - const [editedName, setEditedName] = useState(habit.name); - const [showDeleteModal, setShowDeleteModal] = useState(false); - const userId = auth.currentUser?.uid; - const todayKey = getLocalDateKey(new Date()); - const entry = entries.find(e => e.date === todayKey); - const isCompleted = entry ? entry.completed : false; - - const handleHabitToggle = async () => { - if (!userId || !db) return; - if (entry) { - await updateDoc(doc(db, `artifacts/${appId}/users/${userId}/habit_entries`, entry.id), { completed: !isCompleted }); - } else { - await addDoc(collection(db, `artifacts/${appId}/users/${userId}/habit_entries`), { habitId: habit.id, date: todayKey, completed: true }); - } - }; - - const handleUpdateHabit = async (e) => { - e.preventDefault(); - if (!editedName.trim() || !userId || !db) return; - const habitRef = doc(db, `artifacts/${appId}/users/${userId}/habits`, habit.id); - try { - await updateDoc(habitRef, { name: editedName }); - setIsEditing(false); - } catch (error) { - console.error("Error updating habit:", error); - } - }; - - const handleDeleteHabit = async () => { - if (!userId || !db) return; - setShowDeleteModal(false); - - const entriesPath = `artifacts/${appId}/users/${userId}/habit_entries`; - const q = query(collection(db, entriesPath), where("habitId", "==", habit.id)); - - try { - const querySnapshot = await getDocs(q); - const batch = writeBatch(db); - querySnapshot.forEach((doc) => { - batch.delete(doc.ref); - }); - await batch.commit(); - - const habitRef = doc(db, `artifacts/${appId}/users/${userId}/habits`, habit.id); - await deleteDoc(habitRef); - } catch (error) { - console.error("Error deleting habit and its entries:", error); - } - }; - - return ( - <> - setShowDeleteModal(false)} - onConfirm={handleDeleteHabit} - title="Delete Habit" - message={`Are you sure you want to delete the habit "${habit.name}"? All its tracked history will also be removed.`} - /> -
- {isEditing ? ( -
- setEditedName(e.target.value)} - className="w-full bg-gray-700 border border-gray-600 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500" - autoFocus - /> -
- - -
-
- ) : ( -
-
- {habit.name} -
- - -
-
-
0 ? 'text-orange-400' : 'text-gray-500'}`}> - - {streak} day streak -
-
- )} - - -
- - ); -} - -function HabitCalendar({ habit, entries }) { - const today = new Date(); - const [date, setDate] = useState(new Date(today.getFullYear(), today.getMonth(), 1)); - - const goToPreviousMonth = () => { - setDate(currentDate => new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1)); - }; - - const goToNextMonth = () => { - setDate(currentDate => new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1)); - }; - - const completedDates = useMemo(() => new Set(entries.filter(e => e.completed).map(e => e.date)), [entries]); - - const daysInMonth = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(); - const firstDayOfMonth = new Date(date.getFullYear(), date.getMonth(), 1).getDay(); - const days = Array.from({ length: daysInMonth }, (_, i) => i + 1); - const blanks = Array.from({ length: firstDayOfMonth }, (_, i) => i); - - return ( -
-
- -

- {habit.name} - {date.toLocaleString('default', { month: 'long' })} {date.getFullYear()} -

- -
-
- {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((d, i) =>
{d}
)} -
-
- {blanks.map((b, i) =>
)} - {days.map(d => { - const dateKey = getLocalDateKey(new Date(date.getFullYear(), date.getMonth(), d)); - const isCompleted = completedDates.has(dateKey); - const isToday = dateKey === getLocalDateKey(new Date()); - return ( -
- {d} -
- ); - })} -
-
- ); -} - -function WeeklyReviewView({ tasks, projects, habits, entries }) { - const lastWeek = useMemo(() => { - const end = new Date(); - const start = new Date(); - start.setDate(start.getDate() - 7); - return { start, end }; - }, []); - - const completedTasks = tasks.filter(t => t.completed && t.completedAt && t.completedAt.toDate() >= lastWeek.start); - - const habitConsistency = useMemo(() => { - if (habits.length === 0) return 0; - const last7Days = Array.from({length: 7}, (_, i) => { - const d = new Date(); - d.setDate(d.getDate() - i); - return getLocalDateKey(d); - }); - - let totalPossible = habits.length * 7; - let totalCompleted = 0; - - last7Days.forEach(dateKey => { - habits.forEach(habit => { - if (entries.some(e => e.habitId === habit.id && e.date === dateKey && e.completed)) { - totalCompleted++; - } - }); - }); - return totalPossible > 0 ? Math.round((totalCompleted / totalPossible) * 100) : 0; - }, [habits, entries]); - - return ( -
-

Weekly Review

-

Summary of your activity from {formatDate(lastWeek.start)} to {formatDate(lastWeek.end)}.

- -
-
-

Tasks Completed

-

{completedTasks.length}

-
-
-

Habit Consistency

-

{habitConsistency}%

-
-
- -
-

Completed Tasks This Week

- {completedTasks.length > 0 ? ( -
- {completedTasks.map(task => )} -
- ) :

No tasks completed this week. Let's get to it!

} -
-
- ); -} - -function DailyReminder() { - const [showReminder, setShowReminder] = useState(true); - if (!showReminder) return null; - return ( -
- -
-

Daily Reminder

-

Don't forget to review your goals for the day and plan your tasks. A little planning goes a long way!

-
- -
- ); -} - -function ConfirmModal({ isOpen, onClose, onConfirm, title, message }) { - if (!isOpen) return null; - return ( -
-
-

{title}

-

{message}

-
- - -
-
-
- ); -} - -function AiContextModal({ isOpen, onClose, onConfirm }) { - const [context, setContext] = useState(''); - - const handleConfirm = () => { - onConfirm(context); - setContext(''); - }; - - if (!isOpen) return null; - - return ( -
-
-

Add Extra Context

-

Provide any additional details or instructions for the AI to generate more relevant tasks.

-