mirror of
https://github.com/FranP-code/format_twitter_projects_accounts_tweets.git
synced 2025-10-13 00:32:19 +00:00
Add tweet URL column and seen tracking system
This commit is contained in:
committed by
GitHub
parent
7426c3d8a2
commit
2875936f12
@@ -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 { formatDistanceToNow } from 'date-fns';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
import type { TwitterProject } from '@/lib/csv-loader';
|
import type { TwitterProject } from '@/lib/csv-loader';
|
||||||
|
import { markProjectAsSeen, markProjectAsUnseen, isProjectSeen } from '@/lib/seen-projects';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
|
||||||
interface ProjectCardProps {
|
interface ProjectCardProps {
|
||||||
project: TwitterProject;
|
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 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 (
|
return (
|
||||||
<div className="bg-card rounded-lg border p-6 space-y-4 hover:shadow-lg transition-shadow">
|
<div className={`bg-card rounded-lg border p-6 space-y-4 hover:shadow-lg transition-all ${seen ? 'opacity-75 bg-muted/50' : ''}`}>
|
||||||
{/* Author Info */}
|
{/* Author Info */}
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-semibold">
|
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-semibold">
|
||||||
@@ -42,9 +62,88 @@ export function ProjectCard({ project }: ProjectCardProps) {
|
|||||||
<p className="text-foreground leading-relaxed">{project.project_description}</p>
|
<p className="text-foreground leading-relaxed">{project.project_description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Project URL */}
|
{/* Project Actions */}
|
||||||
{project.project_url && (
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div className="flex items-center space-x-4">
|
||||||
|
{/* Project URL */}
|
||||||
|
{project.project_url && (
|
||||||
|
<a
|
||||||
|
href={project.project_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center space-x-2 text-primary hover:text-primary/80 transition-colors"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
<span className="text-sm font-medium">View Project</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tweet URL */}
|
||||||
|
{project.original_tweet_url && (
|
||||||
|
<a
|
||||||
|
href={project.original_tweet_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center space-x-2 text-blue-500 hover:text-blue-400 transition-colors"
|
||||||
|
>
|
||||||
|
<Twitter className="w-4 h-4" />
|
||||||
|
<span className="text-sm font-medium">View Tweet</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Seen Toggle */}
|
||||||
|
<Button
|
||||||
|
variant={seen ? "secondary" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={toggleSeen}
|
||||||
|
className="ml-auto"
|
||||||
|
>
|
||||||
|
{seen ? (
|
||||||
|
<>
|
||||||
|
<X className="w-4 h-4 mr-1" />
|
||||||
|
Mark Unseen
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Check className="w-4 h-4 mr-1" />
|
||||||
|
Mark Seen
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Engagement Stats */}
|
||||||
|
<div className="flex items-center space-x-6 pt-2 border-t">
|
||||||
|
<div className="flex items-center space-x-1 text-muted-foreground">
|
||||||
|
<Heart className="w-4 h-4" />
|
||||||
|
<span className="text-sm">{project.favorite_count.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-1 text-muted-foreground">
|
||||||
|
<Repeat2 className="w-4 h-4" />
|
||||||
|
<span className="text-sm">{project.retweet_count.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-1 text-muted-foreground">
|
||||||
|
<MessageCircle className="w-4 h-4" />
|
||||||
|
<span className="text-sm">{project.reply_count.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-1 text-muted-foreground">
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
<span className="text-sm">{project.views_count.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category */}
|
||||||
|
{project.category && (
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<span className="inline-block px-2 py-1 text-xs font-medium bg-secondary text-secondary-foreground rounded-full">
|
||||||
|
{project.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
<a
|
<a
|
||||||
href={project.project_url}
|
href={project.project_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -10,27 +10,64 @@ import {
|
|||||||
type SortingState,
|
type SortingState,
|
||||||
type ColumnFiltersState
|
type ColumnFiltersState
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import { ArrowUpDown, ArrowUp, ArrowDown, Search, ExternalLink } from 'lucide-react';
|
import { ArrowUpDown, ArrowUp, ArrowDown, Search, ExternalLink, Twitter, Check, X } from 'lucide-react';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
import type { TwitterProject } from '@/lib/csv-loader';
|
import type { TwitterProject } from '@/lib/csv-loader';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { Input } from './ui/input';
|
import { Input } from './ui/input';
|
||||||
import { Select } from './ui/select';
|
import { Select } from './ui/select';
|
||||||
|
import { markProjectAsSeen, markProjectAsUnseen, isProjectSeen } from '@/lib/seen-projects';
|
||||||
|
|
||||||
interface ProjectsTableProps {
|
interface ProjectsTableProps {
|
||||||
projects: TwitterProject[];
|
projects: TwitterProject[];
|
||||||
title: string;
|
title: string;
|
||||||
showUrlColumn?: boolean;
|
showUrlColumn?: boolean;
|
||||||
|
onSeenStatusChange?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<TwitterProject>();
|
const columnHelper = createColumnHelper<TwitterProject>();
|
||||||
|
|
||||||
export function ProjectsTable({ projects, title, showUrlColumn = true }: ProjectsTableProps) {
|
export function ProjectsTable({ projects, title, showUrlColumn = true, onSeenStatusChange }: ProjectsTableProps) {
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
const [globalFilter, setGlobalFilter] = useState('');
|
const [globalFilter, setGlobalFilter] = useState('');
|
||||||
|
const [seenProjects, setSeenProjects] = useState<Set<string>>(new Set());
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Update seen projects state when component mounts or projects change
|
||||||
|
const updateSeenState = () => {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
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(() => [
|
const columns = useMemo(() => [
|
||||||
columnHelper.accessor('author_name', {
|
columnHelper.accessor('author_name', {
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
@@ -109,6 +146,27 @@ export function ProjectsTable({ projects, title, showUrlColumn = true }: Project
|
|||||||
size: 100,
|
size: 100,
|
||||||
})
|
})
|
||||||
] : []),
|
] : []),
|
||||||
|
columnHelper.accessor('original_tweet_url', {
|
||||||
|
header: 'Tweet',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="w-full flex justify-center">
|
||||||
|
{row.original.original_tweet_url ? (
|
||||||
|
<a
|
||||||
|
href={row.original.original_tweet_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center space-x-1 text-blue-500 hover:text-blue-400 transition-colors"
|
||||||
|
>
|
||||||
|
<Twitter className="w-4 h-4" />
|
||||||
|
<span className="text-sm">Tweet</span>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground text-sm">No Tweet</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
size: 100,
|
||||||
|
}),
|
||||||
columnHelper.accessor('media_thumbnail', {
|
columnHelper.accessor('media_thumbnail', {
|
||||||
header: 'Media',
|
header: 'Media',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
@@ -203,7 +261,39 @@ export function ProjectsTable({ projects, title, showUrlColumn = true }: Project
|
|||||||
),
|
),
|
||||||
size: 140,
|
size: 140,
|
||||||
}),
|
}),
|
||||||
], [showUrlColumn]);
|
columnHelper.display({
|
||||||
|
id: 'actions',
|
||||||
|
header: 'Actions',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const projectId = row.original.id;
|
||||||
|
const isSeen = seenProjects.has(projectId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex justify-center">
|
||||||
|
<Button
|
||||||
|
variant={isSeen ? "secondary" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => toggleSeen(projectId)}
|
||||||
|
className="h-8"
|
||||||
|
>
|
||||||
|
{isSeen ? (
|
||||||
|
<>
|
||||||
|
<X className="w-3 h-3 mr-1" />
|
||||||
|
Unseen
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Check className="w-3 h-3 mr-1" />
|
||||||
|
Seen
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
size: 120,
|
||||||
|
}),
|
||||||
|
], [showUrlColumn, seenProjects]);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: projects,
|
data: projects,
|
||||||
@@ -307,7 +397,7 @@ export function ProjectsTable({ projects, title, showUrlColumn = true }: Project
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={row.id}
|
key={row.id}
|
||||||
className="absolute w-full border-b border-border hover:bg-muted/50 transition-colors grid"
|
className={`absolute w-full border-b border-border hover:bg-muted/50 transition-colors grid ${seenProjects.has(row.original.id) ? 'opacity-60 bg-muted/30' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
height: `${virtualRow.size}px`,
|
height: `${virtualRow.size}px`,
|
||||||
transform: `translateY(${virtualRow.start}px)`,
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { ProjectsTable } from './ProjectsTable';
|
import { ProjectsTable } from './ProjectsTable';
|
||||||
import { ProjectCard } from './ProjectCard';
|
import { ProjectCard } from './ProjectCard';
|
||||||
import { ThemeToggle } from './ThemeToggle';
|
import { ThemeToggle } from './ThemeToggle';
|
||||||
import { LayoutGrid, Table } from 'lucide-react';
|
import { LayoutGrid, Table, Trash2 } from 'lucide-react';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import type { TwitterProject } from '@/lib/csv-loader';
|
import type { TwitterProject } from '@/lib/csv-loader';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||||
|
import { getSeenProjects, clearAllSeenProjects } from '@/lib/seen-projects';
|
||||||
|
|
||||||
interface ProjectsViewProps {
|
interface ProjectsViewProps {
|
||||||
projects: TwitterProject[];
|
projects: TwitterProject[];
|
||||||
@@ -13,12 +14,30 @@ interface ProjectsViewProps {
|
|||||||
|
|
||||||
export function ProjectsView({ projects }: ProjectsViewProps) {
|
export function ProjectsView({ projects }: ProjectsViewProps) {
|
||||||
const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
|
const [viewMode, setViewMode] = useState<'table' | 'cards'>('table');
|
||||||
|
const [seenProjectIds, setSeenProjectIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
// Separate projects with and without URLs
|
// Update seen projects state
|
||||||
const projectsWithUrls = projects.filter(p => p.project_url);
|
const updateSeenProjects = useCallback(() => {
|
||||||
const projectsWithoutUrls = projects.filter(p => !p.project_url);
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
@@ -50,64 +69,125 @@ export function ProjectsView({ projects }: ProjectsViewProps) {
|
|||||||
<LayoutGrid className="w-4 h-4" />
|
<LayoutGrid className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{seenProjects.length > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleClearSeenProjects}
|
||||||
|
className="text-destructive hover:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
|
Clear Seen ({seenProjects.length})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Statistics */}
|
{/* Statistics */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-4 mb-8">
|
||||||
<div className="bg-card rounded-lg border p-6">
|
<div className="bg-card rounded-lg border p-6">
|
||||||
<div className="text-2xl font-bold text-foreground">{projects.length}</div>
|
<div className="text-2xl font-bold text-foreground">{projects.length}</div>
|
||||||
<div className="text-muted-foreground">Total Projects</div>
|
<div className="text-muted-foreground">Total Projects</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-card rounded-lg border p-6">
|
<div className="bg-card rounded-lg border p-6">
|
||||||
<div className="text-2xl font-bold text-green-600">{projectsWithUrls.length}</div>
|
<div className="text-2xl font-bold text-blue-600">{unseenProjects.length}</div>
|
||||||
<div className="text-muted-foreground">With Project URLs</div>
|
<div className="text-muted-foreground">Unseen Projects</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-card rounded-lg border p-6">
|
<div className="bg-card rounded-lg border p-6">
|
||||||
<div className="text-2xl font-bold text-orange-600">{projectsWithoutUrls.length}</div>
|
<div className="text-2xl font-bold text-green-600">{seenProjects.length}</div>
|
||||||
<div className="text-muted-foreground">Missing URLs</div>
|
<div className="text-muted-foreground">Seen Projects</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-card rounded-lg border p-6">
|
||||||
|
<div className="text-2xl font-bold text-purple-600">{projects.filter(p => p.project_url).length}</div>
|
||||||
|
<div className="text-muted-foreground">With URLs</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-card rounded-lg border p-6">
|
||||||
|
<div className="text-2xl font-bold text-orange-600">{projects.filter(p => !p.project_url).length}</div>
|
||||||
|
<div className="text-muted-foreground">No URLs</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<Tabs defaultValue="with-urls" className="space-y-6">
|
<Tabs defaultValue="unseen-with-urls" className="space-y-6">
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
<TabsList className="grid w-full grid-cols-4">
|
||||||
<TabsTrigger value="with-urls">
|
<TabsTrigger value="with-urls">
|
||||||
Projects with URLs ({projectsWithUrls.length})
|
Unseen w/ URLs ({unseenWithUrls.length})
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="without-urls">
|
<TabsTrigger value="without-urls">
|
||||||
Missing URLs ({projectsWithoutUrls.length})
|
Unseen w/o URLs ({unseenWithoutUrls.length})
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="seen-with-urls">
|
||||||
|
Seen w/ URLs ({seenWithUrls.length})
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="seen-without-urls">
|
||||||
|
Seen w/o URLs ({seenWithoutUrls.length})
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="with-urls" className="space-y-6">
|
<TabsContent value="unseen-with-urls" className="space-y-6">
|
||||||
{viewMode === 'table' ? (
|
{viewMode === 'table' ? (
|
||||||
<ProjectsTable
|
<ProjectsTable
|
||||||
projects={projectsWithUrls}
|
projects={unseenWithUrls}
|
||||||
title="Projects with URLs"
|
title="Unseen Projects with URLs"
|
||||||
showUrlColumn={true}
|
showUrlColumn={true}
|
||||||
|
onSeenStatusChange={updateSeenProjects}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{projectsWithUrls.map((project) => (
|
{unseenWithUrls.map((project) => (
|
||||||
<ProjectCard key={project.id} project={project} />
|
<ProjectCard key={project.id} project={project} onSeenStatusChange={updateSeenProjects} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="without-urls" className="space-y-6">
|
<TabsContent value="unseen-without-urls" className="space-y-6">
|
||||||
{viewMode === 'table' ? (
|
{viewMode === 'table' ? (
|
||||||
<ProjectsTable
|
<ProjectsTable
|
||||||
projects={projectsWithoutUrls}
|
projects={unseenWithoutUrls}
|
||||||
title="Projects without URLs"
|
title="Unseen Projects without URLs"
|
||||||
showUrlColumn={false}
|
showUrlColumn={false}
|
||||||
|
onSeenStatusChange={updateSeenProjects}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{projectsWithoutUrls.map((project) => (
|
{unseenWithoutUrls.map((project) => (
|
||||||
<ProjectCard key={project.id} project={project} />
|
<ProjectCard key={project.id} project={project} onSeenStatusChange={updateSeenProjects} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="seen-with-urls" className="space-y-6">
|
||||||
|
{viewMode === 'table' ? (
|
||||||
|
<ProjectsTable
|
||||||
|
projects={seenWithUrls}
|
||||||
|
title="Seen Projects with URLs"
|
||||||
|
showUrlColumn={true}
|
||||||
|
onSeenStatusChange={updateSeenProjects}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{seenWithUrls.map((project) => (
|
||||||
|
<ProjectCard key={project.id} project={project} onSeenStatusChange={updateSeenProjects} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="seen-without-urls" className="space-y-6">
|
||||||
|
{viewMode === 'table' ? (
|
||||||
|
<ProjectsTable
|
||||||
|
projects={seenWithoutUrls}
|
||||||
|
title="Seen Projects without URLs"
|
||||||
|
showUrlColumn={false}
|
||||||
|
onSeenStatusChange={updateSeenProjects}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{seenWithoutUrls.map((project) => (
|
||||||
|
<ProjectCard key={project.id} project={project} onSeenStatusChange={updateSeenProjects} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface TwitterProject {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
project_description: string;
|
project_description: string;
|
||||||
project_url: string | null;
|
project_url: string | null;
|
||||||
|
original_tweet_url: string | null;
|
||||||
media_type: string | null;
|
media_type: string | null;
|
||||||
media_thumbnail: string | null;
|
media_thumbnail: string | null;
|
||||||
media_original: string | null;
|
media_original: string | null;
|
||||||
|
|||||||
42
astro-app/src/lib/seen-projects.ts
Normal file
42
astro-app/src/lib/seen-projects.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Utility functions for tracking seen projects using localStorage
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SEEN_PROJECTS_KEY = 'twitter-projects-seen';
|
||||||
|
|
||||||
|
export function getSeenProjects(): Set<string> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user