feat: support link preview

This commit is contained in:
oiov
2024-08-09 15:51:44 +08:00
parent 8b18680647
commit 88b499854b
7 changed files with 168 additions and 57 deletions
+1 -1
View File
@@ -162,7 +162,7 @@ export default function UsersList({ user }: UrlListProps) {
key={user.id}
className="grid animate-fade-in grid-cols-3 items-center animate-in sm:grid-cols-7"
>
<TableCell className="col-span-1">
<TableCell className="col-span-1 truncate">
<TooltipProvider>
<Tooltip delayDuration={200}>
<TooltipTrigger className="truncate">
@@ -38,6 +38,7 @@ import CountUpFn from "@/components/dashboard/count-up";
import StatusDot from "@/components/dashboard/status-dot";
import { FormType, RecordForm } from "@/components/forms/record-form";
import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
import { LinkPreviewer } from "@/components/shared/link-previewer";
export interface RecordListProps {
user: Pick<User, "id" | "name">;
@@ -211,16 +212,14 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
</Badge>
</TableCell>
<TableCell className="col-span-1">
<Link
className="text-slate-600 hover:text-blue-400 hover:underline dark:text-slate-400"
href={"https://" + record.name}
target="_blank"
prefetch={false}
>
{record.name.endsWith(".wr.do")
? record.name.slice(0, -6)
: record.name}
</Link>
<LinkPreviewer
url={"https://" + record.name}
formatUrl={
"https://" + record.name.endsWith(".wr.do")
? record.name.slice(0, -6)
: record.name
}
/>
</TableCell>
<TableCell className="col-span-2 hidden truncate text-nowrap sm:inline-block">
<TooltipProvider>
@@ -26,11 +26,22 @@ import {
} from "@/components/ui/select";
import BlurImage from "@/components/shared/blur-image";
export interface MetaScrapingProps {
title: string;
description: string;
image: string;
icon: string;
url: string;
lang: string;
author: string;
timestamp: string;
}
export default function MetaScraping() {
const { theme } = useTheme();
const [currentLink, setCurrentLink] = useState("wr.do");
const [protocol, setProtocol] = useState("https://");
const [metaInfo, setMetaInfo] = useState({
const [metaInfo, setMetaInfo] = useState<MetaScrapingProps>({
title: "",
description: "",
image: "",
@@ -68,23 +79,21 @@ export default function MetaScraping() {
};
const handleScrapingScreenshot = async () => {
if (currentScreenshotLink) {
setIsShoting(true);
const res = await fetch(
`/api/scraping/screenshot?url=${protocol}${currentScreenshotLink}`,
);
console.log(res);
if (!res.ok || res.status !== 200) {
toast.error(res.statusText);
} else {
const data = await res.json();
console.log(data);
setScreenshotInfo(data);
toast.success("Success!");
}
setIsShoting(false);
}
// if (currentScreenshotLink) {
// setIsShoting(true);
// const res = await fetch(
// `/api/scraping/screenshot?url=${protocol}${currentScreenshotLink}`,
// );
// if (!res.ok || res.status !== 200) {
// toast.error(res.statusText);
// } else {
// const data = await res.json();
// console.log(data);
// setScreenshotInfo(data);
// toast.success("Success!");
// }
// setIsShoting(false);
// }
};
return (
+6 -15
View File
@@ -35,6 +35,7 @@ import {
} from "@/components/ui/table";
import {
Tooltip,
TooltipArrow,
TooltipContent,
TooltipProvider,
TooltipTrigger,
@@ -45,6 +46,7 @@ import { FormType } from "@/components/forms/record-form";
import { UrlForm } from "@/components/forms/url-form";
import { CopyButton } from "@/components/shared/copy-button";
import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
import { LinkPreviewer } from "@/components/shared/link-previewer";
import UserUrlMetaInfo from "./meta";
@@ -219,21 +221,10 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
/>
</TableCell>
<TableCell className="col-span-1 truncate sm:col-span-2">
<TooltipProvider>
<Tooltip delayDuration={200}>
<TooltipTrigger className="truncate">
<Link
className="line-clamp-2 overflow-hidden overflow-ellipsis whitespace-normal text-slate-600 hover:text-blue-400 hover:underline dark:text-slate-400"
href={short.target}
target="_blank"
prefetch={false}
>
{removeUrlSuffix(short.target)}
</Link>
</TooltipTrigger>
<TooltipContent>{short.target}</TooltipContent>
</Tooltip>
</TooltipProvider>
<LinkPreviewer
url={short.target}
formatUrl={removeUrlSuffix(short.target)}
/>
</TableCell>
<TableCell className="col-span-1 hidden justify-center truncate sm:flex">
<TooltipProvider>
+1 -1
View File
@@ -4,7 +4,7 @@ import { checkUserStatus } from "@/lib/dto/user";
import { getCurrentUser } from "@/lib/session";
import { isLink } from "@/lib/utils";
export const revalidate = 60;
export const revalidate = 600;
export async function GET(req: Request) {
try {
+103
View File
@@ -0,0 +1,103 @@
import { useEffect, useState } from "react";
import Link from "next/link";
import BlogPost from "@/public/_static/illustrations/rocket-crashed.svg";
import { MetaScrapingProps } from "@/app/(protected)/dashboard/scrape/meta-scraping";
import { Skeleton } from "../ui/skeleton";
import {
Tooltip,
TooltipArrow,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
import BlurImage from "./blur-image";
export function LinkPreviewer({
url,
formatUrl,
}: {
url: string;
formatUrl: string;
}) {
const [open, setOpen] = useState(false);
const [isScraping, setIsScraping] = useState(false);
const [metaInfo, setMetaInfo] = useState<MetaScrapingProps | null>(null);
const handleScrapingMeta = async () => {
if (url && open) {
setIsScraping(true);
const res = await fetch(`/api/scraping/meta?url=${url}`);
if (!res.ok || res.status !== 200) {
} else {
const data = await res.json();
setMetaInfo(data);
}
setIsScraping(false);
}
};
useEffect(() => {
handleScrapingMeta();
}, [url, open]);
const renderLink = (text: string) => (
<Link
className="mt-2 line-clamp-2 max-w-64 overflow-hidden truncate overflow-ellipsis whitespace-normal text-slate-600 after:content-['↗'] hover:underline group-hover:text-blue-400 dark:text-slate-400"
href={url}
target="_blank"
prefetch={false}
>
{text}
</Link>
);
return (
<TooltipProvider>
<Tooltip open={open} delayDuration={200}>
<TooltipTrigger
className="truncate hover:text-blue-400 hover:underline"
onClick={() => setOpen(!open)}
>
{formatUrl}
</TooltipTrigger>
<TooltipContent
className="group flex h-64 w-72 flex-col items-center justify-center py-3 shadow-inner transition-all duration-200 hover:bg-gray-50"
onPointerDownOutside={() => setOpen(false)}
onMouseLeave={() => setOpen(false)}
>
<TooltipArrow className="fill-gray-400" />
{isScraping ? (
<div className="relative flex h-full w-full items-center justify-center">
<Skeleton className="absolute h-full w-full rounded-lg" />
<p>Previewing...</p>
</div>
) : metaInfo?.title !== "" ? (
<div className="relative flex h-full w-full items-end justify-start rounded-lg border bg-primary-foreground p-3 group-hover:opacity-80">
<BlurImage
className="absolute left-1/2 top-24 h-2/3 w-2/3 -translate-x-1/2 -translate-y-1/2 transform rounded-lg group-hover:scale-105"
src={metaInfo?.image || BlogPost}
alt={metaInfo?.title || ""}
/>
<div className="max-w-64 space-y-1 truncate">
{renderLink(metaInfo?.title ?? url)}
<p className="max-w-64 truncate text-xs">
{metaInfo?.description}
</p>
</div>
</div>
) : (
<>
<p className="text-lg font-bold text-slate-600">
Faild to preview link
</p>
{renderLink(formatUrl ?? url)}
</>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
0;
+21 -12
View File
@@ -1,17 +1,19 @@
"use client"
"use client";
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipArrow = TooltipPrimitive.Arrow;
const TooltipPortal = TooltipPrimitive.Portal
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipPortal = TooltipPrimitive.Portal;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
@@ -22,11 +24,18 @@ const TooltipContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-[99999] overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
className
className,
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipPortal, TooltipProvider }
export {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipPortal,
TooltipProvider,
TooltipArrow,
};