better link card

This commit is contained in:
daniel31x13 2023-12-23 12:11:47 -05:00
parent 98106b9f25
commit 848a33a53e
15 changed files with 701 additions and 259 deletions

View File

@ -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} />;
})}

View File

@ -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
onClick={() => link.url && window.open(link.url || "", "_blank")}
className="cursor-pointer"
>
<LinkIcon link={link} width="w-12 mb-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
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" />
</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} />
&middot;
{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}

View File

@ -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" ? (

View File

@ -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();

View File

@ -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 };

View File

@ -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 };

View File

@ -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"
);
}

View File

@ -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"
},

View File

@ -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,13 +56,23 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
.status(401)
.json({ response: "You don't have access to this collection." });
const { file, contentType, status } = await readFile(
`archives/${collectionIsAccessible.id}/${linkId + suffix}`
);
if (isPreview) {
const { file, contentType, status } = await readFile(
`archives/preview/${collectionIsAccessible.id}/${linkId}.jpeg`
);
res.setHeader("Content-Type", contentType).status(status as number);
res.setHeader("Content-Type", contentType).status(status as number);
return res.send(file);
return res.send(file);
} else {
const { file, contentType, status } = await readFile(
`archives/${collectionIsAccessible.id}/${linkId + suffix}`
);
res.setHeader("Content-Type", contentType).status(status as number);
return res.send(file);
}
}
// else if (req.method === "POST") {
// const user = await verifyUser({ req, res });

View File

@ -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`,
});
};

View File

@ -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) => {

View File

@ -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])

BIN
resized_og_image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -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: {

744
yarn.lock

File diff suppressed because it is too large Load Diff