diff --git a/README.md b/README.md index be8a1bf..0a520d9 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,13 @@ ProdHub is a modern, all-in-one web application built to help you organize, achieve, and grow. Seamlessly manage projects, tasks, habits, and your calendar from a single, intuitive platformβ€”all with smart AI-powered features and robust security. **Try ProdHub Now** -We are available at: [https://my-productivity-hub-5a3ba.web.app/](https://my-productivity-hub-5a3ba.web.app/) +🌐 Web App: [https://my-productivity-hub-5a3ba.web.app/](https://my-productivity-hub-5a3ba.web.app/) +🧩 Chrome Extension: [ProdHub on the Chrome Web Store](#) *(coming soon)* ![ProdHub](images/dashboard_2.png) +--- + ## Key Features **Dashboard Overview** @@ -30,13 +33,13 @@ Break down projects into actionable steps. Set priorities (High, Medium, Low), a ProdHub uses the Google Gemini API for smart, context-aware planning: -- **Goal Decomposition:** Turns your big ambitions into actionable projects. And with Ai gemerated pre tasks for better understanding. +- **Goal Decomposition:** Turns your big ambitions into actionable projects. And with AI generated pre tasks for better understanding. - **Daily Planning:** Creates a prioritized daily schedule based on your tasks and calendar. - Smart Scheduling: The "Plan My Day" button on the dashboard will trigger an AI that acts as a productivity coach. It will analyze overdue tasks, today's high-priority items, and your fixed Google Calendar appointments. - - Actionable Output: Instead of just a list, the AI will generate a suggested schedule with time blocks and priorities. It will be presented in a clean, easy-to-read modal. For example, it might suggest: "9:00 AM - 10:00 AM: Focus on 'Draft Project Proposal' (Overdue Task)" and then slot other tasks around your scheduled meetings. + - Actionable Output: Instead of just a list, the AI will generate a suggested schedule with time blocks and priorities. It will be presented in a clean, easy-to-read modal. - Context-Aware: If you haven't synced your calendar, the planner will still work with your tasks but will also gently remind you that connecting your calendar can lead to even better plans. @@ -48,13 +51,115 @@ ProdHub uses the Google Gemini API for smart, context-aware planning: **Secure Calendar Integration:** Sync your Google Calendar securely and privately. ProdHub only reads event information to help you plan and never stores or modifies your calendar data. Your data stays yours. - +- Set visibility to **Public** (or **Unlisted** for testing) +- Click **"Submit for Review"** +- Review typically takes **a few hours to a few days** + +#### Extension Manifest Reference + +The extension uses `manifest_version: 3` with the following key configuration: + +```json +{ + "name": "ProdHub Chrome Extension", + "version": "1.0", + "manifest_version": 3, + "permissions": ["sidePanel", "identity"], + "side_panel": { "default_path": "index.html" }, + "action": { "default_title": "Open ProdHub Panel" } +} +``` + +The extension manifest lives at `public/manifest.extension.json` and is automatically copied to `build/manifest.json` by the `build:extension` script. + +--- ## Technology @@ -65,6 +170,9 @@ ProdHub is built for speed, reliability, and a great user experience. - **Styling:** Tailwind CSS - **AI Integration:** Google Gemini API - **Calendar Integration:** Google Calendar API via Google Identity Services +- **Extension:** Chrome Manifest V3 Side Panel API + +--- ## Get Started with ProdHub @@ -138,12 +246,14 @@ Update `src/App.js` to use these variables for your `firebaseConfig`. #### 6. Run and Deploy -- **Start locally:** - `npm start` (opens at `http://localhost:3000`) -- **Build:** - `npm run build` -- **Deploy:** - `firebase deploy` (ensure `firebase.json` is set to use the `build` folder) +| Command | Description | +|---|---| +| `npm start` | Start local dev server at `http://localhost:3000` | +| `npm run build` | Build production web app | +| `npm run build:extension` | Build Chrome extension package in `build/` | +| `firebase deploy` | Deploy web app to Firebase Hosting | + +--- ## Running with Docker 🐳 @@ -158,12 +268,12 @@ This project is fully containerized, allowing you to run it in a consistent and 1. **Clone the Repository** ```bash - git clone [https://github.com/Rupesh4604/my-productivity-hub.git](https://github.com/Rupesh4604/my-productivity-hub.git) + git clone https://github.com/Rupesh4604/my-productivity-hub.git cd ProdHub ``` 2. **Create an Environment File** - Create a file named `.env` in the root of the project. This file will hold your secret keys. Copy the contents of `.env.local` or add the following variables, replacing the placeholder values with your actual credentials: + Create a file named `.env` in the root of the project. Copy the contents of `.env.local` or add the following variables, replacing the placeholder values with your actual credentials: ```env REACT_APP_FIREBASE_API_KEY=AIzaSy... @@ -177,20 +287,16 @@ This project is fully containerized, allowing you to run it in a consistent and ``` 3. **Build and Run the Container** - Open your terminal in the project root and run the following command: ```bash docker-compose up --build ``` - This command will build the Docker image based on the `Dockerfile` and then start the container. - 4. **Access the Application** - Once the container is running, open your web browser and navigate to: - [**http://localhost:3000**](http://localhost:3000) - - You should see the ProdHub application running live! To stop the application, press `Ctrl + C` in your terminal. + Once the container is running, open your browser at [**http://localhost:3000**](http://localhost:3000). + To stop, press `Ctrl + C` in your terminal. +--- **Start your journey with ProdHub today.** diff --git a/package.json b/package.json index e626573..192d97c 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "my-productivity-hub", "version": "0.1.0", "private": true, + "homepage": ".", "dependencies": { "@react-oauth/google": "^0.12.2", "@testing-library/dom": "^10.4.0", @@ -18,6 +19,7 @@ "scripts": { "start": "react-scripts start", "build": "react-scripts build", + "build:extension": "react-scripts build && node -e \"require('fs').copyFileSync('./build/manifest.extension.json', './build/manifest.json')\"", "test": "react-scripts test", "eject": "react-scripts eject" }, diff --git a/prodhub-extension.zip b/prodhub-extension.zip new file mode 100644 index 0000000..e5b4155 Binary files /dev/null and b/prodhub-extension.zip differ diff --git a/public/background.js b/public/background.js new file mode 100644 index 0000000..d7262b8 --- /dev/null +++ b/public/background.js @@ -0,0 +1,3 @@ +chrome.sidePanel + .setPanelBehavior({ openPanelOnActionClick: true }) + .catch((error) => console.error(error)); diff --git a/public/manifest.extension.json b/public/manifest.extension.json new file mode 100644 index 0000000..60bf6da --- /dev/null +++ b/public/manifest.extension.json @@ -0,0 +1,36 @@ +{ + "name": "ProdHub Chrome Extension", + "version": "1.0", + "manifest_version": 3, + "description": "ProdHub – your all-in-one side-panel productivity hub. Manage tasks, habits, goals, and your schedule without ever leaving your current tab.", + "background": { + "service_worker": "background.js" + }, + "permissions": [ + "sidePanel", + "identity" + ], + "oauth2": { + "client_id": "357244560688-4ibqpt07g54ns83qtimb2rtsa3p42uur.apps.googleusercontent.com", + "scopes": [ + "profile", + "email", + "openid" + ] + }, + "host_permissions": [ + "" + ], + "action": { + "default_title": "Open ProdHub Panel", + "default_icon": "logo192.png" + }, + "side_panel": { + "default_path": "index.html" + }, + "icons": { + "128": "logo192.png", + "192": "logo192.png", + "512": "logo512.png" + } +} \ No newline at end of file diff --git a/src/App.js b/src/App.js index 98949f9..0e01799 100644 --- a/src/App.js +++ b/src/App.js @@ -13,6 +13,7 @@ import AllTasksView from './features/tasks/AllTasksView'; import ScheduleView from './features/schedule/ScheduleView'; import HabitTrackerView from './features/habits/HabitTrackerView'; import WeeklyReviewView from './features/review/WeeklyReviewView'; +import { Menu, Book } from 'lucide-react'; function HubApp({ user, handleSignOut }) { const [projects, setProjects] = useState([]); @@ -22,6 +23,7 @@ function HubApp({ user, handleSignOut }) { const [habitEntries, setHabitEntries] = useState([]); const [activeView, setActiveView] = useState('dashboard'); const [selectedProjectId, setSelectedProjectId] = useState(null); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [syncedEvents, setSyncedEvents] = useState([]); const [tokenClient, setTokenClient] = useState(null); @@ -101,18 +103,36 @@ function HubApp({ user, handleSignOut }) { const handleSetView = (view, projectId = null) => { setActiveView(view); setSelectedProjectId(projectId); + setIsSidebarOpen(false); // Auto-close sidebar on mobile }; return ( -
- -
+
+ {/* Mobile Header */} +
+

+ ProdHub +

+ +
+ + {/* Sidebar Container */} +
+ +
+ +
{activeView === 'dashboard' && ( p.id === task.projectId)?.name; - const priorityColor = { High: 'text-red-400', Medium: 'text-yellow-400', Low: 'text-green-400' }; + const priorityColor = { High: 'text-red-400', Medium: 'text-amber-400', Low: 'text-emerald-400' }; + const priorityDot = { High: 'bg-red-400', Medium: 'bg-amber-400', Low: 'bg-emerald-400' }; if (isCompact) { return ( -
- {task.title} -
+
+ + {task.title} +
+ {task.priority &&
} {task.dueDate && {formatDate(new Date(task.dueDate))}} - {projectName && {projectName}} + {projectName && {projectName}}
); @@ -61,69 +71,143 @@ export default function TaskItem({ task, projects = [], isCompact = false }) { title="Delete Task" message={`Are you sure you want to delete this task: "${task.title}"?`} /> -
- -
- {isEditing ? ( -
- setEditTitle(e.target.value)} - className="flex-grow bg-gray-700 border border-gray-600 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500" - autoFocus - /> - - -
- ) : ( -

{task.title}

- )} -
- {task.dueDate && ( -
- {formatDate(new Date(task.dueDate))} -
- )} - {task.priority && ( -
- {task.priority} -
- )} - {projectName && ( -
- {projectName} -
+
+
+ +
+ {isEditing ? ( +
+ + {/* ── MOBILE / extension: stacked layout ── */} +
+ {/* Row 1: Title */} + setEditTitle(e.target.value)} + placeholder="Task title" + className="w-full bg-gray-700/80 border border-gray-600 rounded-xl px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" + autoFocus + /> + {/* Row 2: Priority + Date */} +
+ +
+ + setEditDueDate(e.target.value)} + className="flex-1 min-w-0 bg-transparent text-gray-300 focus:outline-none [color-scheme:dark]" + title="Due date (optional)" + /> + {editDueDate && ( + + )} +
+
+ {/* Row 3: Buttons */} +
+ + +
+
+ + {/* ── DESKTOP (lg+): single inline row ── */} +
+ setEditTitle(e.target.value)} + placeholder="Task title" + className="flex-1 bg-gray-700/80 border border-gray-600 rounded-xl px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" + autoFocus + /> + +
+ + setEditDueDate(e.target.value)} + className="w-28 bg-transparent text-gray-300 focus:outline-none [color-scheme:dark]" + title="Due date (optional)" + /> + {editDueDate && ( + + )} +
+ + +
+ +
+ ) : ( +

{task.title}

)} +
+ {task.dueDate && ( +
+ + {formatDate(new Date(task.dueDate))} +
+ )} + {task.priority && ( +
+
+ {task.priority} +
+ )} + {projectName && ( +
+ + {projectName} +
+ )} +
+
+
+ +
-
-
- -
diff --git a/src/contexts/AuthContext.jsx b/src/contexts/AuthContext.jsx index e94dbd1..08b6a9f 100644 --- a/src/contexts/AuthContext.jsx +++ b/src/contexts/AuthContext.jsx @@ -5,6 +5,7 @@ import { onAuthStateChanged, signInWithEmailAndPassword, signInWithPopup, + signInWithCredential, signOut, } from 'firebase/auth'; import { auth } from '../config/firebase'; @@ -37,8 +38,26 @@ export function AuthProvider({ children }) { const signInWithGoogle = async () => { if (!auth) throw new Error('Auth not configured'); - const provider = new GoogleAuthProvider(); - await signInWithPopup(auth, provider); + + if (window.chrome && window.chrome.identity) { + return new Promise((resolve, reject) => { + window.chrome.identity.getAuthToken({ interactive: true }, async (token) => { + if (window.chrome.runtime.lastError || !token) { + return reject(window.chrome.runtime.lastError || new Error('No OAuth token returned.')); + } + try { + const credential = GoogleAuthProvider.credential(null, token); + await signInWithCredential(auth, credential); + resolve(); + } catch (error) { + reject(error); + } + }); + }); + } else { + const provider = new GoogleAuthProvider(); + await signInWithPopup(auth, provider); + } }; const emailSignIn = async (email, password) => { diff --git a/src/features/dashboard/Dashboard.jsx b/src/features/dashboard/Dashboard.jsx index 8ecf4b1..0c93c1e 100644 --- a/src/features/dashboard/Dashboard.jsx +++ b/src/features/dashboard/Dashboard.jsx @@ -124,59 +124,83 @@ Generate a step-by-step plan.`; error={planningError} retry={handlePlanMyDay} /> -
-
-

Dashboard

+
+ {/* Header */} +
+

Dashboard

+ + {/* Overdue Banner */} {overdueTasks.length > 0 && ( -
-

Overdue Tasks ({overdueTasks.length})

-
+
+

+ + Overdue Tasks ({overdueTasks.length}) +

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

Upcoming Deadlines

+ + {/* Main grid β€” stacked on mobile/panel, side-by-side on desktop */} +
+ {/* Upcoming Deadlines */} +
+

Upcoming Deadlines

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

No upcoming deadlines. Great job!

+

No upcoming deadlines. Great job!

)}
-
-

Projects Overview

-
+ + {/* Projects Overview */} +
+

Projects Overview

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

No projects yet. Add one from the sidebar!

} + {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 index 475ef0c..4258f64 100644 --- a/src/features/dashboard/GoalProgress.jsx +++ b/src/features/dashboard/GoalProgress.jsx @@ -11,29 +11,39 @@ export default function GoalProgress({ goal, projects, onViewChange }) { }, [projects]); return ( -
- {isOpen && ( -
+
{projects.map((p) => ( -
onViewChange('project', p.id)}> -
- {p.name} - {Math.round(p.progress || 0)}% +
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 index 3a1ceb7..f2e8dda 100644 --- a/src/features/habits/HabitTrackerView.jsx +++ b/src/features/habits/HabitTrackerView.jsx @@ -1,5 +1,5 @@ import React, { useMemo, useState } from 'react'; -import { Plus, TrendingUp, Edit2, Trash2 } from 'lucide-react'; +import { Plus, TrendingUp, Edit2, Trash2, Flame } 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'; @@ -56,54 +56,94 @@ export default function HabitTrackerView({ habits, entries }) { }, [habits, entries]); return ( -
-
-

Habit Tracker

-
+
+ {/* ── Header ── */} +
+ {/* Mobile/panel: compact header */} +

Habit Tracker

+
+ {/* ── Add habit form ── */} {isAdding && ( -
+ setNewHabitName(e.target.value)} placeholder="e.g., Read for 30 minutes" - className="flex-grow bg-gray-700 border border-gray-600 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" + className="flex-grow bg-gray-700/80 border border-gray-600/50 rounded-xl px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 md:rounded-md md:bg-gray-700 md:border-gray-600" required + autoFocus /> -
)} + {/* ── Analytics panel ── */} {showAnalytics && ( -
-

Habit Analytics

-
+
+

Habit Analytics

+ {/* Mobile: single column; Desktop: multi-column */} +
{habits.map((habit) => ( e.habitId === habit.id)} /> ))} - {habits.length === 0 &&

No habits to analyze yet.

} + {habits.length === 0 &&

No habits to analyze yet.

}
)} -
+ {/* ── Habit list ── + Mobile / extension panel β†’ compact single-column rows (new design) + Desktop (md+) β†’ old tall multi-column cards + ── */} + + {/* MOBILE layout: compact rows */} +
+ {habits.length === 0 && !isAdding && ( +

No habits defined yet. Add your first one!

+ )} + {habits.map((habit) => ( + e.habitId === habit.id)} + streak={habitStreaks[habit.id] || 0} + /> + ))} +
+ + {/* DESKTOP layout: original tall cards */} +
{habits.length === 0 && !isAdding && (

No habits defined yet. Add your first one!

)} @@ -120,7 +160,8 @@ export default function HabitTrackerView({ habits, entries }) { ); } -function HabitDayCard({ habit, entries, streak }) { +/* ── Compact row card: mobile / extension panel ── */ +function HabitRowCard({ habit, entries, streak }) { const [isEditing, setIsEditing] = useState(false); const [editedName, setEditedName] = useState(habit.name); const [showDeleteModal, setShowDeleteModal] = useState(false); @@ -145,9 +186,8 @@ function HabitDayCard({ habit, entries, streak }) { 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 }); + await updateDoc(doc(db, `artifacts/${appId}/users/${userId}/habits`, habit.id), { name: editedName }); setIsEditing(false); } catch (error) { console.error('Error updating habit:', error); @@ -157,22 +197,112 @@ function HabitDayCard({ habit, entries, streak }) { const handleDeleteHabit = async () => { if (!userId || !db) return; setShowDeleteModal(false); + const q = query(collection(db, `artifacts/${appId}/users/${userId}/habit_entries`), where('habitId', '==', habit.id)); + try { + const snapshot = await getDocs(q); + const batch = writeBatch(db); + snapshot.forEach((d) => batch.delete(d.ref)); + await batch.commit(); + await deleteDoc(doc(db, `artifacts/${appId}/users/${userId}/habits`, habit.id)); + } catch (error) { + console.error('Error deleting habit:', error); + } + }; - const entriesPath = `artifacts/${appId}/users/${userId}/habit_entries`; - const q = query(collection(db, entriesPath), where('habitId', '==', habit.id)); + return ( + <> + setShowDeleteModal(false)} + onConfirm={handleDeleteHabit} + title="Delete Habit" + message={`Are you sure you want to delete the habit "${habit.name}"? All its tracked history will also be removed.`} + /> +
+ {isEditing ? ( +
+ setEditedName(e.target.value)} + className="flex-grow bg-gray-700/80 border border-gray-600 rounded-xl px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50" + autoFocus + /> + + +
+ ) : ( +
+ +
+

{habit.name}

+
0 ? 'text-orange-400' : 'text-gray-600'}`}> + + {streak} day streak +
+
+
+ + +
+
+ )} +
+ + ); +} +/* ── Original tall card: desktop only ── */ +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; + try { + await updateDoc(doc(db, `artifacts/${appId}/users/${userId}/habits`, habit.id), { name: editedName }); + setIsEditing(false); + } catch (error) { + console.error('Error updating habit:', error); + } + }; + + const handleDeleteHabit = async () => { + if (!userId || !db) return; + setShowDeleteModal(false); + const q = query(collection(db, `artifacts/${appId}/users/${userId}/habit_entries`), where('habitId', '==', habit.id)); try { - const querySnapshot = await getDocs(q); + const snapshot = await getDocs(q); const batch = writeBatch(db); - querySnapshot.forEach((docSnap) => { - batch.delete(docSnap.ref); - }); + snapshot.forEach((d) => batch.delete(d.ref)); await batch.commit(); - - const habitRef = doc(db, `artifacts/${appId}/users/${userId}/habits`, habit.id); - await deleteDoc(habitRef); + await deleteDoc(doc(db, `artifacts/${appId}/users/${userId}/habits`, habit.id)); } catch (error) { - console.error('Error deleting habit and its entries:', error); + console.error('Error deleting habit:', error); } }; @@ -196,16 +326,8 @@ function HabitDayCard({ habit, entries, streak }) { autoFocus />
- - + +
) : ( @@ -213,12 +335,8 @@ function HabitDayCard({ habit, entries, streak }) {
{habit.name}
- - + +
0 ? 'text-orange-400' : 'text-gray-500'}`}> @@ -227,13 +345,10 @@ function HabitDayCard({ habit, entries, streak }) {
)} - @@ -242,47 +357,34 @@ function HabitDayCard({ habit, entries, streak }) { ); } +/* ── Calendar (used in analytics β€” shared by both layouts) ── */ 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 goToPreviousMonth = () => setDate((d) => new Date(d.getFullYear(), d.getMonth() - 1, 1)); + const goToNextMonth = () => setDate((d) => new Date(d.getFullYear(), d.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()} + +

+ {habit.name} β€” {date.toLocaleString('default', { month: 'short' })} {date.getFullYear()}

- +
-
- {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((d, i) => ( -
{d}
- ))} +
+ {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((d, i) =>
{d}
)}
-
- {blanks.map((b, i) => ( -
- ))} +
+ {blanks.map((_, i) =>
)} {days.map((d) => { const dateKey = getLocalDateKey(new Date(date.getFullYear(), date.getMonth(), d)); const isCompleted = completedDates.has(dateKey); @@ -290,9 +392,7 @@ function HabitCalendar({ habit, entries }) { return (
{d}
diff --git a/src/features/review/WeeklyReviewView.jsx b/src/features/review/WeeklyReviewView.jsx index a7d6894..47a4637 100644 --- a/src/features/review/WeeklyReviewView.jsx +++ b/src/features/review/WeeklyReviewView.jsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import TaskItem from '../../components/shared/TaskItem'; import { formatDate, getLocalDateKey } from '../../utils/datetime'; +import { CheckCircle2, Percent } from 'lucide-react'; export default function WeeklyReviewView({ tasks, projects, habits, entries }) { const lastWeek = useMemo(() => { @@ -10,7 +11,9 @@ export default function WeeklyReviewView({ tasks, projects, habits, entries }) { return { start, end }; }, []); - const completedTasks = tasks.filter((t) => t.completed && t.completedAt && t.completedAt.toDate() >= lastWeek.start); + const completedTasks = tasks.filter( + (t) => t.completed && t.completedAt && t.completedAt.toDate() >= lastWeek.start + ); const habitConsistency = useMemo(() => { if (habits.length === 0) return 0; @@ -34,33 +37,90 @@ export default function WeeklyReviewView({ tasks, projects, habits, entries }) { }, [habits, entries]); return ( -
-

Weekly Review

-

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

+
+ + {/* Header */} +
+

Weekly Review

+

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

+
-
-
-

Tasks Completed

-

{completedTasks.length}

+ {/* ── MOBILE / extension layout ── */} +
+ {/* Compact 2-col stat cards */} +
+
+ +

{completedTasks.length}

+

Tasks Done

+
+
+ +

{habitConsistency}

+

Habit Rate

+
-
-

Habit Consistency

-

{habitConsistency}%

+ + {/* Habit consistency bar */} +
+
+

Habit Consistency

+ {habitConsistency}% +
+
+
+
+
+ + {/* Completed tasks */} +
+

Completed This Week

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

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

+ )}
-
-

Completed Tasks This Week

- {completedTasks.length > 0 ? ( -
- {completedTasks.map((task) => ( - - ))} + {/* ── DESKTOP layout (original) ── */} +
+ {/* Original large stat cards */} +
+
+

Tasks Completed

+

{completedTasks.length}

- ) : ( -

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

- )} +
+

Habit Consistency

+

{habitConsistency}%

+
+
+ + {/* Completed tasks list */} +
+

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 index 43fb244..fafb343 100644 --- a/src/features/schedule/ScheduleView.jsx +++ b/src/features/schedule/ScheduleView.jsx @@ -1,6 +1,7 @@ import React, { useMemo, useState } from 'react'; import { formatTime } from '../../utils/datetime'; import { fetchCalendarEvents } from '../../services/googleCalendar'; +import { RefreshCw, CalendarDays } from 'lucide-react'; export default function ScheduleView({ projects, tasks, syncedEvents, setSyncedEvents, tokenClient }) { const [isLoading, setIsLoading] = useState(false); @@ -39,6 +40,7 @@ export default function ScheduleView({ projects, tasks, syncedEvents, setSyncedE date: p.deadline ? new Date(p.deadline) : null, type: 'Project Deadline', color: 'bg-purple-500', + dotColor: 'bg-purple-400', })); const taskEvents = tasks @@ -51,7 +53,8 @@ export default function ScheduleView({ projects, tasks, syncedEvents, setSyncedE title: t.title, date: dueDate, type: 'Task Deadline', - color: isOverdue ? 'bg-red-700' : 'bg-green-500', + color: isOverdue ? 'bg-red-700' : 'bg-emerald-500', + dotColor: isOverdue ? 'bg-red-400' : 'bg-emerald-400', isOverdue, }; }); @@ -62,61 +65,58 @@ export default function ScheduleView({ projects, tasks, syncedEvents, setSyncedE }, [projects, tasks, syncedEvents]); return ( -
-
-

Schedule

+
+ {/* Header */} +
+

Schedule

- {error &&
{error}
} -
-

Upcoming Deadlines & Events

-
+ + {error && ( +
{error}
+ )} + + {/* Events list */} +
+

+ + Upcoming Deadlines & Events +

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

{event.title}

-

+ {/* Color bar */} +

+ {/* Info */} +
+

{event.title}

+

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

)) ) : ( -

No scheduled events with deadlines.

+

No scheduled events with deadlines.

)}
diff --git a/src/features/sidebar/Sidebar.jsx b/src/features/sidebar/Sidebar.jsx index f39736d..fbec14b 100644 --- a/src/features/sidebar/Sidebar.jsx +++ b/src/features/sidebar/Sidebar.jsx @@ -56,11 +56,13 @@ export default function Sidebar({ onViewChange, projects, goals, userId, handleS return ( <> setShowGoalModal(false)} userId={userId} /> -