refact link preview cpm
This commit is contained in:
@@ -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"
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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
@@ -40,7 +40,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||
<SessionProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
defaultTheme="light"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user