mirror of
https://github.com/0x2E/fusion.git
synced 2026-05-19 18:30:35 +00:00
add gray fallback for missing feed favicons
This commit is contained in:
@@ -30,6 +30,7 @@ import { useArticleNavigation } from "@/hooks/use-keyboard";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { processArticleContent } from "@/lib/content";
|
||||
import { getFaviconUrl } from "@/lib/api/favicon";
|
||||
import { FeedFavicon } from "@/components/feed/feed-favicon";
|
||||
|
||||
export function ArticleDrawer() {
|
||||
const {
|
||||
@@ -206,11 +207,9 @@ export function ArticleDrawer() {
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm">
|
||||
<span className="flex max-w-48 items-center gap-1.5 rounded bg-muted px-2 py-1 text-xs font-medium text-muted-foreground">
|
||||
{feed && (
|
||||
<img
|
||||
<FeedFavicon
|
||||
src={getFaviconUrl(feed.link, feed.site_url)}
|
||||
alt=""
|
||||
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
||||
loading="lazy"
|
||||
className="h-3.5 w-3.5 rounded-sm"
|
||||
/>
|
||||
)}
|
||||
<span className="truncate">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Circle, CircleCheck, Star, ExternalLink } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn, formatDate, extractSummary } from "@/lib/utils";
|
||||
import type { Item } from "@/lib/api";
|
||||
import { FeedFavicon } from "@/components/feed/feed-favicon";
|
||||
|
||||
interface ArticleItemProps {
|
||||
article: Item;
|
||||
@@ -86,14 +87,7 @@ export function ArticleItem({
|
||||
{extractSummary(article.content, 150)}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
{feedFaviconUrl && (
|
||||
<img
|
||||
src={feedFaviconUrl}
|
||||
alt=""
|
||||
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
||||
loading="lazy"
|
||||
/>
|
||||
)}
|
||||
<FeedFavicon src={feedFaviconUrl} className="h-3.5 w-3.5 rounded-sm" />
|
||||
<span className="truncate font-medium text-muted-foreground">
|
||||
{feedName}
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface FeedFaviconProps {
|
||||
src?: string | null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function FeedFavicon({ src, className }: FeedFaviconProps) {
|
||||
const [loadFailed, setLoadFailed] = useState(!src);
|
||||
|
||||
useEffect(() => {
|
||||
setLoadFailed(!src);
|
||||
}, [src]);
|
||||
|
||||
if (!src || loadFailed) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"inline-block shrink-0 rounded bg-gray-300 dark:bg-gray-600",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
className={cn("shrink-0 rounded", className)}
|
||||
loading="lazy"
|
||||
onError={() => setLoadFailed(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { useUrlState } from "@/hooks/use-url-state";
|
||||
import { useUIStore } from "@/store";
|
||||
import { getFaviconUrl } from "@/lib/api/favicon";
|
||||
import type { Feed } from "@/lib/api";
|
||||
import { FeedFavicon } from "@/components/feed/feed-favicon";
|
||||
import { Settings } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
@@ -30,12 +31,7 @@ export function FeedItem({ feed }: FeedItemProps) {
|
||||
isSelected ? "bg-accent text-accent-foreground" : "hover:bg-accent/50",
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={faviconUrl}
|
||||
alt=""
|
||||
className="h-4 w-4 shrink-0 rounded"
|
||||
loading="lazy"
|
||||
/>
|
||||
<FeedFavicon src={faviconUrl} className="h-4 w-4" />
|
||||
<span className="block min-w-0 max-w-full flex-1 truncate">
|
||||
{feed.name}
|
||||
</span>
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useUIStore } from "@/store";
|
||||
import { useFeedLookup } from "@/queries/feeds";
|
||||
import { useUrlState } from "@/hooks/use-url-state";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { FeedFavicon } from "@/components/feed/feed-favicon";
|
||||
|
||||
export function SearchDialog() {
|
||||
const { isSearchOpen, setSearchOpen, setEditFeedOpen } = useUIStore();
|
||||
@@ -142,11 +143,9 @@ export function SearchDialog() {
|
||||
onSelect={() => handleSelectFeed(feed.id)}
|
||||
className="group gap-2"
|
||||
>
|
||||
<img
|
||||
<FeedFavicon
|
||||
src={getFaviconUrl(feed.link, feed.site_url)}
|
||||
alt=""
|
||||
className="h-4 w-4 shrink-0 rounded-sm"
|
||||
loading="lazy"
|
||||
className="h-4 w-4 rounded-sm"
|
||||
/>
|
||||
<span className="flex-1 truncate">{feed.name}</span>
|
||||
<Button
|
||||
|
||||
@@ -35,6 +35,10 @@ export function getFaviconUrl(feedLink: string, siteUrl?: string): string {
|
||||
domain = extractDomainFromFeedLink(feedLink);
|
||||
}
|
||||
|
||||
if (!domain) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`;
|
||||
}
|
||||
|
||||
@@ -53,6 +57,6 @@ function extractDomainFromFeedLink(feedLink: string): string {
|
||||
|
||||
return hostname;
|
||||
} catch {
|
||||
return "example.com";
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
} from "@/queries/feeds";
|
||||
import { useDeleteGroup, useGroups, useUpdateGroup } from "@/queries/groups";
|
||||
import { useUIStore } from "@/store";
|
||||
import { FeedFavicon } from "@/components/feed/feed-favicon";
|
||||
|
||||
export const Route = createLazyFileRoute("/feeds")({
|
||||
component: FeedsPage,
|
||||
@@ -246,11 +247,16 @@ function FeedsPage() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
{(Object.keys(statusFilterLabels) as StatusFilter[]).map((key) => (
|
||||
<DropdownMenuItem key={key} onSelect={() => setStatusFilter(key)}>
|
||||
{statusFilterLabels[key]}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{(Object.keys(statusFilterLabels) as StatusFilter[]).map(
|
||||
(key) => (
|
||||
<DropdownMenuItem
|
||||
key={key}
|
||||
onSelect={() => setStatusFilter(key)}
|
||||
>
|
||||
{statusFilterLabels[key]}
|
||||
</DropdownMenuItem>
|
||||
),
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
@@ -317,7 +323,10 @@ function FeedsPage() {
|
||||
const isEditing = editingGroupId === group.id;
|
||||
|
||||
return (
|
||||
<div key={group.id} className="overflow-hidden rounded-lg border">
|
||||
<div
|
||||
key={group.id}
|
||||
className="overflow-hidden rounded-lg border"
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!isEditing) toggleGroup(group.id);
|
||||
@@ -421,11 +430,9 @@ function FeedsPage() {
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2.5">
|
||||
<img
|
||||
<FeedFavicon
|
||||
src={getFaviconUrl(feed.link, feed.site_url)}
|
||||
alt=""
|
||||
className="h-5 w-5 shrink-0 rounded"
|
||||
loading="lazy"
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-[13px] font-medium">
|
||||
|
||||
Reference in New Issue
Block a user