better link card
This commit is contained in:
parent
98106b9f25
commit
848a33a53e
|
@ -7,7 +7,7 @@ export default function CardView({
|
|||
links: LinkIncludingShortenedCollectionAndTags[];
|
||||
}) {
|
||||
return (
|
||||
<div className="grid 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5">
|
||||
<div className="grid min-[1900px]:grid-cols-4 2xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||
{links.map((e, i) => {
|
||||
return <LinkCard key={i} link={e} count={i} />;
|
||||
})}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
ArchivedFormat,
|
||||
CollectionIncludingMembersAndLinkCount,
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
} from "@/types/global";
|
||||
|
@ -9,8 +10,10 @@ import unescapeString from "@/lib/client/unescapeString";
|
|||
import LinkActions from "@/components/LinkViews/LinkComponents/LinkActions";
|
||||
import LinkDate from "@/components/LinkViews/LinkComponents/LinkDate";
|
||||
import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection";
|
||||
import LinkIcon from "@/components/LinkViews/LinkComponents/LinkIcon";
|
||||
import Image from "next/image";
|
||||
import { previewAvailable } from "@/lib/shared/getArchiveValidity";
|
||||
import Link from "next/link";
|
||||
import LinkIcon from "./LinkComponents/LinkIcon";
|
||||
|
||||
type Props = {
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
|
@ -47,30 +50,48 @@ export default function LinkGrid({ link, count, className }: Props) {
|
|||
}, [collections, links]);
|
||||
|
||||
return (
|
||||
<div className="border border-solid border-neutral-content bg-base-200 shadow-md hover:shadow-none duration-100 rounded-2xl relative p-3">
|
||||
<div className="border border-solid border-neutral-content bg-base-200 shadow-md hover:shadow-none duration-100 rounded-2xl relative">
|
||||
<div className="relative rounded-t-2xl h-52">
|
||||
{previewAvailable(link) ? (
|
||||
<Image
|
||||
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}&preview=true`}
|
||||
width={1280}
|
||||
height={720}
|
||||
alt=""
|
||||
className="rounded-t-2xl select-none object-cover z-10 h-52 w-full shadow"
|
||||
draggable="false"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
target.style.display = "none";
|
||||
}}
|
||||
/>
|
||||
) : undefined}
|
||||
<div
|
||||
onClick={() => link.url && window.open(link.url || "", "_blank")}
|
||||
className="cursor-pointer"
|
||||
style={{
|
||||
background: "radial-gradient(circle, #ffffff, transparent)",
|
||||
}}
|
||||
className="absolute top-0 left-0 right-0 rounded-t-2xl flex items-center justify-center h-52 shadow rounded-md"
|
||||
>
|
||||
<LinkIcon link={link} width="w-12 mb-3" />
|
||||
<LinkIcon link={link} width="w-12" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3">
|
||||
<p className="truncate w-full">
|
||||
{unescapeString(link.name || link.description) || link.url}
|
||||
</p>
|
||||
|
||||
<div className="mt-1 flex flex-col text-xs text-neutral">
|
||||
<div className="flex items-center gap-2">
|
||||
<LinkCollection link={link} collection={collection} />
|
||||
·
|
||||
{link.url ? (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.open(link.url || "", "_blank");
|
||||
}}
|
||||
<Link
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
className="flex items-center hover:opacity-60 cursor-pointer duration-100"
|
||||
>
|
||||
<p className="truncate">{shortendURL}</p>
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="badge badge-primary badge-sm my-1">
|
||||
{link.type}
|
||||
|
|
|
@ -18,9 +18,11 @@ export default function LinkIcon({
|
|||
" " +
|
||||
(width || "w-12");
|
||||
|
||||
const [showFavicon, setShowFavicon] = React.useState<boolean>(true);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{link.url && url ? (
|
||||
{link.url && url && showFavicon ? (
|
||||
<Image
|
||||
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${link.url}&size=32`}
|
||||
width={64}
|
||||
|
@ -28,11 +30,14 @@ export default function LinkIcon({
|
|||
alt=""
|
||||
className={iconClasses}
|
||||
draggable="false"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
target.style.display = "none";
|
||||
onError={() => {
|
||||
setShowFavicon(false);
|
||||
}}
|
||||
/>
|
||||
) : showFavicon === false ? (
|
||||
<div className="flex items-center justify-center h-12 w-12 bg-base-200 rounded-md shadow">
|
||||
<i className="bi-link-45deg text-4xl text-primary"></i>
|
||||
</div>
|
||||
) : link.type === "pdf" ? (
|
||||
<i className={`bi-file-earmark-pdf ${iconClasses}`}></i>
|
||||
) : link.type === "image" ? (
|
||||
|
|
|
@ -8,6 +8,7 @@ import DOMPurify from "dompurify";
|
|||
import { Collection, Link, User } from "@prisma/client";
|
||||
import validateUrlSize from "./validateUrlSize";
|
||||
import removeFile from "./storage/removeFile";
|
||||
import Jimp from "jimp";
|
||||
|
||||
type LinksAndCollectionAndOwner = Link & {
|
||||
collection: Collection & {
|
||||
|
@ -55,6 +56,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
|||
? "pending"
|
||||
: undefined,
|
||||
readable: !link.readable?.startsWith("archive") ? "pending" : undefined,
|
||||
preview: !link.readable?.startsWith("archive") ? "pending" : undefined,
|
||||
lastPreserved: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
@ -65,10 +67,10 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
|||
} else if (linkType === "pdf") {
|
||||
await pdfHandler(link); // archive pdf
|
||||
return;
|
||||
} else if (user.archiveAsPDF || user.archiveAsScreenshot) {
|
||||
} else if ((user.archiveAsPDF || user.archiveAsScreenshot) && link.url) {
|
||||
// archive url
|
||||
link.url &&
|
||||
(await page.goto(link.url, { waitUntil: "domcontentloaded" }));
|
||||
|
||||
await page.goto(link.url, { waitUntil: "domcontentloaded" });
|
||||
|
||||
const content = await page.content();
|
||||
|
||||
|
@ -110,11 +112,81 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
|||
});
|
||||
}
|
||||
|
||||
// Preview
|
||||
|
||||
const ogImageUrl = await page.evaluate(() => {
|
||||
const metaTag = document.querySelector('meta[property="og:image"]');
|
||||
return metaTag ? (metaTag as any).content : null;
|
||||
});
|
||||
|
||||
if (ogImageUrl) {
|
||||
console.log("Found og:image URL:", ogImageUrl);
|
||||
|
||||
// Download the image
|
||||
const imageResponse = await page.goto(ogImageUrl);
|
||||
|
||||
// Check if imageResponse is not null
|
||||
if (imageResponse) {
|
||||
const buffer = await imageResponse.body();
|
||||
|
||||
// Check if buffer is not null
|
||||
if (buffer) {
|
||||
// Load the image using Jimp
|
||||
Jimp.read(buffer, async (err, image) => {
|
||||
if (image) {
|
||||
image?.resize(1280, Jimp.AUTO).quality(20);
|
||||
await image?.writeAsync("og_image.jpg");
|
||||
const processedBuffer = await image?.getBufferAsync(
|
||||
Jimp.MIME_JPEG
|
||||
);
|
||||
|
||||
createFile({
|
||||
data: processedBuffer,
|
||||
filePath: `archives/preview/${link.collectionId}/${link.id}.jpeg`,
|
||||
}).then(() => {
|
||||
return prisma.link.update({
|
||||
where: { id: link.id },
|
||||
data: {
|
||||
preview: `archives/preview/${link.collectionId}/${link.id}.jpeg`,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error("Error processing the image:", err);
|
||||
});
|
||||
} else {
|
||||
console.log("No image data found.");
|
||||
}
|
||||
} else {
|
||||
console.log("Image response is null.");
|
||||
}
|
||||
} else {
|
||||
console.log("No og:image found");
|
||||
page
|
||||
.screenshot({ type: "jpeg", quality: 20 })
|
||||
.then((screenshot) => {
|
||||
return createFile({
|
||||
data: screenshot,
|
||||
filePath: `archives/preview/${link.collectionId}/${link.id}.jpeg`,
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return prisma.link.update({
|
||||
where: { id: link.id },
|
||||
data: {
|
||||
preview: `archives/preview/${link.collectionId}/${link.id}.jpeg`,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Screenshot/PDF
|
||||
await page.evaluate(
|
||||
autoScroll,
|
||||
Number(process.env.AUTOSCROLL_TIMEOUT) || 30
|
||||
);
|
||||
|
||||
// Check if the user hasn't deleted the link by the time we're done scrolling
|
||||
const linkExists = await prisma.link.findUnique({
|
||||
where: { id: link.id },
|
||||
|
@ -176,6 +248,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
|||
await prisma.link.update({
|
||||
where: { id: link.id },
|
||||
data: {
|
||||
lastPreserved: new Date().toISOString(),
|
||||
readable: !finalLink.readable?.startsWith("archives")
|
||||
? "unavailable"
|
||||
: undefined,
|
||||
|
@ -185,6 +258,9 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
|||
pdf: !finalLink.pdf?.startsWith("archives")
|
||||
? "unavailable"
|
||||
: undefined,
|
||||
preview: !finalLink.preview?.startsWith("archives")
|
||||
? "unavailable"
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
else {
|
||||
|
@ -193,6 +269,9 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
|||
removeFile({
|
||||
filePath: `archives/${link.collectionId}/${link.id}_readability.json`,
|
||||
});
|
||||
removeFile({
|
||||
filePath: `archives/preview/${link.collectionId}/${link.id}.jpeg`,
|
||||
});
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
|
|
@ -5,8 +5,8 @@ export default async function getLink(userId: number, query: LinkRequestQuery) {
|
|||
const POSTGRES_IS_ENABLED = process.env.DATABASE_URL.startsWith("postgresql");
|
||||
|
||||
let order: any;
|
||||
if (query.sort === Sort.DateNewestFirst) order = { createdAt: "desc" };
|
||||
else if (query.sort === Sort.DateOldestFirst) order = { createdAt: "asc" };
|
||||
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
|
||||
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
|
||||
else if (query.sort === Sort.NameAZ) order = { name: "asc" };
|
||||
else if (query.sort === Sort.NameZA) order = { name: "desc" };
|
||||
else if (query.sort === Sort.DescriptionAZ) order = { description: "asc" };
|
||||
|
@ -145,7 +145,7 @@ export default async function getLink(userId: number, query: LinkRequestQuery) {
|
|||
select: { id: true },
|
||||
},
|
||||
},
|
||||
orderBy: order || { createdAt: "desc" },
|
||||
orderBy: order || { id: "desc" },
|
||||
});
|
||||
|
||||
return { response: links, status: 200 };
|
||||
|
|
|
@ -7,8 +7,8 @@ export default async function getLink(
|
|||
const POSTGRES_IS_ENABLED = process.env.DATABASE_URL.startsWith("postgresql");
|
||||
|
||||
let order: any;
|
||||
if (query.sort === Sort.DateNewestFirst) order = { createdAt: "desc" };
|
||||
else if (query.sort === Sort.DateOldestFirst) order = { createdAt: "asc" };
|
||||
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
|
||||
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
|
||||
else if (query.sort === Sort.NameAZ) order = { name: "asc" };
|
||||
else if (query.sort === Sort.NameZA) order = { name: "desc" };
|
||||
else if (query.sort === Sort.DescriptionAZ) order = { description: "asc" };
|
||||
|
@ -81,7 +81,7 @@ export default async function getLink(
|
|||
include: {
|
||||
tags: true,
|
||||
},
|
||||
orderBy: order || { createdAt: "desc" },
|
||||
orderBy: order || { id: "desc" },
|
||||
});
|
||||
|
||||
return { response: links, status: 200 };
|
||||
|
|
|
@ -21,3 +21,12 @@ export function readabilityAvailable(link: any) {
|
|||
link.readable !== "unavailable"
|
||||
);
|
||||
}
|
||||
|
||||
export function previewAvailable(link: any) {
|
||||
return (
|
||||
link &&
|
||||
link.preview &&
|
||||
link.preview !== "pending" &&
|
||||
link.preview !== "unavailable"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
"eslint-config-next": "13.4.9",
|
||||
"formidable": "^3.5.1",
|
||||
"framer-motion": "^10.16.4",
|
||||
"jimp": "^0.22.10",
|
||||
"jsdom": "^22.1.0",
|
||||
"lottie-web": "^5.12.2",
|
||||
"micro": "^10.0.1",
|
||||
|
@ -53,7 +54,6 @@
|
|||
"react-hot-toast": "^2.4.1",
|
||||
"react-image-file-resizer": "^0.4.8",
|
||||
"react-select": "^5.7.4",
|
||||
"sharp": "^0.32.1",
|
||||
"stripe": "^12.13.0",
|
||||
"zustand": "^4.3.8"
|
||||
},
|
||||
|
|
|
@ -19,6 +19,7 @@ export const config = {
|
|||
export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||
const linkId = Number(req.query.linkId);
|
||||
const format = Number(req.query.format);
|
||||
const isPreview = Boolean(req.query.preview);
|
||||
|
||||
let suffix: string;
|
||||
|
||||
|
@ -55,6 +56,15 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
|||
.status(401)
|
||||
.json({ response: "You don't have access to this collection." });
|
||||
|
||||
if (isPreview) {
|
||||
const { file, contentType, status } = await readFile(
|
||||
`archives/preview/${collectionIsAccessible.id}/${linkId}.jpeg`
|
||||
);
|
||||
|
||||
res.setHeader("Content-Type", contentType).status(status as number);
|
||||
|
||||
return res.send(file);
|
||||
} else {
|
||||
const { file, contentType, status } = await readFile(
|
||||
`archives/${collectionIsAccessible.id}/${linkId + suffix}`
|
||||
);
|
||||
|
@ -63,6 +73,7 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
|||
|
||||
return res.send(file);
|
||||
}
|
||||
}
|
||||
// else if (req.method === "POST") {
|
||||
// const user = await verifyUser({ req, res });
|
||||
// if (!user) return;
|
||||
|
|
|
@ -76,6 +76,7 @@ const deleteArchivedFiles = async (link: Link & { collection: Collection }) => {
|
|||
image: null,
|
||||
pdf: null,
|
||||
readable: null,
|
||||
preview: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -88,4 +89,7 @@ const deleteArchivedFiles = async (link: Link & { collection: Collection }) => {
|
|||
await removeFile({
|
||||
filePath: `archives/${link.collection.id}/${link.id}_readability.json`,
|
||||
});
|
||||
await removeFile({
|
||||
filePath: `archives/preview/${link.collection.id}/${link.id}.png`,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -38,7 +38,7 @@ export default function Collections() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||
<div className="grid min-[1900px]:grid-cols-4 2xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||
{sortedCollections
|
||||
.filter((e) => e.ownerId === data?.user.id)
|
||||
.map((e, i) => {
|
||||
|
@ -62,7 +62,7 @@ export default function Collections() {
|
|||
description={"Shared collections you're a member of"}
|
||||
/>
|
||||
|
||||
<div className="grid xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||
<div className="grid min-[1900px]:grid-cols-4 2xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||
{sortedCollections
|
||||
.filter((e) => e.ownerId !== data?.user.id)
|
||||
.map((e, i) => {
|
||||
|
|
|
@ -146,7 +146,7 @@ export default function Dashboard() {
|
|||
{links[0] ? (
|
||||
<div className="w-full">
|
||||
<div
|
||||
className={`grid 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full`}
|
||||
className={`grid min-[1900px]:grid-cols-4 2xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5 w-full`}
|
||||
>
|
||||
{links.slice(0, showLinks).map((e, i) => (
|
||||
<LinkCard key={i} link={e} count={i} />
|
||||
|
@ -261,7 +261,7 @@ export default function Dashboard() {
|
|||
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
|
||||
<div className="w-full">
|
||||
<div
|
||||
className={`grid 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full`}
|
||||
className={`grid min-[1900px]:grid-cols-4 2xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5 w-full`}
|
||||
>
|
||||
{links
|
||||
.filter((e) => e.pinnedBy && e.pinnedBy[0])
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
|
@ -60,7 +60,7 @@ async function processBatch() {
|
|||
],
|
||||
},
|
||||
take: archiveTakeCount,
|
||||
orderBy: { createdAt: "asc" },
|
||||
orderBy: { id: "asc" },
|
||||
include: {
|
||||
collection: {
|
||||
include: {
|
||||
|
@ -114,10 +114,17 @@ async function processBatch() {
|
|||
{
|
||||
readable: "pending",
|
||||
},
|
||||
///////////////////////
|
||||
{
|
||||
preview: null,
|
||||
},
|
||||
{
|
||||
preview: "pending",
|
||||
},
|
||||
],
|
||||
},
|
||||
take: archiveTakeCount,
|
||||
orderBy: { createdAt: "desc" },
|
||||
orderBy: { id: "desc" },
|
||||
include: {
|
||||
collection: {
|
||||
include: {
|
||||
|
|
Ŝarĝante…
Reference in New Issue