(null); const [exitX, setExitX] = useState(0); + const isMobile = useIsMobile(); // Visible stack size const visibleArticles = articles.slice(currentIndex, currentIndex + 3); @@ -78,16 +88,66 @@ export function FocusCardStack({ articles, onMarkRead, onKeep, onEmpty }: FocusC {/* Reading View Modal */} {readingArticle && ( - setReadingArticle(null)} - onNext={(action) => { - setReadingArticle(null); - if (action === 'read') onMarkRead(readingArticle.id); - else onKeep(readingArticle.id); - setCurrentIndex(prev => prev + 1); - }} - /> + isMobile ? ( + setReadingArticle(null)} + onToggleFavorite={onToggleFavorite} + onNext={() => { + onMarkRead(readingArticle.id); + setCurrentIndex(prev => prev + 1); + setReadingArticle(null); + }} + onPrev={() => { + onKeep(readingArticle.id); + // MobileReaderView 'Next' usually goes forward. 'Prev' goes back? + // Actually in MobileReaderView context, swipe left = Next, swipe Right = Prev. + // Here 'Prev' in the context of Focus Stack might mean 'Keep' or 'Undo'? + // The user's request is "validation" (swipe right? no left usually implies next) + // Let's map Next -> MarkRead (Next card), Prev -> Keep (Skip/Next card but keep). + // Wait, usually swipe left = next item. + // In Tinter: Left = Dislike (skip), Right = Like (Keep/Read). + // In Focus: Right = Validé (Read), Left = Keep (Skip). + // So 'Next' (Swipe Left) should map to 'Keep'? + // Let's check MobileReaderView implementation. + // onSwipedLeft: () => onNext() + // onSwipedRight: () => onPrev() + // If we want to consistency with FocusCardStack: + // Stack: Swipe Right (>0) = Mark Read. Swipe Left (<0) = Keep. + // MobileReaderView: Swipe Left (Next). Swipe Right (Prev). + // So MobileReaderView 'Next' (Left) should be 'Keep'. + // MobileReaderView 'Prev' (Right) should be 'Mark Read'. + // But 'Prev' name is confusing. + // Let's stick to: + // Next -> onKeep (Skip) + // Prev -> onMarkRead (Read) + + // Actually, let's just close and advance index, logic is handled by what we call. + // If 'Next' means 'I'm done, show next', it's 'Mark Read'. + // Let's map: + // onNext (Left Swipe) -> Keep (Skip to next) + // onPrev (Right Swipe) -> Mark Read (Done, to next) + // This matches the card stack directions: + // Card Stack: Left Swipe (mx < 0) -> Keep. + // MobileReaderView: Left Swipe -> onNext. + // So onNext === Keep. + + // Card Stack: Right Swipe (mx > 0) -> Mark Read. + // MobileReaderView: Right Swipe -> onPrev. + // So onPrev === Mark Read. + + onKeep(readingArticle.id); + setCurrentIndex(prev => prev + 1); + setReadingArticle(null); + }} + /> + ) : ( + setReadingArticle(null)} + onToggleFavorite={onToggleFavorite} + /> + ) )} diff --git a/web/src/hooks/useIsMobile.ts b/web/src/hooks/useIsMobile.ts new file mode 100644 index 0000000..4b9c878 --- /dev/null +++ b/web/src/hooks/useIsMobile.ts @@ -0,0 +1,14 @@ +import { useState, useEffect } from 'react'; + +export function useIsMobile() { + const [isMobile, setIsMobile] = useState(window.matchMedia('(max-width: 768px)').matches); + + useEffect(() => { + const media = window.matchMedia('(max-width: 768px)'); + const listener = (e: MediaQueryListEvent) => setIsMobile(e.matches); + media.addEventListener('change', listener); + return () => media.removeEventListener('change', listener); + }, []); + + return isMobile; +} diff --git a/web/src/pages/FocusPage.tsx b/web/src/pages/FocusPage.tsx index d257177..abf25c0 100644 --- a/web/src/pages/FocusPage.tsx +++ b/web/src/pages/FocusPage.tsx @@ -39,10 +39,23 @@ export function FocusPage({ onExit }: FocusPageProps) { }, }); + // Mutation to toggle favorite + const toggleFavoriteMutation = useMutation({ + mutationFn: (id: string) => articlesApi.toggleFavorite(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['articles'] }); + queryClient.invalidateQueries({ queryKey: ['feeds'] }); + }, + }); + const handleMarkRead = (id: string) => { markReadMutation.mutate(id); }; + const handleToggleFavorite = (id: string) => { + toggleFavoriteMutation.mutate(id); + }; + const handleKeep = (id: string) => { // Do nothing api-wise, just skip. console.log('Skipped:', id); @@ -96,6 +109,7 @@ export function FocusPage({ onExit }: FocusPageProps) { articles={deck} onMarkRead={handleMarkRead} onKeep={handleKeep} + onToggleFavorite={handleToggleFavorite} onEmpty={() => setIsComplete(true)} />