From e014ded82a630bd91b15be4307125f5580119f4d Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Fri, 13 Feb 2026 13:50:25 -0800 Subject: Analyze page size and fix frontend tests --- .../coverage/src/components/FeedItems.tsx.html | 571 +++++++++++++++++++-- 1 file changed, 536 insertions(+), 35 deletions(-) (limited to 'frontend/coverage/src/components/FeedItems.tsx.html') diff --git a/frontend/coverage/src/components/FeedItems.tsx.html b/frontend/coverage/src/components/FeedItems.tsx.html index 462491f..e0f73cf 100644 --- a/frontend/coverage/src/components/FeedItems.tsx.html +++ b/frontend/coverage/src/components/FeedItems.tsx.html @@ -23,30 +23,30 @@
- 86.36% + 89.34% Statements - 19/22 + 109/122
- 60% + 77.21% Branches - 6/10 + 61/79
- 83.33% + 86.2% Functions - 5/6 + 25/29
- 85% + 89.09% Lines - 17/20 + 98/110
@@ -119,46 +119,345 @@ 54 55 56 -57  +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224  +  +  +  +  +  +  +27x +27x +27x +  +27x +27x +27x +27x +27x +  +27x +8x +1x   +7x +7x   +8x   +8x +8x   +8x +2x +6x +1x     -3x -3x -3x +8x +1x +  +  +  +8x +  +8x +  +  +  +  +8x +  +  +8x +8x +8x +  +  +8x +  +7x +  +  +7x +  +  +6x +1x +  +5x +  +6x +6x +6x +  +  +1x +1x +1x +  +  +  +27x +7x +  +  +27x +  +27x +23x 3x   3x 2x 2x -  2x +2x +2x +1x   -  +2x   2x   1x   +  +  +  +  +    1x +1x +1x +1x +  +1x +  +  +  +  +23x +23x     +27x +2x +2x +2x +  +  +  +27x +2x +  +3x +  +2x +  +  +  +  +  +  +27x 1x +  +2x +  1x     -  +        +27x +24x +  +2x +  +2x +1x +1x   -3x 1x   +  +  +1x +1x +1x +1x +1x 1x     @@ -168,7 +467,42 @@       -2x +24x +15x +15x +  +  +24x +24x +  +24x +  +  +27x +14x +  +  +13x +  +  +  +  +  +  +21x +  +  +  +  +  +  +  +  +  +  +  +  +        @@ -176,24 +510,59 @@      
import { useEffect, useState } from 'react';
-import { useParams } from 'react-router-dom';
+import { useParams, useSearchParams } from 'react-router-dom';
 import type { Item } from '../types';
 import FeedItem from './FeedItem';
 import './FeedItems.css';
  
 export default function FeedItems() {
-    const { feedId } = useParams<{ feedId: string }>();
+    const { feedId, tagName } = useParams<{ feedId: string; tagName: string }>();
+    const [searchParams] = useSearchParams();
+    const filterFn = searchParams.get('filter') || 'unread';
+ 
     const [items, setItems] = useState<Item[]>([]);
     const [loading, setLoading] = useState(true);
+    const [loadingMore, setLoadingMore] = useState(false);
+    const [hasMore, setHasMore] = useState(true);
     const [error, setError] = useState('');
  
-    useEffect(() => {
-        setLoading(true);
+    const fetchItems = (maxId?: string) => {
+        if (maxId) {
+            setLoadingMore(true);
+        } else {
+            setLoading(true);
+            setItems([]);
+        }
         setError('');
  
-        const url = feedId
-            ? `/api/stream?feed_id=${feedId}`
-            : '/api/stream'; // Default or "all" view? For now let's assume we need a feedId or handle "all" logic later
+        let url = '/api/stream';
+        const params = new URLSearchParams();
+ 
+        if (feedId) {
+            params.append('feed_id', feedId);
+        } else if (tagName) {
+            params.append('tag', tagName);
+        }
+ 
+        if (maxId) {
+            params.append('max_id', maxId);
+        }
+ 
+        // Apply filters
+        Iif (filterFn === 'all') {
+            params.append('read_filter', 'all');
+        I} else if (filterFn === 'starred') {
+            params.append('starred', 'true');
+            params.append('read_filter', 'all');
+        } else {
+            // default to unread
+            params.append('read_filter', 'unread');
+        }
+ 
+        const queryString = params.toString();
+        Eif (queryString) {
+            url += `?${queryString}`;
+        }
  
         fetch(url)
             .then((res) => {
@@ -203,29 +572,161 @@ export default function FeedItems() {
                 return res.json();
             })
             .then((data) => {
-                setItems(data);
+                if (maxId) {
+                    setItems((prev) => [...prev, ...data]);
+                } else {
+                    setItems(data);
+                }
+                setHasMore(data.length > 0);
                 setLoading(false);
+                setLoadingMore(false);
             })
-            .catch((err) => {
-                setError(err.message);
-                setLoading(false);
+            .catch((err) => {
+                setError(err.message);
+                setLoading(false);
+                setLoadingMore(false);
             });
-    }, [feedId]);
+    };
+ 
+    useEffect(() => {
+        fetchItems();
+    }, [feedId, tagName, filterFn]);
+ 
+    const [selectedIndex, setSelectedIndex] = useState(-1);
+ 
+    useEffect(() => {
+        const handleKeyDown = (e: KeyboardEvent) => {
+            Iif (items.length === 0) return;
+ 
+            if (e.key === 'j') {
+                setSelectedIndex((prev) => {
+                    const nextIndex = Math.min(prev + 1, items.length - 1);
+                    Eif (nextIndex !== prev) {
+                        const item = items[nextIndex];
+                        if (!item.read) {
+                            markAsRead(item);
+                        }
+                        scrollToItem(nextIndex);
+                    }
+                    return nextIndex;
+                });
+            I} else if (e.key === 'k') {
+                setSelectedIndex((prev) => {
+                    const nextIndex = Math.max(prev - 1, 0);
+                    if (nextIndex !== prev) {
+                        scrollToItem(nextIndex);
+                    }
+                    return nextIndex;
+                });
+            E} else if (e.key === 's') {
+                setSelectedIndex((currentIndex) => {
+                    Eif (currentIndex >= 0 && currentIndex < items.length) {
+                        toggleStar(items[currentIndex]);
+                    }
+                    return currentIndex;
+                });
+            }
+        };
+ 
+        window.addEventListener('keydown', handleKeyDown);
+        return () => window.removeEventListener('keydown', handleKeyDown);
+    }, [items]);
+ 
+    const scrollToItem = (index: number) => {
+        const element = document.getElementById(`item-${index}`);
+        Eif (element) {
+            element.scrollIntoView({ behavior: 'smooth', block: 'start' });
+        }
+    };
+ 
+    const markAsRead = (item: Item) => {
+        const updatedItem = { ...item, read: true };
+        // Optimistic update
+        setItems((prevItems) => prevItems.map((i) => (i._id === item._id ? updatedItem : i)));
+ 
+        fetch(`/api/item/${item._id}`, {
+            method: 'PUT',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({ read: true, starred: item.starred }),
+        }).catch((err) => console.error('Failed to mark read', err));
+    };
+ 
+    const toggleStar = (item: Item) => {
+        const updatedItem = { ...item, starred: !item.starred };
+        // Optimistic update
+        setItems((prevItems) => prevItems.map((i) => (i._id === item._id ? updatedItem : i)));
+ 
+        fetch(`/api/item/${item._id}`, {
+            method: 'PUT',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({ read: item.read, starred: !item.starred }),
+        }).catch((err) => console.error('Failed to toggle star', err));
+    };
+ 
+    useEffect(() => {
+        const observer = new IntersectionObserver(
+            (entries) => {
+                entries.forEach((entry) => {
+                    // Infinity scroll sentinel
+                    if (entry.target.id === 'load-more-sentinel') {
+                        Eif (entry.isIntersecting && !loadingMore && hasMore && items.length > 0) {
+                            fetchItems(String(items[items.length - 1]._id));
+                        }
+                        return;
+                    }
+ 
+                    // If item is not intersecting and is above the viewport, it's been scrolled past
+                    Eif (!entry.isIntersecting && entry.boundingClientRect.top < 0) {
+                        const index = Number(entry.target.getAttribute('data-index'));
+                        Eif (!isNaN(index) && index >= 0 && index < items.length) {
+                            const item = items[index];
+                            Eif (!item.read) {
+                                markAsRead(item);
+                            }
+                        }
+                    }
+                });
+            },
+            { root: null, threshold: 0 }
+        );
+ 
+        items.forEach((_, index) => {
+            const el = document.getElementById(`item-${index}`);
+            Eif (el) observer.observe(el);
+        });
+ 
+        const sentinel = document.getElementById('load-more-sentinel');
+        if (sentinel) observer.observe(sentinel);
+ 
+        return () => observer.disconnect();
+    }, [items, loadingMore, hasMore]);
  
     if (loading) return <div className="feed-items-loading">Loading items...</div>;
-    Iif (error) return <div className="feed-items-error">Error: {error}</div>;
+    if (error) return <div className="feed-items-error">Error: {error}</div>;
+ 
  
     return (
         <div className="feed-items">
-            <h2>Items</h2>
-            {/* TODO: Add Feed Title here if possible, maybe pass from location state or fetch feed details */}
             {items.length === 0 ? (
                 <p>No items found.</p>
             ) : (
                 <ul className="item-list">
-                    {items.map((item) => (
-                        <FeedItem key={item._id} item={item} />
+                    {items.map((item, index) => (
+                        <div
+                            id={`item-${index}`}
+                            key={item._id}
+                            data-index={index}
+                            className={index === selectedIndex ? 'selected-item-container' : ''}
+                            onClick={() => setSelectedIndex(index)}
+                        >
+                            <FeedItem item={item} />
+                        </div>
                     ))}
+                    {hasMore && (
+                        <div id="load-more-sentinel" className="loading-more">
+                            {loadingMore ? 'Loading more...' : ''}
+                        </div>
+                    )}
                 </ul>
             )}
         </div>
@@ -238,7 +739,7 @@ export default function FeedItems() {