Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@tanstack/react-query": "^5.81.2",
"@tanstack/react-router": "^1.121.34",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"@tanstack/zod-form-adapter": "^0.42.1",
"autoprefixer": "^10.4.21",
"class-variance-authority": "^0.7.1",
Expand Down
74 changes: 74 additions & 0 deletions apps/app/src/components/feed/FeedCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { RssFeedItem } from "@/types/rss";

interface FeedCardProps {
item: RssFeedItem;
feedName: string;
feedId: string;
feedImage?: string;
}

export function FeedCard({ item, feedName, feedId, feedImage }: FeedCardProps) {
const handleClick = () => {
if (item.link) {
window.open(item.link, "_blank", "noopener,noreferrer");
}
};

return (
<Card
className="cursor-pointer hover:shadow-md transition-shadow h-full flex flex-col"
onClick={handleClick}
>
<CardContent className="p-4 flex flex-col h-full">
{/* Image */}
{item.image && (
<div className="mb-4">
<img
src={item.image}
alt={item.title}
className="w-full h-40 object-cover rounded-md"
onError={(e) => {
e.currentTarget.style.display = "none";
}}
/>
</div>
)}

{/* Content - flex-grow to push footer to bottom */}
<div className="flex-grow flex flex-col">
{/* Title */}
<h4 className="font-semibold text-base line-clamp-3 mb-3 leading-tight">
{item.title}
</h4>

{/* Description */}
{item.description && (
<p className="text-sm text-muted-foreground line-clamp-3 mb-4 flex-grow">
{item.description}
</p>
)}
</div>

{/* Footer Badge - always at bottom */}
<div className="flex items-center gap-2 mt-auto">
<img
src={feedImage || "/images/feed-image.png"}
alt={feedName}
className="w-5 h-5 rounded-full object-cover flex-shrink-0"
onError={(e) => {
e.currentTarget.src = "/images/feed-image.png";
}}
/>
<Badge variant="secondary" className="text-xs truncate">
{feedName}
</Badge>
<Badge variant="outline" className="text-xs flex-shrink-0">
#{feedId}
</Badge>
</div>
</CardContent>
</Card>
);
}
103 changes: 103 additions & 0 deletions apps/app/src/components/feed/RecentContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { useState, useMemo } from "react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useRssFeed } from "@/hooks/use-rss-feed";
import { FeedCard } from "./FeedCard";

interface RecentContentProps {
feedId: string;
feedName: string;
feedImage?: string;
}

