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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 62 additions & 14 deletions frontend/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions frontend/src/components/Routine/RoutineCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { MoreVertical, Trash2, Share2, Copy, Download, Loader2 } from "lucide-react";
import RoutineOverviewModal from "./RoutineOverviewModal";
import api from "../../api/axios.js";
import { invalidate } from "../../utils/apiCache";
import { exportRoutineToPDF, generateRoutineSummary } from "../../utils/routineExport.js";


Expand Down Expand Up @@ -265,6 +266,8 @@ export default function RoutineCard({
`/routines/${routine._id}`
);

invalidate("/routines");

if (isRoutineStarted) {

handleStopRoutine();
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/context/AuthContext.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createContext, useEffect, useState } from "react";
import api from "../api/axios";
import { clearCache } from "../utils/apiCache";

// create context component
// eslint-disable-next-line react-refresh/only-export-components
Expand All @@ -19,7 +20,7 @@ const AuthProvider = ({ children }) => {
}
setUser(null);
localStorage.removeItem("activeRoutineTasks"); // specifically requested in issue #882

clearCache(); // drop cached API data so the next user starts clean
};

// restore session on app load
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/hooks/useDebounce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useEffect, useState } from "react";

/**
* Returns a debounced copy of `value` that only updates after `delay` ms have
* passed without a change. Used to avoid recomputing/refetching on every
* keystroke (issue #11).
*
* @param {*} value value to debounce
* @param {number} delay debounce delay in ms (default 300)
*/
const useDebounce = (value, delay = 300) => {
const [debounced, setDebounced] = useState(value);

useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);

return debounced;
};

export default useDebounce;
18 changes: 17 additions & 1 deletion frontend/src/hooks/useTasks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { useCallback, useEffect, useState } from "react";
import api from "../api/axios";
import { cachedGet, invalidate } from "../utils/apiCache";

// Mutations to tasks can change analytics-derived data too, so invalidate both.
const invalidateTasks = () => {
invalidate("/tasks");
invalidate("/analytics");
};

