feat: support link preview
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user