finished the public page

This commit is contained in:
daniel31x13 2023-11-19 08:12:37 -05:00
parent b50ec09727
commit 614d92f050
21 changed files with 576 additions and 176 deletions

View File

@ -20,7 +20,7 @@ export default function dashboardItem({ name, value, icon }: Props) {
<p className="text-gray-500 dark:text-gray-400 text-sm tracking-wider">
{name}
</p>
<p className="font-thin text-6xl text-sky-500 dark:text-sky-500">
<p className="font-thin text-6xl text-sky-500 dark:text-sky-500 mt-2">
{value}
</p>
</div>

View File

@ -56,7 +56,7 @@ export default function FilterSearchDropdown({
}
/>
<Checkbox
label="Text Content"
label="Full Content"
state={searchFilter.textContent}
onClick={() =>
setSearchFilter({

View File

@ -20,7 +20,7 @@ type Props = {
onClick?: Function;
};
export default function SettingsSidebar({ className, onClick }: Props) {
export default function LinkSidebar({ className, onClick }: Props) {
const session = useSession();
const userId = session.data?.user.id;

View File

@ -27,7 +27,14 @@ export default function PreservedFormats() {
useEffect(() => {
let interval: NodeJS.Timer | undefined;
if (link?.screenshotPath === "pending" || link?.pdfPath === "pending") {
interval = setInterval(() => getLink(link.id as number), 5000);
let isPublicRoute = router.pathname.startsWith("/public")
? true
: undefined;
interval = setInterval(
() => getLink(link.id as number, isPublicRoute),
5000
);
} else {
if (interval) {
clearInterval(interval);

View File

@ -38,6 +38,7 @@ export default function ProfilePhoto({ src, className, priority }: Props) {
width={112}
priority={priority}
draggable={false}
onError={() => setImage("")}
className={`h-10 w-10 bg-sky-600 dark:bg-sky-600 shadow rounded-full aspect-square border border-slate-200 dark:border-neutral-700 ${
className || ""
}`}

View File

@ -5,6 +5,7 @@ import { Link as LinkType, Tag } from "@prisma/client";
import isValidUrl from "@/lib/client/isValidUrl";
import unescapeString from "@/lib/client/unescapeString";
import { TagIncludingLinkCount } from "@/types/global";
import Link from "next/link";
interface LinksIncludingTags extends LinkType {
tags: TagIncludingLinkCount[];
@ -27,75 +28,69 @@ export default function LinkCard({ link, count }: Props) {
});
return (
<a href={link.url} target="_blank" rel="noreferrer" className="rounded-3xl">
<div className="border border-solid border-sky-100 bg-gradient-to-tr from-slate-200 from-10% to-gray-50 via-20% shadow-md sm:hover:shadow-none duration-100 rounded-3xl cursor-pointer p-5 flex items-start relative gap-5 sm:gap-10 group/item">
<div className="border border-solid border-sky-100 dark:border-neutral-700 bg-gradient-to-tr from-slate-200 dark:from-neutral-800 from-10% to-gray-50 dark:to-[#303030] via-20% shadow hover:shadow-none duration-100 rounded-lg p-3 flex items-start relative gap-3 group/item">
<div className="flex justify-between items-end gap-5 w-full h-full z-0">
<div className="flex flex-col justify-between w-full">
<div className="flex items-center gap-2">
<p className="text-2xl">
{url && (
<>
<Image
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}
width={42}
height={42}
width={30}
height={30}
alt=""
className="select-none mt-3 z-10 rounded-md shadow border-[3px] border-white bg-white"
className="select-none z-10 rounded-md shadow border-[1px] border-white bg-white float-left mr-2"
draggable="false"
onError={(e) => {
const target = e.target as HTMLElement;
target.style.display = "none";
}}
/>
<Image
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}
width={80}
height={80}
alt=""
className="blur-sm absolute left-2 opacity-40 select-none hidden sm:block"
draggable="false"
onError={(e) => {
const target = e.target as HTMLElement;
target.style.display = "none";
}}
/>
</>
)}
<div className="flex justify-between items-center gap-5 w-full h-full z-0">
<div className="flex flex-col justify-between">
<div className="flex items-baseline gap-1">
<p className="text-xs text-gray-500">{count + 1}</p>
<p className="text-lg text-black">
{unescapeString(link.name || link.description)}
</p>
</div>
<p className="text-gray-500 text-sm font-medium">
{unescapeString(link.description)}
</p>
<div className="flex gap-3 items-center flex-wrap my-3">
<div className="flex gap-1 items-center flex-wrap mt-1">
<div className="flex gap-3 items-center flex-wrap my-2">
<div className="flex gap-1 items-center flex-wrap">
{link.tags.map((e, i) => (
<p
<Link
href={"/public/collections/20?q=" + e.name}
key={i}
className="px-2 py-1 bg-sky-200 text-black text-xs rounded-3xl cursor-pointer truncate max-w-[10rem]"
className="px-2 text-xs rounded-md border border-black dark:border-white truncate max-w-[10rem] hover:opacity-50 duration-100"
>
{e.name}
</p>
</Link>
))}
</div>
</div>
<div className="flex gap-2 items-center flex-wrap mt-2">
<p className="text-gray-500">{formattedDate}</p>
<div className="text-black flex items-center gap-1">
<p>{url ? url.host : link.url}</p>
<div className="flex gap-1 items-center flex-wrap text-sm text-gray-500 dark:text-gray-300">
<p>{formattedDate}</p>
<p>·</p>
<Link
href={url ? url.href : link.url}
target="_blank"
className="hover:opacity-50 duration-100 truncate w-52 sm:w-fit"
title={url ? url.href : link.url}
>
{url ? url.host : link.url}
</Link>
</div>
</div>
</div>
<div className="hidden sm:group-hover/item:block duration-100 text-slate-500">
<div className="w-full">
{unescapeString(link.description)}{" "}
<Link
href={`/public/links/${link.id}`}
className="flex gap-1 items-center flex-wrap text-sm text-gray-500 dark:text-gray-300 hover:opacity-50 duration-100 min-w-fit float-right mt-1 ml-2"
>
<p>Read</p>
<FontAwesomeIcon
icon={faChevronRight}
className="w-7 h-7 slide-right-with-fade"
className="w-3 h-3 mt-[0.15rem]"
/>
</Link>
</div>
</div>
</div>
</div>
</a>
);
}

View File

@ -11,11 +11,13 @@ type Props = {
export default function PublicSearchBar({ placeHolder }: Props) {
const router = useRouter();
const [searchQuery, setSearchQuery] = useState("");
const [searchQuery, setSearchQuery] = useState<string>("");
useEffect(() => {
console.log(router);
});
router.query.q
? setSearchQuery(decodeURIComponent(router.query.q as string))
: setSearchQuery("");
}, [router.query.q]);
return (
<div className="flex items-center relative group">
@ -36,16 +38,21 @@ export default function PublicSearchBar({ placeHolder }: Props) {
toast.error("The search query should not contain '%'.");
setSearchQuery(e.target.value.replace("%", ""));
}}
onKeyDown={(e) =>
e.key === "Enter" &&
router.push(
onKeyDown={(e) => {
if (e.key === "Enter") {
if (!searchQuery) {
return router.push("/public/collections/" + router.query.id);
}
return router.push(
"/public/collections/" +
router.query.id +
"?q=" +
encodeURIComponent(searchQuery)
)
encodeURIComponent(searchQuery || "")
);
}
className="border text-sm border-sky-100 bg-white dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 rounded-md pl-7 py-1 pr-1 w-44 sm:w-60 dark:hover:border-neutral-600 md:focus:w-80 hover:border-sky-300 duration-100 outline-none dark:bg-neutral-800"
}}
className="border text-sm border-sky-100 bg-gray-50 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 rounded-md pl-8 py-2 pr-2 w-44 sm:w-60 dark:hover:border-neutral-600 md:focus:w-80 hover:border-sky-300 duration-100 outline-none dark:bg-neutral-800"
/>
</div>
);

View File

@ -5,8 +5,7 @@ import useModalStore from "@/store/modals";
import { useRouter } from "next/router";
import ClickAwayHandler from "@/components/ClickAwayHandler";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBars, faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import Link from "next/link";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import useWindowDimensions from "@/hooks/useWindowDimensions";
import {
faPen,
@ -66,22 +65,22 @@ export default function LinkLayout({ children }: Props) {
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
useEffect(() => {
if (links) setLink(links.find((e) => e.id === Number(router.query.id)));
if (links[0]) setLink(links.find((e) => e.id === Number(router.query.id)));
}, [links]);
useEffect(() => {
if (link)
setLinkCollection(collections.find((e) => e.id === link?.collection.id));
}, [link]);
}, [link, collections]);
return (
<>
<ModalManagement />
<div className="flex mx-auto">
<div className="hidden lg:block fixed left-5 h-screen">
{/* <div className="hidden lg:block fixed left-5 h-screen">
<LinkSidebar />
</div>
</div> */}
<div className="w-full flex flex-col min-h-screen max-w-screen-md mx-auto p-5">
<div className="flex gap-3 mb-5 duration-100 items-center justify-between">
@ -93,19 +92,31 @@ export default function LinkLayout({ children }: Props) {
</div> */}
<div
onClick={() => router.push(`/collections/${linkCollection?.id}`)}
onClick={() => {
if (router.pathname.startsWith("/public")) {
router.push(
`/public/collections/${
linkCollection?.id || link?.collection.id
}`
);
} else {
router.push(`/collections/${linkCollection?.id}`);
}
}}
className="inline-flex gap-1 hover:opacity-60 items-center select-none cursor-pointer p-2 lg:p-0 lg:px-1 lg:my-2 text-gray-500 dark:text-gray-300 rounded-md duration-100"
>
<FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" />
Back{" "}
<span className="hidden sm:inline-block">
to <span className="capitalize">{linkCollection?.name}</span>
to{" "}
<span className="capitalize">
{linkCollection?.name || link?.collection?.name}
</span>
</span>
</div>
<div className="lg:hidden">
<div className="flex gap-5">
{link?.collection.ownerId === userId ||
{link?.collection?.ownerId === userId ||
linkCollection?.members.some(
(e) => e.userId === userId && e.canUpdate
) ? (
@ -150,7 +161,7 @@ export default function LinkLayout({ children }: Props) {
/>
</div>
{link?.collection.ownerId === userId ||
{link?.collection?.ownerId === userId ||
linkCollection?.members.some(
(e) => e.userId === userId && e.canDelete
) ? (
@ -172,7 +183,6 @@ export default function LinkLayout({ children }: Props) {
) : undefined}
</div>
</div>
</div>
{children}

View File

@ -1,5 +1,5 @@
import { prisma } from "@/lib/api/db";
import { Collection, Link, UsersAndCollections } from "@prisma/client";
import { Collection, UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission";
export default async function getLinkById(userId: number, linkId: number) {
@ -27,7 +27,7 @@ export default async function getLinkById(userId: number, linkId: number) {
status: 401,
};
else {
const updatedLink = await prisma.link.findUnique({
const link = await prisma.link.findUnique({
where: {
id: linkId,
},
@ -43,6 +43,6 @@ export default async function getLinkById(userId: number, linkId: number) {
},
});
return { response: updatedLink, status: 200 };
return { response: link, status: 200 };
}
}

View File

@ -0,0 +1,24 @@
import { prisma } from "@/lib/api/db";
export default async function getLinkById(linkId: number) {
if (!linkId)
return {
response: "Please choose a valid link.",
status: 401,
};
const link = await prisma.link.findFirst({
where: {
id: linkId,
collection: {
isPublic: true,
},
},
include: {
tags: true,
collection: true,
},
});
return { response: link, status: 200 };
}

View File

@ -1,25 +1,36 @@
import { prisma } from "@/lib/api/db";
type Props = {
userId: number;
userId?: number;
collectionId?: number;
linkId?: number;
isPublic?: boolean;
};
export default async function getPermission({
userId,
collectionId,
linkId,
isPublic,
}: Props) {
if (linkId) {
const check = await prisma.collection.findFirst({
where: {
[isPublic ? "OR" : "AND"]: [
{
id: collectionId,
OR: [{ ownerId: userId }, { members: { some: { userId } } }],
links: {
some: {
id: linkId,
},
},
},
{
isPublic: isPublic ? true : undefined,
},
],
},
include: { members: true },
});
@ -27,10 +38,15 @@ export default async function getPermission({
} else if (collectionId) {
const check = await prisma.collection.findFirst({
where: {
AND: {
[isPublic ? "OR" : "AND"]: [
{
id: collectionId,
OR: [{ ownerId: userId }, { members: { some: { userId } } }],
},
{
isPublic: isPublic ? true : undefined,
},
],
},
include: { members: true },
});

View File

@ -1,20 +1,20 @@
import type { NextApiRequest, NextApiResponse } from "next";
import getPermission from "@/lib/api/getPermission";
import readFile from "@/lib/api/storage/readFile";
import verifyUser from "@/lib/api/verifyUser";
import { getToken } from "next-auth/jwt";
export default async function Index(req: NextApiRequest, res: NextApiResponse) {
if (!req.query.params)
return res.status(401).json({ response: "Invalid parameters." });
const user = await verifyUser({ req, res });
if (!user) return;
const token = await getToken({ req });
const userId = token?.id;
const collectionId = req.query.params[0];
const linkId = req.query.params[1];
const collectionIsAccessible = await getPermission({
userId: user.id,
userId,
collectionId: Number(collectionId),
});

View File

@ -2,20 +2,20 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "@/lib/api/db";
import readFile from "@/lib/api/storage/readFile";
import verifyUser from "@/lib/api/verifyUser";
import { getToken } from "next-auth/jwt";
export default async function Index(req: NextApiRequest, res: NextApiResponse) {
const queryId = Number(req.query.id);
const user = await verifyUser({ req, res });
if (!user) return;
if (!queryId)
return res
.setHeader("Content-Type", "text/plain")
.status(401)
.send("Invalid parameters.");
if (user.id !== queryId) {
const token = await getToken({ req });
const userId = token?.id;
const targetUser = await prisma.user.findUnique({
where: {
id: queryId,
@ -25,19 +25,39 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
},
});
if (targetUser?.isPrivate) {
if (!userId) {
return res
.setHeader("Content-Type", "text/plain")
.status(400)
.send("File inaccessible.");
}
const user = await prisma.user.findUnique({
where: {
id: userId,
},
include: {
subscriptions: true,
},
});
const whitelistedUsernames = targetUser?.whitelistedUsers.map(
(whitelistedUsername) => whitelistedUsername.username
);
if (
targetUser?.isPrivate &&
user.username &&
!whitelistedUsernames?.includes(user.username)
) {
if (!user?.username) {
return res
.setHeader("Content-Type", "text/plain")
.status(400)
.send("File not found.");
.send("File inaccessible.");
}
if (user.username && !whitelistedUsernames?.includes(user.username)) {
return res
.setHeader("Content-Type", "text/plain")
.status(400)
.send("File inaccessible.");
}
}

View File

@ -1,7 +1,7 @@
import getPublicCollection from "@/lib/api/controllers/public/collections/getPublicCollection";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function collections(
export default async function collection(
req: NextApiRequest,
res: NextApiResponse
) {

View File

@ -0,0 +1,13 @@
import getLinkById from "@/lib/api/controllers/public/links/linkId/getLinkById";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function link(req: NextApiRequest, res: NextApiResponse) {
if (!req?.query?.id) {
return res.status(401).json({ response: "Please choose a valid link." });
}
if (req.method === "GET") {
const link = await getLinkById(Number(req?.query?.id));
return res.status(link.status).json({ response: link.response });
}
}

View File

@ -146,7 +146,7 @@ export default function Index() {
>
<div
id="link-banner"
className="link-banner p-3 mb-6 relative bg-opacity-10 border border-solid border-sky-100 dark:border-neutral-700 shadow-md"
className="link-banner p-5 mb-4 relative bg-opacity-10 border border-solid border-sky-100 dark:border-neutral-700 shadow-md"
>
<div id="link-banner-inner" className="link-banner-inner"></div>

View File

@ -57,8 +57,6 @@ export default function PublicCollections() {
image: "",
});
useEffect(() => {}, []);
const [searchFilter, setSearchFilter] = useState({
name: true,
url: true,

301
pages/public/links/[id].tsx Normal file
View File

@ -0,0 +1,301 @@
import LinkLayout from "@/layouts/LinkLayout";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import useLinkStore from "@/store/links";
import { useRouter } from "next/router";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import Image from "next/image";
import ColorThief, { RGBColor } from "colorthief";
import { useTheme } from "next-themes";
import unescapeString from "@/lib/client/unescapeString";
import isValidUrl from "@/lib/client/isValidUrl";
import DOMPurify from "dompurify";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBoxesStacked, faFolder } from "@fortawesome/free-solid-svg-icons";
import useModalStore from "@/store/modals";
import { useSession } from "next-auth/react";
import { isProbablyReaderable } from "@mozilla/readability";
type LinkContent = {
title: string;
content: string;
textContent: string;
length: number;
excerpt: string;
byline: string;
dir: string;
siteName: string;
lang: string;
};
export default function Index() {
const { theme } = useTheme();
const { links, getLink } = useLinkStore();
const { setModal } = useModalStore();
const session = useSession();
const userId = session.data?.user.id;
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
const [linkContent, setLinkContent] = useState<LinkContent>();
const [imageError, setImageError] = useState<boolean>(false);
const [colorPalette, setColorPalette] = useState<RGBColor[]>();
const router = useRouter();
useEffect(() => {
const fetchLink = async () => {
if (router.query.id) {
await getLink(Number(router.query.id), true);
}
};
fetchLink();
}, []);
useEffect(() => {
if (links[0]) setLink(links.find((e) => e.id === Number(router.query.id)));
}, [links]);
useEffect(() => {
const fetchLinkContent = async () => {
if (
router.query.id &&
link?.readabilityPath &&
link?.readabilityPath !== "pending"
) {
const response = await fetch(`/api/v1/${link?.readabilityPath}`);
const data = await response?.json();
setLinkContent(data);
}
};
fetchLinkContent();
}, [link]);
useEffect(() => {
let interval: NodeJS.Timer | undefined;
if (
link?.screenshotPath === "pending" ||
link?.pdfPath === "pending" ||
link?.readabilityPath === "pending"
) {
interval = setInterval(() => getLink(link.id as number, true), 5000);
} else {
if (interval) {
clearInterval(interval);
}
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [link?.screenshotPath, link?.pdfPath, link?.readabilityPath]);
const colorThief = new ColorThief();
const rgbToHex = (r: number, g: number, b: number): string =>
"#" +
[r, g, b]
.map((x) => {
const hex = x.toString(16);
return hex.length === 1 ? "0" + hex : hex;
})
.join("");
useEffect(() => {
const banner = document.getElementById("link-banner");
const bannerInner = document.getElementById("link-banner-inner");
if (colorPalette && banner && bannerInner) {
if (colorPalette[0] && colorPalette[1]) {
banner.style.background = `linear-gradient(to right, ${rgbToHex(
colorPalette[0][0],
colorPalette[0][1],
colorPalette[0][2]
)}30, ${rgbToHex(
colorPalette[1][0],
colorPalette[1][1],
colorPalette[1][2]
)}30)`;
}
if (colorPalette[2] && colorPalette[3]) {
bannerInner.style.background = `linear-gradient(to left, ${rgbToHex(
colorPalette[2][0],
colorPalette[2][1],
colorPalette[2][2]
)}30, ${rgbToHex(
colorPalette[3][0],
colorPalette[3][1],
colorPalette[3][2]
)})30`;
}
}
}, [colorPalette, theme]);
return (
<LinkLayout>
<div
className={`flex flex-col max-w-screen-md h-full ${
theme === "dark" ? "banner-dark-mode" : "banner-light-mode"
}`}
>
<div
id="link-banner"
className="link-banner p-5 mb-4 relative bg-opacity-10 border border-solid border-sky-100 dark:border-neutral-700 shadow-md"
>
<div id="link-banner-inner" className="link-banner-inner"></div>
<div className={`relative flex flex-col gap-3 items-start`}>
<div className="flex gap-3 items-end">
{!imageError && link?.url && (
<Image
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${link.url}&size=32`}
width={42}
height={42}
alt=""
id={"favicon-" + link.id}
className="select-none mt-2 w-10 rounded-md shadow border-[3px] border-white dark:border-neutral-900 bg-white dark:bg-neutral-900 aspect-square"
draggable="false"
onLoad={(e) => {
try {
const color = colorThief.getPalette(
e.target as HTMLImageElement,
4
);
setColorPalette(color);
} catch (err) {
console.log(err);
}
}}
onError={(e) => {
setImageError(true);
}}
/>
)}
<div className="flex gap-2 text-sm text-gray-500 dark:text-gray-300">
<p className=" min-w-fit">
{link?.createdAt
? new Date(link?.createdAt).toLocaleString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
: undefined}
</p>
{link?.url ? (
<>
<p></p>
<Link
href={link?.url || ""}
title={link?.url}
target="_blank"
className="hover:opacity-60 duration-100 break-all"
>
{isValidUrl(link?.url || "")
? new URL(link?.url as string).host
: undefined}
</Link>
</>
) : undefined}
</div>
</div>
<div className="flex flex-col gap-2">
<p className="capitalize text-2xl sm:text-3xl font-thin">
{unescapeString(link?.name || link?.description || "")}
</p>
<div className="flex gap-1 items-center flex-wrap">
<Link
href={`/collections/${link?.collection?.id}`}
className="flex items-center gap-1 cursor-pointer hover:opacity-60 duration-100 mr-2 z-10"
>
<FontAwesomeIcon
icon={faFolder}
className="w-5 h-5 drop-shadow"
style={{ color: link?.collection?.color }}
/>
<p
title={link?.collection?.name}
className="text-black dark:text-white text-lg truncate max-w-[12rem]"
>
{link?.collection?.name}
</p>
</Link>
{link?.tags.map((e, i) => (
<Link key={i} href={`/tags/${e.id}`} className="z-10">
<p
title={e.name}
className="px-2 py-1 bg-sky-200 text-black dark:text-white dark:bg-sky-900 text-xs rounded-3xl cursor-pointer hover:opacity-60 duration-100 truncate max-w-[19rem]"
>
{e.name}
</p>
</Link>
))}
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-5 h-full">
{link?.readabilityPath?.startsWith("archives") ? (
<div
className="line-break px-3 reader-view"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(linkContent?.content || "") || "",
}}
></div>
) : (
<div className="border border-solid border-sky-100 dark:border-neutral-700 w-full h-full flex flex-col justify-center p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800">
{link?.readabilityPath === "pending" ? (
<p className="text-center">
Generating readable format, please wait...
</p>
) : (
<>
<p className="text-center text-2xl text-black dark:text-white">
There is no reader view for this webpage
</p>
<p className="text-center text-sm text-black dark:text-white">
{link?.collection?.ownerId === userId
? "You can update (refetch) the preserved formats by managing them below"
: "The collections owners can refetch the preserved formats"}
</p>
{link?.collection?.ownerId === userId ? (
<div
onClick={() =>
link
? setModal({
modal: "LINK",
state: true,
active: link,
method: "FORMATS",
})
: undefined
}
className="mt-4 flex gap-2 w-fit mx-auto relative items-center font-semibold select-none cursor-pointer p-2 px-3 rounded-md dark:hover:bg-sky-600 text-white bg-sky-700 hover:bg-sky-600 duration-100"
>
<FontAwesomeIcon
icon={faBoxesStacked}
className="w-5 h-5 duration-100"
/>
<p>Manage preserved formats</p>
</div>
) : undefined}
</>
)}
</div>
)}
</div>
</div>
</LinkLayout>
);
}

View File

@ -17,7 +17,7 @@ type LinkStore = {
addLink: (
body: LinkIncludingShortenedCollectionAndTags
) => Promise<ResponseObject>;
getLink: (linkId: number) => Promise<ResponseObject>;
getLink: (linkId: number, publicRoute?: boolean) => Promise<ResponseObject>;
updateLink: (
link: LinkIncludingShortenedCollectionAndTags
) => Promise<ResponseObject>;
@ -66,8 +66,12 @@ const useLinkStore = create<LinkStore>()((set) => ({
return { ok: response.ok, data: data.response };
},
getLink: async (linkId) => {
const response = await fetch(`/api/v1/links/${linkId}`);
getLink: async (linkId, publicRoute) => {
const path = publicRoute
? `/api/v1/public/links/${linkId}`
: `/api/v1/links/${linkId}`;
const response = await fetch(path);
const data = await response.json();

View File

@ -190,7 +190,11 @@ body {
/* Reader view custom stylings */
.reader-view {
line-height: 3rem;
line-height: 2.8rem;
}
.reader-view p {
font-size: 1.15rem;
line-height: 2.5rem;
}
.reader-view h1 {
font-size: 2.2rem;