finalized link card
This commit is contained in:
parent
848a33a53e
commit
e27fb90f14
|
@ -7,7 +7,7 @@ export default function CardView({
|
||||||
links: LinkIncludingShortenedCollectionAndTags[];
|
links: LinkIncludingShortenedCollectionAndTags[];
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grid min-[1900px]:grid-cols-4 2xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
<div className="grid min-[1900px]:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||||
{links.map((e, i) => {
|
{links.map((e, i) => {
|
||||||
return <LinkCard key={i} link={e} count={i} />;
|
return <LinkCard key={i} link={e} count={i} />;
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
CollectionIncludingMembersAndLinkCount,
|
CollectionIncludingMembersAndLinkCount,
|
||||||
LinkIncludingShortenedCollectionAndTags,
|
LinkIncludingShortenedCollectionAndTags,
|
||||||
} from "@/types/global";
|
} from "@/types/global";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
import useCollectionStore from "@/store/collections";
|
import useCollectionStore from "@/store/collections";
|
||||||
import unescapeString from "@/lib/client/unescapeString";
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
|
@ -14,6 +14,8 @@ import Image from "next/image";
|
||||||
import { previewAvailable } from "@/lib/shared/getArchiveValidity";
|
import { previewAvailable } from "@/lib/shared/getArchiveValidity";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import LinkIcon from "./LinkComponents/LinkIcon";
|
import LinkIcon from "./LinkComponents/LinkIcon";
|
||||||
|
import LinkGroupedIconURL from "./LinkComponents/LinkGroupedIconURL";
|
||||||
|
import useOnScreen from "@/hooks/useOnScreen";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
link: LinkIncludingShortenedCollectionAndTags;
|
link: LinkIncludingShortenedCollectionAndTags;
|
||||||
|
@ -24,7 +26,7 @@ type Props = {
|
||||||
export default function LinkGrid({ link, count, className }: Props) {
|
export default function LinkGrid({ link, count, className }: Props) {
|
||||||
const { collections } = useCollectionStore();
|
const { collections } = useCollectionStore();
|
||||||
|
|
||||||
const { links } = useLinkStore();
|
const { links, getLink } = useLinkStore();
|
||||||
|
|
||||||
let shortendURL;
|
let shortendURL;
|
||||||
|
|
||||||
|
@ -49,79 +51,133 @@ export default function LinkGrid({ link, count, className }: Props) {
|
||||||
);
|
);
|
||||||
}, [collections, links]);
|
}, [collections, links]);
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const isVisible = useOnScreen(ref);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: any;
|
||||||
|
|
||||||
|
if (
|
||||||
|
isVisible &&
|
||||||
|
!link.preview?.startsWith("archives") &&
|
||||||
|
link.preview !== "unavailable"
|
||||||
|
) {
|
||||||
|
interval = setInterval(async () => {
|
||||||
|
getLink(link.id as number);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isVisible]);
|
||||||
|
|
||||||
|
const [showInfo, setShowInfo] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-solid border-neutral-content bg-base-200 shadow-md hover:shadow-none duration-100 rounded-2xl relative">
|
<div
|
||||||
<div className="relative rounded-t-2xl h-52">
|
ref={ref}
|
||||||
|
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-40 overflow-hidden">
|
||||||
{previewAvailable(link) ? (
|
{previewAvailable(link) ? (
|
||||||
<Image
|
<Image
|
||||||
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}&preview=true`}
|
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}&preview=true`}
|
||||||
width={1280}
|
width={1280}
|
||||||
height={720}
|
height={720}
|
||||||
alt=""
|
alt=""
|
||||||
className="rounded-t-2xl select-none object-cover z-10 h-52 w-full shadow"
|
className="rounded-t-2xl select-none object-cover z-10 h-40 w-full shadow"
|
||||||
|
style={{ filter: "blur(2px)" }}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
target.style.display = "none";
|
target.style.display = "none";
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : (
|
||||||
|
<div className="bg-gray-50 duration-100 h-40"></div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={
|
||||||
background: "radial-gradient(circle, #ffffff, transparent)",
|
{
|
||||||
}}
|
// background:
|
||||||
className="absolute top-0 left-0 right-0 rounded-t-2xl flex items-center justify-center h-52 shadow rounded-md"
|
// "radial-gradient(circle, rgba(255, 255, 255, 0.5), transparent)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
className="absolute top-0 left-0 right-0 bottom-0 rounded-t-2xl flex items-center justify-center shadow rounded-md"
|
||||||
>
|
>
|
||||||
<LinkIcon link={link} width="w-12" />
|
<LinkIcon link={link} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-3">
|
<div className="p-3 mt-1">
|
||||||
<p className="truncate w-full">
|
<p className="truncate w-full pr-8 text-primary">
|
||||||
{unescapeString(link.name || link.description) || link.url}
|
{unescapeString(link.name || link.description) || link.url}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-1 flex flex-col text-xs text-neutral">
|
|
||||||
<div className="flex items-center gap-2">
|
<Link
|
||||||
|
href={link.url || ""}
|
||||||
|
target="_blank"
|
||||||
|
title={link.url || ""}
|
||||||
|
className="w-fit hover:opacity-60 duration-100"
|
||||||
|
>
|
||||||
|
<div className="flex gap-1 item-center select-none text-neutral mt-1">
|
||||||
|
<i className="bi-link-45deg text-lg mt-[0.15rem] leading-none"></i>
|
||||||
|
<p className="text-sm truncate">{shortendURL}</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<hr className="divider my-2 -mx-3 last:hidden border-t border-neutral-content h-[1px]" />
|
||||||
|
|
||||||
|
<div className="flex justify-between text-xs text-neutral">
|
||||||
|
<div className="cursor-pointer w-fit">
|
||||||
<LinkCollection link={link} collection={collection} />
|
<LinkCollection link={link} collection={collection} />
|
||||||
·
|
|
||||||
{link.url ? (
|
|
||||||
<Link
|
|
||||||
href={link.url}
|
|
||||||
target="_blank"
|
|
||||||
className="flex items-center hover:opacity-60 cursor-pointer duration-100"
|
|
||||||
>
|
|
||||||
<p className="truncate">{shortendURL}</p>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<div className="badge badge-primary badge-sm my-1">
|
|
||||||
{link.type}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<LinkDate link={link} />
|
<LinkDate link={link} />
|
||||||
</div>
|
</div>
|
||||||
{/* <p className="truncate">{unescapeString(link.description)}</p>
|
|
||||||
{link.tags[0] ? (
|
|
||||||
<div className="flex gap-3 items-center flex-wrap mt-2 truncate relative">
|
|
||||||
<div className="flex gap-1 items-center flex-wrap">
|
|
||||||
{link.tags.map((e, i) => (
|
|
||||||
<Link
|
|
||||||
href={"/tags/" + e.id}
|
|
||||||
key={i}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
|
|
||||||
>
|
|
||||||
#{e.name}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : undefined} */}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LinkActions link={link} collection={collection} />
|
{showInfo ? (
|
||||||
|
<div className="p-3 absolute z-30 top-0 left-0 right-0 bottom-0 bg-base-200 rounded-2xl fade-in overflow-y-auto">
|
||||||
|
<div
|
||||||
|
onClick={() => setShowInfo(!showInfo)}
|
||||||
|
className=" float-right btn btn-sm outline-none btn-circle btn-ghost z-10"
|
||||||
|
>
|
||||||
|
<i className="bi-x text-neutral text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
<div className="pb-3 mt-1">
|
||||||
|
<p>{unescapeString(link.description)}</p>
|
||||||
|
{link.tags[0] ? (
|
||||||
|
<div className="flex gap-3 items-center flex-wrap mt-2 truncate relative">
|
||||||
|
<div className="flex gap-1 items-center flex-wrap">
|
||||||
|
{link.tags.map((e, i) => (
|
||||||
|
<Link
|
||||||
|
href={"/tags/" + e.id}
|
||||||
|
key={i}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
|
||||||
|
>
|
||||||
|
#{e.name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
<LinkActions
|
||||||
|
link={link}
|
||||||
|
collection={collection}
|
||||||
|
position="top-[10.75rem] right-3"
|
||||||
|
toggleShowInfo={() => setShowInfo(!showInfo)}
|
||||||
|
linkInfo={showInfo}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,16 @@ type Props = {
|
||||||
link: LinkIncludingShortenedCollectionAndTags;
|
link: LinkIncludingShortenedCollectionAndTags;
|
||||||
collection: CollectionIncludingMembersAndLinkCount;
|
collection: CollectionIncludingMembersAndLinkCount;
|
||||||
position?: string;
|
position?: string;
|
||||||
|
toggleShowInfo: () => void;
|
||||||
|
linkInfo: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LinkActions({ link, collection, position }: Props) {
|
export default function LinkActions({
|
||||||
|
link,
|
||||||
|
toggleShowInfo,
|
||||||
|
position,
|
||||||
|
linkInfo,
|
||||||
|
}: Props) {
|
||||||
const permissions = usePermissions(link.collection.id as number);
|
const permissions = usePermissions(link.collection.id as number);
|
||||||
|
|
||||||
const [editLinkModal, setEditLinkModal] = useState(false);
|
const [editLinkModal, setEditLinkModal] = useState(false);
|
||||||
|
@ -85,6 +92,18 @@ export default function LinkActions({ link, collection, position }: Props) {
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
toggleShowInfo();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!linkInfo ? "Show" : "Hide"} Link Info
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
{permissions === true || permissions?.canUpdate ? (
|
{permissions === true || permissions?.canUpdate ? (
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
|
@ -106,7 +125,6 @@ export default function LinkActions({ link, collection, position }: Props) {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
(document?.activeElement as HTMLElement)?.blur();
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
setPreservedFormatsModal(true);
|
setPreservedFormatsModal(true);
|
||||||
// updateArchive();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Preserved Formats
|
Preserved Formats
|
||||||
|
|
|
@ -21,6 +21,7 @@ export default function LinkCollection({
|
||||||
router.push(`/collections/${link.collection.id}`);
|
router.push(`/collections/${link.collection.id}`);
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100"
|
className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100"
|
||||||
|
title={collection?.name}
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
className="bi-folder-fill text-lg drop-shadow"
|
className="bi-folder-fill text-lg drop-shadow"
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||||
|
import Image from "next/image";
|
||||||
|
import isValidUrl from "@/lib/shared/isValidUrl";
|
||||||
|
import React from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function LinkGroupedIconURL({
|
||||||
|
link,
|
||||||
|
}: {
|
||||||
|
link: LinkIncludingShortenedCollectionAndTags;
|
||||||
|
}) {
|
||||||
|
const url =
|
||||||
|
isValidUrl(link.url || "") && link.url ? new URL(link.url) : undefined;
|
||||||
|
|
||||||
|
const [showFavicon, setShowFavicon] = React.useState<boolean>(true);
|
||||||
|
|
||||||
|
let shortendURL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
shortendURL = new URL(link.url || "").host.toLowerCase();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={link.url || ""} target="_blank">
|
||||||
|
<div className="bg-white shadow-md rounded-md border-[2px] flex gap-1 item-center justify-center border-white select-none z-10 max-w-full">
|
||||||
|
{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}
|
||||||
|
height={64}
|
||||||
|
alt=""
|
||||||
|
className="w-5 h-5 rounded"
|
||||||
|
draggable="false"
|
||||||
|
onError={() => {
|
||||||
|
setShowFavicon(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : showFavicon === false ? (
|
||||||
|
<i className="bi-link-45deg text-xl leading-none text-black"></i>
|
||||||
|
) : link.type === "pdf" ? (
|
||||||
|
<i className={`bi-file-earmark-pdf`}></i>
|
||||||
|
) : link.type === "image" ? (
|
||||||
|
<i className={`bi-file-earmark-image`}></i>
|
||||||
|
) : undefined}
|
||||||
|
<p className="truncate bg-white text-black mr-1">
|
||||||
|
<p className="text-sm">{shortendURL}</p>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
|
@ -14,35 +14,31 @@ export default function LinkIcon({
|
||||||
isValidUrl(link.url || "") && link.url ? new URL(link.url) : undefined;
|
isValidUrl(link.url || "") && link.url ? new URL(link.url) : undefined;
|
||||||
|
|
||||||
const iconClasses: string =
|
const iconClasses: string =
|
||||||
"bg-white text-primary shadow rounded-md border-[2px] border-white select-none z-10" +
|
"bg-white shadow rounded-md border-[2px] flex item-center justify-center border-white select-none z-10" +
|
||||||
" " +
|
" " +
|
||||||
(width || "w-12");
|
(width || "w-12");
|
||||||
|
|
||||||
const [showFavicon, setShowFavicon] = React.useState<boolean>(true);
|
const [showFavicon, setShowFavicon] = React.useState<boolean>(true);
|
||||||
|
|
||||||
return (
|
return link.url && url && showFavicon ? (
|
||||||
<div>
|
<Image
|
||||||
{link.url && url && showFavicon ? (
|
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${link.url}&size=32`}
|
||||||
<Image
|
width={64}
|
||||||
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${link.url}&size=32`}
|
height={64}
|
||||||
width={64}
|
alt=""
|
||||||
height={64}
|
className={iconClasses}
|
||||||
alt=""
|
draggable="false"
|
||||||
className={iconClasses}
|
onError={() => {
|
||||||
draggable="false"
|
setShowFavicon(false);
|
||||||
onError={() => {
|
}}
|
||||||
setShowFavicon(false);
|
/>
|
||||||
}}
|
) : showFavicon === false ? (
|
||||||
/>
|
<div className={iconClasses}>
|
||||||
) : showFavicon === false ? (
|
<i className="bi-link-45deg text-4xl text-black"></i>
|
||||||
<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" ? (
|
|
||||||
<i className={`bi-file-earmark-image ${iconClasses}`}></i>
|
|
||||||
) : undefined}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
) : link.type === "pdf" ? (
|
||||||
|
<i className={`bi-file-earmark-pdf ${iconClasses}`}></i>
|
||||||
|
) : link.type === "image" ? (
|
||||||
|
<i className={`bi-file-earmark-image ${iconClasses}`}></i>
|
||||||
|
) : undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import LinkActions from "@/components/LinkViews/LinkComponents/LinkActions";
|
||||||
import LinkDate from "@/components/LinkViews/LinkComponents/LinkDate";
|
import LinkDate from "@/components/LinkViews/LinkComponents/LinkDate";
|
||||||
import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection";
|
import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection";
|
||||||
import LinkIcon from "@/components/LinkViews/LinkComponents/LinkIcon";
|
import LinkIcon from "@/components/LinkViews/LinkComponents/LinkIcon";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
link: LinkIncludingShortenedCollectionAndTags;
|
link: LinkIncludingShortenedCollectionAndTags;
|
||||||
|
@ -45,6 +46,8 @@ export default function LinkCardCompact({ link, count, className }: Props) {
|
||||||
);
|
);
|
||||||
}, [collections, links]);
|
}, [collections, links]);
|
||||||
|
|
||||||
|
const [showInfo, setShowInfo] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="border-neutral-content relative hover:bg-base-300 duration-200 rounded-lg">
|
<div className="border-neutral-content relative hover:bg-base-300 duration-200 rounded-lg">
|
||||||
|
@ -57,7 +60,7 @@ export default function LinkCardCompact({ link, count, className }: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-[calc(100%-56px)] ml-2">
|
<div className="w-[calc(100%-56px)] ml-2">
|
||||||
<p className="line-clamp-1 mr-8">
|
<p className="line-clamp-1 mr-8 text-primary">
|
||||||
{unescapeString(link.name || link.description) || link.url}
|
{unescapeString(link.name || link.description) || link.url}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -86,7 +89,34 @@ export default function LinkCardCompact({ link, count, className }: Props) {
|
||||||
link={link}
|
link={link}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
position="top-3 right-0 sm:right-3"
|
position="top-3 right-0 sm:right-3"
|
||||||
|
toggleShowInfo={() => setShowInfo(!showInfo)}
|
||||||
|
linkInfo={showInfo}
|
||||||
/>
|
/>
|
||||||
|
{showInfo ? (
|
||||||
|
<div className="pl-10">
|
||||||
|
<div className="pb-3 mt-1">
|
||||||
|
<p>{unescapeString(link.description)}</p>
|
||||||
|
{link.tags[0] ? (
|
||||||
|
<div className="flex gap-3 items-center flex-wrap mt-2 truncate relative">
|
||||||
|
<div className="flex gap-1 items-center flex-wrap">
|
||||||
|
{link.tags.map((e, i) => (
|
||||||
|
<Link
|
||||||
|
href={"/tags/" + e.id}
|
||||||
|
key={i}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
|
||||||
|
>
|
||||||
|
#{e.name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divider my-0 last:hidden h-[1px]"></div>
|
<div className="divider my-0 last:hidden h-[1px]"></div>
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default function Modal({ toggleModal, className, children }: Props) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-y-auto pt-2 sm:py-2 fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex justify-center items-center fade-in z-30">
|
<div className="overflow-y-auto pt-2 sm:py-2 fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex justify-center items-center fade-in z-40">
|
||||||
<ClickAwayHandler
|
<ClickAwayHandler
|
||||||
onClickOutside={toggleModal}
|
onClickOutside={toggleModal}
|
||||||
className={`w-full mt-auto sm:m-auto sm:w-11/12 sm:max-w-2xl ${
|
className={`w-full mt-auto sm:m-auto sm:w-11/12 sm:max-w-2xl ${
|
||||||
|
|
|
@ -162,7 +162,7 @@ export default function Navbar() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{sidebar ? (
|
{sidebar ? (
|
||||||
<div className="fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-30">
|
<div className="fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-40">
|
||||||
<ClickAwayHandler className="h-full" onClickOutside={toggleSidebar}>
|
<ClickAwayHandler className="h-full" onClickOutside={toggleSidebar}>
|
||||||
<div className="slide-right h-full shadow-lg">
|
<div className="slide-right h-full shadow-lg">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { RefObject, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
export default function useOnScreen(ref: RefObject<HTMLElement>) {
|
||||||
|
const [isIntersecting, setIntersecting] = useState(false);
|
||||||
|
|
||||||
|
const observer = useMemo(
|
||||||
|
() =>
|
||||||
|
new IntersectionObserver(([entry]) =>
|
||||||
|
setIntersecting(entry.isIntersecting)
|
||||||
|
),
|
||||||
|
[ref]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
observer.observe(ref.current as HTMLElement);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return isIntersecting;
|
||||||
|
}
|
|
@ -61,13 +61,13 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (linkType === "image") {
|
if (linkType === "image" && !link.image?.startsWith("archive")) {
|
||||||
await imageHandler(link, imageExtension); // archive image (jpeg/png)
|
await imageHandler(link, imageExtension); // archive image (jpeg/png)
|
||||||
return;
|
return;
|
||||||
} else if (linkType === "pdf") {
|
} else if (linkType === "pdf" && !link.pdf?.startsWith("archive")) {
|
||||||
await pdfHandler(link); // archive pdf
|
await pdfHandler(link); // archive pdf
|
||||||
return;
|
return;
|
||||||
} else if ((user.archiveAsPDF || user.archiveAsScreenshot) && link.url) {
|
} else if (link.url) {
|
||||||
// archive url
|
// archive url
|
||||||
|
|
||||||
await page.goto(link.url, { waitUntil: "domcontentloaded" });
|
await page.goto(link.url, { waitUntil: "domcontentloaded" });
|
||||||
|
@ -97,7 +97,11 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
const articleText = article?.textContent
|
const articleText = article?.textContent
|
||||||
.replace(/ +(?= )/g, "") // strip out multiple spaces
|
.replace(/ +(?= )/g, "") // strip out multiple spaces
|
||||||
.replace(/(\r\n|\n|\r)/gm, " "); // strip out line breaks
|
.replace(/(\r\n|\n|\r)/gm, " "); // strip out line breaks
|
||||||
if (articleText && articleText !== "") {
|
if (
|
||||||
|
articleText &&
|
||||||
|
articleText !== "" &&
|
||||||
|
!link.readable?.startsWith("archive")
|
||||||
|
) {
|
||||||
await createFile({
|
await createFile({
|
||||||
data: JSON.stringify(article),
|
data: JSON.stringify(article),
|
||||||
filePath: `archives/${targetLink.collectionId}/${link.id}_readability.json`,
|
filePath: `archives/${targetLink.collectionId}/${link.id}_readability.json`,
|
||||||
|
@ -126,7 +130,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
const imageResponse = await page.goto(ogImageUrl);
|
const imageResponse = await page.goto(ogImageUrl);
|
||||||
|
|
||||||
// Check if imageResponse is not null
|
// Check if imageResponse is not null
|
||||||
if (imageResponse) {
|
if (imageResponse && !link.preview?.startsWith("archive")) {
|
||||||
const buffer = await imageResponse.body();
|
const buffer = await imageResponse.body();
|
||||||
|
|
||||||
// Check if buffer is not null
|
// Check if buffer is not null
|
||||||
|
@ -135,7 +139,6 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
Jimp.read(buffer, async (err, image) => {
|
Jimp.read(buffer, async (err, image) => {
|
||||||
if (image) {
|
if (image) {
|
||||||
image?.resize(1280, Jimp.AUTO).quality(20);
|
image?.resize(1280, Jimp.AUTO).quality(20);
|
||||||
await image?.writeAsync("og_image.jpg");
|
|
||||||
const processedBuffer = await image?.getBufferAsync(
|
const processedBuffer = await image?.getBufferAsync(
|
||||||
Jimp.MIME_JPEG
|
Jimp.MIME_JPEG
|
||||||
);
|
);
|
||||||
|
@ -161,7 +164,9 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
} else {
|
} else {
|
||||||
console.log("Image response is null.");
|
console.log("Image response is null.");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
await page.goBack();
|
||||||
|
} else if (!link.preview?.startsWith("archive")) {
|
||||||
console.log("No og:image found");
|
console.log("No og:image found");
|
||||||
page
|
page
|
||||||
.screenshot({ type: "jpeg", quality: 20 })
|
.screenshot({ type: "jpeg", quality: 20 })
|
||||||
|
@ -194,7 +199,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
if (linkExists) {
|
if (linkExists) {
|
||||||
const processingPromises = [];
|
const processingPromises = [];
|
||||||
|
|
||||||
if (user.archiveAsScreenshot) {
|
if (user.archiveAsScreenshot && !link.image?.startsWith("archive")) {
|
||||||
processingPromises.push(
|
processingPromises.push(
|
||||||
page.screenshot({ fullPage: true }).then((screenshot) => {
|
page.screenshot({ fullPage: true }).then((screenshot) => {
|
||||||
return createFile({
|
return createFile({
|
||||||
|
@ -204,7 +209,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (user.archiveAsPDF) {
|
if (user.archiveAsPDF && !link.pdf?.startsWith("archive")) {
|
||||||
processingPromises.push(
|
processingPromises.push(
|
||||||
page
|
page
|
||||||
.pdf({
|
.pdf({
|
||||||
|
|
|
@ -146,7 +146,7 @@ export default function Dashboard() {
|
||||||
{links[0] ? (
|
{links[0] ? (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div
|
<div
|
||||||
className={`grid min-[1900px]:grid-cols-4 2xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5 w-full`}
|
className={`grid min-[1900px]:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5 w-full`}
|
||||||
>
|
>
|
||||||
{links.slice(0, showLinks).map((e, i) => (
|
{links.slice(0, showLinks).map((e, i) => (
|
||||||
<LinkCard key={i} link={e} count={i} />
|
<LinkCard key={i} link={e} count={i} />
|
||||||
|
@ -261,7 +261,7 @@ export default function Dashboard() {
|
||||||
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
|
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div
|
<div
|
||||||
className={`grid min-[1900px]:grid-cols-4 2xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5 w-full`}
|
className={`grid min-[1900px]:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5 w-full`}
|
||||||
>
|
>
|
||||||
{links
|
{links
|
||||||
.filter((e) => e.pinnedBy && e.pinnedBy[0])
|
.filter((e) => e.pinnedBy && e.pinnedBy[0])
|
||||||
|
|
|
@ -56,7 +56,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-in {
|
.fade-in {
|
||||||
animation: fade-in-animation 100ms;
|
animation: fade-in-animation 200ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fade-in-animation {
|
@keyframes fade-in-animation {
|
||||||
|
|
Ŝarĝante…
Reference in New Issue