From 5eed28aa9c094cbba066cd3e2dee0130433e2634 Mon Sep 17 00:00:00 2001 From: Christopher Lates Date: Fri, 19 Dec 2025 22:13:17 -0500 Subject: [PATCH 1/8] :untrack next-env.d.ts --- comments.json | 1175 ------------------------------------------------- next-env.d.ts | 6 - 2 files changed, 1181 deletions(-) delete mode 100644 comments.json delete mode 100755 next-env.d.ts diff --git a/comments.json b/comments.json deleted file mode 100644 index bfe4406..0000000 --- a/comments.json +++ /dev/null @@ -1,1175 +0,0 @@ -[ - { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772622", - "pull_request_review_id": 3596999137, - "id": 2633772622, - "node_id": "PRRC_kwDONLllc86c_C5O", - "diff_hunk": "@@ -1,8 +1,46 @@\n import { Hero } from \"@/components/hero\"\n import { HowItWorks } from \"@/components/how-it-works\"\n-import { HomeMapWrapper } from \"@/components/home-map-wrapper\"\n+import { adminSupabase } from \"@/lib/supabase-admin\"\n+import { SightingWithBook } from \"@/components/ledger-list\"\n+import { SightingsFeed } from \"@/components/sightings-feed\"\n+\n+export const dynamic = \"force-dynamic\"\n+\n+export default async function Home() {\n+ const { data: sightingsData, error } = await adminSupabase\n+ .from(\"sightings\")\n+ .select(\"*, book:books(*)\")\n+ .order(\"created_at\", { ascending: false })\n+ .limit(25)\n+\n+ if (error) {\n+ console.error(\"Error fetching sightings:\", error)", - "path": "app/page.tsx", - "commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "original_commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "user": { - "login": "Copilot", - "id": 175728472, - "node_id": "BOT_kgDOCnlnWA", - "avatar_url": "https://avatars.githubusercontent.com/in/946600?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Copilot", - "html_url": "https://github.com/apps/copilot-pull-request-reviewer", - "followers_url": "https://api.github.com/users/Copilot/followers", - "following_url": "https://api.github.com/users/Copilot/following{/other_user}", - "gists_url": "https://api.github.com/users/Copilot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Copilot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Copilot/subscriptions", - "organizations_url": "https://api.github.com/users/Copilot/orgs", - "repos_url": "https://api.github.com/users/Copilot/repos", - "events_url": "https://api.github.com/users/Copilot/events{/privacy}", - "received_events_url": "https://api.github.com/users/Copilot/received_events", - "type": "Bot", - "user_view_type": "public", - "site_admin": false - }, - "body": "Silent error handling: When an error occurs fetching sightings, it's logged to console but the page continues to render with an empty array. Users won't see any indication that data failed to load. Consider displaying an error message or error state to inform users when data fetching fails.\n```suggestion\n console.error(\"Error fetching sightings:\", error)\n return (\n <>\n \n
\n \n
\n

\n Error loading recent sightings\n

\n

\n We're having trouble fetching the latest sightings right now. Please try again\n in a few moments.\n

\n
\n \n
\n \n )\n```", - "created_at": "2025-12-19T06:00:36Z", - "updated_at": "2025-12-19T06:00:42Z", - "html_url": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772622", - "pull_request_url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25", - "_links": { - "self": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772622" - }, - "html": { - "href": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772622" - }, - "pull_request": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25" - } - }, - "reactions": { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772622/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": null, - "original_start_line": null, - "start_side": null, - "line": 17, - "original_line": 17, - "side": "RIGHT", - "author_association": "CONTRIBUTOR", - "original_position": 18, - "position": 18, - "subject_type": "line" - }, - { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772644", - "pull_request_review_id": 3596999137, - "id": 2633772644, - "node_id": "PRRC_kwDONLllc86c_C5k", - "diff_hunk": "@@ -0,0 +1,43 @@\n+\"use client\"\n+\n+import { useState, useEffect } from \"react\"\n+import { SightingWithBook, LedgerList } from \"@/components/ledger-list\"\n+import { HomeMapWrapper } from \"@/components/home-map-wrapper\"\n+\n+interface SightingsFeedProps {\n+ initialSightings: SightingWithBook[]\n+}\n+\n+export function SightingsFeed({ initialSightings }: SightingsFeedProps) {\n+ const [displayedSightings, setDisplayedSightings] = useState([])\n+ const [currentIndex, setCurrentIndex] = useState(0)\n+\n+ useEffect(() => {\n+ // If we've shown all sightings, stop.\n+ if (currentIndex >= initialSightings.length) return\n+\n+ const timer = setInterval(() => {\n+ setDisplayedSightings((prev) => {\n+ const nextItem = initialSightings[currentIndex]\n+ // Prepend new item to appear at the top/newest\n+ return [nextItem, ...prev]\n+ })\n+ setCurrentIndex((prev) => prev + 1)\n+ }, 5000)", - "path": "components/sightings-feed.tsx", - "commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "original_commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "user": { - "login": "Copilot", - "id": 175728472, - "node_id": "BOT_kgDOCnlnWA", - "avatar_url": "https://avatars.githubusercontent.com/in/946600?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Copilot", - "html_url": "https://github.com/apps/copilot-pull-request-reviewer", - "followers_url": "https://api.github.com/users/Copilot/followers", - "following_url": "https://api.github.com/users/Copilot/following{/other_user}", - "gists_url": "https://api.github.com/users/Copilot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Copilot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Copilot/subscriptions", - "organizations_url": "https://api.github.com/users/Copilot/orgs", - "repos_url": "https://api.github.com/users/Copilot/repos", - "events_url": "https://api.github.com/users/Copilot/events{/privacy}", - "received_events_url": "https://api.github.com/users/Copilot/received_events", - "type": "Bot", - "user_view_type": "public", - "site_admin": false - }, - "body": "Magic number without explanation: The timer interval of 5000ms (5 seconds) is hardcoded without explanation. Consider extracting this to a named constant (e.g., `SIGHTING_DISPLAY_INTERVAL_MS`) to improve code readability and make it easier to adjust this timing in the future.", - "created_at": "2025-12-19T06:00:36Z", - "updated_at": "2025-12-19T06:00:42Z", - "html_url": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772644", - "pull_request_url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25", - "_links": { - "self": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772644" - }, - "html": { - "href": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772644" - }, - "pull_request": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25" - } - }, - "reactions": { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772644/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": null, - "original_start_line": null, - "start_side": null, - "line": 26, - "original_line": 26, - "side": "RIGHT", - "author_association": "CONTRIBUTOR", - "original_position": 26, - "position": 26, - "subject_type": "line" - }, - { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772656", - "pull_request_review_id": 3596999137, - "id": 2633772656, - "node_id": "PRRC_kwDONLllc86c_C5w", - "diff_hunk": "@@ -0,0 +1,35 @@\n+const { createClient } = require(\"@supabase/supabase-js\")\n+\n+const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.DB_KEY, {\n+ auth: {\n+ persistSession: false,\n+ autoRefreshToken: false,\n+ },\n+})", - "path": "scripts/verify-sightings.js", - "commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "original_commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "user": { - "login": "Copilot", - "id": 175728472, - "node_id": "BOT_kgDOCnlnWA", - "avatar_url": "https://avatars.githubusercontent.com/in/946600?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Copilot", - "html_url": "https://github.com/apps/copilot-pull-request-reviewer", - "followers_url": "https://api.github.com/users/Copilot/followers", - "following_url": "https://api.github.com/users/Copilot/following{/other_user}", - "gists_url": "https://api.github.com/users/Copilot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Copilot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Copilot/subscriptions", - "organizations_url": "https://api.github.com/users/Copilot/orgs", - "repos_url": "https://api.github.com/users/Copilot/repos", - "events_url": "https://api.github.com/users/Copilot/events{/privacy}", - "received_events_url": "https://api.github.com/users/Copilot/received_events", - "type": "Bot", - "user_view_type": "public", - "site_admin": false - }, - "body": "Inconsistent environment variable naming: The script uses `process.env.DB_KEY` on line 3, but based on other files in the codebase (like lib/supabase-admin.ts), this should likely be a service role key or admin key. The variable name `DB_KEY` is ambiguous - it's unclear whether this is a database connection string, an API key, or a service role key. Consider using a more descriptive name like `SUPABASE_SERVICE_ROLE_KEY` for clarity.\n```suggestion\nconst supabase = createClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL,\n process.env.SUPABASE_SERVICE_ROLE_KEY,\n {\n auth: {\n persistSession: false,\n autoRefreshToken: false,\n },\n }\n)\n```", - "created_at": "2025-12-19T06:00:37Z", - "updated_at": "2025-12-19T06:00:42Z", - "html_url": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772656", - "pull_request_url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25", - "_links": { - "self": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772656" - }, - "html": { - "href": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772656" - }, - "pull_request": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25" - } - }, - "reactions": { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772656/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": 3, - "original_start_line": 3, - "start_side": "RIGHT", - "line": 8, - "original_line": 8, - "side": "RIGHT", - "author_association": "CONTRIBUTOR", - "original_position": 8, - "position": 8, - "subject_type": "line" - }, - { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772664", - "pull_request_review_id": 3596999137, - "id": 2633772664, - "node_id": "PRRC_kwDONLllc86c_C54", - "diff_hunk": "@@ -0,0 +1,89 @@\n+\"use client\"\n+\n+import { ScrollArea } from \"@/components/ui/scroll-area\"\n+import { Sighting } from \"@/lib/types\" // Assuming we can use or extend this\n+import Image from \"next/image\"\n+\n+// We need an extended type because the basic Sighting type might not have the joined book data.\n+// Based on the query: .select('*, books(*)') -> returns 'books' property or .select('*, book:books(*)') returns 'book'\n+// The user request example had .select('*, books(*)')\n+// I'll assume we map it to a comfortable structure or use the raw result.\n+// Let's define the prop type clearly.\n+\n+export type SightingWithBook = Sighting & {\n+ book: {\n+ title: string\n+ author: string\n+ cover_url?: string\n+ } | null\n+}\n+// Note: Supabase join via books(*) usually returns an object if it's many-to-one, or array if one-to-many.\n+// sightings.book_id -> books.id. This is Many-to-One (Sighting belongs to Book).\n+// So 'books' should be a single object (or null).\n+\n+interface LedgerListProps {\n+ sightings: SightingWithBook[]\n+}\n+\n+export function LedgerList({ sightings }: LedgerListProps) {\n+ // Logic lifted to parent SightingsFeed.\n+ // Rendering the passed prop directly.\n+\n+ return (\n+
\n+
\n+

Recent Sightings

\n+

Live activity from the community

\n+
\n+ \n+
\n+ {sightings.map((sighting) => {\n+ // Handle case where book might is missing or joined differently, but typing helps.\n+ const book = sighting.book\n+ if (!book) return null\n+\n+ return (\n+ \n+
\n+ \n+
\n+
\n+ \n+ {sighting.book?.title}\n+ \n+ \n+ {sighting.book?.author}", - "path": "components/ledger-list.tsx", - "commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "original_commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "user": { - "login": "Copilot", - "id": 175728472, - "node_id": "BOT_kgDOCnlnWA", - "avatar_url": "https://avatars.githubusercontent.com/in/946600?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Copilot", - "html_url": "https://github.com/apps/copilot-pull-request-reviewer", - "followers_url": "https://api.github.com/users/Copilot/followers", - "following_url": "https://api.github.com/users/Copilot/following{/other_user}", - "gists_url": "https://api.github.com/users/Copilot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Copilot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Copilot/subscriptions", - "organizations_url": "https://api.github.com/users/Copilot/orgs", - "repos_url": "https://api.github.com/users/Copilot/repos", - "events_url": "https://api.github.com/users/Copilot/events{/privacy}", - "received_events_url": "https://api.github.com/users/Copilot/received_events", - "type": "Bot", - "user_view_type": "public", - "site_admin": false - }, - "body": "Redundant optional chaining: `sighting.book` is already accessed and null-checked on line 42-43 where we early return if it's falsy. Using `sighting.book?.title` here is redundant since we know `book` exists at this point. The same issue exists for accessing the author on line 64. Consider using `book.title` and `book.author` directly since the variable is already available and validated.\n```suggestion\n {book.title}\n \n \n {book.author}\n```", - "created_at": "2025-12-19T06:00:37Z", - "updated_at": "2025-12-19T06:00:42Z", - "html_url": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772664", - "pull_request_url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25", - "_links": { - "self": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772664" - }, - "html": { - "href": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772664" - }, - "pull_request": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25" - } - }, - "reactions": { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772664/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": 61, - "original_start_line": 61, - "start_side": "RIGHT", - "line": 64, - "original_line": 64, - "side": "RIGHT", - "author_association": "CONTRIBUTOR", - "original_position": 64, - "position": 64, - "subject_type": "line" - }, - { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772676", - "pull_request_review_id": 3596999137, - "id": 2633772676, - "node_id": "PRRC_kwDONLllc86c_C6E", - "diff_hunk": "@@ -0,0 +1,43 @@\n+\"use client\"\n+\n+import { useState, useEffect } from \"react\"\n+import { SightingWithBook, LedgerList } from \"@/components/ledger-list\"\n+import { HomeMapWrapper } from \"@/components/home-map-wrapper\"\n+\n+interface SightingsFeedProps {\n+ initialSightings: SightingWithBook[]\n+}\n+\n+export function SightingsFeed({ initialSightings }: SightingsFeedProps) {\n+ const [displayedSightings, setDisplayedSightings] = useState([])\n+ const [currentIndex, setCurrentIndex] = useState(0)\n+\n+ useEffect(() => {\n+ // If we've shown all sightings, stop.\n+ if (currentIndex >= initialSightings.length) return\n+\n+ const timer = setInterval(() => {\n+ setDisplayedSightings((prev) => {\n+ const nextItem = initialSightings[currentIndex]\n+ // Prepend new item to appear at the top/newest\n+ return [nextItem, ...prev]\n+ })\n+ setCurrentIndex((prev) => prev + 1)", - "path": "components/sightings-feed.tsx", - "commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "original_commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "user": { - "login": "Copilot", - "id": 175728472, - "node_id": "BOT_kgDOCnlnWA", - "avatar_url": "https://avatars.githubusercontent.com/in/946600?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Copilot", - "html_url": "https://github.com/apps/copilot-pull-request-reviewer", - "followers_url": "https://api.github.com/users/Copilot/followers", - "following_url": "https://api.github.com/users/Copilot/following{/other_user}", - "gists_url": "https://api.github.com/users/Copilot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Copilot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Copilot/subscriptions", - "organizations_url": "https://api.github.com/users/Copilot/orgs", - "repos_url": "https://api.github.com/users/Copilot/repos", - "events_url": "https://api.github.com/users/Copilot/events{/privacy}", - "received_events_url": "https://api.github.com/users/Copilot/received_events", - "type": "Bot", - "user_view_type": "public", - "site_admin": false - }, - "body": "Accessing array element without bounds checking: When `currentIndex` equals `initialSightings.length - 1` and the timer fires, incrementing currentIndex to `initialSightings.length` will cause `initialSightings[currentIndex]` on line 21 to be undefined. This undefined value will be prepended to the displayed sightings array, potentially causing rendering issues.\n```suggestion\n setCurrentIndex((prevIndex) => {\n // If we've reached or passed the end, clear the timer and stop updating.\n if (prevIndex >= initialSightings.length) {\n clearInterval(timer)\n return prevIndex\n }\n\n // Use the current index value safely within bounds.\n const nextItem = initialSightings[prevIndex]\n setDisplayedSightings((prevSightings) => {\n // Prepend new item to appear at the top/newest\n return [nextItem, ...prevSightings]\n })\n\n // Advance to the next index for the next tick.\n return prevIndex + 1\n })\n```", - "created_at": "2025-12-19T06:00:37Z", - "updated_at": "2025-12-19T06:00:42Z", - "html_url": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772676", - "pull_request_url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25", - "_links": { - "self": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772676" - }, - "html": { - "href": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772676" - }, - "pull_request": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25" - } - }, - "reactions": { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772676/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": 20, - "original_start_line": 20, - "start_side": "RIGHT", - "line": 25, - "original_line": 25, - "side": "RIGHT", - "author_association": "CONTRIBUTOR", - "original_position": 25, - "position": 25, - "subject_type": "line" - }, - { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772682", - "pull_request_review_id": 3596999137, - "id": 2633772682, - "node_id": "PRRC_kwDONLllc86c_C6K", - "diff_hunk": "@@ -1,8 +1,46 @@\n import { Hero } from \"@/components/hero\"\n import { HowItWorks } from \"@/components/how-it-works\"\n-import { HomeMapWrapper } from \"@/components/home-map-wrapper\"\n+import { adminSupabase } from \"@/lib/supabase-admin\"\n+import { SightingWithBook } from \"@/components/ledger-list\"\n+import { SightingsFeed } from \"@/components/sightings-feed\"\n+\n+export const dynamic = \"force-dynamic\"\n+\n+export default async function Home() {\n+ const { data: sightingsData, error } = await adminSupabase\n+ .from(\"sightings\")\n+ .select(\"*, book:books(*)\")\n+ .order(\"created_at\", { ascending: false })\n+ .limit(25)\n+\n+ if (error) {\n+ console.error(\"Error fetching sightings:\", error)\n+ }\n+\n+ // Cast the data to our type, assuming the join worked as expected.\n+ const rawSightings = (sightingsData || []) as unknown as SightingWithBook[]\n+\n+ // Manually fetch user info if user_id is present (since we can't easily join auth.users)\n+ const sightings = await Promise.all(\n+ rawSightings.map(async (sighting) => {\n+ if (sighting.user_id) {\n+ const { data: userData } = await adminSupabase.auth.admin.getUserById(sighting.user_id)\n+ if (userData.user) {\n+ return {\n+ ...sighting,\n+ user: {\n+ email: userData.user.email || \"\",\n+ },\n+ }\n+ }\n+ }\n+ return sighting\n+ })\n+ )", - "path": "app/page.tsx", - "commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "original_commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "user": { - "login": "Copilot", - "id": 175728472, - "node_id": "BOT_kgDOCnlnWA", - "avatar_url": "https://avatars.githubusercontent.com/in/946600?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Copilot", - "html_url": "https://github.com/apps/copilot-pull-request-reviewer", - "followers_url": "https://api.github.com/users/Copilot/followers", - "following_url": "https://api.github.com/users/Copilot/following{/other_user}", - "gists_url": "https://api.github.com/users/Copilot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Copilot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Copilot/subscriptions", - "organizations_url": "https://api.github.com/users/Copilot/orgs", - "repos_url": "https://api.github.com/users/Copilot/repos", - "events_url": "https://api.github.com/users/Copilot/events{/privacy}", - "received_events_url": "https://api.github.com/users/Copilot/received_events", - "type": "Bot", - "user_view_type": "public", - "site_admin": false - }, - "body": "N+1 query pattern: For each sighting, a separate API call is made to fetch user data using `adminSupabase.auth.admin.getUserById()`. With 25 sightings (limit on line 14), this results in up to 25 sequential database calls. This significantly impacts performance. Consider batching user data fetches or restructuring the query to fetch user information more efficiently.\n```suggestion\n // Collect unique user IDs from the sightings so we can fetch user info in a single query.\n const userIds = Array.from(\n new Set(\n rawSightings\n .map((sighting) => sighting.user_id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n )\n )\n\n // Batch-fetch user info from auth.users for all relevant user IDs.\n let usersById = new Map()\n if (userIds.length > 0) {\n const { data: usersData, error: usersError } = await adminSupabase\n .from(\"auth.users\")\n .select(\"id, email\")\n .in(\"id\", userIds)\n\n if (usersError) {\n console.error(\"Error fetching users for sightings:\", usersError)\n } else if (usersData) {\n usersById = new Map(usersData.map((user) => [user.id, user]))\n }\n }\n\n // Attach user info to sightings using the pre-fetched user map.\n const sightings = rawSightings.map((sighting) => {\n if (sighting.user_id) {\n const user = usersById.get(sighting.user_id)\n if (user) {\n return {\n ...sighting,\n user: {\n email: user.email || \"\",\n },\n }\n }\n }\n return sighting\n })\n```", - "created_at": "2025-12-19T06:00:38Z", - "updated_at": "2025-12-19T06:00:43Z", - "html_url": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772682", - "pull_request_url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25", - "_links": { - "self": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772682" - }, - "html": { - "href": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772682" - }, - "pull_request": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25" - } - }, - "reactions": { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772682/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": 23, - "original_start_line": 23, - "start_side": "RIGHT", - "line": 39, - "original_line": 39, - "side": "RIGHT", - "author_association": "CONTRIBUTOR", - "original_position": 40, - "position": 40, - "subject_type": "line" - }, - { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772696", - "pull_request_review_id": 3596999137, - "id": 2633772696, - "node_id": "PRRC_kwDONLllc86c_C6Y", - "diff_hunk": "@@ -0,0 +1,152 @@\n+const { createClient } = require(\"@supabase/supabase-js\")\n+\n+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL\n+const supabaseKey = process.env.DB_KEY\n+\n+if (!supabaseUrl || !supabaseKey) {\n+ console.error(\"Missing Supabase credentials in environment.\")\n+ process.exit(1)\n+}\n+\n+const adminSupabase = createClient(supabaseUrl, supabaseKey, {\n+ auth: {\n+ persistSession: false,\n+ autoRefreshToken: false,\n+ },\n+})\n+\n+const LOCATIONS = [\n+ { name: \"Herndon\", lat: 38.9696, lon: -77.3861 },\n+ { name: \"Reston\", lat: 38.9586, lon: -77.357 },\n+ { name: \"Sterling\", lat: 39.0067, lon: -77.4291 },\n+ { name: \"Ashburn\", lat: 39.0438, lon: -77.4874 },\n+]\n+\n+const SEARCH_TERMS = [\n+ \"bestsellers 2024\",\n+ \"classic novels\",\n+ \"science fiction\",\n+ \"mystery thriller\",\n+ \"fantasy books\",\n+ \"biography\",\n+]\n+\n+function getRandomInt(min, max) {\n+ return Math.floor(Math.random() * (max - min + 1)) + min\n+}\n+\n+function getRandomLocation() {\n+ const loc = LOCATIONS[getRandomInt(0, LOCATIONS.length - 1)]\n+ const latJitter = (Math.random() - 0.5) * 0.04\n+ const lonJitter = (Math.random() - 0.5) * 0.04\n+ return {\n+ lat: loc.lat + latJitter,\n+ lon: loc.lon + lonJitter,\n+ name: loc.name,\n+ }\n+}\n+\n+async function fetchGoogleBooks() {\n+ console.log(\"Fetching books from Google Books API...\")\n+ let allBooks = []\n+\n+ for (const term of SEARCH_TERMS) {\n+ try {\n+ const res = await fetch(\n+ `https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent(term)}&maxResults=10&printType=books&langRestrict=en`\n+ )\n+ if (!res.ok) continue\n+ const data = await res.json()\n+ if (data.items) {\n+ const books = data.items\n+ .map((item) => {\n+ const info = item.volumeInfo\n+ let cover = info.imageLinks?.thumbnail || info.imageLinks?.smallThumbnail\n+ if (cover) cover = cover.replace(\"http:\", \"https:\")\n+\n+ if (!info.title || !info.authors || !cover) return null\n+\n+ return {\n+ title: info.title,\n+ author: info.authors[0],\n+ cover: cover,\n+ }\n+ })\n+ .filter((b) => b !== null)", - "path": "scripts/seed-standalone.js", - "commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "original_commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "user": { - "login": "Copilot", - "id": 175728472, - "node_id": "BOT_kgDOCnlnWA", - "avatar_url": "https://avatars.githubusercontent.com/in/946600?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Copilot", - "html_url": "https://github.com/apps/copilot-pull-request-reviewer", - "followers_url": "https://api.github.com/users/Copilot/followers", - "following_url": "https://api.github.com/users/Copilot/following{/other_user}", - "gists_url": "https://api.github.com/users/Copilot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Copilot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Copilot/subscriptions", - "organizations_url": "https://api.github.com/users/Copilot/orgs", - "repos_url": "https://api.github.com/users/Copilot/repos", - "events_url": "https://api.github.com/users/Copilot/events{/privacy}", - "received_events_url": "https://api.github.com/users/Copilot/received_events", - "type": "Bot", - "user_view_type": "public", - "site_admin": false - }, - "body": "Variable name `b` is not descriptive. In the filter operation on line 75, using a single-letter variable name reduces code readability. Consider using a more descriptive name like `book` to make the filter operation clearer.\n```suggestion\n .filter((book) => book !== null)\n```", - "created_at": "2025-12-19T06:00:38Z", - "updated_at": "2025-12-19T06:00:43Z", - "html_url": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772696", - "pull_request_url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25", - "_links": { - "self": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772696" - }, - "html": { - "href": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772696" - }, - "pull_request": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25" - } - }, - "reactions": { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772696/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": null, - "original_start_line": null, - "start_side": null, - "line": 75, - "original_line": 75, - "side": "RIGHT", - "author_association": "CONTRIBUTOR", - "original_position": 75, - "position": 75, - "subject_type": "line" - }, - { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772708", - "pull_request_review_id": 3596999137, - "id": 2633772708, - "node_id": "PRRC_kwDONLllc86c_C6k", - "diff_hunk": "@@ -0,0 +1,89 @@\n+\"use client\"\n+\n+import { ScrollArea } from \"@/components/ui/scroll-area\"\n+import { Sighting } from \"@/lib/types\" // Assuming we can use or extend this\n+import Image from \"next/image\"\n+\n+// We need an extended type because the basic Sighting type might not have the joined book data.\n+// Based on the query: .select('*, books(*)') -> returns 'books' property or .select('*, book:books(*)') returns 'book'\n+// The user request example had .select('*, books(*)')\n+// I'll assume we map it to a comfortable structure or use the raw result.\n+// Let's define the prop type clearly.\n+\n+export type SightingWithBook = Sighting & {\n+ book: {\n+ title: string\n+ author: string\n+ cover_url?: string\n+ } | null\n+}\n+// Note: Supabase join via books(*) usually returns an object if it's many-to-one, or array if one-to-many.\n+// sightings.book_id -> books.id. This is Many-to-One (Sighting belongs to Book).\n+// So 'books' should be a single object (or null).\n+\n+interface LedgerListProps {\n+ sightings: SightingWithBook[]\n+}\n+\n+export function LedgerList({ sightings }: LedgerListProps) {\n+ // Logic lifted to parent SightingsFeed.\n+ // Rendering the passed prop directly.\n+\n+ return (\n+
\n+
\n+

Recent Sightings

\n+

Live activity from the community

\n+
\n+ \n+
\n+ {sightings.map((sighting) => {\n+ // Handle case where book might is missing or joined differently, but typing helps.\n+ const book = sighting.book\n+ if (!book) return null\n+\n+ return (\n+ \n+
\n+ returns 'books' property or .select('*, book:books(*)') returns 'book'\n+// The user request example had .select('*, books(*)')\n+// I'll assume we map it to a comfortable structure or use the raw result.\n+// Let's define the prop type clearly.\n+\n+export type SightingWithBook = Sighting & {\n+ book: {\n+ title: string\n+ author: string\n+ cover_url?: string\n+ } | null\n+}\n+// Note: Supabase join via books(*) usually returns an object if it's many-to-one, or array if one-to-many.\n+// sightings.book_id -> books.id. This is Many-to-One (Sighting belongs to Book).\n+// So 'books' should be a single object (or null).\n+\n+interface LedgerListProps {\n+ sightings: SightingWithBook[]\n+}\n+\n+export function LedgerList({ sightings }: LedgerListProps) {\n+ // Logic lifted to parent SightingsFeed.\n+ // Rendering the passed prop directly.\n+\n+ return (\n+
\n+
\n+

Recent Sightings

\n+

Live activity from the community

\n+
\n+ \n+
\n+ {sightings.map((sighting) => {\n+ // Handle case where book might is missing or joined differently, but typing helps.\n+ const book = sighting.book\n+ if (!book) return null\n+\n+ return (\n+ returns 'books' property or .select('*, book:books(*)') returns 'book'\n+// The user request example had .select('*, books(*)')\n+// I'll assume we map it to a comfortable structure or use the raw result.\n+// Let's define the prop type clearly.\n+\n+export type SightingWithBook = Sighting & {\n+ book: {\n+ title: string\n+ author: string\n+ cover_url?: string\n+ } | null\n+}\n+// Note: Supabase join via books(*) usually returns an object if it's many-to-one, or array if one-to-many.\n+// sightings.book_id -> books.id. This is Many-to-One (Sighting belongs to Book).\n+// So 'books' should be a single object (or null).\n+\n+interface LedgerListProps {\n+ sightings: SightingWithBook[]\n+}\n+\n+export function LedgerList({ sightings }: LedgerListProps) {\n+ // Logic lifted to parent SightingsFeed.\n+ // Rendering the passed prop directly.\n+\n+ return (\n+
\n+
\n+

Recent Sightings

\n+

Live activity from the community

\n+
\n+ \n+
\n+ {sightings.map((sighting) => {\n+ // Handle case where book might is missing or joined differently, but typing helps.\n+ const book = sighting.book\n+ if (!book) return null\n+\n+ return (\n+ \n+
\n+ \n+
\n+
\n+ \n+ {sighting.book?.title}\n+ \n+ \n+ {sighting.book?.author}", - "path": "components/ledger-list.tsx", - "commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "original_commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "user": { - "login": "Copilot", - "id": 175728472, - "node_id": "BOT_kgDOCnlnWA", - "avatar_url": "https://avatars.githubusercontent.com/in/946600?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Copilot", - "html_url": "https://github.com/apps/copilot-pull-request-reviewer", - "followers_url": "https://api.github.com/users/Copilot/followers", - "following_url": "https://api.github.com/users/Copilot/following{/other_user}", - "gists_url": "https://api.github.com/users/Copilot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Copilot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Copilot/subscriptions", - "organizations_url": "https://api.github.com/users/Copilot/orgs", - "repos_url": "https://api.github.com/users/Copilot/repos", - "events_url": "https://api.github.com/users/Copilot/events{/privacy}", - "received_events_url": "https://api.github.com/users/Copilot/received_events", - "type": "Bot", - "user_view_type": "public", - "site_admin": false - }, - "body": "Redundant optional chaining: `sighting.book` is already accessed and null-checked on line 42-43 where we early return if it's falsy. Using `sighting.book?.author` here is redundant since we know `book` exists at this point. Consider using `book.author` directly since the variable is already available and validated.\n```suggestion\n {book.title}\n \n \n {book.author}\n```", - "created_at": "2025-12-19T06:00:39Z", - "updated_at": "2025-12-19T06:00:43Z", - "html_url": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772739", - "pull_request_url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25", - "_links": { - "self": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772739" - }, - "html": { - "href": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772739" - }, - "pull_request": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25" - } - }, - "reactions": { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772739/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": 61, - "original_start_line": 61, - "start_side": "RIGHT", - "line": 64, - "original_line": 64, - "side": "RIGHT", - "author_association": "CONTRIBUTOR", - "original_position": 64, - "position": 64, - "subject_type": "line" - }, - { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772760", - "pull_request_review_id": 3596999137, - "id": 2633772760, - "node_id": "PRRC_kwDONLllc86c_C7Y", - "diff_hunk": "@@ -1,8 +1,46 @@\n import { Hero } from \"@/components/hero\"\n import { HowItWorks } from \"@/components/how-it-works\"\n-import { HomeMapWrapper } from \"@/components/home-map-wrapper\"\n+import { adminSupabase } from \"@/lib/supabase-admin\"\n+import { SightingWithBook } from \"@/components/ledger-list\"\n+import { SightingsFeed } from \"@/components/sightings-feed\"\n+\n+export const dynamic = \"force-dynamic\"\n+\n+export default async function Home() {\n+ const { data: sightingsData, error } = await adminSupabase\n+ .from(\"sightings\")\n+ .select(\"*, book:books(*)\")\n+ .order(\"created_at\", { ascending: false })\n+ .limit(25)\n+\n+ if (error) {\n+ console.error(\"Error fetching sightings:\", error)\n+ }\n+\n+ // Cast the data to our type, assuming the join worked as expected.\n+ const rawSightings = (sightingsData || []) as unknown as SightingWithBook[]", - "path": "app/page.tsx", - "commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "original_commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "user": { - "login": "Copilot", - "id": 175728472, - "node_id": "BOT_kgDOCnlnWA", - "avatar_url": "https://avatars.githubusercontent.com/in/946600?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Copilot", - "html_url": "https://github.com/apps/copilot-pull-request-reviewer", - "followers_url": "https://api.github.com/users/Copilot/followers", - "following_url": "https://api.github.com/users/Copilot/following{/other_user}", - "gists_url": "https://api.github.com/users/Copilot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Copilot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Copilot/subscriptions", - "organizations_url": "https://api.github.com/users/Copilot/orgs", - "repos_url": "https://api.github.com/users/Copilot/repos", - "events_url": "https://api.github.com/users/Copilot/events{/privacy}", - "received_events_url": "https://api.github.com/users/Copilot/received_events", - "type": "Bot", - "user_view_type": "public", - "site_admin": false - }, - "body": "Unsafe type casting: The cast `as unknown as SightingWithBook[]` bypasses TypeScript's type safety. The query uses `.select(\"*, book:books(*)\")` which should return data where each sighting has a `book` property, but this type assertion doesn't validate that the actual runtime data matches the expected structure. If the join fails or returns unexpected data, this could cause runtime errors in components expecting properly typed data.", - "created_at": "2025-12-19T06:00:39Z", - "updated_at": "2025-12-19T06:00:43Z", - "html_url": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772760", - "pull_request_url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25", - "_links": { - "self": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772760" - }, - "html": { - "href": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772760" - }, - "pull_request": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25" - } - }, - "reactions": { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772760/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": null, - "original_start_line": null, - "start_side": null, - "line": 21, - "original_line": 21, - "side": "RIGHT", - "author_association": "CONTRIBUTOR", - "original_position": 22, - "position": 22, - "subject_type": "line" - }, - { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772769", - "pull_request_review_id": 3596999137, - "id": 2633772769, - "node_id": "PRRC_kwDONLllc86c_C7h", - "diff_hunk": "@@ -0,0 +1,152 @@\n+const { createClient } = require(\"@supabase/supabase-js\")\n+\n+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL\n+const supabaseKey = process.env.DB_KEY\n+\n+if (!supabaseUrl || !supabaseKey) {\n+ console.error(\"Missing Supabase credentials in environment.\")\n+ process.exit(1)\n+}\n+\n+const adminSupabase = createClient(supabaseUrl, supabaseKey, {\n+ auth: {\n+ persistSession: false,\n+ autoRefreshToken: false,\n+ },\n+})\n+\n+const LOCATIONS = [\n+ { name: \"Herndon\", lat: 38.9696, lon: -77.3861 },\n+ { name: \"Reston\", lat: 38.9586, lon: -77.357 },\n+ { name: \"Sterling\", lat: 39.0067, lon: -77.4291 },\n+ { name: \"Ashburn\", lat: 39.0438, lon: -77.4874 },\n+]\n+\n+const SEARCH_TERMS = [\n+ \"bestsellers 2024\",\n+ \"classic novels\",\n+ \"science fiction\",\n+ \"mystery thriller\",\n+ \"fantasy books\",\n+ \"biography\",\n+]\n+\n+function getRandomInt(min, max) {\n+ return Math.floor(Math.random() * (max - min + 1)) + min\n+}\n+\n+function getRandomLocation() {\n+ const loc = LOCATIONS[getRandomInt(0, LOCATIONS.length - 1)]\n+ const latJitter = (Math.random() - 0.5) * 0.04\n+ const lonJitter = (Math.random() - 0.5) * 0.04\n+ return {\n+ lat: loc.lat + latJitter,\n+ lon: loc.lon + lonJitter,\n+ name: loc.name,\n+ }\n+}\n+\n+async function fetchGoogleBooks() {\n+ console.log(\"Fetching books from Google Books API...\")\n+ let allBooks = []\n+\n+ for (const term of SEARCH_TERMS) {\n+ try {\n+ const res = await fetch(\n+ `https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent(term)}&maxResults=10&printType=books&langRestrict=en`\n+ )\n+ if (!res.ok) continue\n+ const data = await res.json()\n+ if (data.items) {\n+ const books = data.items\n+ .map((item) => {\n+ const info = item.volumeInfo\n+ let cover = info.imageLinks?.thumbnail || info.imageLinks?.smallThumbnail\n+ if (cover) cover = cover.replace(\"http:\", \"https:\")\n+\n+ if (!info.title || !info.authors || !cover) return null\n+\n+ return {\n+ title: info.title,\n+ author: info.authors[0],\n+ cover: cover,\n+ }\n+ })\n+ .filter((b) => b !== null)\n+ allBooks = [...allBooks, ...books]\n+ }\n+ } catch (e) {\n+ console.error(`Failed to fetch for ${term}:`, e.message)\n+ }\n+ }\n+\n+ // Shuffle\n+ allBooks.sort(() => Math.random() - 0.5)\n+ // Unique by title to avoid duplicates\n+ const uniqueBooks = Array.from(new Map(allBooks.map((item) => [item.title, item])).values())\n+\n+ console.log(`Fetched ${uniqueBooks.length} unique realistic books.`)\n+ return uniqueBooks\n+}\n+\n+async function seed() {\n+ const freshBooks = await fetchGoogleBooks()\n+\n+ // Fallback if API fails\n+ if (freshBooks.length === 0) {\n+ console.error(\"No books fetched using API. Aborting realistic seed.\")\n+ return\n+ }\n+\n+ // Limit to 30 or available\n+ const booksToInsert = freshBooks.slice(0, 30)\n+ console.log(`Starting seed with ${booksToInsert.length} books...`)\n+\n+ let seededCount = 0\n+\n+ for (const bookMeta of booksToInsert) {\n+ const location = getRandomLocation()\n+ const code = Math.random().toString(36).substring(7).toUpperCase()\n+\n+ // 1. Create Book\n+ const { data: book, error: bookError } = await adminSupabase\n+ .from(\"books\")\n+ .insert({\n+ title: bookMeta.title,\n+ author: bookMeta.author,\n+ cover_url: bookMeta.cover,\n+ code: code,\n+ location: location.name,\n+ lat: location.lat,\n+ lon: location.lon,\n+ })\n+ .select()\n+ .single()\n+\n+ if (bookError) {\n+ console.error(\"Error creating book:\", bookError.message)\n+ continue\n+ }\n+\n+ // 2. Create Sighting\n+ const { error: sightingError } = await adminSupabase.from(\"sightings\").insert({\n+ book_id: book.id,\n+ location: location.name,\n+ lat: location.lat,\n+ lon: location.lon,\n+ sighting_type: \"DROP\",\n+ created_at: new Date(Date.now() - Math.random() * 24 * 60 * 60 * 1000).toISOString(),", - "path": "scripts/seed-standalone.js", - "commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "original_commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "user": { - "login": "Copilot", - "id": 175728472, - "node_id": "BOT_kgDOCnlnWA", - "avatar_url": "https://avatars.githubusercontent.com/in/946600?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Copilot", - "html_url": "https://github.com/apps/copilot-pull-request-reviewer", - "followers_url": "https://api.github.com/users/Copilot/followers", - "following_url": "https://api.github.com/users/Copilot/following{/other_user}", - "gists_url": "https://api.github.com/users/Copilot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Copilot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Copilot/subscriptions", - "organizations_url": "https://api.github.com/users/Copilot/orgs", - "repos_url": "https://api.github.com/users/Copilot/repos", - "events_url": "https://api.github.com/users/Copilot/events{/privacy}", - "received_events_url": "https://api.github.com/users/Copilot/received_events", - "type": "Bot", - "user_view_type": "public", - "site_admin": false - }, - "body": "Random timestamp generation creates dates in the past 24 hours, but the comment on line 27 says \"should be oldest of the batch\" and line 29 says \"should be newest of the batch\". The script creates sightings with random timestamps within 24 hours, which means they won't necessarily be in any predictable order. This inconsistency between the comment expectation and the actual seeding behavior could lead to confusion when debugging.", - "created_at": "2025-12-19T06:00:39Z", - "updated_at": "2025-12-19T06:00:43Z", - "html_url": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772769", - "pull_request_url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25", - "_links": { - "self": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772769" - }, - "html": { - "href": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772769" - }, - "pull_request": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25" - } - }, - "reactions": { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772769/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": null, - "original_start_line": null, - "start_side": null, - "line": 138, - "original_line": 138, - "side": "RIGHT", - "author_association": "CONTRIBUTOR", - "original_position": 138, - "position": 138, - "subject_type": "line" - }, - { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772784", - "pull_request_review_id": 3596999137, - "id": 2633772784, - "node_id": "PRRC_kwDONLllc86c_C7w", - "diff_hunk": "@@ -0,0 +1,43 @@\n+\"use client\"\n+\n+import { useState, useEffect } from \"react\"\n+import { SightingWithBook, LedgerList } from \"@/components/ledger-list\"\n+import { HomeMapWrapper } from \"@/components/home-map-wrapper\"\n+\n+interface SightingsFeedProps {\n+ initialSightings: SightingWithBook[]\n+}\n+\n+export function SightingsFeed({ initialSightings }: SightingsFeedProps) {\n+ const [displayedSightings, setDisplayedSightings] = useState([])\n+ const [currentIndex, setCurrentIndex] = useState(0)\n+\n+ useEffect(() => {\n+ // If we've shown all sightings, stop.\n+ if (currentIndex >= initialSightings.length) return", - "path": "components/sightings-feed.tsx", - "commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "original_commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "user": { - "login": "Copilot", - "id": 175728472, - "node_id": "BOT_kgDOCnlnWA", - "avatar_url": "https://avatars.githubusercontent.com/in/946600?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Copilot", - "html_url": "https://github.com/apps/copilot-pull-request-reviewer", - "followers_url": "https://api.github.com/users/Copilot/followers", - "following_url": "https://api.github.com/users/Copilot/following{/other_user}", - "gists_url": "https://api.github.com/users/Copilot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Copilot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Copilot/subscriptions", - "organizations_url": "https://api.github.com/users/Copilot/orgs", - "repos_url": "https://api.github.com/users/Copilot/repos", - "events_url": "https://api.github.com/users/Copilot/events{/privacy}", - "received_events_url": "https://api.github.com/users/Copilot/received_events", - "type": "Bot", - "user_view_type": "public", - "site_admin": false - }, - "body": "The condition `currentIndex >= initialSightings.length` will prevent the timer from clearing when the last item is processed. The useEffect dependencies include `currentIndex`, so when `currentIndex` becomes equal to `initialSightings.length`, the early return happens before the timer is set up, but any previously set timer continues running. This creates a memory leak. Consider adding a check after incrementing currentIndex to clear the timer, or restructure the logic to ensure cleanup happens properly.", - "created_at": "2025-12-19T06:00:39Z", - "updated_at": "2025-12-19T06:00:43Z", - "html_url": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772784", - "pull_request_url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25", - "_links": { - "self": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772784" - }, - "html": { - "href": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772784" - }, - "pull_request": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25" - } - }, - "reactions": { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772784/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": 15, - "original_start_line": 15, - "start_side": "RIGHT", - "line": 17, - "original_line": 17, - "side": "RIGHT", - "author_association": "CONTRIBUTOR", - "original_position": 17, - "position": 17, - "subject_type": "line" - }, - { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772795", - "pull_request_review_id": 3596999137, - "id": 2633772795, - "node_id": "PRRC_kwDONLllc86c_C77", - "diff_hunk": "@@ -0,0 +1,152 @@\n+const { createClient } = require(\"@supabase/supabase-js\")\n+\n+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL\n+const supabaseKey = process.env.DB_KEY\n+\n+if (!supabaseUrl || !supabaseKey) {\n+ console.error(\"Missing Supabase credentials in environment.\")\n+ process.exit(1)\n+}\n+\n+const adminSupabase = createClient(supabaseUrl, supabaseKey, {\n+ auth: {\n+ persistSession: false,\n+ autoRefreshToken: false,\n+ },\n+})\n+\n+const LOCATIONS = [\n+ { name: \"Herndon\", lat: 38.9696, lon: -77.3861 },\n+ { name: \"Reston\", lat: 38.9586, lon: -77.357 },\n+ { name: \"Sterling\", lat: 39.0067, lon: -77.4291 },\n+ { name: \"Ashburn\", lat: 39.0438, lon: -77.4874 },\n+]\n+\n+const SEARCH_TERMS = [\n+ \"bestsellers 2024\",\n+ \"classic novels\",\n+ \"science fiction\",\n+ \"mystery thriller\",\n+ \"fantasy books\",\n+ \"biography\",\n+]\n+\n+function getRandomInt(min, max) {\n+ return Math.floor(Math.random() * (max - min + 1)) + min\n+}\n+\n+function getRandomLocation() {\n+ const loc = LOCATIONS[getRandomInt(0, LOCATIONS.length - 1)]\n+ const latJitter = (Math.random() - 0.5) * 0.04\n+ const lonJitter = (Math.random() - 0.5) * 0.04\n+ return {\n+ lat: loc.lat + latJitter,\n+ lon: loc.lon + lonJitter,\n+ name: loc.name,\n+ }\n+}\n+\n+async function fetchGoogleBooks() {\n+ console.log(\"Fetching books from Google Books API...\")\n+ let allBooks = []\n+\n+ for (const term of SEARCH_TERMS) {\n+ try {\n+ const res = await fetch(\n+ `https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent(term)}&maxResults=10&printType=books&langRestrict=en`\n+ )\n+ if (!res.ok) continue\n+ const data = await res.json()\n+ if (data.items) {\n+ const books = data.items\n+ .map((item) => {\n+ const info = item.volumeInfo\n+ let cover = info.imageLinks?.thumbnail || info.imageLinks?.smallThumbnail\n+ if (cover) cover = cover.replace(\"http:\", \"https:\")\n+\n+ if (!info.title || !info.authors || !cover) return null\n+\n+ return {\n+ title: info.title,\n+ author: info.authors[0],\n+ cover: cover,\n+ }\n+ })\n+ .filter((b) => b !== null)\n+ allBooks = [...allBooks, ...books]\n+ }\n+ } catch (e) {\n+ console.error(`Failed to fetch for ${term}:`, e.message)\n+ }\n+ }\n+\n+ // Shuffle\n+ allBooks.sort(() => Math.random() - 0.5)", - "path": "scripts/seed-standalone.js", - "commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "original_commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "user": { - "login": "Copilot", - "id": 175728472, - "node_id": "BOT_kgDOCnlnWA", - "avatar_url": "https://avatars.githubusercontent.com/in/946600?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Copilot", - "html_url": "https://github.com/apps/copilot-pull-request-reviewer", - "followers_url": "https://api.github.com/users/Copilot/followers", - "following_url": "https://api.github.com/users/Copilot/following{/other_user}", - "gists_url": "https://api.github.com/users/Copilot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Copilot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Copilot/subscriptions", - "organizations_url": "https://api.github.com/users/Copilot/orgs", - "repos_url": "https://api.github.com/users/Copilot/repos", - "events_url": "https://api.github.com/users/Copilot/events{/privacy}", - "received_events_url": "https://api.github.com/users/Copilot/received_events", - "type": "Bot", - "user_view_type": "public", - "site_admin": false - }, - "body": "Inefficient shuffling algorithm: Using `sort(() => Math.random() - 0.5)` is not a proper shuffle algorithm and has biased distribution. This is a known anti-pattern. Consider using the Fisher-Yates shuffle algorithm for proper randomization.", - "created_at": "2025-12-19T06:00:40Z", - "updated_at": "2025-12-19T06:00:43Z", - "html_url": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772795", - "pull_request_url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25", - "_links": { - "self": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772795" - }, - "html": { - "href": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772795" - }, - "pull_request": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25" - } - }, - "reactions": { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772795/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": null, - "original_start_line": null, - "start_side": null, - "line": 84, - "original_line": 84, - "side": "RIGHT", - "author_association": "CONTRIBUTOR", - "original_position": 84, - "position": 84, - "subject_type": "line" - }, - { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772806", - "pull_request_review_id": 3596999137, - "id": 2633772806, - "node_id": "PRRC_kwDONLllc86c_C8G", - "diff_hunk": "@@ -1,8 +1,46 @@\n import { Hero } from \"@/components/hero\"\n import { HowItWorks } from \"@/components/how-it-works\"\n-import { HomeMapWrapper } from \"@/components/home-map-wrapper\"\n+import { adminSupabase } from \"@/lib/supabase-admin\"\n+import { SightingWithBook } from \"@/components/ledger-list\"\n+import { SightingsFeed } from \"@/components/sightings-feed\"\n+\n+export const dynamic = \"force-dynamic\"\n+\n+export default async function Home() {\n+ const { data: sightingsData, error } = await adminSupabase\n+ .from(\"sightings\")\n+ .select(\"*, book:books(*)\")\n+ .order(\"created_at\", { ascending: false })\n+ .limit(25)\n+\n+ if (error) {\n+ console.error(\"Error fetching sightings:\", error)\n+ }\n+\n+ // Cast the data to our type, assuming the join worked as expected.\n+ const rawSightings = (sightingsData || []) as unknown as SightingWithBook[]\n+\n+ // Manually fetch user info if user_id is present (since we can't easily join auth.users)\n+ const sightings = await Promise.all(\n+ rawSightings.map(async (sighting) => {\n+ if (sighting.user_id) {\n+ const { data: userData } = await adminSupabase.auth.admin.getUserById(sighting.user_id)\n+ if (userData.user) {\n+ return {\n+ ...sighting,\n+ user: {\n+ email: userData.user.email || \"\",\n+ },\n+ }", - "path": "app/page.tsx", - "commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "original_commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "user": { - "login": "Copilot", - "id": 175728472, - "node_id": "BOT_kgDOCnlnWA", - "avatar_url": "https://avatars.githubusercontent.com/in/946600?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Copilot", - "html_url": "https://github.com/apps/copilot-pull-request-reviewer", - "followers_url": "https://api.github.com/users/Copilot/followers", - "following_url": "https://api.github.com/users/Copilot/following{/other_user}", - "gists_url": "https://api.github.com/users/Copilot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Copilot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Copilot/subscriptions", - "organizations_url": "https://api.github.com/users/Copilot/orgs", - "repos_url": "https://api.github.com/users/Copilot/repos", - "events_url": "https://api.github.com/users/Copilot/events{/privacy}", - "received_events_url": "https://api.github.com/users/Copilot/received_events", - "type": "Bot", - "user_view_type": "public", - "site_admin": false - }, - "body": "Exposing user email in client-side code: User email addresses are being fetched server-side and passed to client components without any sanitization or permission checks. The email is then displayed publicly (split at @ and showing the username part). Consider whether this information should be publicly visible and if users have consented to this level of information sharing. Additionally, verify that appropriate privacy controls are in place.", - "created_at": "2025-12-19T06:00:40Z", - "updated_at": "2025-12-19T06:00:43Z", - "html_url": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772806", - "pull_request_url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25", - "_links": { - "self": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772806" - }, - "html": { - "href": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772806" - }, - "pull_request": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25" - } - }, - "reactions": { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772806/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": 24, - "original_start_line": 24, - "start_side": "RIGHT", - "line": 34, - "original_line": 34, - "side": "RIGHT", - "author_association": "CONTRIBUTOR", - "original_position": 35, - "position": 35, - "subject_type": "line" - }, - { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772822", - "pull_request_review_id": 3596999137, - "id": 2633772822, - "node_id": "PRRC_kwDONLllc86c_C8W", - "diff_hunk": "@@ -0,0 +1,89 @@\n+\"use client\"\n+\n+import { ScrollArea } from \"@/components/ui/scroll-area\"\n+import { Sighting } from \"@/lib/types\" // Assuming we can use or extend this\n+import Image from \"next/image\"\n+\n+// We need an extended type because the basic Sighting type might not have the joined book data.\n+// Based on the query: .select('*, books(*)') -> returns 'books' property or .select('*, book:books(*)') returns 'book'\n+// The user request example had .select('*, books(*)')\n+// I'll assume we map it to a comfortable structure or use the raw result.\n+// Let's define the prop type clearly.\n+\n+export type SightingWithBook = Sighting & {\n+ book: {\n+ title: string\n+ author: string\n+ cover_url?: string\n+ } | null\n+}\n+// Note: Supabase join via books(*) usually returns an object if it's many-to-one, or array if one-to-many.\n+// sightings.book_id -> books.id. This is Many-to-One (Sighting belongs to Book).\n+// So 'books' should be a single object (or null).\n+\n+interface LedgerListProps {\n+ sightings: SightingWithBook[]\n+}\n+\n+export function LedgerList({ sightings }: LedgerListProps) {\n+ // Logic lifted to parent SightingsFeed.\n+ // Rendering the passed prop directly.\n+\n+ return (\n+
\n+
\n+

Recent Sightings

\n+

Live activity from the community

\n+
\n+ \n+
\n+ {sightings.map((sighting) => {\n+ // Handle case where book might is missing or joined differently, but typing helps.", - "path": "components/ledger-list.tsx", - "commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "original_commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "user": { - "login": "Copilot", - "id": 175728472, - "node_id": "BOT_kgDOCnlnWA", - "avatar_url": "https://avatars.githubusercontent.com/in/946600?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Copilot", - "html_url": "https://github.com/apps/copilot-pull-request-reviewer", - "followers_url": "https://api.github.com/users/Copilot/followers", - "following_url": "https://api.github.com/users/Copilot/following{/other_user}", - "gists_url": "https://api.github.com/users/Copilot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Copilot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Copilot/subscriptions", - "organizations_url": "https://api.github.com/users/Copilot/orgs", - "repos_url": "https://api.github.com/users/Copilot/repos", - "events_url": "https://api.github.com/users/Copilot/events{/privacy}", - "received_events_url": "https://api.github.com/users/Copilot/received_events", - "type": "Bot", - "user_view_type": "public", - "site_admin": false - }, - "body": "The comment is outdated or misleading. Line 41 says \"Handle case where book might is missing\" but \"might is\" is grammatically incorrect. It should be either \"might be missing\" or \"is missing\". This appears to be a typo in the comment.\n```suggestion\n // Handle case where book might be missing or joined differently, but typing helps.\n```", - "created_at": "2025-12-19T06:00:40Z", - "updated_at": "2025-12-19T06:00:43Z", - "html_url": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772822", - "pull_request_url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25", - "_links": { - "self": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772822" - }, - "html": { - "href": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772822" - }, - "pull_request": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25" - } - }, - "reactions": { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772822/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": null, - "original_start_line": null, - "start_side": null, - "line": 41, - "original_line": 41, - "side": "RIGHT", - "author_association": "CONTRIBUTOR", - "original_position": 41, - "position": 41, - "subject_type": "line" - }, - { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772839", - "pull_request_review_id": 3596999137, - "id": 2633772839, - "node_id": "PRRC_kwDONLllc86c_C8n", - "diff_hunk": "@@ -68,7 +68,22 @@ export default function HomeMap() {\n attribution='© OpenStreetMap contributors'\n url=\"https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png\"\n />\n- \n+ \n+ {sightings.map((sighting) => {\n+ if (sighting.lat && sighting.lon) {\n+ return (\n+ \n+ \n+
\n+ {sighting.book?.title}\n+ {sighting.book?.author}\n+
\n+
", - "path": "components/home-map.tsx", - "commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "original_commit_id": "42114370876f310f6e498c27f38a4dbd7a5b0570", - "user": { - "login": "Copilot", - "id": 175728472, - "node_id": "BOT_kgDOCnlnWA", - "avatar_url": "https://avatars.githubusercontent.com/in/946600?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Copilot", - "html_url": "https://github.com/apps/copilot-pull-request-reviewer", - "followers_url": "https://api.github.com/users/Copilot/followers", - "following_url": "https://api.github.com/users/Copilot/following{/other_user}", - "gists_url": "https://api.github.com/users/Copilot/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Copilot/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Copilot/subscriptions", - "organizations_url": "https://api.github.com/users/Copilot/orgs", - "repos_url": "https://api.github.com/users/Copilot/repos", - "events_url": "https://api.github.com/users/Copilot/events{/privacy}", - "received_events_url": "https://api.github.com/users/Copilot/received_events", - "type": "Bot", - "user_view_type": "public", - "site_admin": false - }, - "body": "Missing alt text for popup content: The popup displays book information but doesn't provide any accessible context for screen readers. Consider adding aria-labels or more semantic HTML structure to make the popup content accessible to users relying on assistive technology.", - "created_at": "2025-12-19T06:00:40Z", - "updated_at": "2025-12-19T06:00:43Z", - "html_url": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772839", - "pull_request_url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25", - "_links": { - "self": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772839" - }, - "html": { - "href": "https://github.com/clates/lfl-booktracker/pull/25#discussion_r2633772839" - }, - "pull_request": { - "href": "https://api.github.com/repos/clates/lfl-booktracker/pulls/25" - } - }, - "reactions": { - "url": "https://api.github.com/repos/clates/lfl-booktracker/pulls/comments/2633772839/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "start_line": 76, - "original_start_line": 76, - "start_side": "RIGHT", - "line": 81, - "original_line": 81, - "side": "RIGHT", - "author_association": "CONTRIBUTOR", - "original_position": 96, - "position": 96, - "subject_type": "line" - } -] diff --git a/next-env.d.ts b/next-env.d.ts deleted file mode 100755 index 7a70f65..0000000 --- a/next-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// -/// -import "./.next/types/routes.d.ts" - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. From 20130fe3d0f0972cb199aa7e5fc1f5478b2f6466 Mon Sep 17 00:00:00 2001 From: Christopher Lates Date: Fri, 19 Dec 2025 22:55:29 -0500 Subject: [PATCH 2/8] drawer --- components/add-sighting-drawer.tsx | 242 +++++++++++++++++++++++++++++ components/hero.tsx | 2 + components/sightings-feed.tsx | 29 ++-- components/ui/drawer.tsx | 4 +- 4 files changed, 259 insertions(+), 18 deletions(-) create mode 100644 components/add-sighting-drawer.tsx diff --git a/components/add-sighting-drawer.tsx b/components/add-sighting-drawer.tsx new file mode 100644 index 0000000..0b309f9 --- /dev/null +++ b/components/add-sighting-drawer.tsx @@ -0,0 +1,242 @@ +"use client" + +import { useState, useEffect } from "react" +import { BookOpen, Loader2, MapPin } from "lucide-react" + +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer" +import { Button } from "@/components/ui/button" +import { GoogleBookSearch } from "@/components/google-book-search" +import { ParchmentFrame } from "@/components/ui/parchment-frame" +import { useToast } from "@/hooks/use-toast" +import useLocation from "@/hooks/use-location" + +export function AddSightingDrawer() { + const [open, setOpen] = useState(false) + const [selectedBook, setSelectedBook] = useState<{ + title: string + authors?: string[] + coverUrl?: string + } | null>(null) + + const [generatedCode, setGeneratedCode] = useState("") + const [isGenerating, setIsGenerating] = useState(false) + const { toast } = useToast() + const { latitude, longitude } = useLocation() + + function getCookie(name: string) { + if (typeof document === "undefined") return null + const value = `; ${document.cookie}` + const parts = value.split(`; ${name}=`) + if (parts.length === 2) return parts.pop()?.split(";").shift() + return null + } + + // Ensure anonymous ID cookie exists + useEffect(() => { + if (!getCookie("lfl_anonymous_id")) { + const newId = crypto.randomUUID() + // Set cookie for 1 year + document.cookie = `lfl_anonymous_id=${newId}; path=/; max-age=31536000; SameSite=Lax; Secure` + } + }, []) + + async function handleGenerate() { + if (!selectedBook) return + + if (!latitude || !longitude) { + toast({ + title: "Location Required", + description: "We need your location to generate a code.", + variant: "destructive", + }) + return + } + + setIsGenerating(true) + try { + const anonymousId = getCookie("lfl_anonymous_id") + const response = await fetch("/api/books/generate", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + book: selectedBook, + location: { + lat: latitude, + long: longitude, + }, + anonymousId, + }), + }) + + if (!response.ok) { + throw new Error("Failed to generate code") + } + + const { code } = await response.json() + setGeneratedCode(code) + toast({ + title: "Success", + description: "Book code generated successfully!", + }) + } catch (error) { + console.error(error) + toast({ + title: "Error", + description: "Failed to generate ID. Please try again.", + variant: "destructive", + }) + } finally { + setIsGenerating(false) + } + } + + function formatCode(code: string) { + return `${code.slice(0, 4)}-${code.slice(4, 6)}-${code.slice(6, 9)}`.toUpperCase() + } + + function resetState() { + setGeneratedCode("") + setSelectedBook(null) + } + + return ( + + +
+

+ Can't find your book? + Register it here +

+
+
+ +
+ + + Register a New Book + + + Generate a unique tracking code for your book to start its journey. + + + +
+ + {!generatedCode ? ( +
+ {!selectedBook ? ( +
+

+ Search for your book +

+ +
+ ) : ( +
+
+ {selectedBook.coverUrl ? ( + {selectedBook.title} + ) : ( +
+ +
+ )} +
+

+ {selectedBook.title} +

+

+ {selectedBook.authors?.join(", ")} +

+ +
+
+ + +
+ )} +
+ ) : ( +
+
+

+ Ready for Adventure! +

+

+ Write this code on the inside cover: +

+
+ +
+

+ {formatCode(generatedCode)} +

+
+ + +
+ )} +
+
+ + + + + + +
+
+
+ ) +} diff --git a/components/hero.tsx b/components/hero.tsx index d1de0cd..110ceee 100644 --- a/components/hero.tsx +++ b/components/hero.tsx @@ -1,4 +1,5 @@ import { BookSearch } from "@/components/book-search" +import { AddSightingDrawer } from "@/components/add-sighting-drawer" export function Hero() { return ( @@ -15,6 +16,7 @@ export function Hero() {
+
diff --git a/components/sightings-feed.tsx b/components/sightings-feed.tsx index 3e289fc..3d3e106 100644 --- a/components/sightings-feed.tsx +++ b/components/sightings-feed.tsx @@ -19,22 +19,19 @@ export function SightingsFeed({ initialSightings }: SightingsFeedProps) { if (currentIndex >= initialSightings.length) return const timer = setInterval(() => { - setCurrentIndex((prevIndex) => { - // Stop if we reached the end - if (prevIndex >= initialSightings.length) { - clearInterval(timer) - return prevIndex - } - - const nextItem = initialSightings[prevIndex] - - // Safety check if nextItem is undefined (though length check above should prevent this) - if (nextItem) { - setDisplayedSightings((prevSightings) => [nextItem, ...prevSightings]) - } - - return prevIndex + 1 - }) + const nextItem = initialSightings[currentIndex] + + if (nextItem) { + setDisplayedSightings((prevSightings) => { + // Prevent duplicates + if (prevSightings.some((s) => s.id === nextItem.id)) { + return prevSightings + } + return [nextItem, ...prevSightings] + }) + } + + setCurrentIndex((prev) => prev + 1) }, SIGHTING_DISPLAY_INTERVAL_MS) return () => clearInterval(timer) diff --git a/components/ui/drawer.tsx b/components/ui/drawer.tsx index f189c42..d692265 100755 --- a/components/ui/drawer.tsx +++ b/components/ui/drawer.tsx @@ -25,7 +25,7 @@ const DrawerOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( )) @@ -40,7 +40,7 @@ const DrawerContent = React.forwardRef< Date: Fri, 19 Dec 2025 23:19:31 -0500 Subject: [PATCH 3/8] fixup --- components/add-sighting-drawer.tsx | 66 ++++++++++++++++++++++++------ components/ui/toast.tsx | 2 +- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/components/add-sighting-drawer.tsx b/components/add-sighting-drawer.tsx index 0b309f9..db23a0d 100644 --- a/components/add-sighting-drawer.tsx +++ b/components/add-sighting-drawer.tsx @@ -30,7 +30,32 @@ export function AddSightingDrawer() { const [generatedCode, setGeneratedCode] = useState("") const [isGenerating, setIsGenerating] = useState(false) const { toast } = useToast() - const { latitude, longitude } = useLocation() + const { latitude, longitude, error: locationError } = useLocation() + const [locationTimeout, setLocationTimeout] = useState(false) + + // Handle location timeout and errors + useEffect(() => { + // If we have location or explicit error, no need to wait + if (latitude || locationError) { + setLocationTimeout(false) + return + } + + // Only start timeout if drawer is open and we don't have location yet + if (open && !latitude && !locationError) { + const timer = setTimeout(() => { + setLocationTimeout(true) + toast({ + title: "Location Access Required", + description: + "We couldn't get your location. Please check your browser settings and try again.", + variant: "destructive", + }) + }, 5000) + + return () => clearTimeout(timer) + } + }, [open, latitude, locationError, toast]) function getCookie(name: string) { if (typeof document === "undefined") return null @@ -53,11 +78,20 @@ export function AddSightingDrawer() { if (!selectedBook) return if (!latitude || !longitude) { - toast({ - title: "Location Required", - description: "We need your location to generate a code.", - variant: "destructive", - }) + // If we timed out or have an error, explain why again + if (locationTimeout || locationError) { + toast({ + title: "Location Access Required", + description: "Please enable location services to generate a code.", + variant: "destructive", + }) + } else { + toast({ + title: "Location Required", + description: "We need your location to generate a code.", + variant: "destructive", + }) + } return } @@ -115,8 +149,10 @@ export function AddSightingDrawer() {

- Can't find your book? - Register it here + Donating a book? + + Track its travels here +

@@ -176,7 +212,7 @@ export function AddSightingDrawer() {
diff --git a/components/sightings-feed.tsx b/components/sightings-feed.tsx index 3e289fc..bdad152 100644 --- a/components/sightings-feed.tsx +++ b/components/sightings-feed.tsx @@ -28,9 +28,14 @@ export function SightingsFeed({ initialSightings }: SightingsFeedProps) { const nextItem = initialSightings[prevIndex] - // Safety check if nextItem is undefined (though length check above should prevent this) if (nextItem) { - setDisplayedSightings((prevSightings) => [nextItem, ...prevSightings]) + setDisplayedSightings((prevSightings) => { + // Prevent duplicates + if (prevSightings.some((s) => s.id === nextItem.id)) { + return prevSightings + } + return [nextItem, ...prevSightings] + }) } return prevIndex + 1 @@ -38,10 +43,10 @@ export function SightingsFeed({ initialSightings }: SightingsFeedProps) { }, SIGHTING_DISPLAY_INTERVAL_MS) return () => clearInterval(timer) - }, [currentIndex, initialSightings]) + }, [initialSightings]) return ( -
+
{/* Map takes up more space on desktop */} From d8f275584895224d61e27523076968e528703469 Mon Sep 17 00:00:00 2001 From: Christopher Lates Date: Sat, 20 Dec 2025 00:33:44 -0500 Subject: [PATCH 6/8] remove search from nav --- components/navigation.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/components/navigation.tsx b/components/navigation.tsx index 0839ec7..4f74b4c 100644 --- a/components/navigation.tsx +++ b/components/navigation.tsx @@ -50,10 +50,6 @@ export function Navigation() { TaleTrail