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
-
-
-
- Sign in with Google
-
-
-
-
- {isSigningUp ? 'Already have an account?' : "Don't have an account?"}
- setIsSigningUp(!isSigningUp)} className="font-medium text-blue-400 hover:underline ml-1">
- {isSigningUp ? 'Sign In' : 'Sign Up'}
-
-
-
-
- );
-}
-
-// 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} />
-
-
-
ProdHub
-
- onViewChange('dashboard')} className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"> Dashboard
- onViewChange('weekly_review')} className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"> Weekly Review
- onViewChange('habit_tracker')} className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"> Habit Tracker
- onViewChange('all_tasks')} className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"> All Tasks
- onViewChange('schedule')} className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"> Schedule
-
-
-
Goals
- setShowGoalModal(true)} className="w-full flex items-center gap-3 px-3 py-2 mt-2 rounded-md text-purple-400 hover:bg-purple-900/50 transition-colors"> AI Goal Planner
- {goals.map(goal => (
-
- ))}
-
-
-
-
Projects
- {standaloneProjects.map(p => (
-
onViewChange('project', p.id)} className="w-full text-left flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors truncate">
-
- {p.name}
-
- ))}
-
setIsAddingProject(!isAddingProject)} className="w-full flex items-center gap-3 px-3 py-2 mt-2 rounded-md text-blue-400 hover:bg-blue-900/50 transition-colors"> Add Project
- {isAddingProject && (
-
- )}
-
-
-
-
- Sign Out
-
-
- >
- );
-}
-
-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 ? (
-
- ) : (
-
setIsOpen(!isOpen)} className="flex items-center gap-2 flex-grow text-left">
- {isOpen ? : }
- {goal.name}
-
- )}
-
- setIsEditing(true)} className="p-1 text-gray-400 hover:text-white">
- setShowDeleteModal(true)} className="p-1 text-gray-400 hover:text-red-400">
-
-
- {isOpen && (
-
- {projects.map(p => (
-
onViewChange('project', p.id)} className="w-full text-left flex items-center gap-3 px-3 py-1.5 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors truncate">
-
- {p.name}
-
- ))}
- {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
-
- {isPlanning ?
: }
- Plan My Day
-
-
- {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 (
-
-
setIsOpen(!isOpen)} className="w-full text-left">
-
-
- {isOpen ? : }
- {goal.name}
-
- {Math.round(goalProgress)}%
-
-
-
- {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 ? (
-
- ) : (
-
-
{project.name} {project.type}
-
- setEditingProject({...project})} className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-md">
- setShowDeleteModal('project')} className="p-2 text-gray-400 hover:text-red-500 hover:bg-gray-700 rounded-md">
-
-
- )}
-
-
Progress {Math.round(project.progress || 0)}%
-
-
-
-
-
To-Do List
-
-
setShowAiContextModal(true)} disabled={isGenerating} className="flex items-center gap-2 bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded-md font-semibold transition-colors disabled:bg-purple-800 disabled:cursor-not-allowed">
- {isGenerating ?
: }
- {isGenerating ? 'Generating...' : '✨ Generate Tasks'}
-
-
setIsAddingTask(!isAddingTask)} className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-md font-semibold transition-colors"> {isAddingTask ? 'Cancel' : 'Add Task'}
-
-
- {geminiError &&
{geminiError}
}
- {isAddingTask && (
-
- )}
-
- {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: setFilter(e.target.value)} value={filter} className="bg-gray-700 border border-gray-600 rounded-md px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500">All Active Completed
-
Sort by: setSortBy(e.target.value)} value={sortBy} className="bg-gray-700 border border-gray-600 rounded-md px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500">Due Date Priority Project
-
-
{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
-
- {isLoading ?
: }
- {isLoading ? 'Syncing...' : (syncedEvents.length > 0 ? 'Refresh Calendar' : 'Sync with Google Calendar')}
-
-
- {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}"?`} />
-
-
-
- {task.completed && }
-
-
-
- {isEditing ? (
-
- ) : (
-
{task.title}
- )}
-
- {task.dueDate &&
{formatDate(new Date(task.dueDate))}
}
- {task.priority &&
{task.priority}
}
- {projectName &&
{projectName}
}
-
-
-
- setIsEditing(!isEditing)} className="p-1.5 text-gray-400 hover:text-white hover:bg-gray-700 rounded-md">
- setShowDeleteModal(true)} className="p-1.5 text-gray-400 hover:text-red-500 hover:bg-gray-700 rounded-md">
-
-
- >
- );
-}
-
-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
-
-
setShowAnalytics(!showAnalytics)} className="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 px-4 py-2 rounded-md font-semibold transition-colors">
- {showAnalytics ? 'Hide Analytics' : 'Show Analytics'}
-
-
setIsAdding(!isAdding)} className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-md font-semibold transition-colors">
- {isAdding ? 'Cancel' : 'New Habit'}
-
-
-
-
- {isAdding && (
-
- )}
-
- {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 ? (
-
- ) : (
-
-
-
{habit.name}
-
- setIsEditing(true)} className="p-1 text-gray-400 hover:text-white rounded-md hover:bg-gray-700">
- setShowDeleteModal(true)} className="p-1 text-gray-400 hover:text-red-400 rounded-md hover:bg-gray-700">
-
-
-
0 ? 'text-orange-400' : 'text-gray-500'}`}>
-
- {streak} day streak
-
-
- )}
-
-
- {isCompleted ? "Completed Today!" : "Mark as Done"}
-
-
- >
- );
-}
-
-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!
-
-
setShowReminder(false)} className="p-1 text-blue-300 hover:text-white">
-
- );
-}
-
-function ConfirmModal({ isOpen, onClose, onConfirm, title, message }) {
- if (!isOpen) return null;
- return (
-
-
-
{title}
-
{message}
-
- Cancel
- Confirm
-
-
-
- );
-}
-
-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.
-
-
- );
-}
-
-function GoalPlannerModal({ isOpen, onClose, userId }) {
- const [goal, setGoal] = useState('');
- const [isGenerating, setIsGenerating] = useState(false);
- const [error, setError] = useState('');
-
- const handleGeneratePlan = async () => {
- const apiKey = process.env.REACT_APP_GEMINI_API_KEY;
- if (!apiKey) {
- setError("Gemini API key is not configured.");
- return;
- }
- if (!goal.trim()) {
- setError("Please enter a goal.");
- return;
- }
- setIsGenerating(true);
- setError('');
-
- const prompt = `You are an expert project planner. A user has a high-level goal: "${goal}".
-
- Your task is to decompose this goal into a series of 2 to 4 smaller, actionable projects. For each project, you must also generate a list of 3 to 5 specific tasks to complete it.
-
- The output must be a JSON object. For each project, provide a 'name' and a 'type' (e.g., "Course", "Personal", "Bootcamp"). For each task, provide a 'title' and a 'priority' ('High', 'Medium', or 'Low').`;
-
- const payload = {
- contents: [{ role: "user", parts: [{ text: prompt }] }],
- generationConfig: {
- responseMimeType: "application/json",
- responseSchema: {
- type: "OBJECT",
- properties: {
- projects: {
- type: "ARRAY",
- items: {
- type: "OBJECT",
- properties: {
- name: { type: "STRING" },
- type: { type: "STRING", enum: ["Course", "Conference", "Seminar", "Bootcamp", "Personal"] },
- tasks: {
- type: "ARRAY",
- items: {
- type: "OBJECT",
- properties: {
- title: { type: "STRING" },
- priority: { type: "STRING", enum: ["High", "Medium", "Low"] }
- },
- required: ["title", "priority"]
- }
- }
- },
- required: ["name", "type", "tasks"]
- }
- }
- },
- required: ["projects"]
- }
- }
- };
-
- try {
- const result = await callGeminiWithRetry(payload, apiKey);
-
- if (result.candidates && result.candidates[0].content.parts[0].text) {
- const plan = JSON.parse(result.candidates[0].content.parts[0].text);
- if (plan.projects && plan.projects.length > 0) {
- const batch = writeBatch(db);
-
- // 1. Create the Goal
- const goalRef = doc(collection(db, `artifacts/${appId}/users/${userId}/goals`));
- batch.set(goalRef, { name: goal, createdAt: new Date() });
-
- // 2. Create Projects and Tasks
- plan.projects.forEach(proj => {
- const projectRef = doc(collection(db, `artifacts/${appId}/users/${userId}/projects`));
- batch.set(projectRef, {
- name: proj.name,
- type: proj.type,
- goalId: goalRef.id,
- createdAt: new Date(),
- status: 'In Progress',
- progress: 0
- });
-
- proj.tasks.forEach(task => {
- const taskRef = doc(collection(db, `artifacts/${appId}/users/${userId}/tasks`));
- batch.set(taskRef, {
- ...task,
- projectId: projectRef.id,
- completed: false,
- createdAt: new Date(),
- dueDate: ''
- });
- });
- });
-
- await batch.commit();
- setGoal('');
- onClose();
- } else {
- throw new Error("The AI did not generate any projects for this goal.");
- }
- } else {
- throw new Error("Received an invalid response from the AI.");
- }
- } catch (e) {
- console.error("AI Goal Planner Error:", e);
- setError(e.message || "An unexpected error occurred.");
- } finally {
- setIsGenerating(false);
- }
- };
-
- if (!isOpen) return null;
-
- return (
-
-
-
-
AI Goal Planner
-
-
-
Describe a high-level goal, and the AI will break it down into actionable projects and tasks for you.
-
-
- );
-}
-
-function DailyPlannerModal({ isOpen, onClose, plan, isLoading, error, retry }) {
- if (!isOpen) return null;
-
- return (
-
-
-
-
Your Daily Plan
-
-
-
- {isLoading && (
-
-
-
Your AI assistant is planning your day...
-
- )}
- {error && (
-
-
Error: {error}
-
- Try Again
-
-
- )}
- {plan && (
-
') }}>
-
- )}
-
-
-
- Close
-
-
-
-
+ return user ? (
+
+ ) : (
+
);
}
diff --git a/src/App2.js b/src/App2.js
deleted file mode 100644
index 8532da4..0000000
--- a/src/App2.js
+++ /dev/null
@@ -1,1453 +0,0 @@
-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';
-
-// --- Firebase Configuration ---
-// It's recommended to store these in environment variables
-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;
-// A simple check to see if the config is placeholder or not
-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
-
-
-
- Sign in with Google
-
-
-
-
- {isSigningUp ? 'Already have an account?' : "Don't have an account?"}
- setIsSigningUp(!isSigningUp)} className="font-medium text-blue-400 hover:underline ml-1">
- {isSigningUp ? 'Sign In' : 'Sign Up'}
-
-
-
-
- );
-}
-
-// This is the main content of your application
-function HubApp({ user, handleSignOut }) {
- const [projects, setProjects] = useState([]);
- const [tasks, setTasks] = useState([]);
- const [habits, setHabits] = useState([]);
- const [goals, setGoals] = useState([]);
- 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);
- }, []);
-
- // 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: '' });
- setTokenClient(client);
- }
- }, [isGsiScriptLoaded]);
-
- // Effect to subscribe to Firestore data
- useEffect(() => {
- if (!user || !db) return;
- 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() }));
- setProjects(projectsData);
- if (selectedProjectId && !snapshot.docs.some(doc => doc.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 habitsQuery = query(collection(db, `${basePath}/habits`));
- const unsubscribeHabits = onSnapshot(habitsQuery, (snapshot) => setHabits(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }))));
-
- const goalsQuery = query(collection(db, `${basePath}/goals`));
- const unsubscribeGoals = onSnapshot(goalsQuery, (snapshot) => setGoals(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }))));
-
- const habitEntriesQuery = query(collection(db, `${basePath}/habit_entries`));
- const unsubscribeHabitEntries = onSnapshot(habitEntriesQuery, (snapshot) => setHabitEntries(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }))));
-
- // Cleanup subscriptions on component unmount
- return () => {
- unsubscribeProjects();
- unsubscribeTasks();
- unsubscribeHabits();
- unsubscribeGoals();
- unsubscribeHabitEntries();
- };
- }, [user, selectedProjectId]); // Re-run if user changes
-
- const selectedProject = useMemo(() => projects.find(p => p.id === selectedProjectId), [selectedProjectId, projects]);
-
- const handleSetView = (view, projectId = null) => {
- setActiveView(view);
- setSelectedProjectId(projectId);
- };
-
- return (
-
-
-
- {activeView === 'dashboard' && }
- {activeView === 'project' && selectedProject && }
- {activeView === 'all_tasks' && }
- {activeView === 'schedule' && }
- {activeView === 'habit_tracker' && }
- {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);
- }
- };
-
- 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} />
-
-
-
ProdHub
-
- onViewChange('dashboard')} className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"> Dashboard
- onViewChange('weekly_review')} className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"> Weekly Review
- onViewChange('habit_tracker')} className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"> Habit Tracker
- onViewChange('all_tasks')} className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"> All Tasks
- onViewChange('schedule')} className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"> Schedule
-
-
-
Goals
- setShowGoalModal(true)} className="w-full flex items-center gap-3 px-3 py-2 mt-2 rounded-md text-purple-400 hover:bg-purple-900/50 transition-colors"> AI Goal Planner
- {goals.map(goal => (
-
- ))}
-
-
-
-
Projects
- {standaloneProjects.map(p => (
-
onViewChange('project', p.id)} className="w-full text-left flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors truncate">
-
- {p.name}
-
- ))}
-
setIsAddingProject(!isAddingProject)} className="w-full flex items-center gap-3 px-3 py-2 mt-2 rounded-md text-blue-400 hover:bg-blue-900/50 transition-colors"> Add Project
- {isAddingProject && (
-
- )}
-
-
-
-
- Sign Out
-
-
- >
- );
-}
-
-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 ? (
-
- ) : (
-
setIsOpen(!isOpen)} className="flex items-center gap-2 flex-grow text-left">
- {isOpen ? : }
- {goal.name}
-
- )}
-
- setIsEditing(true)} className="p-1 text-gray-400 hover:text-white">
- setShowDeleteModal(true)} className="p-1 text-gray-400 hover:text-red-400">
-
-
- {isOpen && (
-
- {projects.map(p => (
-
onViewChange('project', p.id)} className="w-full text-left flex items-center gap-3 px-3 py-1.5 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors truncate">
-
- {p.name}
-
- ))}
- {projects.length === 0 &&
No projects yet.
}
-
- )}
-
- );
-}
-
-function Dashboard({ projects, tasks, 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 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
-
- {isPlanning ?
: }
- Plan My Day
-
-
- {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
-
- {projects.length > 0 ? projects.map(p => (
-
onViewChange('project', p.id)}>
-
{p.name} {Math.round(p.progress || 0)}%
-
-
- )) :
No projects yet. Add one from the sidebar!
}
-
-
-
-
-
- >
- );
-}
-
-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 ? (
-
- ) : (
-
-
{project.name} {project.type}
-
- setEditingProject({...project})} className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-md">
- setShowDeleteModal('project')} className="p-2 text-gray-400 hover:text-red-500 hover:bg-gray-700 rounded-md">
-
-
- )}
-
-
Progress {Math.round(project.progress || 0)}%
-
-
-
-
-
To-Do List
-
-
setShowAiContextModal(true)} disabled={isGenerating} className="flex items-center gap-2 bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded-md font-semibold transition-colors disabled:bg-purple-800 disabled:cursor-not-allowed">
- {isGenerating ?
: }
- {isGenerating ? 'Generating...' : '✨ Generate Tasks'}
-
-
setIsAddingTask(!isAddingTask)} className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-md font-semibold transition-colors"> {isAddingTask ? 'Cancel' : 'Add Task'}
-
-
- {geminiError &&
{geminiError}
}
- {isAddingTask && (
-
- )}
-
- {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: setFilter(e.target.value)} value={filter} className="bg-gray-700 border border-gray-600 rounded-md px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500">All Active Completed
-
Sort by: setSortBy(e.target.value)} value={sortBy} className="bg-gray-700 border border-gray-600 rounded-md px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500">Due Date Priority Project
-
-
{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
-
- {isLoading ?
: }
- {isLoading ? 'Syncing...' : (syncedEvents.length > 0 ? 'Refresh Calendar' : 'Sync with Google Calendar')}
-
-
- {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}"?`} />
-
-
-
- {task.completed && }
-
-
-
- {isEditing ? (
-
- ) : (
-
{task.title}
- )}
-
- {task.dueDate &&
{formatDate(new Date(task.dueDate))}
}
- {task.priority &&
{task.priority}
}
- {projectName &&
{projectName}
}
-
-
-
- setIsEditing(!isEditing)} className="p-1.5 text-gray-400 hover:text-white hover:bg-gray-700 rounded-md">
- setShowDeleteModal(true)} className="p-1.5 text-gray-400 hover:text-red-500 hover:bg-gray-700 rounded-md">
-
-
- >
- );
-}
-
-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
-
-
setShowAnalytics(!showAnalytics)} className="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 px-4 py-2 rounded-md font-semibold transition-colors">
- {showAnalytics ? 'Hide Analytics' : 'Show Analytics'}
-
-
setIsAdding(!isAdding)} className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-md font-semibold transition-colors">
- {isAdding ? 'Cancel' : 'New Habit'}
-
-
-
-
- {isAdding && (
-
- )}
-
- {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 ? (
-
- ) : (
-
-
-
{habit.name}
-
- setIsEditing(true)} className="p-1 text-gray-400 hover:text-white rounded-md hover:bg-gray-700">
- setShowDeleteModal(true)} className="p-1 text-gray-400 hover:text-red-400 rounded-md hover:bg-gray-700">
-
-
-
0 ? 'text-orange-400' : 'text-gray-500'}`}>
-
- {streak} day streak
-
-
- )}
-
-
- {isCompleted ? "Completed Today!" : "Mark as Done"}
-
-
- >
- );
-}
-
-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!
-
-
setShowReminder(false)} className="p-1 text-blue-300 hover:text-white">
-
- );
-}
-
-function ConfirmModal({ isOpen, onClose, onConfirm, title, message }) {
- if (!isOpen) return null;
- return (
-
-
-
{title}
-
{message}
-
- Cancel
- Confirm
-
-
-
- );
-}
-
-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.
-
-
- );
-}
-
-function GoalPlannerModal({ isOpen, onClose, userId }) {
- const [goal, setGoal] = useState('');
- const [isGenerating, setIsGenerating] = useState(false);
- const [error, setError] = useState('');
-
- const handleGeneratePlan = async () => {
- const apiKey = process.env.REACT_APP_GEMINI_API_KEY;
- if (!apiKey) {
- setError("Gemini API key is not configured.");
- return;
- }
- if (!goal.trim()) {
- setError("Please enter a goal.");
- return;
- }
- setIsGenerating(true);
- setError('');
-
- const prompt = `You are an expert project planner. A user has a high-level goal: "${goal}".
-
- Your task is to decompose this goal into a series of 2 to 4 smaller, actionable projects. For each project, you must also generate a list of 3 to 5 specific tasks to complete it.
-
- The output must be a JSON object. For each project, provide a 'name' and a 'type' (e.g., "Course", "Personal", "Bootcamp"). For each task, provide a 'title' and a 'priority' ('High', 'Medium', or 'Low').`;
-
- const payload = {
- contents: [{ role: "user", parts: [{ text: prompt }] }],
- generationConfig: {
- responseMimeType: "application/json",
- responseSchema: {
- type: "OBJECT",
- properties: {
- projects: {
- type: "ARRAY",
- items: {
- type: "OBJECT",
- properties: {
- name: { type: "STRING" },
- type: { type: "STRING", enum: ["Course", "Conference", "Seminar", "Bootcamp", "Personal"] },
- tasks: {
- type: "ARRAY",
- items: {
- type: "OBJECT",
- properties: {
- title: { type: "STRING" },
- priority: { type: "STRING", enum: ["High", "Medium", "Low"] }
- },
- required: ["title", "priority"]
- }
- }
- },
- required: ["name", "type", "tasks"]
- }
- }
- },
- required: ["projects"]
- }
- }
- };
-
- try {
- const result = await callGeminiWithRetry(payload, apiKey);
-
- if (result.candidates && result.candidates[0].content.parts[0].text) {
- const plan = JSON.parse(result.candidates[0].content.parts[0].text);
- if (plan.projects && plan.projects.length > 0) {
- const batch = writeBatch(db);
-
- // 1. Create the Goal
- const goalRef = doc(collection(db, `artifacts/${appId}/users/${userId}/goals`));
- batch.set(goalRef, { name: goal, createdAt: new Date() });
-
- // 2. Create Projects and Tasks
- plan.projects.forEach(proj => {
- const projectRef = doc(collection(db, `artifacts/${appId}/users/${userId}/projects`));
- batch.set(projectRef, {
- name: proj.name,
- type: proj.type,
- goalId: goalRef.id,
- createdAt: new Date(),
- status: 'In Progress',
- progress: 0
- });
-
- proj.tasks.forEach(task => {
- const taskRef = doc(collection(db, `artifacts/${appId}/users/${userId}/tasks`));
- batch.set(taskRef, {
- ...task,
- projectId: projectRef.id,
- completed: false,
- createdAt: new Date(),
- dueDate: ''
- });
- });
- });
-
- await batch.commit();
- setGoal('');
- onClose();
- } else {
- throw new Error("The AI did not generate any projects for this goal.");
- }
- } else {
- throw new Error("Received an invalid response from the AI.");
- }
- } catch (e) {
- console.error("AI Goal Planner Error:", e);
- setError(e.message || "An unexpected error occurred.");
- } finally {
- setIsGenerating(false);
- }
- };
-
- if (!isOpen) return null;
-
- return (
-
-
-
-
AI Goal Planner
-
-
-
Describe a high-level goal, and the AI will break it down into actionable projects and tasks for you.
-
-
- );
-}
-
-function DailyPlannerModal({ isOpen, onClose, plan, isLoading, error, retry }) {
- if (!isOpen) return null;
-
- return (
-
-
-
-
Your Daily Plan
-
-
-
- {isLoading && (
-
-
-
Your AI assistant is planning your day...
-
- )}
- {error && (
-
-
Error: {error}
-
- Try Again
-
-
- )}
- {plan && (
-
') }}>
-
- )}
-
-
-
- Close
-
-
-
-
- );
-}
diff --git a/src/App_v1.js b/src/App_v1.js
deleted file mode 100644
index e82d4fb..0000000
--- a/src/App_v1.js
+++ /dev/null
@@ -1,1089 +0,0 @@
-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, Clock, Edit2, Flame, Info, LogOut, Plus, Repeat, Save, Sparkles, Tag, Trash2, TrendingUp, X } from 'lucide-react';
-import { callGeminiWithRetry } from './services/geminiService';
-
-// --- 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;
-// A simple check to see if the config is placeholder or not
-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
-
-
-
- Sign in with Google
-
-
-
-
- {isSigningUp ? 'Already have an account?' : "Don't have an account?"}
- setIsSigningUp(!isSigningUp)} className="font-medium text-blue-400 hover:underline ml-1">
- {isSigningUp ? 'Sign In' : 'Sign Up'}
-
-
-
-
- );
-}
-
-// This is the main content of your application
-function HubApp({ user, handleSignOut }) {
- const [projects, setProjects] = useState([]);
- const [tasks, setTasks] = useState([]);
- const [habits, setHabits] = useState([]);
- 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);
- }, []);
-
- // 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: '' });
- setTokenClient(client);
- }
- }, [isGsiScriptLoaded]);
-
- // Effect to subscribe to Firestore data
- useEffect(() => {
- if (!user || !db) return;
- 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() }));
- setProjects(projectsData);
- // If the selected project is deleted, navigate back to the dashboard
- if (selectedProjectId && !snapshot.docs.some(doc => doc.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 habitsQuery = query(collection(db, `${basePath}/habits`));
- const unsubscribeHabits = onSnapshot(habitsQuery, (snapshot) => setHabits(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }))));
-
- const habitEntriesQuery = query(collection(db, `${basePath}/habit_entries`));
- const unsubscribeHabitEntries = onSnapshot(habitEntriesQuery, (snapshot) => setHabitEntries(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }))));
-
- // Cleanup subscriptions on component unmount
- return () => {
- unsubscribeProjects();
- unsubscribeTasks();
- unsubscribeHabits();
- unsubscribeHabitEntries();
- };
- }, [user, selectedProjectId]); // Re-run if user changes
-
- const selectedProject = useMemo(() => projects.find(p => p.id === selectedProjectId), [selectedProjectId, projects]);
-
- const handleSetView = (view, projectId = null) => {
- setActiveView(view);
- setSelectedProjectId(projectId);
- };
-
- return (
-
-
-
- {activeView === 'dashboard' && }
- {activeView === 'project' && selectedProject && }
- {activeView === 'all_tasks' && }
- {activeView === 'schedule' && }
- {activeView === 'habit_tracker' && }
- {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);
- }
- };
-
- if (!isFirebaseConfigured || !GOOGLE_CLIENT_ID) {
- return ;
- }
-
- if (!isAuthReady) {
- return ;
- }
-
- return user ? : ;
-}
-
-// --- Components ---
-function Sidebar({ onViewChange, projects, userId, handleSignOut }) {
- const [isAddingProject, setIsAddingProject] = useState(false);
- const [newProjectName, setNewProjectName] = useState('');
- const [newProjectType, setNewProjectType] = useState('Course');
-
- 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 };
- try {
- await addDoc(collection(db, `artifacts/${appId}/users/${userId}/projects`), project);
- setNewProjectName('');
- setIsAddingProject(false);
- } catch (error) {
- console.error("Error adding project:", error);
- }
- };
-
- return (
-
-
-
ProdHub
-
- onViewChange('dashboard')} className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"> Dashboard
- onViewChange('weekly_review')} className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"> Weekly Review
- onViewChange('habit_tracker')} className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"> Habit Tracker
- onViewChange('all_tasks')} className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"> All Tasks
- onViewChange('schedule')} className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"> Schedule
-
-
Projects
- {projects.map(p => (
-
onViewChange('project', p.id)} className="w-full text-left flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors truncate">
-
- {p.name}
-
- ))}
-
setIsAddingProject(!isAddingProject)} className="w-full flex items-center gap-3 px-3 py-2 mt-2 rounded-md text-blue-400 hover:bg-blue-900/50 transition-colors"> Add Project
- {isAddingProject && (
-
- )}
-
-
-
-
- Sign Out
-
-
- );
-}
-
-function Dashboard({ projects, tasks, onViewChange }) {
- const today = new Date();
- today.setHours(0,0,0,0);
- 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);
-
- return (
-
-
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
-
- {projects.length > 0 ? projects.map(p => (
-
onViewChange('project', p.id)}>
-
{p.name} {Math.round(p.progress || 0)}%
-
-
- )) :
No projects yet. Add one from the sidebar!
}
-
-
-
-
-
- );
-}
-
-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 ? (
-
- ) : (
-
-
{project.name} {project.type}
-
- setEditingProject({...project})} className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-md">
- setShowDeleteModal('project')} className="p-2 text-gray-400 hover:text-red-500 hover:bg-gray-700 rounded-md">
-
-
- )}
-
-
Progress {Math.round(project.progress || 0)}%
-
-
-
-
-
To-Do List
-
-
setShowAiContextModal(true)} disabled={isGenerating} className="flex items-center gap-2 bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded-md font-semibold transition-colors disabled:bg-purple-800 disabled:cursor-not-allowed">
- {isGenerating ?
: }
- {isGenerating ? 'Generating...' : '✨ Generate Tasks'}
-
-
setIsAddingTask(!isAddingTask)} className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-md font-semibold transition-colors"> {isAddingTask ? 'Cancel' : 'Add Task'}
-
-
- {geminiError &&
{geminiError}
}
- {isAddingTask && (
-
- )}
-
- {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: setFilter(e.target.value)} value={filter} className="bg-gray-700 border border-gray-600 rounded-md px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500">All Active Completed
-
Sort by: setSortBy(e.target.value)} value={sortBy} className="bg-gray-700 border border-gray-600 rounded-md px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500">Due Date Priority Project
-
-
{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
-
- {isLoading ?
: }
- {isLoading ? 'Syncing...' : (syncedEvents.length > 0 ? 'Refresh Calendar' : 'Sync with Google Calendar')}
-
-
- {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}"?`} />
-
-
-
- {task.completed && }
-
-
-
- {isEditing ? (
-
- ) : (
-
{task.title}
- )}
-
- {task.dueDate &&
{formatDate(new Date(task.dueDate))}
}
- {task.priority &&
{task.priority}
}
- {projectName &&
{projectName}
}
-
-
-
- setIsEditing(!isEditing)} className="p-1.5 text-gray-400 hover:text-white hover:bg-gray-700 rounded-md">
- setShowDeleteModal(true)} className="p-1.5 text-gray-400 hover:text-red-500 hover:bg-gray-700 rounded-md">
-
-
- >
- );
-}
-
-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
-
-
setShowAnalytics(!showAnalytics)} className="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 px-4 py-2 rounded-md font-semibold transition-colors">
- {showAnalytics ? 'Hide Analytics' : 'Show Analytics'}
-
-
setIsAdding(!isAdding)} className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-md font-semibold transition-colors">
- {isAdding ? 'Cancel' : 'New Habit'}
-
-
-
-
- {isAdding && (
-
- )}
-
- {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 ? (
-
- ) : (
-
-
-
{habit.name}
-
- setIsEditing(true)} className="p-1 text-gray-400 hover:text-white rounded-md hover:bg-gray-700">
- setShowDeleteModal(true)} className="p-1 text-gray-400 hover:text-red-400 rounded-md hover:bg-gray-700">
-
-
-
0 ? 'text-orange-400' : 'text-gray-500'}`}>
-
- {streak} day streak
-
-
- )}
-
-
- {isCompleted ? "Completed Today!" : "Mark as Done"}
-
-
- >
- );
-}
-
-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!
-
-
setShowReminder(false)} className="p-1 text-blue-300 hover:text-white">
-
- );
-}
-
-function ConfirmModal({ isOpen, onClose, onConfirm, title, message }) {
- if (!isOpen) return null;
- return (
-
-
-
{title}
-
{message}
-
- Cancel
- Confirm
-
-
-
- );
-}
-
-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.
-
-
- );
-}
diff --git a/src/components/auth/LoginScreen.jsx b/src/components/auth/LoginScreen.jsx
new file mode 100644
index 0000000..6e37d10
--- /dev/null
+++ b/src/components/auth/LoginScreen.jsx
@@ -0,0 +1,109 @@
+import React, { useState } from 'react';
+
+export default function LoginScreen({ onGoogleSignIn, onEmailSignIn, onEmailSignUp }) {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [isSigningUp, setIsSigningUp] = useState(false);
+
+ const handleGoogleSignIn = async () => {
+ try {
+ await onGoogleSignIn();
+ } catch (err) {
+ setError(err.message);
+ console.error('Google Sign-In Error:', err);
+ }
+ };
+
+ const handleEmailAuth = async (e) => {
+ e.preventDefault();
+ setError('');
+ try {
+ if (isSigningUp) {
+ await onEmailSignUp(email, password);
+ } else {
+ await onEmailSignIn(email, password);
+ }
+ } catch (err) {
+ setError(err.message);
+ }
+ };
+
+ return (
+
+
+
+
Welcome to ProdHub
+
Sign in to continue
+
+
+
+
+
+
+
+
+
+ Sign in with Google
+
+
+
+
+ Or continue with
+
+
+
+
+ {isSigningUp ? 'Already have an account?' : "Don't have an account?"}
+ setIsSigningUp(!isSigningUp)}
+ className="font-medium text-blue-400 hover:underline ml-1"
+ >
+ {isSigningUp ? 'Sign In' : 'Sign Up'}
+
+
+
+
+ );
+}
diff --git a/src/components/layout/ConfigurationNeeded.jsx b/src/components/layout/ConfigurationNeeded.jsx
new file mode 100644
index 0000000..525d50f
--- /dev/null
+++ b/src/components/layout/ConfigurationNeeded.jsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { Info } from 'lucide-react';
+
+export default 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.
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/modals/AiContextModal.jsx b/src/components/modals/AiContextModal.jsx
new file mode 100644
index 0000000..5e9d285
--- /dev/null
+++ b/src/components/modals/AiContextModal.jsx
@@ -0,0 +1,35 @@
+import React, { useState } from 'react';
+
+export default 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.
+
+
+ );
+}
diff --git a/src/components/modals/ConfirmModal.jsx b/src/components/modals/ConfirmModal.jsx
new file mode 100644
index 0000000..2f38558
--- /dev/null
+++ b/src/components/modals/ConfirmModal.jsx
@@ -0,0 +1,27 @@
+import React from 'react';
+
+export default function ConfirmModal({ isOpen, onClose, onConfirm, title, message }) {
+ if (!isOpen) return null;
+ return (
+
+
+
{title}
+
{message}
+
+
+ Cancel
+
+
+ Confirm
+
+
+
+
+ );
+}
diff --git a/src/components/modals/DailyPlannerModal.jsx b/src/components/modals/DailyPlannerModal.jsx
new file mode 100644
index 0000000..b36ea09
--- /dev/null
+++ b/src/components/modals/DailyPlannerModal.jsx
@@ -0,0 +1,45 @@
+import React from 'react';
+
+export default function DailyPlannerModal({ isOpen, onClose, plan, isLoading, error, retry }) {
+ if (!isOpen) return null;
+
+ return (
+
+
+
+
Your Daily Plan
+
+ X
+
+
+
+ {isLoading && (
+
+
+
Your AI assistant is planning your day...
+
+ )}
+ {error && (
+
+
Error: {error}
+
+ Try Again
+
+
+ )}
+ {plan && (
+
') }}
+ >
+ )}
+
+
+
+ Close
+
+
+
+
+ );
+}
diff --git a/src/components/modals/GoalPlannerModal.jsx b/src/components/modals/GoalPlannerModal.jsx
new file mode 100644
index 0000000..f718d11
--- /dev/null
+++ b/src/components/modals/GoalPlannerModal.jsx
@@ -0,0 +1,152 @@
+import React, { useState } from 'react';
+import { collection, doc, writeBatch } from 'firebase/firestore';
+import { callGeminiWithRetry } from '../../services/geminiService';
+import { GEMINI_API_KEY, appId } from '../../config/env';
+import { db } from '../../config/firebase';
+
+export default function GoalPlannerModal({ isOpen, onClose, userId }) {
+ const [goal, setGoal] = useState('');
+ const [isGenerating, setIsGenerating] = useState(false);
+ const [error, setError] = useState('');
+
+ const handleGeneratePlan = async () => {
+ const apiKey = GEMINI_API_KEY;
+ if (!apiKey) {
+ setError('Gemini API key is not configured.');
+ return;
+ }
+ if (!goal.trim()) {
+ setError('Please enter a goal.');
+ return;
+ }
+ setIsGenerating(true);
+ setError('');
+
+ const prompt = `You are an expert project planner. A user has a high-level goal: "${goal}".
+
+Your task is to decompose this goal into a series of 2 to 4 smaller, actionable projects. For each project, you must also generate a list of 3 to 5 specific tasks to complete it.
+
+The output must be a JSON object. For each project, provide a 'name' and a 'type' (e.g., "Course", "Personal", "Bootcamp"). For each task, provide a 'title' and a 'priority' ('High', 'Medium', or 'Low').`;
+
+ const payload = {
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
+ generationConfig: {
+ responseMimeType: 'application/json',
+ responseSchema: {
+ type: 'OBJECT',
+ properties: {
+ projects: {
+ type: 'ARRAY',
+ items: {
+ type: 'OBJECT',
+ properties: {
+ name: { type: 'STRING' },
+ type: { type: 'STRING', enum: ['Course', 'Conference', 'Seminar', 'Bootcamp', 'Personal'] },
+ tasks: {
+ type: 'ARRAY',
+ items: {
+ type: 'OBJECT',
+ properties: {
+ title: { type: 'STRING' },
+ priority: { type: 'STRING', enum: ['High', 'Medium', 'Low'] },
+ },
+ required: ['title', 'priority'],
+ },
+ },
+ },
+ required: ['name', 'type', 'tasks'],
+ },
+ },
+ },
+ required: ['projects'],
+ },
+ },
+ };
+
+ try {
+ const result = await callGeminiWithRetry(payload, apiKey);
+
+ if (result.candidates && result.candidates[0].content.parts[0].text) {
+ const plan = JSON.parse(result.candidates[0].content.parts[0].text);
+ if (plan.projects && plan.projects.length > 0) {
+ const batch = writeBatch(db);
+
+ const goalRef = doc(collection(db, `artifacts/${appId}/users/${userId}/goals`));
+ batch.set(goalRef, { name: goal, createdAt: new Date() });
+
+ plan.projects.forEach((proj) => {
+ const projectRef = doc(collection(db, `artifacts/${appId}/users/${userId}/projects`));
+ batch.set(projectRef, {
+ name: proj.name,
+ type: proj.type,
+ goalId: goalRef.id,
+ createdAt: new Date(),
+ status: 'In Progress',
+ progress: 0,
+ });
+
+ proj.tasks.forEach((task) => {
+ const taskRef = doc(collection(db, `artifacts/${appId}/users/${userId}/tasks`));
+ batch.set(taskRef, {
+ ...task,
+ projectId: projectRef.id,
+ completed: false,
+ createdAt: new Date(),
+ dueDate: '',
+ });
+ });
+ });
+
+ await batch.commit();
+ setGoal('');
+ onClose();
+ } else {
+ throw new Error('The AI did not generate any projects for this goal.');
+ }
+ } else {
+ throw new Error('Received an invalid response from the AI.');
+ }
+ } catch (e) {
+ console.error('AI Goal Planner Error:', e);
+ setError(e.message || 'An unexpected error occurred.');
+ } finally {
+ setIsGenerating(false);
+ }
+ };
+
+ if (!isOpen) return null;
+
+ return (
+
+
+
+
AI Goal Planner
+
+ X
+
+
+
Describe a high-level goal, and the AI will break it down into actionable projects and tasks for you.
+
+
+ );
+}
diff --git a/src/components/shared/TaskItem.jsx b/src/components/shared/TaskItem.jsx
new file mode 100644
index 0000000..bbb8cab
--- /dev/null
+++ b/src/components/shared/TaskItem.jsx
@@ -0,0 +1,131 @@
+import React, { useState } from 'react';
+import { Book, Calendar, CheckSquare, Edit2, Save, Tag, Trash2 } from 'lucide-react';
+import { doc, updateDoc, deleteDoc } from 'firebase/firestore';
+import { auth, db } from '../../config/firebase';
+import { appId } from '../../config/env';
+import { formatDate } from '../../utils/datetime';
+import ConfirmModal from '../modals/ConfirmModal';
+
+export default 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}"?`}
+ />
+
+
+
+ {task.completed && }
+
+
+
+ {isEditing ? (
+
+ ) : (
+
{task.title}
+ )}
+
+ {task.dueDate && (
+
+ {formatDate(new Date(task.dueDate))}
+
+ )}
+ {task.priority && (
+
+ {task.priority}
+
+ )}
+ {projectName && (
+
+ {projectName}
+
+ )}
+
+
+
+ setIsEditing(!isEditing)}
+ className="p-1.5 text-gray-400 hover:text-white hover:bg-gray-700 rounded-md"
+ >
+
+
+ setShowDeleteModal(true)}
+ className="p-1.5 text-gray-400 hover:text-red-500 hover:bg-gray-700 rounded-md"
+ >
+
+
+
+
+ >
+ );
+}
diff --git a/src/config/env.js b/src/config/env.js
new file mode 100644
index 0000000..71afd13
--- /dev/null
+++ b/src/config/env.js
@@ -0,0 +1,18 @@
+export const appId = 'my-prod-hub';
+
+export 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,
+};
+
+export const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID || '';
+export const GEMINI_API_KEY = process.env.REACT_APP_GEMINI_API_KEY || '';
+export const GEMINI_MODEL = 'gemini-2.5-flash';
+
+export const isFirebaseConfigured = Boolean(
+ firebaseConfig.apiKey && firebaseConfig.apiKey !== 'YOUR_API_KEY'
+);
diff --git a/src/config/firebase.js b/src/config/firebase.js
new file mode 100644
index 0000000..ddff42c
--- /dev/null
+++ b/src/config/firebase.js
@@ -0,0 +1,21 @@
+import { initializeApp } from 'firebase/app';
+import { getAuth } from 'firebase/auth';
+import { getFirestore } from 'firebase/firestore';
+import { firebaseConfig, isFirebaseConfigured } from './env';
+
+let app = null;
+let auth = null;
+let db = null;
+
+if (isFirebaseConfigured) {
+ try {
+ app = initializeApp(firebaseConfig);
+ auth = getAuth(app);
+ db = getFirestore(app);
+ } catch (error) {
+ console.error('Firebase initialization error:', error);
+ }
+}
+
+export { app, auth, db };
+export { isFirebaseConfigured } from './env';
diff --git a/src/contexts/AuthContext.jsx b/src/contexts/AuthContext.jsx
new file mode 100644
index 0000000..e94dbd1
--- /dev/null
+++ b/src/contexts/AuthContext.jsx
@@ -0,0 +1,67 @@
+import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
+import {
+ GoogleAuthProvider,
+ createUserWithEmailAndPassword,
+ onAuthStateChanged,
+ signInWithEmailAndPassword,
+ signInWithPopup,
+ signOut,
+} from 'firebase/auth';
+import { auth } from '../config/firebase';
+import { isFirebaseConfigured } from '../config/env';
+
+const AuthContext = createContext({
+ user: null,
+ isAuthReady: !isFirebaseConfigured,
+ signInWithGoogle: async () => {},
+ emailSignIn: async () => {},
+ emailSignUp: async () => {},
+ signOutUser: async () => {},
+});
+
+export function AuthProvider({ children }) {
+ const [user, setUser] = useState(null);
+ const [isAuthReady, setIsAuthReady] = useState(!isFirebaseConfigured);
+
+ useEffect(() => {
+ if (!auth) {
+ setIsAuthReady(true);
+ return undefined;
+ }
+ const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
+ setUser(currentUser);
+ setIsAuthReady(true);
+ });
+ return unsubscribe;
+ }, []);
+
+ const signInWithGoogle = async () => {
+ if (!auth) throw new Error('Auth not configured');
+ const provider = new GoogleAuthProvider();
+ await signInWithPopup(auth, provider);
+ };
+
+ const emailSignIn = async (email, password) => {
+ if (!auth) throw new Error('Auth not configured');
+ await signInWithEmailAndPassword(auth, email, password);
+ };
+
+ const emailSignUp = async (email, password) => {
+ if (!auth) throw new Error('Auth not configured');
+ await createUserWithEmailAndPassword(auth, email, password);
+ };
+
+ const signOutUser = async () => {
+ if (!auth) return;
+ await signOut(auth);
+ };
+
+ const value = useMemo(
+ () => ({ user, isAuthReady, signInWithGoogle, emailSignIn, emailSignUp, signOutUser }),
+ [user, isAuthReady]
+ );
+
+ return {children} ;
+}
+
+export const useAuth = () => useContext(AuthContext);
diff --git a/src/features/dashboard/DailyReminder.jsx b/src/features/dashboard/DailyReminder.jsx
new file mode 100644
index 0000000..56d64ee
--- /dev/null
+++ b/src/features/dashboard/DailyReminder.jsx
@@ -0,0 +1,19 @@
+import React, { useState } from 'react';
+import { Clock, X } from 'lucide-react';
+
+export default 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!
+
+
setShowReminder(false)} className="p-1 text-blue-300 hover:text-white">
+
+
+
+ );
+}
diff --git a/src/features/dashboard/Dashboard.jsx b/src/features/dashboard/Dashboard.jsx
new file mode 100644
index 0000000..8ecf4b1
--- /dev/null
+++ b/src/features/dashboard/Dashboard.jsx
@@ -0,0 +1,186 @@
+import React, { useMemo, useState } from 'react';
+import { Sparkles } from 'lucide-react';
+import { callGeminiWithRetry } from '../../services/geminiService';
+import { GEMINI_API_KEY } from '../../config/env';
+import { formatTime, getLocalDateKey } from '../../utils/datetime';
+import TaskItem from '../../components/shared/TaskItem';
+import GoalProgress from './GoalProgress';
+import DailyPlannerModal from '../../components/modals/DailyPlannerModal';
+
+export default 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 = 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
+
+ {isPlanning ?
: }
+ Plan My Day
+
+
+ {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!
}
+
+
+
+
+ >
+ );
+}
diff --git a/src/features/dashboard/GoalProgress.jsx b/src/features/dashboard/GoalProgress.jsx
new file mode 100644
index 0000000..475ef0c
--- /dev/null
+++ b/src/features/dashboard/GoalProgress.jsx
@@ -0,0 +1,44 @@
+import React, { useMemo, useState } from 'react';
+import { ChevronDown, ChevronRight } from 'lucide-react';
+
+export default function GoalProgress({ goal, projects, onViewChange }) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ 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 (
+
+
setIsOpen(!isOpen)} className="w-full text-left">
+
+
+ {isOpen ? : }
+ {goal.name}
+
+ {Math.round(goalProgress)}%
+
+
+
+ {isOpen && (
+
+ {projects.map((p) => (
+
onViewChange('project', p.id)}>
+
+ {p.name}
+ {Math.round(p.progress || 0)}%
+
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/features/habits/HabitTrackerView.jsx b/src/features/habits/HabitTrackerView.jsx
new file mode 100644
index 0000000..3a1ceb7
--- /dev/null
+++ b/src/features/habits/HabitTrackerView.jsx
@@ -0,0 +1,304 @@
+import React, { useMemo, useState } from 'react';
+import { Plus, TrendingUp, Edit2, Trash2 } from 'lucide-react';
+import { addDoc, collection, deleteDoc, doc, getDocs, query, updateDoc, where, writeBatch } from 'firebase/firestore';
+import { auth, db } from '../../config/firebase';
+import { appId } from '../../config/env';
+import { getLocalDateKey } from '../../utils/datetime';
+import ConfirmModal from '../../components/modals/ConfirmModal';
+
+export default 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
+
+
setShowAnalytics(!showAnalytics)}
+ className="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 px-4 py-2 rounded-md font-semibold transition-colors"
+ >
+ {showAnalytics ? 'Hide Analytics' : 'Show Analytics'}
+
+
setIsAdding(!isAdding)}
+ className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-md font-semibold transition-colors"
+ >
+ {isAdding ? 'Cancel' : 'New Habit'}
+
+
+
+
+ {isAdding && (
+
+ )}
+
+ {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((docSnap) => {
+ batch.delete(docSnap.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 ? (
+
+ ) : (
+
+
+
{habit.name}
+
+ setIsEditing(true)} className="p-1 text-gray-400 hover:text-white rounded-md hover:bg-gray-700">
+
+
+ setShowDeleteModal(true)} className="p-1 text-gray-400 hover:text-red-400 rounded-md hover:bg-gray-700">
+
+
+
+
+
0 ? 'text-orange-400' : 'text-gray-500'}`}>
+
+ {streak} day streak
+
+
+ )}
+
+
+ {isCompleted ? 'Completed Today!' : 'Mark as Done'}
+
+
+ >
+ );
+}
+
+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}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/features/project/ProjectDetail.jsx b/src/features/project/ProjectDetail.jsx
new file mode 100644
index 0000000..6c6c288
--- /dev/null
+++ b/src/features/project/ProjectDetail.jsx
@@ -0,0 +1,337 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { doc, updateDoc, addDoc, collection, deleteDoc } from 'firebase/firestore';
+import { Edit2, Plus, Save, Sparkles, Trash2, X } from 'lucide-react';
+import { auth, db } from '../../config/firebase';
+import { appId, GEMINI_API_KEY } from '../../config/env';
+import { callGeminiWithRetry } from '../../services/geminiService';
+import TaskItem from '../../components/shared/TaskItem';
+import ConfirmModal from '../../components/modals/ConfirmModal';
+import AiContextModal from '../../components/modals/AiContextModal';
+
+export default function ProjectDetail({ project, allTasks, syncedEvents }) {
+ const [isAddingTask, setIsAddingTask] = useState(false);
+ const [newTask, setNewTask] = useState({ title: '', dueDate: '', priority: 'Medium' });
+ const [sortMode, setSortMode] = useState('deadline');
+ 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(() => {
+ const priorityOrderHigh = { High: 0, Medium: 1, Low: 2 };
+ const priorityOrderLow = { Low: 0, Medium: 1, High: 2 };
+ const filtered = allTasks.filter((t) => t.projectId === project.id);
+
+ if (sortMode === 'random') {
+ const shuffle = (list) => {
+ const arr = list.slice();
+ for (let i = arr.length - 1; i > 0; i -= 1) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [arr[i], arr[j]] = [arr[j], arr[i]];
+ }
+ return arr;
+ };
+ const incomplete = filtered.filter((t) => !t.completed);
+ const completed = filtered.filter((t) => t.completed);
+ return [...shuffle(incomplete), ...shuffle(completed)];
+ }
+
+ const parseDueDate = (value) => {
+ const ts = Date.parse(value);
+ return Number.isFinite(ts) ? ts : Number.POSITIVE_INFINITY;
+ };
+
+ const sorted = filtered.slice().sort((a, b) => {
+ // Always keep incomplete tasks above completed tasks
+ if (a.completed !== b.completed) return a.completed ? 1 : -1;
+
+ let cmp = 0;
+ switch (sortMode) {
+ case 'priority-high-low':
+ cmp = (priorityOrderHigh[a.priority] ?? 99) - (priorityOrderHigh[b.priority] ?? 99);
+ break;
+ case 'priority-low-high':
+ cmp = (priorityOrderLow[a.priority] ?? 99) - (priorityOrderLow[b.priority] ?? 99);
+ break;
+ case 'deadline':
+ cmp = parseDueDate(a.dueDate) - parseDueDate(b.dueDate);
+ break;
+ default:
+ cmp = 0;
+ }
+ return cmp;
+ });
+
+ return sorted;
+ }, [allTasks, project.id, sortMode]);
+
+ 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);
+ 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 = 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 ${e.date.toDateString()}`).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 ? (
+
+ ) : (
+
+
+
{project.name}
+
+ {project.type}
+
+
+
+ setEditingProject({ ...project })} className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-md">
+
+
+ setShowDeleteModal('project')} className="p-2 text-gray-400 hover:text-red-500 hover:bg-gray-700 rounded-md">
+
+
+
+
+ )}
+
+
+ Progress
+ {Math.round(project.progress || 0)}%
+
+
+
+
+
+
To-Do List
+
+
Sort
+
setSortMode(e.target.value)}
+ className="bg-gray-700 border border-gray-600 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ Deadline: Earliest First
+ Priority: Low → High
+ Priority: High → Low
+ Random
+
+
setShowAiContextModal(true)}
+ disabled={isGenerating}
+ className="flex items-center gap-2 bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded-md font-semibold transition-colors disabled:bg-purple-800 disabled:cursor-not-allowed"
+ >
+ {isGenerating ?
: }
+ {isGenerating ? 'Generating...' : '✨ Generate Tasks'}
+
+
setIsAddingTask(!isAddingTask)}
+ className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-md font-semibold transition-colors"
+ >
+ {isAddingTask ? 'Cancel' : 'Add Task'}
+
+
+
+ {geminiError &&
{geminiError}
}
+ {isAddingTask && (
+
+ )}
+
+ {tasks.length > 0 ? (
+ tasks.map((task) =>
)
+ ) : (
+
No tasks for this project yet. Try generating some with AI!
+ )}
+
+
+
+ );
+}
diff --git a/src/features/review/WeeklyReviewView.jsx b/src/features/review/WeeklyReviewView.jsx
new file mode 100644
index 0000000..a7d6894
--- /dev/null
+++ b/src/features/review/WeeklyReviewView.jsx
@@ -0,0 +1,66 @@
+import React, { useMemo } from 'react';
+import TaskItem from '../../components/shared/TaskItem';
+import { formatDate, getLocalDateKey } from '../../utils/datetime';
+
+export default 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!
+ )}
+
+
+ );
+}
diff --git a/src/features/schedule/ScheduleView.jsx b/src/features/schedule/ScheduleView.jsx
new file mode 100644
index 0000000..43fb244
--- /dev/null
+++ b/src/features/schedule/ScheduleView.jsx
@@ -0,0 +1,125 @@
+import React, { useMemo, useState } from 'react';
+import { formatTime } from '../../utils/datetime';
+import { fetchCalendarEvents } from '../../services/googleCalendar';
+
+export default 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 formattedEvents = await fetchCalendarEvents(tokenResponse.access_token);
+ 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);
+
+ 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)
+ .map((t) => {
+ const dueDate = t.dueDate ? new Date(t.dueDate) : null;
+ const isOverdue = dueDate && dueDate < today;
+ return {
+ id: `task-${t.id}`,
+ title: t.title,
+ date: dueDate,
+ type: 'Task Deadline',
+ color: isOverdue ? 'bg-red-700' : 'bg-green-500',
+ isOverdue,
+ };
+ });
+
+ return [...projectEvents, ...taskEvents, ...syncedEvents]
+ .filter((e) => e.date && !isNaN(e.date))
+ .sort((a, b) => a.date - b.date);
+ }, [projects, tasks, syncedEvents]);
+
+ return (
+
+
+
Schedule
+
+ {isLoading ? (
+
+ ) : (
+
+
+
+
+
+
+ )}
+ {isLoading ? 'Syncing...' : syncedEvents.length > 0 ? 'Refresh Calendar' : 'Sync with Google Calendar'}
+
+
+ {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.
+ )}
+
+
+
+ );
+}
diff --git a/src/features/sidebar/Sidebar.jsx b/src/features/sidebar/Sidebar.jsx
new file mode 100644
index 0000000..75ea433
--- /dev/null
+++ b/src/features/sidebar/Sidebar.jsx
@@ -0,0 +1,264 @@
+import React, { useMemo, useState } from 'react';
+import { Book, Calendar, CheckSquare, ChevronDown, ChevronRight, Edit2, LogOut, Plus, Repeat, Save, Sparkles, Trash2, TrendingUp } from 'lucide-react';
+import { collection, doc, addDoc, updateDoc, writeBatch, getDocs, query, where } from 'firebase/firestore';
+import { db } from '../../config/firebase';
+import { appId } from '../../config/env';
+import GoalPlannerModal from '../../components/modals/GoalPlannerModal';
+import ConfirmModal from '../../components/modals/ConfirmModal';
+
+export default 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} />
+
+
+
+ ProdHub
+
+
+ onViewChange('dashboard')}
+ className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"
+ >
+ Dashboard
+
+ onViewChange('weekly_review')}
+ className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"
+ >
+ Weekly Review
+
+ onViewChange('habit_tracker')}
+ className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"
+ >
+ Habit Tracker
+
+ onViewChange('all_tasks')}
+ className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"
+ >
+ All Tasks
+
+ onViewChange('schedule')}
+ className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors"
+ >
+ Schedule
+
+
+
+
Goals
+ setShowGoalModal(true)}
+ className="w-full flex items-center gap-3 px-3 py-2 mt-2 rounded-md text-purple-400 hover:bg-purple-900/50 transition-colors"
+ >
+ AI Goal Planner
+
+ {goals.map((goal) => (
+
+ ))}
+
+
+
+
Projects
+ {standaloneProjects.map((p) => (
+
onViewChange('project', p.id)}
+ className="w-full text-left flex items-center gap-3 px-3 py-2 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors truncate"
+ >
+
+ {p.name}
+
+ ))}
+
setIsAddingProject(!isAddingProject)}
+ className="w-full flex items-center gap-3 px-3 py-2 mt-2 rounded-md text-blue-400 hover:bg-blue-900/50 transition-colors"
+ >
+ Add Project
+
+ {isAddingProject && (
+
+ )}
+
+
+
+
+ Sign Out
+
+
+ >
+ );
+}
+
+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);
+
+ batch.delete(doc(db, `artifacts/${appId}/users/${userId}/goals`, goal.id));
+
+ 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 ? (
+
+ ) : (
+
setIsOpen(!isOpen)} className="flex items-center gap-2 flex-grow text-left">
+ {isOpen ? : }
+ {goal.name}
+
+ )}
+
+ setIsEditing(true)} className="p-1 text-gray-400 hover:text-white">
+
+
+ setShowDeleteModal(true)} className="p-1 text-gray-400 hover:text-red-400">
+
+
+
+
+ {isOpen && (
+
+ {projects.map((p) => (
+
onViewChange('project', p.id)}
+ className="w-full text-left flex items-center gap-3 px-3 py-1.5 rounded-md text-gray-300 hover:bg-gray-700/50 transition-colors truncate"
+ >
+
+ {p.name}
+
+ ))}
+ {projects.length === 0 &&
No projects yet.
}
+
+ )}
+
+ );
+}
diff --git a/src/features/tasks/AllTasksView.jsx b/src/features/tasks/AllTasksView.jsx
new file mode 100644
index 0000000..0b93774
--- /dev/null
+++ b/src/features/tasks/AllTasksView.jsx
@@ -0,0 +1,65 @@
+import React, { useMemo, useState } from 'react';
+import TaskItem from '../../components/shared/TaskItem';
+
+export default 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:
+ setFilter(e.target.value)}
+ value={filter}
+ className="bg-gray-700 border border-gray-600 rounded-md px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ All
+ Active
+ Completed
+
+
+
+ Sort by:
+ setSortBy(e.target.value)}
+ value={sortBy}
+ className="bg-gray-700 border border-gray-600 rounded-md px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ Due Date
+ Priority
+ Project
+
+
+
+
+ {filteredAndSortedTasks.length > 0 ? (
+ filteredAndSortedTasks.map((task) =>
)
+ ) : (
+
No tasks found.
+ )}
+
+
+ );
+}
diff --git a/src/index.js b/src/index.js
index 2cb1087..5be6466 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,10 +2,13 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
+import { AuthProvider } from './contexts/AuthContext';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
-
+
+
+
);
diff --git a/src/services/geminiService.js b/src/services/geminiService.js
index f639712..c42305d 100644
--- a/src/services/geminiService.js
+++ b/src/services/geminiService.js
@@ -1,4 +1,4 @@
-const GEMINI_MODEL = 'gemini-2.5-flash';
+import { GEMINI_MODEL } from '../config/env';
export const callGeminiWithRetry = async (payload, apiKey, maxRetries = 3) => {
let retries = 0;
diff --git a/src/services/googleCalendar.js b/src/services/googleCalendar.js
new file mode 100644
index 0000000..75474c6
--- /dev/null
+++ b/src/services/googleCalendar.js
@@ -0,0 +1,57 @@
+const CALENDAR_SCOPE = 'https://www.googleapis.com/auth/calendar.readonly';
+
+export const loadGoogleIdentityScript = () =>
+ new Promise((resolve, reject) => {
+ if (window.google?.accounts?.oauth2) {
+ resolve(true);
+ return;
+ }
+ const existing = document.querySelector('script[src="https://accounts.google.com/gsi/client"]');
+ if (existing) {
+ existing.addEventListener('load', () => resolve(true));
+ existing.addEventListener('error', reject);
+ return;
+ }
+ const script = document.createElement('script');
+ script.src = 'https://accounts.google.com/gsi/client';
+ script.async = true;
+ script.defer = true;
+ script.onload = () => resolve(true);
+ script.onerror = reject;
+ document.body.appendChild(script);
+ });
+
+export const createCalendarTokenClient = ({ clientId, onToken, onError }) => {
+ if (!window.google?.accounts?.oauth2) return null;
+ const client = window.google.accounts.oauth2.initTokenClient({
+ client_id: clientId,
+ scope: CALENDAR_SCOPE,
+ callback: (tokenResponse) => {
+ if (tokenResponse?.access_token) {
+ onToken(tokenResponse.access_token);
+ } else if (onError) {
+ onError(new Error('No access token returned'));
+ }
+ },
+ });
+ return client;
+};
+
+export const fetchCalendarEvents = async (accessToken) => {
+ 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 ${accessToken}` } }
+ );
+ if (!response.ok) throw new Error('Failed to fetch calendar events');
+ const data = await response.json();
+ return 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',
+ }));
+};
diff --git a/src/utils/datetime.js b/src/utils/datetime.js
new file mode 100644
index 0000000..abfd65a
--- /dev/null
+++ b/src/utils/datetime.js
@@ -0,0 +1,20 @@
+export const formatDate = (date) => {
+ if (!date) return 'N/A';
+ const d = date instanceof Date ? date : date.toDate?.() ?? new Date(date);
+ return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric' }).format(d);
+};
+
+export const formatTime = (date) => {
+ if (!date) return '';
+ const d = new Date(date);
+ 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);
+};
+
+export const getLocalDateKey = (date) => {
+ const d = new Date(date);
+ d.setMinutes(d.getMinutes() - d.getTimezoneOffset());
+ return d.toISOString().split('T')[0];
+};