finalized link card
This commit is contained in:
parent
848a33a53e
commit
e27fb90f14
|
@ -7,7 +7,7 @@ export default function CardView({
|
|||
links: LinkIncludingShortenedCollectionAndTags[];
|
||||
}) {
|
||||
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) => {
|
||||
return <LinkCard key={i} link={e} count={i} />;
|
||||
})}
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
CollectionIncludingMembersAndLinkCount,
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
} from "@/types/global";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useLinkStore from "@/store/links";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
|
@ -14,6 +14,8 @@ import Image from "next/image";
|
|||
import { previewAvailable } from "@/lib/shared/getArchiveValidity";
|
||||
import Link from "next/link";
|
||||
import LinkIcon from "./LinkComponents/LinkIcon";
|
||||
import LinkGroupedIconURL from "./LinkComponents/LinkGroupedIconURL";
|
||||
import useOnScreen from "@/hooks/useOnScreen";
|
||||
|
||||
type Props = {
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
|
@ -24,7 +26,7 @@ type Props = {
|
|||
export default function LinkGrid({ link, count, className }: Props) {
|
||||
const { collections } = useCollectionStore();
|
||||
|
||||
const { links } = useLinkStore();
|
||||
const { links, getLink } = useLinkStore();
|
||||
|
||||
let shortendURL;
|
||||
|
||||
|
@ -49,58 +51,104 @@ export default function LinkGrid({ link, count, className }: Props) {
|
|||
);
|
||||
}, [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 (
|
||||
<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">
|
||||
<div
|
||||
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) ? (
|
||||
<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"
|
||||
className="rounded-t-2xl select-none object-cover z-10 h-40 w-full shadow"
|
||||
style={{ filter: "blur(2px)" }}
|
||||
draggable="false"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
target.style.display = "none";
|
||||
}}
|
||||
/>
|
||||
) : undefined}
|
||||
) : (
|
||||
<div className="bg-gray-50 duration-100 h-40"></div>
|
||||
)}
|
||||
<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"
|
||||
style={
|
||||
{
|
||||
// background:
|
||||
// "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 className="p-3">
|
||||
<p className="truncate w-full">
|
||||
<div className="p-3 mt-1">
|
||||
<p className="truncate w-full pr-8 text-primary">
|
||||
{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 ? (
|
||||
|
||||
<Link
|
||||
href={link.url}
|
||||
href={link.url || ""}
|
||||
target="_blank"
|
||||
className="flex items-center hover:opacity-60 cursor-pointer duration-100"
|
||||
title={link.url || ""}
|
||||
className="w-fit hover:opacity-60 duration-100"
|
||||
>
|
||||
<p className="truncate">{shortendURL}</p>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="badge badge-primary badge-sm my-1">
|
||||
{link.type}
|
||||
<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} />
|
||||
</div>
|
||||
<LinkDate link={link} />
|
||||
</div>
|
||||
{/* <p className="truncate">{unescapeString(link.description)}</p>
|
||||
</div>
|
||||
|
||||
{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">
|
||||
|
@ -118,10 +166,18 @@ export default function LinkGrid({ link, count, className }: Props) {
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : undefined} */}
|
||||
) : undefined}
|
||||
</div>
|
||||
</div>
|
||||
) : undefined}
|
||||
|
||||
<LinkActions link={link} collection={collection} />
|
||||
<LinkActions
|
||||
link={link}
|
||||
collection={collection}
|
||||
position="top-[10.75rem] right-3"
|
||||
toggleShowInfo={() => setShowInfo(!showInfo)}
|
||||
linkInfo={showInfo}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,9 +15,16 @@ type Props = {
|
|||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
collection: CollectionIncludingMembersAndLinkCount;
|
||||
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 [editLinkModal, setEditLinkModal] = useState(false);
|
||||
|
@ -85,6 +92,18 @@ export default function LinkActions({ link, collection, position }: Props) {
|
|||
</div>
|
||||
</li>
|
||||
) : 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 ? (
|
||||
<li>
|
||||
<div
|
||||
|
@ -106,7 +125,6 @@ export default function LinkActions({ link, collection, position }: Props) {
|
|||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setPreservedFormatsModal(true);
|
||||
// updateArchive();
|
||||
}}
|
||||
>
|
||||
Preserved Formats
|
||||
|
|
|
@ -21,6 +21,7 @@ export default function LinkCollection({
|
|||
router.push(`/collections/${link.collection.id}`);
|
||||
}}
|
||||
className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100"
|
||||
title={collection?.name}
|
||||
>
|
||||
<i
|
||||
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,15 +14,13 @@ export default function LinkIcon({
|
|||
isValidUrl(link.url || "") && link.url ? new URL(link.url) : undefined;
|
||||
|
||||
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");
|
||||
|
||||
const [showFavicon, setShowFavicon] = React.useState<boolean>(true);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{link.url && url && showFavicon ? (
|
||||
return 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}
|
||||
|
@ -35,14 +33,12 @@ export default function LinkIcon({
|
|||
}}
|
||||
/>
|
||||
) : 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 className={iconClasses}>
|
||||
<i className="bi-link-45deg text-4xl text-black"></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>
|
||||
);
|
||||
) : undefined;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ 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 Link from "next/link";
|
||||
|
||||
type Props = {
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
|
@ -45,6 +46,8 @@ export default function LinkCardCompact({ link, count, className }: Props) {
|
|||
);
|
||||
}, [collections, links]);
|
||||
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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 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}
|
||||
</p>
|
||||
|
||||
|
@ -86,7 +89,34 @@ export default function LinkCardCompact({ link, count, className }: Props) {
|
|||
link={link}
|
||||
collection={collection}
|
||||
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 className="divider my-0 last:hidden h-[1px]"></div>
|
||||
|
|
|
@ -16,7 +16,7 @@ export default function Modal({ toggleModal, className, children }: Props) {
|
|||
});
|
||||
|
||||
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
|
||||
onClickOutside={toggleModal}
|
||||
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>
|
||||
{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}>
|
||||
<div className="slide-right h-full shadow-lg">
|
||||
<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)
|
||||
return;
|
||||
} else if (linkType === "pdf") {
|
||||
} else if (linkType === "pdf" && !link.pdf?.startsWith("archive")) {
|
||||
await pdfHandler(link); // archive pdf
|
||||
return;
|
||||
} else if ((user.archiveAsPDF || user.archiveAsScreenshot) && link.url) {
|
||||
} else if (link.url) {
|
||||
// archive url
|
||||
|
||||
await page.goto(link.url, { waitUntil: "domcontentloaded" });
|
||||
|
@ -97,7 +97,11 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
|||
const articleText = article?.textContent
|
||||
.replace(/ +(?= )/g, "") // strip out multiple spaces
|
||||
.replace(/(\r\n|\n|\r)/gm, " "); // strip out line breaks
|
||||
if (articleText && articleText !== "") {
|
||||
if (
|
||||
articleText &&
|
||||
articleText !== "" &&
|
||||
!link.readable?.startsWith("archive")
|
||||
) {
|
||||
await createFile({
|
||||
data: JSON.stringify(article),
|
||||
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);
|
||||
|
||||
// Check if imageResponse is not null
|
||||
if (imageResponse) {
|
||||
if (imageResponse && !link.preview?.startsWith("archive")) {
|
||||
const buffer = await imageResponse.body();
|
||||
|
||||
// Check if buffer is not null
|
||||
|
@ -135,7 +139,6 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
|||
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
|
||||
);
|
||||
|
@ -161,7 +164,9 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
|||
} else {
|
||||
console.log("Image response is null.");
|
||||
}
|
||||
} else {
|
||||
|
||||
await page.goBack();
|
||||
} else if (!link.preview?.startsWith("archive")) {
|
||||
console.log("No og:image found");
|
||||
page
|
||||
.screenshot({ type: "jpeg", quality: 20 })
|
||||
|
@ -194,7 +199,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
|||
if (linkExists) {
|
||||
const processingPromises = [];
|
||||
|
||||
if (user.archiveAsScreenshot) {
|
||||
if (user.archiveAsScreenshot && !link.image?.startsWith("archive")) {
|
||||
processingPromises.push(
|
||||
page.screenshot({ fullPage: true }).then((screenshot) => {
|
||||
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(
|
||||
page
|
||||
.pdf({
|
||||
|
|
|
@ -146,7 +146,7 @@ export default function Dashboard() {
|
|||
{links[0] ? (
|
||||
<div className="w-full">
|
||||
<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) => (
|
||||
<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 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
|
||||
.filter((e) => e.pinnedBy && e.pinnedBy[0])
|
||||
|
|
|
@ -56,7 +56,7 @@ body {
|
|||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fade-in-animation 100ms;
|
||||
animation: fade-in-animation 200ms;
|
||||
}
|
||||
|
||||
@keyframes fade-in-animation {
|
||||
|
|
Ŝarĝante…
Reference in New Issue