feat: enhance article component with favicon support and layout adjustment

This commit is contained in:
Anish Sarkar 2026-02-11 21:33:46 +05:30
parent 25c1fa0f49
commit be1b6f370f
2 changed files with 42 additions and 23 deletions

View file

@ -8,7 +8,8 @@ import {
FileTextIcon, FileTextIcon,
UserIcon, UserIcon,
} from "lucide-react"; } from "lucide-react";
import { Component, type ReactNode, useCallback } from "react"; import { Component, type ReactNode, useCallback, useState } from "react";
import Image from "next/image";
import { z } from "zod"; import { z } from "zod";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
@ -126,6 +127,30 @@ function formatWordCount(count: number): string {
return `${count} words`; return `${count} words`;
} }
/**
* Favicon component that fetches the site icon via Google's favicon service,
* falling back to BookOpenIcon on error.
*/
function SiteFavicon({ domain }: { domain: string }) {
const [failed, setFailed] = useState(false);
if (failed) {
return <BookOpenIcon className="size-5 text-primary" />;
}
return (
<Image
src={`https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=64`}
alt={`${domain} favicon`}
width={28}
height={28}
className="size-5 sm:size-7 rounded-sm"
onError={() => setFailed(true)}
unoptimized
/>
);
}
/** /**
* Article card component for displaying scraped webpage content * Article card component for displaying scraped webpage content
*/ */
@ -198,27 +223,33 @@ export function Article({
}} }}
> >
{/* Header */} {/* Header */}
<CardContent className="p-4"> <CardContent className="p-3 sm:p-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-2.5 sm:gap-3">
{/* Icon */} {/* Favicon / Icon */}
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/10"> {domain ? (
<BookOpenIcon className="size-5 text-primary" /> <div className="flex size-8 sm:size-10 shrink-0 items-center justify-center">
</div> <SiteFavicon domain={domain} />
</div>
) : (
<div className="flex size-8 sm:size-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
<BookOpenIcon className="size-4 sm:size-5 text-primary" />
</div>
)}
{/* Content */} {/* Content */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{/* Title */} {/* Title */}
<h3 className="font-semibold text-sm line-clamp-2 group-hover:text-primary transition-colors"> <h3 className="font-semibold text-xs sm:text-sm line-clamp-2 group-hover:text-primary transition-colors">
{title} {title}
</h3> </h3>
{/* Description */} {/* Description */}
{description && ( {description && (
<p className="text-muted-foreground text-xs mt-1 line-clamp-2">{description}</p> <p className="text-muted-foreground text-[10px] sm:text-xs mt-1 line-clamp-2">{description}</p>
)} )}
{/* Metadata row */} {/* Metadata row */}
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 mt-2 text-xs text-muted-foreground"> <div className="flex flex-wrap items-center gap-x-2 sm:gap-x-3 gap-y-1 mt-1.5 sm:mt-2 text-[10px] sm:text-xs text-muted-foreground">
{domain && ( {domain && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@ -275,12 +306,6 @@ export function Article({
</div> </div>
</div> </div>
{/* External link indicator */}
{href && (
<div className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
<ExternalLinkIcon className="size-4 text-muted-foreground" />
</div>
)}
</div> </div>
{/* Response actions */} {/* Response actions */}

View file

@ -87,18 +87,12 @@ function ScrapeCancelledState({ url }: { url: string }) {
* Parsed Article component with error handling * Parsed Article component with error handling
*/ */
function ParsedArticle({ result }: { result: unknown }) { function ParsedArticle({ result }: { result: unknown }) {
const article = parseSerializableArticle(result); const { description, ...article } = parseSerializableArticle(result);
return ( return (
<Article <Article
{...article} {...article}
maxWidth="480px" maxWidth="480px"
responseActions={[{ id: "open", label: "Open Link", variant: "default" }]}
onResponseAction={(id) => {
if (id === "open" && article.href) {
window.open(article.href, "_blank", "noopener,noreferrer");
}
}}
/> />
); );
} }