const DEFAULT_PAGE = 1;
const DEFAULT_LIMIT = 100;
Expand All @@ -23,7 +30,7 @@ const useTasks = ({
async (pageToFetch = page) => {
try {
setLoading(true);
const response = await api.get("/tasks", {
const response = await cachedGet("/tasks", {
params: {
page: pageToFetch,
limit: initialLimit,
Expand Down Expand Up @@ -61,6 +68,8 @@ const useTasks = ({

console.log("Task added:", response.data);

invalidateTasks();

if (page === DEFAULT_PAGE) {
await getTasks(DEFAULT_PAGE);
} else {
Expand Down Expand Up @@ -102,9 +111,11 @@ const useTasks = ({

try {
await api.put(`/tasks/${id}`, updates);
invalidateTasks();
await getTasks(page);
} catch (error) {
console.log(error?.response?.data?.message || "Failed to update task");
invalidateTasks();
await getTasks(page);
}
};
Expand All @@ -113,18 +124,23 @@ const useTasks = ({
const deleteTask = async (id) => {
await api.delete(`/tasks/${id}`);
setTasks((prev) => prev.filter((t) => t._id !== id));
invalidateTasks();
await getTasks(page);
};

// bulk delete tasks
const bulkDelete = async (ids) => {
await api.post("/tasks/bulk-delete", { ids });
// bulk delete also pulls tasks out of routines on the backend
invalidateTasks();
invalidate("/routines");
await getTasks(page);
};

// bulk edit tasks
const bulkUpdate = async (ids, updates) => {
await Promise.all(ids.map((id) => api.put(`/tasks/${id}`, updates)));
invalidateTasks();
await getTasks(page);
};

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/Analytics.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
Clock,
Briefcase
} from "lucide-react";
import api from "../api/axios";
import { cachedGet } from "../utils/apiCache";
import html2canvas from "html2canvas";

export default function Analytics() {
Expand All @@ -28,7 +28,7 @@ export default function Analytics() {
const fetchAnalytics = async () => {
try {
setLoading(true);
const res = await api.get("/analytics");
const res = await cachedGet("/analytics");
if (res.data.success) {
setStats(res.data.stats);
} else {
Expand Down
25 changes: 17 additions & 8 deletions frontend/src/pages/DailyJournal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
AlertTriangle
} from "lucide-react";
import api from "../api/axios.js";
import { cachedGet, invalidate } from "../utils/apiCache";
import useDebounce from "../hooks/useDebounce";
import LoadingSpinner from "../components/common/LoadingSpinner";

const MOODS = [
Expand Down Expand Up @@ -56,6 +58,8 @@ export default function DailyJournal() {

// Filters
const [searchQuery, setSearchQuery] = useState("");
// Server-side search fires per keystroke β€” debounce the value the request reads (#11)
const debouncedSearchQuery = useDebounce(searchQuery, 400);
const [moodFilter, setMoodFilter] = useState("");
const [tagFilter, setTagFilter] = useState("");
const [startDate, setStartDate] = useState("");
Expand All @@ -82,9 +86,9 @@ export default function DailyJournal() {
const fetchJournals = useCallback(async () => {
try {
setLoadingList(true);
const res = await api.get("/journal", {
const res = await cachedGet("/journal", {
params: {
search: searchQuery,
search: debouncedSearchQuery,
mood: moodFilter,
tag: tagFilter,
startDate,
Expand All @@ -99,7 +103,7 @@ export default function DailyJournal() {
} finally {
setLoadingList(false);
}
}, [searchQuery, moodFilter, tagFilter, startDate, endDate]);
}, [debouncedSearchQuery, moodFilter, tagFilter, startDate, endDate]);

// Fetch journals on query change
useEffect(() => {
Expand All @@ -112,7 +116,7 @@ export default function DailyJournal() {
try {
setErrorMsg("");
setSuccessMsg("");
const res = await api.get(`/journal/by-date/${selectedDate}`);
const res = await cachedGet(`/journal/by-date/${selectedDate}`);
if (res.data.success && res.data.journal) {
const entry = res.data.journal;
setTitle(entry.title || "");
Expand Down Expand Up @@ -141,7 +145,7 @@ export default function DailyJournal() {
const fetchAnalytics = async () => {
try {
setLoadingAnalytics(true);
const res = await api.get("/journal/analytics");
const res = await cachedGet("/journal/analytics");
if (res.data.success) {
setAnalytics(res.data.analytics);
}
Expand Down Expand Up @@ -187,7 +191,10 @@ export default function DailyJournal() {
setContent(savedJournal.content || "");
setMood(savedJournal.mood || "neutral");
setTags(savedJournal.tags || []);


// Entry changed β€” drop cached journal/analytics reads so they refetch fresh
invalidate("/journal");
invalidate("/analytics");
fetchJournals();
setTimeout(() => setSuccessMsg(""), 3000);
}
Expand Down Expand Up @@ -216,6 +223,8 @@ export default function DailyJournal() {
setTags([]);
setActiveEntryId(null);
setIsEditing(false);
invalidate("/journal");
invalidate("/analytics");
fetchJournals();
setTimeout(() => setSuccessMsg(""), 3000);
}
Expand Down Expand Up @@ -281,7 +290,7 @@ export default function DailyJournal() {
const handleEditExisting = async (date) => {
setSelectedDate(date);
try {
const res = await api.get(`/journal/by-date/${date}`);
const res = await cachedGet(`/journal/by-date/${date}`);
if (res.data.success && res.data.journal) {
const entry = res.data.journal;
setTitle(entry.title || "");
Expand Down Expand Up @@ -338,7 +347,7 @@ export default function DailyJournal() {
// Open editor with current loaded entry values
const handleStartEdit = async () => {
try {
const res = await api.get(`/journal/by-date/${selectedDate}`);
const res = await cachedGet(`/journal/by-date/${selectedDate}`);
if (res.data.success && res.data.journal) {
const entry = res.data.journal;
setTitle(entry.title || "");
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/pages/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import DashboardTasks from "../components/Dashboard/DashboardTasks";
import ReflectionSummary from "../components/Dashboard/ReflectionSummary";
import ContributionHeatmap from "../components/Dashboard/ContributionHeatmap";
import api from "../api/axios.js";
import { cachedGet, invalidate } from "../utils/apiCache";
import useTasks from "../hooks/useTasks.js";
import useMixedTasks from "../hooks/useMixedTasks.js";
import { getGreeting } from "../utils/getGreeting";
Expand Down Expand Up @@ -132,7 +133,7 @@ export default function Dashboard() {
const fetchRoutines = async () => {
try {
setLoadingRoutines(true);
const res = await api.get("/routines");
const res = await cachedGet("/routines");
setSavedRoutines(res.data.routines || []);
} catch (err) {
console.error(err);
Expand All @@ -145,7 +146,7 @@ export default function Dashboard() {
const fetchTodayJournal = async () => {
try {
const todayStr = new Date().toLocaleDateString("en-CA");
const res = await api.get(`/journal/by-date/${todayStr}`);
const res = await cachedGet(`/journal/by-date/${todayStr}`);
if (res.data.success && res.data.journal) {
setTodayJournal(res.data.journal);
} else {
Expand Down Expand Up @@ -189,6 +190,8 @@ export default function Dashboard() {

const duplicatedRoutine = res.data.routine || res.data.routines?.[0];

invalidate("/routines");

// Optimistic UI update
if (duplicatedRoutine) {
setSavedRoutines((prevRoutines) => [
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/pages/ForgeMode.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Settings, Music, Moon, AlertCircle, RefreshCw, Layers
} from "lucide-react";
import api from "../api/axios";
import { cachedGet, invalidate } from "../utils/apiCache";

// Available copyright-free ambient soundscapes
const SOUNDSCAPES = [
Expand Down Expand Up @@ -52,12 +53,12 @@ export default function ForgeMode() {
setIsLoading(true);
try {
// Fetch user tasks library
const tasksRes = await api.get("/tasks");
const tasksRes = await cachedGet("/tasks");
const fetchedTasks = tasksRes.data.tasks || [];
setTasks(fetchedTasks);

// Fetch routines to search for currently active routine task
const routinesRes = await api.get("/routines");
const routinesRes = await cachedGet("/routines");
const fetchedRoutines = routinesRes.data.routines || [];

// Auto-load scheduled task if active routine exists
Expand Down Expand Up @@ -237,6 +238,9 @@ export default function ForgeMode() {
actualDuration: elapsedMinutes
});
}
// Completing a task here changes the task list + analytics elsewhere
invalidate("/tasks");
invalidate("/analytics");
} catch (err) {
console.error("Failed to log focus task success to MERN backend:", err);
}
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/pages/RoutineBuilder.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useNavigate } from "react-router-dom";
import { ArrowLeft, Download, Loader2 } from "lucide-react";
import { toPng } from "html-to-image";
import api from "../api/axios.js";
import { cachedGet, invalidate } from "../utils/apiCache";
import EmptyState from "../components/EmptyState";
import { useScrollThenOpen } from "../hooks/useScrollThenOpen.js";
import { routineTemplates } from '../utils/routineTemplate';
Expand Down Expand Up @@ -120,7 +121,7 @@ export default function RoutineBuilder() {
const fetchRoutines = async () => {
try {
setLoadingRoutines(true);
const res = await api.get("/routines");
const res = await cachedGet("/routines");
setSavedRoutines(
Array.isArray(res.data.routines) ? res.data.routines : []
);
Expand Down Expand Up @@ -151,6 +152,7 @@ export default function RoutineBuilder() {
});

const createdRoutine = res.data.routine || res.data.routines?.[0];
invalidate("/routines");
if (createdRoutine) {
setSavedRoutines((prevRoutines) => [
createdRoutine,
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/pages/Tasks.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import useTasks from "../hooks/useTasks";
import useDebounce from "../hooks/useDebounce";
import TaskItem from "../components/Task/TaskItem";
import TaskFormModal from "../components/Task/TaskFormModal";
import KanbanBoard from "../components/Task/KanbanBoard";
Expand Down Expand Up @@ -44,6 +45,9 @@ export default function Tasks() {
const [taskError, setTaskError] = useState("");
const [selectedCategories, setSelectedCategories] = useState([]);
const [searchTerm, setSearchTerm] = useState("");
// Filtering is client-side, so debounce the term the filter reads off of β€”
// the input stays instant, but we avoid re-filtering on every keystroke (#11).
const debouncedSearchTerm = useDebounce(searchTerm, 300);
const [statusFilter, setStatusFilter] = useState("all");
const [selectedIds, setSelectedIds] = useState([]);
const [isNotesOpen, setIsNotesOpen] = useState(false);
Expand Down Expand Up @@ -149,7 +153,7 @@ export default function Tasks() {
};

const filteredTasks = useMemo(() => {
const normalizedSearchTerm = searchTerm.trim().toLowerCase();
const normalizedSearchTerm = debouncedSearchTerm.trim().toLowerCase();

return tasks.filter((task) => {
const title = String(task.title ?? "").toLowerCase();
Expand All @@ -171,7 +175,7 @@ export default function Tasks() {

return matchesSearch && matchesCategory && matchesStatus;
});
}, [searchTerm, selectedCategories, statusFilter, tasks]);
}, [debouncedSearchTerm, selectedCategories, statusFilter, tasks]);

const totalPages = pagination.totalPages;
const hasPreviousPage = page > 1;
Expand Down
Loading