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 */}
+
+
+
+ {/* 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