Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2026-04-27 - [Concurrent & Lazy Data Fetching in Detail Pages]
**Learning:** Sequential network requests blocking the initial render path create noticeable UI latency, particularly in detail pages that need both a single entity and a list of related entities (e.g., project details + lists within the project). Further, fetching secondary data that is only needed after specific user interactions (e.g., fetching *all* lists just in case the user wants to add a list) compounds this delay unnecessarily.
**Action:** When a page needs multiple independent pieces of data, always dispatch the network requests concurrently using `Promise.all()`. For secondary data, defer fetching until the user interacts with the feature requiring it (lazy fetching), keeping the initial load fast and lean.
9 changes: 9 additions & 0 deletions evaluate_page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import re

with open("src/app/projects/[id]/page.tsx", "r") as f:
content = f.read()

if "fetchProjectAndLists" not in content:
print("Function not found")
else:
print("Function found")
Comment on lines +1 to +9
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Python script appears to be a one-off verification helper (it just checks for a substring in a TSX file) and introduces an otherwise-unused Python artifact into a Node/Next.js repo (there are no npm scripts or other Python utilities). Recommend removing it from the PR, or moving it under scripts/ and rewriting it in TS/JS (or wiring it into CI) if it’s intended to be kept. Also, re is imported but unused.

Suggested change
import re
with open("src/app/projects/[id]/page.tsx", "r") as f:
content = f.read()
if "fetchProjectAndLists" not in content:
print("Function not found")
else:
print("Function found")
import argparse
import sys
def main() -> int:
parser = argparse.ArgumentParser(
description="Verify that a source file contains a required substring."
)
parser.add_argument(
"file_path",
nargs="?",
default="src/app/projects/[id]/page.tsx",
help="Path to the file to inspect.",
)
parser.add_argument(
"expected_text",
nargs="?",
default="fetchProjectAndLists",
help="Substring that must be present in the file.",
)
args = parser.parse_args()
with open(args.file_path, "r", encoding="utf-8") as f:
content = f.read()
if args.expected_text not in content:
print("Function not found")
return 1
print("Function found")
return 0
if __name__ == "__main__":
sys.exit(main())

Copilot uses AI. Check for mistakes.
80 changes: 49 additions & 31 deletions src/app/projects/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,48 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st
const [editName, setEditName] = useState("");
const [editDescription, setEditDescription] = useState("");
const [showAddList, setShowAddList] = useState(false);
const [hasFetchedAllLists, setHasFetchedAllLists] = useState(false);
const [newListName, setNewListName] = useState("");
const [isCreating, setIsCreating] = useState(false);
const [isShareDialogOpen, setIsShareDialogOpen] = useState(false);

useEffect(() => {
const fetchProjectAndLists = async () => {
try {
setIsLoading(true);

// ⚡ Bolt: Fetch project details and project lists concurrently
const [projectResponse, listsResponse] = await Promise.all([
fetch(`/api/projects/${projectId}`),
fetch(`/api/projects/${projectId}/lists`),
]);

// ⚡ Bolt: Parse JSON concurrently
const [projectResult, listsResult] = await Promise.all([
projectResponse.json(),
listsResponse.json(),
]);

if (!projectResult.success) {
setError(projectResult.error || "Project not found");
return;
}

setProject(projectResult.data);
setEditName(projectResult.data.name);
setEditDescription(projectResult.data.description || "");

if (listsResult.success) {
setLists(listsResult.data);
}
} catch (err) {
console.error("Error fetching project:", err);
setError("Failed to load project");
} finally {
setIsLoading(false);
}
};

if (isLoaded && !isSignedIn) {
router.push(`/sign-in?redirect_url=/projects/${projectId}`);
return;
Expand All @@ -53,43 +90,24 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st
}
}, [isLoaded, isSignedIn, projectId, router]);

const fetchProjectAndLists = async () => {
const fetchAllUserLists = async () => {
try {
setIsLoading(true);

// Fetch project details
const projectResponse = await fetch(`/api/projects/${projectId}`);
const projectResult = await projectResponse.json();

if (!projectResult.success) {
setError(projectResult.error || "Project not found");
return;
}

setProject(projectResult.data);
setEditName(projectResult.data.name);
setEditDescription(projectResult.data.description || "");

// Fetch lists in project
const listsResponse = await fetch(`/api/projects/${projectId}/lists`);
const listsResult = await listsResponse.json();

if (listsResult.success) {
setLists(listsResult.data);
}

// Fetch all user lists (for adding to project)
const allListsResponse = await fetch("/api/lists");
const allListsResult = await allListsResponse.json();

if (allListsResult.success) {
setAllLists(allListsResult.data);
setHasFetchedAllLists(true);
}
} catch (err) {
console.error("Error fetching project:", err);
setError("Failed to load project");
} finally {
setIsLoading(false);
console.error("Error fetching all user lists:", err);
}
Comment on lines 99 to +105
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchAllUserLists swallows failures (only logs to console). If /api/lists fails or returns non-success, the “Add existing list” section will silently stay empty with no user feedback. Consider setting error (or a dedicated allListsError / loading state for the add-list panel) when the request fails or when allListsResult.success is false so the UI can explain what happened.

Copilot uses AI. Check for mistakes.
};

const handleToggleAddList = () => {
setShowAddList(!showAddList);
if (!showAddList && !hasFetchedAllLists) {
fetchAllUserLists();
}
};

Expand Down Expand Up @@ -321,7 +339,7 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st
<WikiButton onClick={() => setIsShareDialogOpen(true)}>
Share
</WikiButton>
<WikiButton variant="primary" onClick={() => setShowAddList(!showAddList)}>
<WikiButton variant="primary" onClick={handleToggleAddList}>
{showAddList ? "Cancel" : "Add List"}
</WikiButton>
</div>
Expand Down Expand Up @@ -400,7 +418,7 @@ export default function ProjectDetailPage({ params }: { params: Promise<{ id: st
<p className="text-wiki-text-muted mb-4">
This project has no lists yet.
</p>
<WikiButton variant="primary" onClick={() => setShowAddList(true)}>
<WikiButton variant="primary" onClick={() => { setShowAddList(true); if (!hasFetchedAllLists) fetchAllUserLists(); }}>
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This inline click handler duplicates the “open add list + lazy fetch all lists” behavior implemented in handleToggleAddList, which increases the chance of the two code paths drifting (and makes it harder to add loading/error UI for the add-list panel). Consider reusing the shared handler or extracting a dedicated handleOpenAddList helper used by both buttons.

Suggested change
<WikiButton variant="primary" onClick={() => { setShowAddList(true); if (!hasFetchedAllLists) fetchAllUserLists(); }}>
<WikiButton
variant="primary"
onClick={() => {
if (!showAddList) {
handleToggleAddList();
}
}}
>

Copilot uses AI. Check for mistakes.
Add Your First List
</WikiButton>
</div>
Expand Down
Loading