mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
feat: enhance article component with favicon support and layout adjustment
This commit is contained in:
parent
25c1fa0f49
commit
be1b6f370f
2 changed files with 42 additions and 23 deletions
|
|
@ -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 */}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue