From 803b51193a3a76cf52fc24f16a0615b6cf43b5ce Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Thu, 12 Feb 2026 13:10:51 +0100 Subject: [PATCH] Standardize Focus Mode Article View & Fix UI --- web/src/components/focus/FocusArticleView.tsx | 100 ------------------ web/src/components/focus/FocusCardStack.tsx | 84 ++++++++++++--- web/src/hooks/useIsMobile.ts | 14 +++ web/src/pages/FocusPage.tsx | 14 +++ 4 files changed, 100 insertions(+), 112 deletions(-) delete mode 100644 web/src/components/focus/FocusArticleView.tsx create mode 100644 web/src/hooks/useIsMobile.ts diff --git a/web/src/components/focus/FocusArticleView.tsx b/web/src/components/focus/FocusArticleView.tsx deleted file mode 100644 index f4b8ca3..0000000 --- a/web/src/components/focus/FocusArticleView.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { motion } from 'framer-motion'; -import type { Article } from '../../api/articles'; - -interface FocusArticleViewProps { - article: Article; - onClose: () => void; - onNext: (action: 'read' | 'keep') => void; -} - -export function FocusArticleView({ article, onClose, onNext }: FocusArticleViewProps) { - // Format content for readability - const formattedDate = new Date(article.published_at || article.created_at).toLocaleDateString('fr-FR', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - }); - - return ( - -
- {/* Close Button */} - - - {/* Content Scroll View */} -
- {/* Hero Image */} - {article.image_url && ( -
-
-
-
- )} - -
- {/* Header */} -
-
- {article.feed_title} - • - {formattedDate} -
-

- {article.title} -

-
-
- - {/* Article Body */} -
- {article.content ? ( -
- ) : ( -

- {article.summary || "Contenu non disponible."} -

- )} -
- - {/* Actions Footer - In Flow */} -
- - -
-
-
-
- - ); -} diff --git a/web/src/components/focus/FocusCardStack.tsx b/web/src/components/focus/FocusCardStack.tsx index 7052dc2..3d83163 100644 --- a/web/src/components/focus/FocusCardStack.tsx +++ b/web/src/components/focus/FocusCardStack.tsx @@ -3,19 +3,29 @@ import { useDrag } from '@use-gesture/react'; import { motion, AnimatePresence, useMotionValue, useTransform, useSpring } from 'framer-motion'; import type { Article } from '../../api/articles'; import { FocusCard } from './FocusCard'; -import { FocusArticleView } from './FocusArticleView'; +import { ReaderView } from '../ReaderView'; +import { MobileReaderView } from '../MobileReaderView'; +import { useIsMobile } from '../../hooks/useIsMobile'; +// The CardStack props don't have onToggleFavorite. We might need to add it or Mock it. +// ReaderView requires onToggleFavorite. +// Let's check FocusCardStackProps. It does NOT have it. +// We should add it to props or just use a dummy one for now if not strictly required by user? +// User said "meme system". Dashboard passes `toggleFavoriteMutation.mutate`. +// FocusPage handles mutations. We should pass `onToggleFavorite` to FocusCardStack. interface FocusCardStackProps { articles: Article[]; onMarkRead: (id: string) => void; onKeep: (id: string) => void; + onToggleFavorite: (id: string) => void; onEmpty: () => void; } -export function FocusCardStack({ articles, onMarkRead, onKeep, onEmpty }: FocusCardStackProps) { +export function FocusCardStack({ articles, onMarkRead, onKeep, onToggleFavorite, onEmpty }: FocusCardStackProps) { const [currentIndex, setCurrentIndex] = useState(0); const [readingArticle, setReadingArticle] = useState
(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)} />