refact link preview cpm

This commit is contained in:
oiov
2024-11-01 15:50:58 +08:00
parent 413abe900b
commit 257dc285b9
7 changed files with 72 additions and 79 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ export default async function DashboardPage() {
linkText="Short urls."
/>
<UserUrlsList
user={{ id: user.id, name: user.name || "" }}
user={{ id: user.id, name: user.name || "", apiKey: user.apiKey || "" }}
action="/api/url/admin"
/>
</>
+1 -1
View File
@@ -25,7 +25,7 @@ export default async function DashboardPage() {
linkText="Short urls."
/>
<UserUrlsList
user={{ id: user.id, name: user.name || "" }}
user={{ id: user.id, name: user.name || "", apiKey: user.apiKey || "" }}
action="/api/url"
/>
</>
+2 -1
View File
@@ -50,7 +50,7 @@ import { PaginationWrapper } from "@/components/shared/pagination";
import UserUrlMetaInfo from "./meta";
export interface UrlListProps {
user: Pick<User, "id" | "name">;
user: Pick<User, "id" | "name" | "apiKey">;
action: string;
}
@@ -221,6 +221,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
</TableCell>
<TableCell className="col-span-1 truncate sm:col-span-2">
<LinkPreviewer
apiKey={user.apiKey ?? ""}
url={short.target}
formatUrl={removeUrlSuffix(short.target)}
/>
+4 -6
View File
@@ -1,7 +1,5 @@
import { env } from "@/env.mjs";
import { checkApiKey } from "@/lib/dto/api-key";
import { checkUserStatus } from "@/lib/dto/user";
import { getCurrentUser } from "@/lib/session";
import { isLink } from "@/lib/utils";
export const revalidate = 60;
@@ -13,10 +11,10 @@ export async function GET(req: Request) {
const url = new URL(req.url);
const link = url.searchParams.get("url");
const full = url.searchParams.get("full") || "false";
const width = url.searchParams.get("width") || "1600";
const height = url.searchParams.get("height") || "1200";
const viewportWidth = url.searchParams.get("viewportWidth") || "1080";
const viewportHeight = url.searchParams.get("viewportHeight") || "1080";
const width = url.searchParams.get("width") || "1200";
const height = url.searchParams.get("height") || "750";
const viewportWidth = url.searchParams.get("viewportWidth") || "1200";
const viewportHeight = url.searchParams.get("viewportHeight") || "750";
const forceReload = url.searchParams.get("forceReload") || "false";
const isMobile = url.searchParams.get("isMobile") || "false";
const isDarkMode = url.searchParams.get("isDarkMode") || "false";
+1 -1
View File
@@ -40,7 +40,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
<SessionProvider>
<ThemeProvider
attribute="class"
defaultTheme="dark"
defaultTheme="light"
enableSystem
disableTransitionOnChange
>
+19
View File
@@ -129,4 +129,23 @@ export const Icons = {
link: Link,
mail: Mail,
bug: Bug,
outLink: ({ ...props }: LucideProps) => (
<svg
width="0.95em"
height="0.95em"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
{...props}
>
<path
d="M20 13.5001C20 14.8946 20 15.5919 19.8618 16.1673C19.4229 17.9956 17.9955 19.423 16.1672 19.8619C15.5918 20.0001 14.8945 20.0001 13.5 20.0001H12C9.19974 20.0001 7.79961 20.0001 6.73005 19.4551C5.78924 18.9758 5.02433 18.2109 4.54497 17.2701C4 16.2005 4 14.8004 4 12.0001V11.5001C4 9.17035 4 8.0055 4.3806 7.08664C4.88807 5.8615 5.86144 4.88813 7.08658 4.38066C7.86344 4.05888 8.81614 4.00915 10.5 4.00146M19.7597 9.45455C20.0221 7.8217 20.0697 6.16984 19.9019 4.54138C19.8898 4.42328 19.838 4.31854 19.7597 4.24027M19.7597 4.24027C19.6815 4.16201 19.5767 4.11023 19.4586 4.09806C17.8302 3.93025 16.1783 3.97792 14.5455 4.24027M19.7597 4.24027L10 14"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
></path>
</svg>
),
};
+44 -69
View File
@@ -1,8 +1,5 @@
import { useEffect, useState } from "react";
import { 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 {
@@ -12,94 +9,72 @@ import {
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
import BlurImage, { BlurImg } from "./blur-image";
import BlurImage from "./blur-image";
import { Icons } from "./icons";
export function LinkPreviewer({
apiKey,
url,
formatUrl,
}: {
apiKey: string;
url: string;
formatUrl: string;
}) {
const [open, setOpen] = useState(false);
const [isScraping, setIsScraping] = useState(false);
const [metaInfo, setMetaInfo] = useState<MetaScrapingProps | null>(null);
const [screenshotInfo, setScreenshotInfo] = useState({
tmp_url: "",
});
const handleScrapingMeta = async () => {
if (url && open) {
setIsScraping(true);
const res = await fetch(`/api/scraping/meta?url=${url}`);
const handleScrapingScreenshot = async () => {
if (url) {
const res = await fetch(
`/api/scraping/screenshot?url=${url}&key=${apiKey}&width=1200&height=750&viewportWidth=1200&viewportHeight=750`,
);
if (!res.ok || res.status !== 200) {
} else {
const data = await res.json();
setMetaInfo(data);
const blob = await res.blob();
const imageUrl = URL.createObjectURL(blob);
setScreenshotInfo({
tmp_url: imageUrl,
});
}
setIsScraping(false);
}
};
useEffect(() => {
handleScrapingMeta();
}, [url, open]);
const renderLink = (text: string) => (
<Link
className="line-clamp-1 max-w-64 overflow-hidden truncate overflow-ellipsis whitespace-normal text-slate-600 transition-colors duration-300 after:content-['↗'] hover:underline group-hover:text-blue-400 dark:text-slate-400"
href={url}
target="_blank"
prefetch={false}
>
{text}
</Link>
);
const handleOpenChange = (open: boolean) => {
if (open) {
handleScrapingScreenshot();
}
};
return (
<TooltipProvider>
<Tooltip open={open} delayDuration={200}>
<TooltipTrigger
className="truncate hover:text-blue-400 hover:underline"
onClick={() => setOpen(!open)}
>
{formatUrl}
<Tooltip delayDuration={200} onOpenChange={handleOpenChange}>
<TooltipTrigger className="truncate hover:text-blue-400 hover:underline">
<Link className="flex items-center" target="_blank" href={url}>
{formatUrl} <Icons.outLink className="ml-0.5 mt-0.5" />
</Link>
</TooltipTrigger>
<TooltipContent
className="group flex h-full max-h-56 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)}
align="start"
className="group flex h-[187px] w-[300px] flex-col items-center justify-center rounded-lg bg-gradient-to-br from-black to-gray-500 p-0 shadow-inner transition-all duration-200"
>
<TooltipArrow className="fill-gray-400" />
{isScraping ? (
<div className="relative flex h-full min-h-48 w-full items-center justify-center">
<Skeleton className="absolute h-full w-full rounded-lg" />
<p>Previewing...</p>
</div>
) : metaInfo?.title ? (
<div className="flex h-full w-full flex-col items-center justify-start rounded-lg border bg-primary-foreground group-hover:opacity-80">
<BlurImg
className={
"w-full rounded-t-lg " + metaInfo?.image
? "h-32 group-hover:scale-110 group-hover:opacity-95"
: ""
}
src={
metaInfo?.image || "/_static/illustrations/rocket-crashed.svg"
}
alt={metaInfo?.title || ""}
/>
<div className="mr-auto max-w-64 truncate p-2">
{renderLink(metaInfo?.title ?? url)}
<p className="max-w-64 truncate text-xs text-slate-500">
{metaInfo?.description}
</p>
</div>
</div>
{screenshotInfo.tmp_url ? (
<BlurImage
className={
"rounded-md border bg-primary-foreground group-hover:scale-110 group-hover:opacity-95"
}
src={
screenshotInfo.tmp_url ||
"/_static/illustrations/rocket-crashed.svg"
}
width={1200}
height={750}
alt={`Preview of ${url}`}
/>
) : (
<div className="flex h-full min-h-48 w-full flex-col items-center justify-center rounded-lg border bg-primary-foreground group-hover:opacity-80">
<p className="mb-2 text-lg font-bold text-slate-600">
Faild to preview link
</p>
{renderLink(formatUrl ?? url)}
</div>
<Skeleton className="h-[187px] w-[300px]" />
)}
</TooltipContent>
</Tooltip>