export function RecentContent({
feedId,
feedName,
feedImage,
}: RecentContentProps) {
const [currentPage, setCurrentPage] = useState(0);
const { hasRssFeed, rssData, isLoading, isError } = useRssFeed(feedId);

// Get recent items (latest 9 items, 3 pages of 3 items each)
const recentItems = useMemo(() => {
if (!rssData || rssData.length === 0) return [];

// Sort by date (most recent first) and take first 9 items
const sortedItems = [...rssData]
.sort(
(a, b) => new Date(b.pubDate).getTime() - new Date(a.pubDate).getTime(),
)
.slice(0, 9);

// Group into pages of 3
const pages = [];
for (let i = 0; i < sortedItems.length; i += 3) {
pages.push(sortedItems.slice(i, i + 3));
}

return pages;
}, [rssData]);

const maxPages = recentItems.length;

const handlePrevious = () => {
setCurrentPage((prev) => (prev > 0 ? prev - 1 : maxPages - 1));
};

const handleNext = () => {
setCurrentPage((prev) => (prev < maxPages - 1 ? prev + 1 : 0));
};

// Don't render if no RSS feed or no data
if (!hasRssFeed || isLoading || isError || recentItems.length === 0) {
return null;
}

const currentItems = recentItems[currentPage] || [];

return (
<div className="space-y-4">
{/* Header with Navigation - Responsive */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<h3 className="leading-8 text-xl sm:text-[24px] font-semibold">
Recent Content
</h3>
{maxPages > 1 && (
<div className="flex items-center gap-1">
<Button
variant="outline"
size="icon"
onClick={handlePrevious}
className="h-8 w-8 p-3"
aria-label="Previous page"
>
<ArrowLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={handleNext}
className="h-8 w-8 p-3"
aria-label="Next page"
>
<ArrowRight className="h-4 w-4" />
</Button>
</div>
)}
</div>

{/* Content Cards - Responsive Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{currentItems.map((item, index) => (
<FeedCard
key={item.guid || `${currentPage}-${index}`}
item={item}
feedName={feedName}
feedId={feedId}
feedImage={feedImage}
/>
))}
</div>
</div>
);
}
53 changes: 53 additions & 0 deletions apps/app/src/components/rss-feed/FilterBadges.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";

interface FilterBadge {
type: string;
value: string;
label: string;
}

interface FilterBadgesProps {
activeFilters: FilterBadge[];
onRemoveFilter: (filterType: string) => void;
onClearAll: () => void;
}

export function FilterBadges({
activeFilters,
onRemoveFilter,
onClearAll,
}: FilterBadgesProps) {
if (activeFilters.length === 0) return null;

return (
<div className="flex flex-wrap gap-2 mb-4">
{activeFilters.map((filter) => (
<Badge
key={filter.type}
variant="secondary"
className="inline-flex items-center gap-1 px-3 py-1"
>
<span>{filter.label}</span>
<button
onClick={() => onRemoveFilter(filter.type)}
className="hover:bg-muted rounded-full p-0.5 ml-1"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
{activeFilters.length > 1 && (
<Button
variant="ghost"
size="sm"
onClick={onClearAll}
className="h-auto px-3 py-1 text-xs"
>
Clear all
</Button>
)}
</div>
);
}
99 changes: 99 additions & 0 deletions apps/app/src/components/rss-feed/FilterControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";

interface FilterControlsProps {
showFilters: boolean;
sortBy: "recent" | "oldest";
platformFilter: string;
categoryFilter: string;
availableCategories: string[];
onSortChange: (value: "recent" | "oldest") => void;
onPlatformChange: (value: string) => void;
onCategoryChange: (value: string) => void;
onApplyFilters: () => void;
onResetFilters: () => void;
}

export function FilterControls({
showFilters,
sortBy,
platformFilter,
categoryFilter,
availableCategories,
onSortChange,
onPlatformChange,
onCategoryChange,
onApplyFilters,
onResetFilters,
}: FilterControlsProps) {
if (!showFilters) return null;

return (
<Card className="mb-6">
<CardContent className="p-4">
<div className="flex md:flex-row flex-col w-full justify-between items-center gap-6">
<div className="w-full">
<p className="text-sm font-medium mb-1">Sort By</p>
<Select value={sortBy} onValueChange={onSortChange}>
<SelectTrigger className="bg-input text-foreground">
<SelectValue placeholder="Most Recent" />
</SelectTrigger>
<SelectContent>
<SelectItem value="recent">Most Recent</SelectItem>
<SelectItem value="oldest">Oldest First</SelectItem>
</SelectContent>
</Select>
</div>

<div className="w-full">
<p className="text-sm font-medium mb-1">Platform</p>
<Select value={platformFilter} onValueChange={onPlatformChange}>
<SelectTrigger className="bg-input text-foreground">
<SelectValue placeholder="All Platforms" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Platforms</SelectItem>
<SelectItem value="twitter">Twitter</SelectItem>
<SelectItem value="youtube">YouTube</SelectItem>
<SelectItem value="github">GitHub</SelectItem>
<SelectItem value="reddit">Reddit</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</div>

<div className="w-full">
<p className="text-sm font-medium mb-1">Category</p>
<Select value={categoryFilter} onValueChange={onCategoryChange}>
<SelectTrigger className="bg-input text-foreground">
<SelectValue placeholder="All Categories" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
{availableCategories.map((category) => (
<SelectItem key={category} value={category}>
{category}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>

<div className="flex justify-end mt-4 gap-2">
<Button variant="outline" onClick={onResetFilters}>
Reset
</Button>
<Button onClick={onApplyFilters}>Apply Filters</Button>
</div>
</CardContent>
</Card>
);
}
Loading