From 2875936f127a686b35916fc2854f9b1d77211849 Mon Sep 17 00:00:00 2001 From: Francisco Pessano <76450203+FranP-code@users.noreply.github.com> Date: Sun, 13 Jul 2025 20:59:15 -0300 Subject: [PATCH] Add tweet URL column and seen tracking system --- astro-app/src/components/ProjectCard.tsx | 111 +++++++++++++++++- astro-app/src/components/ProjectsTable.tsx | 98 +++++++++++++++- astro-app/src/components/ProjectsView.tsx | 130 +++++++++++++++++---- astro-app/src/lib/csv-loader.ts | 1 + astro-app/src/lib/seen-projects.ts | 42 +++++++ 5 files changed, 347 insertions(+), 35 deletions(-) create mode 100644 astro-app/src/lib/seen-projects.ts diff --git a/astro-app/src/components/ProjectCard.tsx b/astro-app/src/components/ProjectCard.tsx index 287b103..905773d 100644 --- a/astro-app/src/components/ProjectCard.tsx +++ b/astro-app/src/components/ProjectCard.tsx @@ -1,16 +1,36 @@ -import { ExternalLink, Heart, MessageCircle, Repeat2, Eye } from 'lucide-react'; +import { ExternalLink, Heart, MessageCircle, Repeat2, Eye, Twitter, Check, X } from 'lucide-react'; import { formatDistanceToNow } from 'date-fns'; +import { useState, useEffect } from 'react'; import type { TwitterProject } from '@/lib/csv-loader'; +import { markProjectAsSeen, markProjectAsUnseen, isProjectSeen } from '@/lib/seen-projects'; +import { Button } from './ui/button'; interface ProjectCardProps { project: TwitterProject; + onSeenStatusChange?: () => void; } -export function ProjectCard({ project }: ProjectCardProps) { +export function ProjectCard({ project, onSeenStatusChange }: ProjectCardProps) { const formattedDate = formatDistanceToNow(new Date(project.created_at), { addSuffix: true }); + const [seen, setSeen] = useState(false); + + useEffect(() => { + setSeen(isProjectSeen(project.id)); + }, [project.id]); + + const toggleSeen = () => { + if (seen) { + markProjectAsUnseen(project.id); + setSeen(false); + } else { + markProjectAsSeen(project.id); + setSeen(true); + } + onSeenStatusChange?.(); + }; return ( -
+
{/* Author Info */}
@@ -42,9 +62,88 @@ export function ProjectCard({ project }: ProjectCardProps) {

{project.project_description}

- {/* Project URL */} - {project.project_url && ( -
+ {/* Project Actions */} +
+
+ {/* Project URL */} + {project.project_url && ( + + + View Project + + )} + + {/* Tweet URL */} + {project.original_tweet_url && ( + + + View Tweet + + )} +
+ + {/* Seen Toggle */} + +
+ + {/* Engagement Stats */} +
+
+ + {project.favorite_count.toLocaleString()} +
+
+ + {project.retweet_count.toLocaleString()} +
+
+ + {project.reply_count.toLocaleString()} +
+
+ + {project.views_count.toLocaleString()} +
+
+ + {/* Category */} + {project.category && ( +
+ + {project.category} + +
+ )} +
+ ); +} void; } const columnHelper = createColumnHelper(); -export function ProjectsTable({ projects, title, showUrlColumn = true }: ProjectsTableProps) { +export function ProjectsTable({ projects, title, showUrlColumn = true, onSeenStatusChange }: ProjectsTableProps) { const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); const [globalFilter, setGlobalFilter] = useState(''); + const [seenProjects, setSeenProjects] = useState>(new Set()); const parentRef = useRef(null); + useEffect(() => { + // Update seen projects state when component mounts or projects change + const updateSeenState = () => { + const seen = new Set(); + projects.forEach(project => { + if (isProjectSeen(project.id)) { + seen.add(project.id); + } + }); + setSeenProjects(seen); + }; + + updateSeenState(); + }, [projects]); + + const toggleSeen = (projectId: string) => { + const isCurrentlySeen = seenProjects.has(projectId); + + if (isCurrentlySeen) { + markProjectAsUnseen(projectId); + setSeenProjects(prev => { + const newSet = new Set(prev); + newSet.delete(projectId); + return newSet; + }); + } else { + markProjectAsSeen(projectId); + setSeenProjects(prev => new Set(prev).add(projectId)); + } + + onSeenStatusChange?.(); + }; + const columns = useMemo(() => [ columnHelper.accessor('author_name', { header: ({ column }) => ( @@ -109,6 +146,27 @@ export function ProjectsTable({ projects, title, showUrlColumn = true }: Project size: 100, }) ] : []), + columnHelper.accessor('original_tweet_url', { + header: 'Tweet', + cell: ({ row }) => ( +
+ {row.original.original_tweet_url ? ( + + + Tweet + + ) : ( + No Tweet + )} +
+ ), + size: 100, + }), columnHelper.accessor('media_thumbnail', { header: 'Media', cell: ({ row }) => ( @@ -203,7 +261,39 @@ export function ProjectsTable({ projects, title, showUrlColumn = true }: Project ), size: 140, }), - ], [showUrlColumn]); + columnHelper.display({ + id: 'actions', + header: 'Actions', + cell: ({ row }) => { + const projectId = row.original.id; + const isSeen = seenProjects.has(projectId); + + return ( +
+ +
+ ); + }, + size: 120, + }), + ], [showUrlColumn, seenProjects]); const table = useReactTable({ data: projects, @@ -307,7 +397,7 @@ export function ProjectsTable({ projects, title, showUrlColumn = true }: Project return (
('table'); + const [seenProjectIds, setSeenProjectIds] = useState>(new Set()); - // Separate projects with and without URLs - const projectsWithUrls = projects.filter(p => p.project_url); - const projectsWithoutUrls = projects.filter(p => !p.project_url); + // Update seen projects state + const updateSeenProjects = useCallback(() => { + setSeenProjectIds(getSeenProjects()); + }, []); - console.log(projects); + useEffect(() => { + updateSeenProjects(); + }, [updateSeenProjects]); + + // Separate projects by URL and seen status + const unseenProjects = projects.filter(p => !seenProjectIds.has(p.id)); + const seenProjects = projects.filter(p => seenProjectIds.has(p.id)); + + const unseenWithUrls = unseenProjects.filter(p => p.project_url); + const unseenWithoutUrls = unseenProjects.filter(p => !p.project_url); + const seenWithUrls = seenProjects.filter(p => p.project_url); + const seenWithoutUrls = seenProjects.filter(p => !p.project_url); + + const handleClearSeenProjects = () => { + clearAllSeenProjects(); + updateSeenProjects(); + }; return (
@@ -50,64 +69,125 @@ export function ProjectsView({ projects }: ProjectsViewProps) {
+ {seenProjects.length > 0 && ( + + )}
{/* Statistics */} -
+
{projects.length}
Total Projects
-
{projectsWithUrls.length}
-
With Project URLs
+
{unseenProjects.length}
+
Unseen Projects
-
{projectsWithoutUrls.length}
-
Missing URLs
+
{seenProjects.length}
+
Seen Projects
+
+
+
{projects.filter(p => p.project_url).length}
+
With URLs
+
+
+
{projects.filter(p => !p.project_url).length}
+
No URLs
{/* Content */} - - + + - Projects with URLs ({projectsWithUrls.length}) + Unseen w/ URLs ({unseenWithUrls.length}) - Missing URLs ({projectsWithoutUrls.length}) + Unseen w/o URLs ({unseenWithoutUrls.length}) + + + Seen w/ URLs ({seenWithUrls.length}) + + + Seen w/o URLs ({seenWithoutUrls.length}) - + {viewMode === 'table' ? ( ) : (
- {projectsWithUrls.map((project) => ( - + {unseenWithUrls.map((project) => ( + ))}
)}
- + {viewMode === 'table' ? ( ) : (
- {projectsWithoutUrls.map((project) => ( - + {unseenWithoutUrls.map((project) => ( + + ))} +
+ )} +
+ + + {viewMode === 'table' ? ( + + ) : ( +
+ {seenWithUrls.map((project) => ( + + ))} +
+ )} +
+ + + {viewMode === 'table' ? ( + + ) : ( +
+ {seenWithoutUrls.map((project) => ( + ))}
)} diff --git a/astro-app/src/lib/csv-loader.ts b/astro-app/src/lib/csv-loader.ts index 9f3e5b7..c284416 100644 --- a/astro-app/src/lib/csv-loader.ts +++ b/astro-app/src/lib/csv-loader.ts @@ -7,6 +7,7 @@ export interface TwitterProject { created_at: string; project_description: string; project_url: string | null; + original_tweet_url: string | null; media_type: string | null; media_thumbnail: string | null; media_original: string | null; diff --git a/astro-app/src/lib/seen-projects.ts b/astro-app/src/lib/seen-projects.ts new file mode 100644 index 0000000..3ee8d9d --- /dev/null +++ b/astro-app/src/lib/seen-projects.ts @@ -0,0 +1,42 @@ +/** + * Utility functions for tracking seen projects using localStorage + */ + +const SEEN_PROJECTS_KEY = 'twitter-projects-seen'; + +export function getSeenProjects(): Set { + if (typeof window === 'undefined') return new Set(); + + try { + const stored = localStorage.getItem(SEEN_PROJECTS_KEY); + return stored ? new Set(JSON.parse(stored)) : new Set(); + } catch { + return new Set(); + } +} + +export function markProjectAsSeen(projectId: string): void { + if (typeof window === 'undefined') return; + + const seenProjects = getSeenProjects(); + seenProjects.add(projectId); + localStorage.setItem(SEEN_PROJECTS_KEY, JSON.stringify([...seenProjects])); +} + +export function markProjectAsUnseen(projectId: string): void { + if (typeof window === 'undefined') return; + + const seenProjects = getSeenProjects(); + seenProjects.delete(projectId); + localStorage.setItem(SEEN_PROJECTS_KEY, JSON.stringify([...seenProjects])); +} + +export function isProjectSeen(projectId: string): boolean { + if (typeof window === 'undefined') return false; + return getSeenProjects().has(projectId); +} + +export function clearAllSeenProjects(): void { + if (typeof window === 'undefined') return; + localStorage.removeItem(SEEN_PROJECTS_KEY); +} \ No newline at end of file